From e65a233e94c161a8b938015d584520cd9c2c5aec Mon Sep 17 00:00:00 2001 From: droon Date: Wed, 10 Jun 2020 23:32:21 -0700 Subject: [PATCH 0001/1220] pdchu --- padbuildimg/__init__.py | 4 + padbuildimg/assets/OpenSans-ExtraBold.ttf | Bin 0 -> 222584 bytes padbuildimg/assets/awk/1.png | Bin 0 -> 3169 bytes padbuildimg/assets/awk/10.png | Bin 0 -> 3016 bytes padbuildimg/assets/awk/11.png | Bin 0 -> 2860 bytes padbuildimg/assets/awk/12.png | Bin 0 -> 3531 bytes padbuildimg/assets/awk/13.png | Bin 0 -> 3379 bytes padbuildimg/assets/awk/14.png | Bin 0 -> 3205 bytes padbuildimg/assets/awk/15.png | Bin 0 -> 3222 bytes padbuildimg/assets/awk/16.png | Bin 0 -> 3255 bytes padbuildimg/assets/awk/17.png | Bin 0 -> 3233 bytes padbuildimg/assets/awk/18.png | Bin 0 -> 3265 bytes padbuildimg/assets/awk/19.png | Bin 0 -> 2890 bytes padbuildimg/assets/awk/2.png | Bin 0 -> 3173 bytes padbuildimg/assets/awk/20.png | Bin 0 -> 3333 bytes padbuildimg/assets/awk/21.png | Bin 0 -> 2523 bytes padbuildimg/assets/awk/22.png | Bin 0 -> 3206 bytes padbuildimg/assets/awk/23.png | Bin 0 -> 3284 bytes padbuildimg/assets/awk/24.png | Bin 0 -> 3360 bytes padbuildimg/assets/awk/25.png | Bin 0 -> 3317 bytes padbuildimg/assets/awk/26.png | Bin 0 -> 3251 bytes padbuildimg/assets/awk/27.png | Bin 0 -> 2466 bytes padbuildimg/assets/awk/28.png | Bin 0 -> 2287 bytes padbuildimg/assets/awk/29.png | Bin 0 -> 1685 bytes padbuildimg/assets/awk/3.png | Bin 0 -> 2947 bytes padbuildimg/assets/awk/30.png | Bin 0 -> 1766 bytes padbuildimg/assets/awk/31.png | Bin 0 -> 1674 bytes padbuildimg/assets/awk/32.png | Bin 0 -> 1764 bytes padbuildimg/assets/awk/33.png | Bin 0 -> 1681 bytes padbuildimg/assets/awk/34.png | Bin 0 -> 1621 bytes padbuildimg/assets/awk/35.png | Bin 0 -> 1718 bytes padbuildimg/assets/awk/36.png | Bin 0 -> 1643 bytes padbuildimg/assets/awk/37.png | Bin 0 -> 1812 bytes padbuildimg/assets/awk/38.png | Bin 0 -> 1721 bytes padbuildimg/assets/awk/39.png | Bin 0 -> 1651 bytes padbuildimg/assets/awk/4.png | Bin 0 -> 3155 bytes padbuildimg/assets/awk/40.png | Bin 0 -> 2052 bytes padbuildimg/assets/awk/41.png | Bin 0 -> 1799 bytes padbuildimg/assets/awk/42.png | Bin 0 -> 1636 bytes padbuildimg/assets/awk/43.png | Bin 0 -> 1742 bytes padbuildimg/assets/awk/44.png | Bin 0 -> 1727 bytes padbuildimg/assets/awk/45.png | Bin 0 -> 1678 bytes padbuildimg/assets/awk/46.png | Bin 0 -> 1765 bytes padbuildimg/assets/awk/47.png | Bin 0 -> 1660 bytes padbuildimg/assets/awk/48.png | Bin 0 -> 5453 bytes padbuildimg/assets/awk/49.png | Bin 0 -> 1623 bytes padbuildimg/assets/awk/5.png | Bin 0 -> 3163 bytes padbuildimg/assets/awk/50.png | Bin 0 -> 2452 bytes padbuildimg/assets/awk/51.png | Bin 0 -> 1617 bytes padbuildimg/assets/awk/52.png | Bin 0 -> 1733 bytes padbuildimg/assets/awk/53.png | Bin 0 -> 1460 bytes padbuildimg/assets/awk/54.png | Bin 0 -> 1833 bytes padbuildimg/assets/awk/55.png | Bin 0 -> 1681 bytes padbuildimg/assets/awk/56.png | Bin 0 -> 1384 bytes padbuildimg/assets/awk/57.png | Bin 0 -> 1583 bytes padbuildimg/assets/awk/58.png | Bin 0 -> 1599 bytes padbuildimg/assets/awk/59.png | Bin 0 -> 1335 bytes padbuildimg/assets/awk/6.png | Bin 0 -> 3199 bytes padbuildimg/assets/awk/60.png | Bin 0 -> 1506 bytes padbuildimg/assets/awk/61.png | Bin 0 -> 1657 bytes padbuildimg/assets/awk/62.png | Bin 0 -> 2509 bytes padbuildimg/assets/awk/63.png | Bin 0 -> 1144 bytes padbuildimg/assets/awk/64.png | Bin 0 -> 2881 bytes padbuildimg/assets/awk/65.png | Bin 0 -> 2239 bytes padbuildimg/assets/awk/66.png | Bin 0 -> 2225 bytes padbuildimg/assets/awk/67.png | Bin 0 -> 2027 bytes padbuildimg/assets/awk/68.png | Bin 0 -> 2219 bytes padbuildimg/assets/awk/69.png | Bin 0 -> 2381 bytes padbuildimg/assets/awk/7.png | Bin 0 -> 3204 bytes padbuildimg/assets/awk/70.png | Bin 0 -> 2390 bytes padbuildimg/assets/awk/71.png | Bin 0 -> 2441 bytes padbuildimg/assets/awk/72.png | Bin 0 -> 2507 bytes padbuildimg/assets/awk/8.png | Bin 0 -> 3210 bytes padbuildimg/assets/awk/9.png | Bin 0 -> 3177 bytes padbuildimg/assets/circle.png | Bin 0 -> 1607 bytes padbuildimg/assets/delay_buffer.png | Bin 0 -> 32137 bytes padbuildimg/assets/lat/aak.png | Bin 0 -> 6778 bytes padbuildimg/assets/lat/ah.png | Bin 0 -> 12290 bytes padbuildimg/assets/lat/all.png | Bin 0 -> 11031 bytes padbuildimg/assets/lat/atk+.png | Bin 0 -> 6741 bytes padbuildimg/assets/lat/atk.png | Bin 0 -> 15229 bytes padbuildimg/assets/lat/awk.png | Bin 0 -> 7152 bytes padbuildimg/assets/lat/bak.png | Bin 0 -> 6832 bytes padbuildimg/assets/lat/bres+.png | Bin 0 -> 6563 bytes padbuildimg/assets/lat/bres.png | Bin 0 -> 12830 bytes padbuildimg/assets/lat/dek.png | Bin 0 -> 6629 bytes padbuildimg/assets/lat/dres+.png | Bin 0 -> 6649 bytes padbuildimg/assets/lat/dres.png | Bin 0 -> 13506 bytes padbuildimg/assets/lat/drk.png | Bin 0 -> 6873 bytes padbuildimg/assets/lat/enk.png | Bin 0 -> 7413 bytes padbuildimg/assets/lat/evk.png | Bin 0 -> 6633 bytes padbuildimg/assets/lat/gok.png | Bin 0 -> 7058 bytes padbuildimg/assets/lat/gres+.png | Bin 0 -> 6572 bytes padbuildimg/assets/lat/gres.png | Bin 0 -> 12879 bytes padbuildimg/assets/lat/hek.png | Bin 0 -> 6971 bytes padbuildimg/assets/lat/hp+.png | Bin 0 -> 6634 bytes padbuildimg/assets/lat/hp.png | Bin 0 -> 15033 bytes padbuildimg/assets/lat/lres+.png | Bin 0 -> 6584 bytes padbuildimg/assets/lat/lres.png | Bin 0 -> 12699 bytes padbuildimg/assets/lat/mak.png | Bin 0 -> 6643 bytes padbuildimg/assets/lat/phk.png | Bin 0 -> 7188 bytes padbuildimg/assets/lat/rcv+.png | Bin 0 -> 6601 bytes padbuildimg/assets/lat/rcv.png | Bin 0 -> 14117 bytes padbuildimg/assets/lat/rek.png | Bin 0 -> 6923 bytes padbuildimg/assets/lat/rres+.png | Bin 0 -> 6567 bytes padbuildimg/assets/lat/rres.png | Bin 0 -> 12762 bytes padbuildimg/assets/lat/sdr.png | Bin 0 -> 12451 bytes padbuildimg/assets/lat/te+.png | Bin 0 -> 6320 bytes padbuildimg/assets/lat/te.png | Bin 0 -> 13407 bytes padbuildimg/assets/star.png | Bin 0 -> 1951 bytes padbuildimg/info.json | 3 + padbuildimg/padbuildimg.py | 804 ++++++++++++++++++++++ 112 files changed, 811 insertions(+) create mode 100644 padbuildimg/__init__.py create mode 100644 padbuildimg/assets/OpenSans-ExtraBold.ttf create mode 100644 padbuildimg/assets/awk/1.png create mode 100644 padbuildimg/assets/awk/10.png create mode 100644 padbuildimg/assets/awk/11.png create mode 100644 padbuildimg/assets/awk/12.png create mode 100644 padbuildimg/assets/awk/13.png create mode 100644 padbuildimg/assets/awk/14.png create mode 100644 padbuildimg/assets/awk/15.png create mode 100644 padbuildimg/assets/awk/16.png create mode 100644 padbuildimg/assets/awk/17.png create mode 100644 padbuildimg/assets/awk/18.png create mode 100644 padbuildimg/assets/awk/19.png create mode 100644 padbuildimg/assets/awk/2.png create mode 100644 padbuildimg/assets/awk/20.png create mode 100644 padbuildimg/assets/awk/21.png create mode 100644 padbuildimg/assets/awk/22.png create mode 100644 padbuildimg/assets/awk/23.png create mode 100644 padbuildimg/assets/awk/24.png create mode 100644 padbuildimg/assets/awk/25.png create mode 100644 padbuildimg/assets/awk/26.png create mode 100644 padbuildimg/assets/awk/27.png create mode 100644 padbuildimg/assets/awk/28.png create mode 100644 padbuildimg/assets/awk/29.png create mode 100644 padbuildimg/assets/awk/3.png create mode 100644 padbuildimg/assets/awk/30.png create mode 100644 padbuildimg/assets/awk/31.png create mode 100644 padbuildimg/assets/awk/32.png create mode 100644 padbuildimg/assets/awk/33.png create mode 100644 padbuildimg/assets/awk/34.png create mode 100644 padbuildimg/assets/awk/35.png create mode 100644 padbuildimg/assets/awk/36.png create mode 100644 padbuildimg/assets/awk/37.png create mode 100644 padbuildimg/assets/awk/38.png create mode 100644 padbuildimg/assets/awk/39.png create mode 100644 padbuildimg/assets/awk/4.png create mode 100644 padbuildimg/assets/awk/40.png create mode 100644 padbuildimg/assets/awk/41.png create mode 100644 padbuildimg/assets/awk/42.png create mode 100644 padbuildimg/assets/awk/43.png create mode 100644 padbuildimg/assets/awk/44.png create mode 100644 padbuildimg/assets/awk/45.png create mode 100644 padbuildimg/assets/awk/46.png create mode 100644 padbuildimg/assets/awk/47.png create mode 100644 padbuildimg/assets/awk/48.png create mode 100644 padbuildimg/assets/awk/49.png create mode 100644 padbuildimg/assets/awk/5.png create mode 100644 padbuildimg/assets/awk/50.png create mode 100644 padbuildimg/assets/awk/51.png create mode 100644 padbuildimg/assets/awk/52.png create mode 100644 padbuildimg/assets/awk/53.png create mode 100644 padbuildimg/assets/awk/54.png create mode 100644 padbuildimg/assets/awk/55.png create mode 100644 padbuildimg/assets/awk/56.png create mode 100644 padbuildimg/assets/awk/57.png create mode 100644 padbuildimg/assets/awk/58.png create mode 100644 padbuildimg/assets/awk/59.png create mode 100644 padbuildimg/assets/awk/6.png create mode 100644 padbuildimg/assets/awk/60.png create mode 100644 padbuildimg/assets/awk/61.png create mode 100644 padbuildimg/assets/awk/62.png create mode 100644 padbuildimg/assets/awk/63.png create mode 100644 padbuildimg/assets/awk/64.png create mode 100644 padbuildimg/assets/awk/65.png create mode 100644 padbuildimg/assets/awk/66.png create mode 100644 padbuildimg/assets/awk/67.png create mode 100644 padbuildimg/assets/awk/68.png create mode 100644 padbuildimg/assets/awk/69.png create mode 100644 padbuildimg/assets/awk/7.png create mode 100644 padbuildimg/assets/awk/70.png create mode 100644 padbuildimg/assets/awk/71.png create mode 100644 padbuildimg/assets/awk/72.png create mode 100644 padbuildimg/assets/awk/8.png create mode 100644 padbuildimg/assets/awk/9.png create mode 100644 padbuildimg/assets/circle.png create mode 100644 padbuildimg/assets/delay_buffer.png create mode 100644 padbuildimg/assets/lat/aak.png create mode 100644 padbuildimg/assets/lat/ah.png create mode 100644 padbuildimg/assets/lat/all.png create mode 100644 padbuildimg/assets/lat/atk+.png create mode 100644 padbuildimg/assets/lat/atk.png create mode 100644 padbuildimg/assets/lat/awk.png create mode 100644 padbuildimg/assets/lat/bak.png create mode 100644 padbuildimg/assets/lat/bres+.png create mode 100644 padbuildimg/assets/lat/bres.png create mode 100644 padbuildimg/assets/lat/dek.png create mode 100644 padbuildimg/assets/lat/dres+.png create mode 100644 padbuildimg/assets/lat/dres.png create mode 100644 padbuildimg/assets/lat/drk.png create mode 100644 padbuildimg/assets/lat/enk.png create mode 100644 padbuildimg/assets/lat/evk.png create mode 100644 padbuildimg/assets/lat/gok.png create mode 100644 padbuildimg/assets/lat/gres+.png create mode 100644 padbuildimg/assets/lat/gres.png create mode 100644 padbuildimg/assets/lat/hek.png create mode 100644 padbuildimg/assets/lat/hp+.png create mode 100644 padbuildimg/assets/lat/hp.png create mode 100644 padbuildimg/assets/lat/lres+.png create mode 100644 padbuildimg/assets/lat/lres.png create mode 100644 padbuildimg/assets/lat/mak.png create mode 100644 padbuildimg/assets/lat/phk.png create mode 100644 padbuildimg/assets/lat/rcv+.png create mode 100644 padbuildimg/assets/lat/rcv.png create mode 100644 padbuildimg/assets/lat/rek.png create mode 100644 padbuildimg/assets/lat/rres+.png create mode 100644 padbuildimg/assets/lat/rres.png create mode 100644 padbuildimg/assets/lat/sdr.png create mode 100644 padbuildimg/assets/lat/te+.png create mode 100644 padbuildimg/assets/lat/te.png create mode 100644 padbuildimg/assets/star.png create mode 100644 padbuildimg/info.json create mode 100644 padbuildimg/padbuildimg.py diff --git a/padbuildimg/__init__.py b/padbuildimg/__init__.py new file mode 100644 index 00000000..fb5cbbbc --- /dev/null +++ b/padbuildimg/__init__.py @@ -0,0 +1,4 @@ +from .padbuildimg import * + +def setup(bot): + bot.add_cog(PadBuildImage(bot)) diff --git a/padbuildimg/assets/OpenSans-ExtraBold.ttf b/padbuildimg/assets/OpenSans-ExtraBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..21f6f84a0799946fc4ae02c52b27e61c3762c745 GIT binary patch literal 222584 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

JCpRvl#DQP|lT&d5C|l4=^h^rBdmExUY#)Qc2^a zQaW^j+Yy3C>T(TZsE`Pe$TaHWGPdVKd8kSg9IrynZ`e?UlHDnAe^3P$6!ivd5(O^9 z9KiMR$eqo3U{iwoEJ1tnrG9>z$+^9PlXGEqnqv2ajl(9{fuzu2mGGLqHyz{seI+Z`!6sATXwdZX49ci>pspGLbZ3s4GQB# zxT5kh`OFu#_vGQ>CxTH^IGpx*`X_t{Q(!b|BVy?S-+={65HrrZ%uPxs=E=a}IE>&^ zh{8D&@MOl&eZHp@G@_9BTsZJM#A^yxou!kV)~8D=iRAg(^^9qfi6A}A>FdFs=ShnQ zYMVw*NQ+rJj`>B=bnqMazD23^cxzvakjod09UP&k3cp+sXn?H$% z?JZ^pG%Ry0i2?tsG*#P>I1dsYp5R34wgmL$D4PbV-BNa` zxc-oo-zO@V%bNTdl+8?%50UAlz+{Nw{53mem$Q4+dN?|Ry}iX{1M{CbomPj=&)20& znHhpK?JarYJdIeZ3tb&LV@d<$SSA|{)Q4~YYDD1qCbFFG* z6&F0RA<~jwv!CKm5FS0YLE{*7!%z1`d=Jr6i0||rx(S+z*#Ii6I>VR* zebqFrQQ^!|sgjLyjEFV|X^%u_L+NIO8A##!IukLRX^xMD+a#aM;@S$fY9$NV!{|Ah zWgXXKtx!RGDbaYus7~tAhIe7Oc=COjc_AwI<3S%<9P8F`derq*5|+G-&~*lBkm(n! z&*l@tT%VksFe}@FHB)Njp9=%O($RwdlwM>C59nS0)|`I3+K=rE^Am81LL_L%;kdzw zWwjWqF1t89Gw26zCMrsdf*KQ+`GSXxdvrYNkAb95u|Cnh2eh~9zcf_9%!#Q!?=WrH zx%^rq<#0oeOmdp`4HlZJ2ItiHaNiTOpKyk(=CL0ATuUC&(Mq&DM41lE`};TO(BA1x z9&U({YWN)4;jhq{Y$w*}VVmHesSFi)p5{lKprtR)QaJVoujoYSr;&)=7=s2Sj_+me za38OsV|ZB3rO(I5u!vx{i{K`#P}Xuj<^c9yzu61#RjmCzkFRu5t3i5Pa+3#*KQy0C(#N+V$~_y?n4_q|n_w58QPa!B7KXoAWUDUqZ}xz>B4X%f?b-=51r0Le=W8 zCUsh7E2)Z;s*B;@&jO=K9f@-0v*p+WjXC-{WLP}gLVmn$tAfIZp&5j&r=H=9^_4a%N*$k?cX38#s4U z)biwhNpFKu9i_E!?lB82++*#N@sq=MBru`|MA8HcvC-q6G2dJz`)$GisBoTJ?Q~UQ zjZ08Kzg^1hP&3O%JSM7eoaCaH!tq>EkS{Vndq|vUFs@JLc3}Z5U^Ztt{xb5Js3Sre z$+TpRnS63q%+!b(Nr^dZ!7bbwXR?tg{&TSW1Qt3`m$cW#!N(tS$Bv4Ge*|Tjg0yI+ zFJbwd3W$f1zZ;;^Am5o68q*myOc`j_lNYhdkI6+9XD539Tk!MOO+;fT&!f7!Ml}H z%YNVp=ej&bU_XJP7hb$AkCT1ob-Kr6btUnO>Vrd3Y10_&uI(t9;U19-^=DjATsqx7 tI?a3EDBnlAQS5ewzJCk<5@9UDpvXA&Vsm@fZg0niwD%V$A2xAS{|6+R&KCdx literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/41.png b/padbuildimg/assets/awk/41.png new file mode 100644 index 0000000000000000000000000000000000000000..7f0f4f61f4d7e3763aa717fe50027f5cd812790e GIT binary patch literal 1799 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 SmZ#Jo0czK*^Za3z6#hSUq%(*B literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/43.png b/padbuildimg/assets/awk/43.png new file mode 100644 index 0000000000000000000000000000000000000000..aba33f37b1a29029b4bf45cae57743287b87b158 GIT binary patch literal 1742 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/46.png b/padbuildimg/assets/awk/46.png new file mode 100644 index 0000000000000000000000000000000000000000..29b0a7f9f476a894214ebbd625233e0d1d24cd6a GIT binary patch literal 1765 zcmWkvdr(tn7C+y8BscH75J-SPZU8OX2tghV*y4qPg%+8BR9QuP(T=(b8l)C%QNJX- ztky{_0&6?%MW+@V$HqsMRYz|MuB_T!rb|b0a5kaX)#~o1ELzyD%f>&>`OTa&=QY1` z=InEny{1vdDgi)KQe0ROMsrv=Mi$mTxsT5@~JUwq&DR*4?9mE zZ2oWg$Il}+6jv+t!v8wWNS(a^DCR~t{)H8 z5TyPN{JfF-ag!K7gNF9;mm`J{!$tQ6Ok5dpHm_M|3I)jT>!KX2z_M%r7I%~Ng_zIh zct|I9Yo~s;c-?JSp*Cn=7Jq+1W6~RAv-Q%zM}Vj^zs7m}=*$esZgYsY2ti6W!TPEP z3t+bivNB-uD;=SZ)M8U8n8pM0JR`5my;>w*Bl={n>FqF~a=hwqL*G+ynmlxubnMi5 ztx-6rGD}u3$tS!4uRS8Y$9fRKnEtNANx5?@SXQfzuE*``l1!lE~!{gy|k zGe(=LBxm3o`pt`pQdoCRp4R`d{h?i4*np4Sb_8P0c~ z*qpZwR-Y7D$;tI1Q>m~b0gtCi0w+i|WGsqPXWKGMHw&BLL^|sf2An}+Q10{<+{&=w z`I+1%(s6Z=EbhxcN)(V3t4ch>Rr+b~Ba_tTw*C*)iL%MjG+69F4<3QcBO`rR+-;Ps z^HuQ{pRhj?#EZt#!1caQK9{1|hVH$s#^^k10XF{*P8VLP{#Y^B`Ja>C9d{VgQJjHMW z_l4UGKf$~mAcdMFud>N8$c9Tu@6uEqOGHM!MjFY)-DPGY5FK=m z%9ZJAt`S)Y-e@4ih=&qg+{dr|7F3qL1jn#~SC$lCqtvZ*r1!=$IX($?(BYLexxs@PPBax2$4zAO53naZfd{ksdm7lD>>1C@~u?&#~SeGLp=ec1v% z@k_DC6dx{?#Lm4Rf=gfN_1M44iE8h-G?836eu3g25}hlhJXu$z!nQvu@CPrdbjBsa z))L_<&2VBt>t0UI$D=>m@)+{tZzqxg2E=*0mU*5h~{Lj~Z)H5JjV5%P3>PrXUgbhzq#V6?5lQ7EUz{!4GPDi|C zVU_paJ|l$^BpY~UX&__O&} z@<3gVfrwh%7@KNLaaCjwf2xYh!)vleW#asdzT7B$cZJ{#Ch_31cFS|8_aI+Un!ri? zR#4VB^yKfBga=<8@&_y<^NKW5+Y@(-JRo~zxs^V0sW*Up8_6}}igUj(JXmgeat}0y jqV`uxjp9>`cpUyt)Zn_;E%&;@F9}MD$_h^v2=4y_Pk~#} literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/47.png b/padbuildimg/assets/awk/47.png new file mode 100644 index 0000000000000000000000000000000000000000..11a5cdef05cffb273c8632464e2e169b4dcea134 GIT binary patch literal 1660 zcmW+%dr(y86+hp-d++YuyRX1@cZC%$EmabE?DEK{*v(ynVRe+bA((7y7_ML&u%a6j zi;D5v1(t`Y-6W1#GfBNz?W`t6B#w{Prgsr=3yy=Rqf#=nDENqNT&Z=iV(ijC&iT!J zXTEcO-}%m?LntlLs*}_Jpk3iAE{kJoTsRdS*Wda6;EV&~as7M?0Gl``2tNDV5GgaW^8s(jb6&^T(9~Zvar!D~g@v zjbl~gv2_G#yaA7X#oc*Nnm>d3JNR=6{Yb@`LQyMMN__2T5*uSt@><=G1cPWW7^0B- zDfxae=A9f5c4C)q`cYcI=f{l3tXn1>IIXoBElIB$F?o z+pSRLzV|ZhvO+-~Odi)0nnWG8#wK!jK+Vs|3(9~FNdrXCU)RXNtN$eP7ijPX!sxJN za7TB+DMLtCGt;WnTlg}9CyFc2z^Lkdh*UGtnq5$D%;Uj(dlm^^A1T~#)o1MA1WCa_ zK(>X$$XF>pV$IpMRY99K96e9^71B>kaIHcV;LAK1YM!)O9oQZTGZg=|x0}U(Hm%p+ zaB!&{miM#L)5bmu2@3nhni{-TP3rhI>~T7g@GbJ|)wCuNf5(4=S~=HW!DiHIB<8nh zl98%$7r|yxHTj@0us`nQ%Fk1Quh4lPlNcM!inMXce6Y6<9*E#>Xni}}st<%<>^3x5 zcsRi=ItQJ3qEPAX!J&StUiNE~rL<}Id!OEw_8(}cZ#ICh@B44I&%CI1|hkh&J5zc+hK4-rjicfaur`qZOnEZ_jzGljiJi-kH_d}dlf&x z?a66cEK3Hsqzd$tTV1>pg4;3-aAy??Z7cQ_(c1WTl)Nlo^IoLlHTk&rrAd~g$it%# z$dL=&o-|qwN32F^;CdSy%O%pg!Pw-1gmbKk8>$U#SIZf#_ndB}f%gnkk`r~lL!y>J zr8!$P7HNo@d?Oy8h2R%DE8MTfW`WTmaSMsxo!r23IA~atk{&TxasMJ>x7?%HeT_MB z6$Kyi`%7Il@GNP4cQe#)&9%+tFXAWJZ0Ybl>{o~*zWjx_pyFf{@bWh7TX;V_UDl zO3cvF%=qIqI!671*WsnahwGXQH2vZ=$?0+Q;F-GtE~aEC!GhEm!%OB3^m8wZ0D$1_H#$RT;8e#MBo8Oj6XOq<_wtNfD`d%@%Q^DcD plym-I;Y{c9SE7Z&P8m2E#@Bq1_2k$)dGSpFE0&iQcNdAi{{cavJ$V2C literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/48.png b/padbuildimg/assets/awk/48.png new file mode 100644 index 0000000000000000000000000000000000000000..f68e34c117fb599e4f86530e156c18f50d5268c6 GIT binary patch literal 5453 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 DNz`PE literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/49.png b/padbuildimg/assets/awk/49.png new file mode 100644 index 0000000000000000000000000000000000000000..374bf10f45bc32bef4baf7a64122d16fba5b363c GIT binary patch literal 1623 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 bw+RE1_E($@f}C;K`rWt1Ibmxod}96uSZHZq literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/50.png b/padbuildimg/assets/awk/50.png new file mode 100644 index 0000000000000000000000000000000000000000..fd105333b815c7d5cb5694b989820c0caea983d6 GIT binary patch literal 2452 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 OVIgOJe)iW{y#0S{hW|7G literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/54.png b/padbuildimg/assets/awk/54.png new file mode 100644 index 0000000000000000000000000000000000000000..279b928297ccc394ba402554a0a7975979a01401 GIT binary patch literal 1833 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/59.png b/padbuildimg/assets/awk/59.png new file mode 100644 index 0000000000000000000000000000000000000000..89a7e1d095bab9f7433a3cf509037b926d618ec6 GIT binary patch literal 1335 zcmWkuZA?>V6h8Ofwzs8)UTL8fioJZMvI-XjEsU_+O$LZ!LEO}oaX|)-Xsn_HSRpUe zQJfQqAJ94GV9HRZ1|hi3IM)sk5@#$%7e$}T_7d6M_M=Xp<_=j1(L z%1rh43h)8|@AR}a**t3bpv59SPuHF_@*uIKZ7BtiDi#BSaLEqgscWjFw`0lZq^@d3a-hNs~G5*+}lU5E1q-sfA-r>`;Ql#dqNoz2Gy}ynRHOl!>vNyk$tf$R%Y`~$lScUg zx0BUIs#)vj)BD=G33iiKppMW$f~PMJ0Y3j9F-UTzm0~MgUI(4u8=I{L*sC5=iVG}^ zg|j#)C&zK%l`U#YP(_(JLsve*2%=GtAcQ^PYk}ih8p@nDQm$4i1r7|a7Zx!igR*ix zS+bSM$4y7@{R`kLqch|h16(rDG;+CI*h6)fovW-VQF!^P*{#MG=n+vTW8oqz<51v! zDXLW$KC#QGmOj1R9%+hRCp#tTC(qc2wac8Lb=Y*Z2tk3-(GY&ZT_x{T{_!Dma5%`P zG)$*#ItK$$8sIhS776iYn;w>mNB~!rq=k%oR@6bv3l3o4>QEt95ZwXvAw@YnMLY7CHG8S@a-58=2s0x z50ON|T{k|!j-EB1S(+zqPsN6U+s=_uW9~^_k3R__Ok3NNNJS=5hCCVGpev3C8BJ z@F|{FvNEd*6u$jjQ zN2BIv&-fB`tOA7x4Pk9_shPx(P%1`xQG)*|dElZBh1V4kah#6Lyi3R9bSmcE>Si!= zVMxnNS1UY=5lh8wf4!8?4w3v_HK~jx1G+lPCO!DE8>&?>@y02tBk2l~t5}9~H}m~> zK}R4r#*0e!p`8c%R#r3k+S0hHRVtYCe)=-8WE(W~*v#Hw+0~jq$*0D2{r&kFb9X|$y zrR_jaJD8yFwY{Y>S*57pA#wK~p|>O~{!8ciR??jaIf2!4XiR`0#4+j`7b#5R%O3a0 zW$N)Z`WyJ9)3stA#k*n_tzKK(*E|}8k|R=*@{QPVJ3+C6jc@NyxnhV(iCQONVac)4 zbcM?K^{?(Z!1@IjKJ}aJEW6eJ4mFj;ps#*!I|>x5CnM1K2F(0LLB=D`HIR&No~aCb zX$aGy+zQS`F7C46>ECf?#)Kc+OxvyH=qMxPHZ5uYk(xaThLpB$v8WUjgJE?4_M DqQ=Vb literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/6.png b/padbuildimg/assets/awk/6.png new file mode 100644 index 0000000000000000000000000000000000000000..73255c38aa93440f86fdb76d0533244adfd9194d GIT binary patch literal 3199 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 Nm^~vq^He(L_#Z}lBfJ0r literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/61.png b/padbuildimg/assets/awk/61.png new file mode 100644 index 0000000000000000000000000000000000000000..aa69abc1db3fac528ed1232aaaee214b562a3315 GIT binary patch literal 1657 zcmWlaeNaignC(Eqj9~QJiVCcGo(L4Jy>S?lyFlMMZb7f4t|JIUnzw z=bYbp&pz+EXO%Le3;>j?%gZWxweUenM0|Uz<4mmT$32^U!y#5TZ)5fP^0E+tn^xNLAJ-rG*_{-`tcU8-k zEfXmpL8f1ZnR;saW!5~0M)%Pt(?*em$_OV-)H>prtBq+%CCTq!(|I+FMx#l>@&ja( zKqDpu8u_I$rYDP7Y^!mmaQ#(Bz{~hAdtu9cvdKxqQ$**(tWXRM&5@dXC$1^q-4jIT zQb`ESQJifO;>hxdPm=h_;&kCUfy%^0-NF7Zv2T5S;vJ60WEqe7q;H`inzG< zT6y@P9?#-`k+*nfktvoDWj;!BNb$FlXdvi2)g|{oDwSokd#E2ex*)wsCKH9Fs%KOl zlJ_`4hmSsKqKoQC8*s;Bcr&BHuRNVdVP9z}8m(l%Ej0DP z@y)QJ-T;K;?Dp_ch4=b~Bys*YVc1TE;7(3_yLU(Mk3VsOZnxH?;AR(@qHyT|*xSif z=RiU&HfTR`$z&?qvg!bO)wKtON^-97uT#t;wQP@eBQqi{&+Laa zn0{^veXJ2BJfSbN^t?DM&7^Q&c=Df8EtR(k4J7ftS#rSwJrpS6Of%RoWICxfwT51% zC~JL4R$h?icZc3;Z0R5u9ez`8;nqiTI5hBas*4VXe+U@B_$0ZfPTmgs(=%eo_^uh(p(AL&*+O18DRb^ z-26_?MOBQG2R>pHuNcNYBL0LZlprNk^jovSgog!xKgNzMrchr|Hr^0I-uKAxdC{5Y zX->ozGrBAW_W!6(wx(52ldty5IqIOzM(VX@_ToWQ9_uC%Dr<#*Gm&I~TMYNvlsHVb*$7+>p6y zL}$ewBJIC`%03)iRmf8qoziFHTMZ<8v@q%K+zr3UHyRzxhbtp2G7QQMhQJ|xXq$Vw zmR@e*Z%*@07nw|#ubEL+Z?a3JS=b4N+hXspCH7r#od+$ph`IUN;@&lz`{PNIs=wEXz;$e5}DPP}i%@2I`SJU?eJ5o}i} zWyztY(o>{A=;JhC7xa?!VSD@uF*aK0Q*x_5se!wBwgQ-P&;`GP@9P;g|Ik!#Usrl9 zop(EKSHkaSN%C<;yw_|jG~@<7nmWfJ-E$BBofh7zI#MNIXVVQea}o_rjZX#le^VQA zqY2TcMNm|=L{I}4-T*YJxY0g8(>b}&W|*j*m_u*;0Sm-;3xpYyzb$^jwk+PlrLL2u vOT2y;4LReT#cwy`Ig4_}9DbSs4Fl>|8uBi7%NFzB1gw5$UD=;KOvnEL(UC+F literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/62.png b/padbuildimg/assets/awk/62.png new file mode 100644 index 0000000000000000000000000000000000000000..4b34c92840da2293b26eccaee6fb7e7d5279d98b GIT binary patch literal 2509 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 VY0>h6e7irjW=2-HB12;Me*kYj0i^%{ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/65.png b/padbuildimg/assets/awk/65.png new file mode 100644 index 0000000000000000000000000000000000000000..36a665524947d048caa1955dc00c4094451c76c4 GIT binary patch literal 2239 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/67.png b/padbuildimg/assets/awk/67.png new file mode 100644 index 0000000000000000000000000000000000000000..6389f4961656c1db85dd7e528c84da797d3ed398 GIT binary patch literal 2027 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/69.png b/padbuildimg/assets/awk/69.png new file mode 100644 index 0000000000000000000000000000000000000000..bed098807a7a2ed856883aada6482b764e07127a GIT binary patch literal 2381 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 Jf5*%->fhU{f3yGq literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/70.png b/padbuildimg/assets/awk/70.png new file mode 100644 index 0000000000000000000000000000000000000000..5bdc241470524eaa3cbe613e995a2472d3e11c0d GIT binary patch literal 2390 zcmV-c390spP)Vl&|0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU)_(?=TRCwC7S9?%Y*BSrayLX=~APWl! zf(RnMQ0pVLQEME>N39bzHEkx*G0{B6X^T3kjV7g?s%bJrTa2}7>ZonBYSO4<##F0O zNVHMYs2~&s0s(o-vOJb$AMAbfJLm3#MEjpPJKx@W&pG$|eedsF;hek&0{t|IU88&5p1 z5MDkl?2t4~8%;MHc-zYT`218UzO6B1(Ku{By`EWmJ3W3P{3}#gPB#tRp4K25ulJ(!Yd?Q>jI#3mF~iOL+3!)% zK>V~GI|@JBNS4)FB+__-yR8SZ)q~sx34Ej1)sM21E`$OqGLnI$1OeB|gXn1v5>pFC zeJ>U^hmr49WeqjOU1-0p3(5AgQD5hWn2H67aBPx?->yU8v~QgFu*qguF?dGx8&=7N z99uLtq$ zlcEWGLjvk3*W*%5NX!fuCl=UR*yn)ErO*_WQ%_rv=lCx}3fA{|fxbb|JH{Bi7c&yY zrWnAa&+S7DE+|QO$#VzO0ENwttZ5vjvT8GUq{|e2GJ^0{84OX~}*b$qH zcDIQ3cHk9F!m56OIT$es66eihis5jDNplst*|o6QZFU1DFN{O|%{~-`M4YoYFfu0t zCD+Ok3AS|9m49<6;XC~$Y<{LC4+Q(X^(xffAZnaZdY&NQ>` zR)m9^3XB@joST99Le|L=Wx(RR5lD_s!jcs~fX7WGG!VpWX(6Txv(Zl`eQd!bELbuX zAqs|xn2CADB?ts%%94IedvrSDjPY2yF%9t}%maKGF&rUcb{6sxa7`YhVaX;;%bbmO z|MVB+t;-`x1$4^2xS$ndO2#<+aPdOSpF0=R$4tT}xQLdJi^uTVA3uweAD+al3G)$8 zk$x{vEex)ZrFp6e%;zM3#O#q_u}i3}azWNqJh$;_lwL1GT}dOR=8nc2ySHJ_+eZ-U z58?2Y19)}AZ_rlKfk$)K;`vwB@b{~$+A!B{7{W7RcBVPQl|ouSmqe4P?kMVzjkn6n zuz6Df{!q9Ddk+1cGz*cRjM)43VdQOCtv3pFKlUGbXJCzuty_1a^i~PxYKiyppE{vkZM=7b|tBV(9iq`Yd%U|VI>e9MDLE3GhF4am-R4gjA& z`)^!4e{FzS@r5sN_Tx+Z-fERFc5E`Q=ea8_XesR-!cz_Bx)3g}JlgNg^9cFfA$WRw zF(y5Os@Y(3C@#K)qeuQR@O;_vp+aSY$!t2eK3w2Rc(S$`E2 z4E9{+LyCrk)F?F6H=s9k8C&xA!eNQW)z7cP>+_&s$1$qyA_dkhm`zp`{O)~p1>87S zRD_VL8;;ZwL-<7o!%+y2;qrd2v5F(Byxm2Hb6gZ$mu&C^dtsq^dHKu{)YWvs&o(e9t?!2&&6WCYBjNCDZ%MD@jG7BYJ z50)fnps6GkLUcbuwaR_k4@)*9Dcj7`O<4X~nzn3fI=pv7IQ#25$o|3bRBRGjn)-34 z)XV*4rQ`C9Wlm&Gr}KNN-laO}z*%>r7hj(0paw)fHV8<|wIemn2!mf4YI0G);%%9T zO*f%ve=7vZC>|}SnDUfkxCwo385O5H(RjtfBeSC|1er==e4-IXlK?Lr{O{BT;U+;@ zbK&ii`apc5gtRORqUrp{PCf4S5bYP?a7tlNV8o1QOn5wj4yY34zk-*?-nhgLWJz(U zrnb$ebCARidJpCM?EinavEIIOX%$+k{Mo`^y)*Hw{puoGIs5%JPRx73siaS~8Xh)m zOJzVU+UF9B54f0BpCa7%?_FmfjDHUwNWU#QPLvw~{GR{=01vdk_cYKtZ2$lO07*qo IM6N<$g72Yz82|tP literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/71.png b/padbuildimg/assets/awk/71.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb2062486151d661fe7fd7c16983f505abd30a9 GIT binary patch literal 2441 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 Hu0mjfNH&M~ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/72.png b/padbuildimg/assets/awk/72.png new file mode 100644 index 0000000000000000000000000000000000000000..5ce6b081cb8a4c5f4b9892a82f652b076886baca GIT binary patch literal 2507 zcmV;+2{iVJP)Vl&|0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU*ZAnByRCwC7R|{}d)fxWo-Me?+uVhKc zYth|6AOX=@6)QMKiqJ|cDmt`w#MZq*(k^mYY zKn5c(A&>_l33==$+1>0byLa!a|2-QBsneNG2XE$NH|Lz3@BH8Y{g2BvDt`t?KRWnY z`g{$*d>TA1{-e`4sYCxB2d#xZAxzBPPrv`BLqj$tp}%7onx@eTcTq4ojzekC&v}aM zwRGkI!h4Jsek^ow>)4ly`KKu$u-E&9;YvLkF9tC zvGE~gY4U^$8qPk;e3`-xslwH8;JWz560L&vkr{+7Bt}rD6m##(Af7r`}=0E zhz*r47NSw$tY~;WnnU}pIA-1GV>m=53|zr5U(;oxM2FeTLC_r-dw4Ob7I_d@ zS%$gOu0&I7KTd5OhG!0kl8_C-#l;M4xHF3P8=A1?U<I$hAp1U$Iw zcFZiAH$`AAEiHI({r$MyF5rz2UDhh#gIEpiH&@I_DBl)1Sa#jVYvgB;2UF7K>7GApu>()I0-o#^QK&3eyPYz^QGF($)#}7*!_)8=M z(jtCVX2N)u`IH5wxRY-qdDB*60tLp$H9z@MXRU7 zAFwjKLZ~qhsW9_y_*|t>MqRj~=6Y0ARZYq5Ub7$GtRII*4k9$y539w7TqKHFsAJ$A zIR$riC9udT;@xTwf%CY#dknwqmx!Ih8Fr5ex>~T>z-8b%M_+gc%Vx=b5vw0qfrK`- z9%;OaVZ`Bv3*Wl+7B>B;$wNL);kIS>p>9DX*01{wcBk^#M4{z0ao92@AxWWCccT{- zbL_C#jAW(JaS8+HH56z!abf{TwxB#%K812BKIoEqaRmGE)LU!M&;S0NwfGDNp?d}V zXw4dY)YODDh3pS{Q@E!)i8uj&=MN!-zT{*%gj7bB>ny=&xPbO^6lcaI^mK(uP)1JI zCUU)oE9$H9*M}a%vSrJ0*Ihq{tSI=$ThCy}P6yuHwwuJsI}0;R7LL3u;ZIL*!rc#j8-8B_+-kGo)ps_+X0_rQH+>%; z@7PXRjK|XOC>nNe!jaxE&Xpo}kqs4afz@^9|pF?Kh zQ~)wz`_A{Ud)Hw|`8*VbLV{Q@;{9}gA3ogiHdQT+bs>#=-p0x9u1Q@OjRRe3ES5ne z9Dyq5Scx?N&kcii!*T;xko7#fiKa>H0yejinUgqrqy@`wxfbzM5~5g0HlfM@-hSg{ zSUfhW2RzcFNz4gWW83ciIMmR{9~^qNTzqPr&y_Z%LPqXJTNi9`EO!jy9(m&;&eC_(H< zXxZ0B{h7@OejGk>oXxfF?Lds02EinvdEXJHSIG#zB}P9%pgFxZC8Srg2H z1JQgGvEef;Dec;R9CzIAVtvAe4`+y<@C;m5&vlx~=hASOk+6D1433Oo#*9+b&h?_K zl0);}1L$ZQVxg4iZl+poWhp6-924iU6O9PXD#4BQbyVq!apJQadV5Bp#meCHi%3QC zlVPqDj5TltDJP>OXn{XuL3%h3$u@?EUYSeTB8$OxL+ztjvetv^hl{E8Rgr8LsHTrH z%PRxZ;GU%*aGeQ`a%!1B?S%U}0W0qh$43>IMGj7Pp&PEe%7wab`r$00 zu86YU!?O?VHC$Kfs}RGFY^gzMwe2FkpZ)juwnR9OKQ3=4mu6>jucQ~?9%su^TG@X6 z>S?IIZ<-pIZ!=wN*uj%I?c*0l_@Ll6I7?gdCVHjC5#u{T8``T<6%wlFRq;x7XKQ188 z8WrcK?}#w}VT-pi1P0LQ6nzjVJUm<@9HK#{`h&nQ7;IZZOG}-HP^U$Z=xByIi6;M3 z0fDDssRRn0KqdjU710=SDBTdq^YnWNM2fBLU&18Xk3{iO24bKoAh3oeh)CRy>l>Oz zcf|k4jlV_HoFXWAkRzT(4y9sw=iw*+lg!KAKO5Qx^1RWvqY`+Bf(}NIv7tmfiEf23 z1oFOU;0QQ<6K$xjmX5iOrWP1x1_6U%P?#nh0o4P;5ZVYGO~_A;zvIGnAv$1$nJE|s zfrG(j5I96v+Z3V;fg->N1k?ollWRqy(a|I<{-<36&+ZQ{{2#gcrc^wdPNq7M$-zG> zz&?OXC({DR6o4rT3Q%$-kZ|O1n)3GXe2*4^rxL>OICCnQ2>6y?eZt?^H_`cDaewFH z{-2zIc+P;fi{rnF<>wZ!0=K7sl|FCrSM%UWypEyrT7w!Oyu!yPSZjqaabgTVPLBxo z>OayJO`htSa5qTwH>jN4-5xft)+#}XXI@|en}5sIIK+PNpiqRKGOTTH(r;`(;*=)l z5Oe1N`!Ybq86`v%B3c$*5D{UXk}22onNUzz6gj!b$c|tw5F5DHh_8;zrTENtOh$Y@ zH)c3C#Id}9lBi=QgZ5oM*JI6m?6@`e`wI}tjjZ8WO-6kb;JW!*imkKe$(&U0Ph9r& z5S^9RmoE_u-dwAo3B_5eB^|o55p3h#7fr1Te<}ZZNj@1Ghxrv%F!-3b)F%@)yX$Ld z|M@w`^NpKz7kz*^&I8t^qLtx^sp@Cf_Su0`B=gYcc10r^Ct8QnkY7Df8GSoggH_H- zK?h!1PL-FPMg=G;KWtQV5q+U)oSlowT2_{y6xIu9RDba?MNPcts$;)sF;f_o-}G4| zk-R^5ZezkBu zwUA=g@$4L2tnz;M>~^;2k-JUgj4N%L&9v+@t(Q>Mx%-j@lJ2gxRA%pLF)E(PyS_PU*_k>NR8#NA>0#n2RM939O};Xz6{we z`;I^;M<$NoW*yjZ+~xMQ3I4_251~w!3q@bGmrB|Ah(~goEU)?W5Fx$=*2QHFp-zRD z8~4O0PQ$*ef&C(dZgY;p8`!L=RUuDdo6d)GaG$D>L?<|U@!7-;ww}lFW|dc>Su#pR zSFhV1kC3zN$wNPQ;?a1kQ9S3+qlD3g2+Rn*ZDpx`ebAlM%T<)J`uNAIoY}R{C-*NW z^}V^7d&fvY6zTv`DYqzJB(mI*nY&ph5o)6tlzFFMPxk)o?5CC|t5G#)S#E(HT7PCA zc(>-w#9C~uF}Y{oRHsuMjII^UD7=>{za%8E(evp2hLDPQoWnjT+3~a(-{mcSu(-e@ z5tEF0W`>o2%I*j#`?dMWBaxxt3d9{ft|WRnMq*UY%2vi31nbd)oTZB2l*Y%zm*B zQak+cdw4g^Kei)_fN|bolLNJ59lMmyn#X)h*;8wte0^x^xZ%*@T@7%#=(Ls^elOC( znce{Dl>B1E2}KF3FV^n`1?{d=%6&VUCH$7 zN^xPhj=f|_3uPva{i&SlF3o@9h;&bLcIxP-w-cd-e$R{&iV_<0#npZ0rWRc7AkYlO zPcYptp)u?Hh_7|bfrH$F4e_00U=omtnh$at zpB%m9UB8ME;84X+M@!$`Lgt*i5UoG?)MTiosy2))mzpBTW>uH3Rri(I$SrY;McE61FIx6 zeZR~|R&J?v4TlEB`=DBw{UKf$A5TO5`vq1=VM=ARSCq>PXJ*>>WYX{?rq`L3d}i1{ zw8A?q{yeeEl@Rt;-K)H?>n8MA0~fh8!yk?7(?ymoCKm5qpImFW*DNmpnUK2ozRe16 zKBP)&EdAQh*BAzB=q5LPMcX7=h)0Q)8ur*0PX@#qa*XPCnw;oj8*LzQgDcCP^U?QG zrAdU`q9@EV9jboONstrfj7?*IGlP*xsP*$*mkde!gQ&RsetC Su+;Y7ft48&@eqFI;=cj*)16)b literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/9.png b/padbuildimg/assets/awk/9.png new file mode 100644 index 0000000000000000000000000000000000000000..974914b5b314c0c42386e087aa3dd4408338a5e3 GIT binary patch literal 3177 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/circle.png b/padbuildimg/assets/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..684bf0feb2f4398987ebf98b4f8c4fadf5f64773 GIT binary patch literal 1607 zcmV-N2Dtf&P)P000~a1^@s6lq3|;000IKNklH&Ql(M_lA|CX2=%^dmEM4&UiX5k z7Zq~1d8d^UlmO z^UiaQ;wd5`+km}5Kky^qZ6FV%fjY1NOafEDx4RP|}`9uJ-rkz*qAdl4zh^GPxG zKPDo{$78A|RDT-yB~XZTWV?Y&#aZLQW`(*}BhnG3VUkX^vb{Y^XKN=di4>+`2EZ&Z z1bq4^D)mT$H1G@HH(KlVs#{eHrQ6(DxkJsX;|D%L6d(u+g>Bj->;&m#n%&($AfLRyFWh}_mzr0n<~cMxmq3R^IzmJq4pS*i!^Ac$T9PSRlWml$B`S>y=iWK1c4l&g zIPO;#Pyjvv%)j0mh8i7mAtI-G=1YwAlh=<|7e`^e7Vp|q1b_=b^Ru(t!(cRI- z&ySurlW=VU9Wq(`oRj;1 zhSqw(0NwzYUYcgPx=hWl;e{SyJO?fMnNOr6LLK6T9yPzla&?*MrRl924q1j_^Z=AM zR;jr)bliP;;uM}PS~Ma;1VmAd^2X{`D|;-!1kiqjb~R!OSn4q@cs>yk2n8J}v>$Dm zO~d#7b%0DVgB4mx07URV0%`h3AOTpRMJAa6!1w)iqgt)r0mygtks^us8Y1~WlL+E# zQj#RUeK!ErYW0p$EEcB$j_i4x?RF#|hl#Icmat{l_Tu_Tzb;KQ%QqZA?r|l;-E>=af>M zKYor}YYtu2h@+8)AfbLZqb+1x?-58C`#Bn2)#TDUIDh;cN-5^&=jSFSCQ8QW=;+ee z*x2>;^>ydaD+l@H%ms3(9k3zj^$4j7Qg0#?U%+o_abtV6>BokUOYPv3vllq@%0brF z)@oy8W7kJVN0%(ean^>0hHhjsnZl`4r{4PL{WDg=O7MqIFZ1pFIiwLn$^ct|1)>y) zKnMhaxEkdd^zYom$7g=Uhd=%R9qI7u)vGf@Lqj(l$5{i|1)S{b>$^NMGBPhB+Veg6 zY;s&qT|6V1fiAI+ro|pei+wyT_D@=ydLS+K(X?a+y5!WwGxFKwxOl!NBBDn|M&|qa z`Yr<}fn5sN2J8V27Yc=UFJ8R(=7|$0c6D@gBv#f}x%KZ`{B!ymUwrc=cguHacnw;T zExg$CB5%I>21oWEX79^;>CSYqv9aNgj*c!}x^(G_+1c5@1J{5#1=uvdDX$rZ@lJPl z_kq)=Pro!cIQYVW0|z?V+uQ9R3JAjxt(&ByXfm&5S=gq{=H_N_^XAQs;o;$XSFT){ zUs+kXskNQ}z6MrRd?p3tfP=tWK)$!Pw`Xv0uxH=CecK9!LOP$%r?c5?!nSQA2m)QH zRQ!d7h5GF5Y<*^CraC-4TwYvUEYti|x(?g`N`Sk?P)3}n7uW~92D}7h+uGXFxm+&Q z)6-+OwY8bHZL1&%#BrRcTrLNtQpt53rw&wr`MCNSU=eTu{U{A8pe06>kLR>IZe<&g zj9aVXEeQY*aDY{y2+YMuNJ>rpek7&A-Tv7{UFx^5xj( zJ+it_By<%AO47V}ow06@ks36~qycOKRbVaF9#7!#{{Y%Pb0f`fant|+002ovPDHLk FV1nAI14sY> literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/delay_buffer.png b/padbuildimg/assets/delay_buffer.png new file mode 100644 index 0000000000000000000000000000000000000000..7b27dd11fad940da28577cae0a412db8ff6bf2a6 GIT binary patch literal 32137 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 QqW}N^07*qoM6N<$g2g9t*Z=?k literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/aak.png b/padbuildimg/assets/lat/aak.png new file mode 100644 index 0000000000000000000000000000000000000000..2e58f09a9d6eee9173b690a45b3e5071ed10df63 GIT binary patch literal 6778 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/ah.png b/padbuildimg/assets/lat/ah.png new file mode 100644 index 0000000000000000000000000000000000000000..9f4d0a4ee89b88e0824a0d7ac909f62ae7bee280 GIT binary patch literal 12290 zcmV+dF#XSoP)P000>X1^@s6#OZ}&001fZdQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3>vb{)Brr2k_TwS>S7EC=H`y@OtUe+C38ilVY! zR`t_MiM#`Dzz1_PhsQPR|NftC{_DT~%Ra>>6Jjo@ru6W?P(yW%AIfw8cmK6dIDg+? z>ieAi{c-d8gySL4zyAK3*8BR#>jh$8W~_{OOzRp7uWOe>a7B())t+%|`FzHvVk8W1-)?_Un^>d%`tWpRfP> zV!OR(*?ZmHwUDq>GVi87wsL;rg&k8~Yb5?;{)_ya&!5Vl!WIV}n|QPHXAno$VeM^$ zownI+*ZFtY!6gQ_9Oib;4J@w53g)kF9H#{RvD;!|wB1?D^b!K5y8< zCr`^Oon*?{2)JweGs_p~s$j?xok>2H`Wph$D?W%BZ7FH~l32XPVi}GV5&1E<$OA6<1n$ zl~q^U#M<^d?6}j;yX?B#r`KMu{_ZvV&2#_kHT!x^9t-Dn`IpytIOmTgtl%WdXL!s7 zi^sD(KtMbB45$ym$#e4AQyobW9bU6h&Y4yo!-HX27q|WN-7nAmqu)#n|H^OfZ=N%9 z-Tx2I8M*HD+@F5?!E3XfisFxhl!dbC<2S&zQ);fXR55H@eB!n$Ey4kQx&3MgO$Ls8>J2Wn-+wQ|$n~Tp}2)o=|mEYuheP-UnHLSZ_ znsJ0Oo;dZ)-qoa3_p$-t3bFh7V4dx|-pCtvEN6*52qXQ@E45hIZSe12d9T24TU7*4 zt&lP~YU9dT4?fkeZuOJj`ZRv$-K_bs_E{NIy=#LE-uP+Vdl7$m+0U2I9$#%;rNTn$ z+%oo$FY_$n@=RV2Vj@h5<3L25z60j$W9_f6F~44(%jMZmU{vfS{#+jt-FTY z@^n48k(P_KkFh-oRYvl~E<=i~FiR&xU<593SrC`je5TZ`FxIpipyAbZ&NXA^`gv`# ziI-3!Bd)`z!H~)xW*f^xqQO!D8JI~fxTL$i_-&QE(yo<#t=QIN&9pHdKWDo~#-<@n zV@dK1gcjE;o@pfgdVJ{1mlS*1{mIZ67iV9>O*`ROy|1brvjcEq&*^tGd#&1f2^JWX z-9t@$zRZ_S08HOm+Mh>eYNbq`PF!nT?0!IpV(_C>(}|IKVq|WYgr!t!?9w~9#Rsp( z^-Q-Wn;U_m;6WS5-eT7N8ZS)%XCl{PD}A)8sH;7%7;=qHQH&`Dz`Yu{%@bTkn=zj2 zgysRdU0iaeqE*dZHbfRUDd@-Sn=&dEWkZ zCG{=^Ht$MG#MrR+hHQShDxZH{mHD;VKzfX4RAPj5a*6lY=6~Lp|F*H+%~=BYCu34@ zUK{D(xB0o4|GSmU=k@$Atz`JTPb)q9SSedAlnD2*h;+ND^mW-k7K-Id5SSYUGJGTL z9!mzIhhUTuqx9$%Km|bI1NAGR|i zb@C_o?wci*CakkXYu1!v&G4!xz`APc^`smtGkjatCLZ3{Nfv0)u1vfM(C)jl802t-1U8iZnQw(luI z0t{cN2pPYxHth6^4@-R&f-8fBJo_NC2$KjKlQCA`l~jQ{&iu^fo?A(Ydug6iThJFI zU82rGow$lyM^ZCj7Ko@{+f&+1b@+;LYK1C|tq9qUW$c!kyh6q=;WU4is<)*?4W9`Xj~pm?#Zd8P#P?p$}Ph|6zd?^ zU7t3>!{Pi58ed~5l*kdW8yuW^yA;9MP?ZY14+JP^-4an>@kX85%QA#r$y^KpK59A9 zNLmw%6dfE4*~4>*STz;N-N%E?3xT{-VaHFeX@tbGJ>6(8M0Li9!9zJg&`IzFU?4ji zDUuQQM&w;iSa zVBIHXB&D;NjapWBa$ZmX#!GF$I#LE7Ko(N(Y#hRlY9$i+Bz&><$8!;0jxJiB3Wy6w zZvCmL5LlyiSak=IlB08syfVd@=kvwaBa+(heOAhLe#{yP$ z8Am84E8@gg7x|Ee2>?m7Vv98>U3eJ;h35&;wu}=1W}VMq8ZLstpUnnMi&Wcm@hDp3G(qPO!fQW$|y0#kCFw~mb|ZT6*(QD1Cr>#bqw z_Y&q$5F7g(jNp@b5>F_nSS7Y>PTV{*%|rThg@CIH}?urcIPue4?Y8*Vq!IOv-~uK2oe(; zRsysp7>jg&3S2D!2L0Ztb9vTddE$$`lR18&Ja(lNESV9E261`EAazt=z+EM=fiw)X zWsn3b5u)m-v6)yEal-u&2zZt|v7n-xhdo#A#DsWi>6)k$roj}#xNeUTLy>ObR)*+j z2)sUO96bF>!pB_?7tj$&p)^lM=?Gwu z)kJRg2j!t!k*c%QUwgDCwRLl_<++)tV-@~oPf@koB8CXa)sU-jH_B7MbHIOV1MV!6 zW@8XZyIcsy=iW4h_Yz162(F3sSGX^^R;UsDj_@N9k20t!-)LrQ&_|KV z1-if=@Dl6=Sd}oc)~WsmDsY5lr2^ooG8wjsNC9U9C<2~60?$nLXXtsRjKo2;+;L#% zOlncWYctq0B433;d~`y z1!$YkK1hnfgRZnB3c#Hn+8&+imlw#s`YOmijo@c?x#ODeQw zpEUsfqmuw2Z4J`sT;LL-j9&&HrFTy3-4F={%qDBPOWv<^Qsg;CPRdE@rd=X11suZ= zUTtc1rg#|AI`!UNw}m|ElX}Ilu-Jj9sSw7)*PfAy4KQ$`(nv)MNr47&A*oWbc@vzz z65bF%t_+s#m2;T8+BB7GketGLKr_-}d0Q5$0v)EzlBJYUlbQ=2C~^6)P+@n(_C!np z&<+eYiT6=v@VG>18no(r;rjIGdP%sr(S z_!R@>(UAyMSM1k_ow)Uss{y^^x5$=YZ7@xn+quvurF7 zabS9Zo=xpFCikXlPyD5W!0rz^A&X#Ra-=N z+P--CkeRKz8wC6 zxA^od=qKJ2cOhs|ZLqPtvRbo4G|}b)5-i{txIK{)Wbw)OX6>S9vFON4>jZ6!7|` z?u%pWn-akBi5EQk*~y$i*P@)G&VE5Mm!~z*|r* zC`XH!C%mY)hYgZt3x2q$=hU;-gG*5JKn~1hYz-87enG9Gk<^csCdo;NU_8 z=xO39%@#o|)X1YQ9z3Y23kvhU-KmV|f&9m??IlL*7`v%+f6R;!;<^bswT6Hk>M$vx zRyur)mmne3YS1ymDa`H}>gR_U-jjy0RjRni_eAR+Wl*ybQm%W6++6t2fS8kX#{bel z4-iLyDqEME>IRPUM(mW(wJ<^+Q`f_aEY}khXmK=?frEB@BiN+ovh|K#;^hy|{LY6| zC(o5#2TDJZK{CF9Dr5ZCcd7wgpp5zyq#Hq4m0BdDNNG`t41NW6KBwvdm_j%rq3h{w z7^;~_CUTD4hqB;3NUG&gTaZ?S$;gSw=%CvuCBUGl(Y$)$ujnP*Md__6ezIm(=aFo? zBNX61f=KVHs7grsBsjU&Y5{D!DtU&^vL&73yt!`ZF*+kTMVi_mr?*4OkS@~|75#>Y zoDDU}G94+p5O&adSZ4wq)DlgURqSdI1{jUcL(QYF5obMva_F1~sKhuz#kD1sfYhUp zM95(2tKC?O($u5~8Fi2g#kkoAT+vyTnxV|L)*j{tVLFtVzik9M_AEfxS$6B-#V;La zkbp&k50Q=9Y+H>79jBx&36>)sKq>X?9}Jv9{tW}ge*hLF30W{HHYR|&wkm75a;jmW z4(N1T-s6$vOV#0U4=@vO4O=fqaPpTj2?;Q7g)z73+^p(jPPYk0sJe!Fau8UDqV20i zZ3waAkwPNwJ%bc;IK2f+WNUy>^)iz@T>Xt?yS;jkc(G0>3Uw!_nx7!EsCYRspou_b z*b@u{UjhD7QdO~v)*`-tZ^fZA)z79r!?z};$4&v|re4}_%-m4BN~L^?Q|Is0e5ym> zPAVSv zF%PV@0v#kU&LcA@aSl#n)wJysBn2loF2lbwFRm>auO;jdQ5y)@sU(lolOaR~zLF}B zxRGxySaC{FCOg6wDIwy3lf?B5FitV#r#5OvA%bNE>cX=Wdalog%?dk*W#>$q?tp$Y zY6{?#981HST+L3$tKbV&?8;HEx>bTs02D)o(x4Fn*tDdvx^tVjkN{mOzi2TEmSGuH~G>Ql6gzVPW0-$bu&XjEkl_Ex$F%dx!9+JW9|3zG@#+j>Z!;yhW{t{=sk5 z-{LRiD7+l(M&#S5Ar^V^k?kQaWC{4uJ7q@b3ah0Kd6@m=?XV({ir+glzo?L4(xekm zq&23^5RImjCO~=XbaXq3aG(psG~{*wh2UXrBfuLB8(aqPCwyB6#h?_BSE(x$NiY!V z=pZn~4|U{q2vX!zLnM$x!NNC(Ju()YZb-CPHId?S*BNStK9hF17~JYAwr-#(KtgaA z(hBoOcM;sMIqcBjWF!bQDMknZ*=(^c+265llj5dYSslcWC%4DP*cGpdKh}U#00yKU z<}3Z7v+OBpLn=Z++1;j)2GRtm?{5nsld%Bt61HJ^;>v+XRzeI2b{URDe0f0tk_{y* zzfgw-qFq7*+)*3>BBh|+!ytBvE~Mo&D8t;q8q^CH1iC^y!*U1ixNIPRUEmS@^R(ga zWJF8*2$9&|1F~uKX+EEM89g*UoQe=ZYQV6q^d1F`b1?^Kuwa`kcIDUeC$Vbm_DCJYpwZ#_Cf zj13=6J) zAOCm^GJD1GtW8b92_z?(L4@^NTl2Uzh?0U9{Z`!5%Yrd;&BZ`vxAc*#l6#l?in(3x zs}aou^6HD-#SW`J%YAe`F+xbjf~SwT@z|95Q$fZpNvcoV>8OksXSG3+UhsKrSH%uPoWMFrFiTv#CLjqa(19&tl{^8xl{BFe52BYK5@}8Rj$tBs zu4ezyj5;2RWEMG=SZ$$|)Omh%?xPbx)gmIgXcmd6ol*=sDo6MmO0wV_tjcP!@(yth z=o&$qb>?8tnd1Xcr#LX52L**O5RX;|x;v@G5RDoMn(`G&!1y&72g4+!QrP5PP;Das zh#6Ax#u)s#>@*S=7$)V?i8fh-b@cI$noh)s`$GcAI!H6Y zD#D@psJ8NHgH;x>A5lvga^5#W!G?%qB&iCyAanu{|_N%V?Ph&4J^f*08P5h_(nbMhF*( zzltsYQMTbfxQbjcN=GP`J_2hd>|ZTch?NL+t_M0r=39kCMVIp`>fW}G>atfU-AzS% z8NyyoxZ3arz;Y|iuiAsb1Jw{}y(U2ji+Z~<^LAx)SPh?qXoYvAV}9?-^h5KX)0J7@ zPSqenTyS6wuJATx%&9N$Qr}M1w3Pjv3|Ze#)g~149H~9ptO5IUv@lg>fgxz0T``|W z;4@h;irAnm!x{kwNP$zchQCw|eMqN+SF33Nn0Ff2Q=Y3mdpZyO91jBl?(G$Mm5r`J zrUW3x(h<>;XFh`q198k22sxNY?oCG(EZ$M_Jtg-f4}a9zk`CuAX_zc!$hZCsA_)E< zV^zW#%3}s@RPR*>@H$e~vFjDRO^PX8g^1WErU`sP7T&Xyi)4*Iuzt0^`6c#)eo*Eh zf=?Ne2-H!8h7T;DHAz}co2Q^&W7C9;?yTX(*wGN?rPOS1lQ$)TgUa6Xorg#|-ibQ3 zQafeqH2lmBfl}0vB}CX7=GodI*nL?Dt4Z6 z*zztWBI<2ClNY$CBahi1C@#lWHq1uP8Dmo?69yge7B3+_{YKQl6wkbkMtC(k^g*8D zywMifGgg_JU4`TmLeTHiQ!470cJ&BaHE1e|#f10A6!N11yCZq+mBBo`s8w9Iz0&Z0 zj6=M`4|ICOCm?mHB6uu$Q2}XR)ieb}u!*-$d*&?=1 zodNb%Z*sXhS9hlFSjZ!IsswL zrNpr!M2Gaj>Zm1xf@(>%)96sk)N&noqK=9^T~i&?-v!i2jqoSrMT09L-Gpg6lcQoP z-Jo-|6B54S`ru4sAr`Y*Ol$s$}~W0ScA62$i$@TM^}$W%{4cli(PlQimK3C=h@q{27~JQO{-1fZ$k z+Ek>y#r^<}02lg9(l{tAkur-y4)Ta|!5$6FwXBm)Lu8SLaUj;3X0Xck^%00rD_1(7 zDUw6t>DG`C$wP_KTOtHt!Xqpv_a9Ao4&}ssLOf4E?ljTg@@Z0+nZDU z!IRZNGzZu*Ex-+Jv7-Zns9K^fYHE`LCR#Hybj5=MWHlxK3}PlQUHveCc(iPgE_(kO z5RR1lQf6yb1<0mkpb4R7>@jGH!`-WIXS_Of-85B^1+bEg7SU&{U2P$C0aLpsFjF+9 zf_O`xgB@`==dh^upyu;AfW=;u4#12cpwgMTnL-4C{u zMQVUABnx0`rdu(Ri1>2oKAc0(iO8)1WAalycNDp_1qn|f14KdxXw$Zce{8A=Q?@pu z&iZvlJ7|JpQ+X2s1`I=%R_(C4+SUK>a4;h4<3vR5bn4^qrNfki(GkxRonVl^!eOdQ zN12{&=8$`bSNR~S; zy@v)DlIGJidbnXbr{Rr4a~gmh;E^se2C3X%6!vS&doCgLUpn4fZ85c1%urkN_E&z! zY(YP@`7hiTG``Ih^Usj}-y?byZ(!F(ofwUB1V9AuHFego)|p_^pyE`6xR9)8!#d?Z zn814pM0~Hq72EeT2>9m2=mKs>L$?w%Fg&7$)+oLZ2E~mBi*=}zHSdnWpWBRT?=11O z20}zhAdjc|&G!KDO9u(G%|i#|-m^;W0STEj$xh7_ZQ-dIp-kIMghaQ|D*XgQIj37~ zAAF17)c--7=A-1rkMi%wp8%2`aOO#uAM&9`M<1>xQF{AnJG^sQ_vWRB+{y3|-aZps z0h^aVCRBs`+p6P29Nqq$B^q!$0D#hss#^e$=J%53_l9NwK^KJ99l74`v5qes|m+p8- zlRzeUs1E3*3LdSh_UzIece14x4W5y>b7*Lhau2hbO9QkPxu8zlDr&e9zU&>@rcqE{ zgv=rz{+kRhOBoKikw6*Y#{@V3Cd2wOq$7&c*_n$x_ z$Z@jPp?2Y4a1{-q9Dttu07bBx(bdha1Mpe6&dRutj_9jrO&4@x5uMSoN|-JzDM4&6Njlcty`Kmr(+P9*E* zKi{c@3oL0Tg{6RFnMAdHrVcfV4p^M#8QV_x@ZfWBc~mM3gi(WXIdaz!x(=f7N7cx1 zpZ5GsewN-Xme%~Zrjay)q`R4+W%w4J9HN10l4=07##V-;kWTb?t<~L`RUP#haBEAb z`v=)4Hwps9Z*&vJJ@-+E{SW~ij%ji|3eV3Wgl^Tm;R9?#5CdvSF8AzFW6&f&Tr}xu z7Je2Cai{a1QJsB)(;Zs)F2gCsKA)f?x_a8OV1zr(II(^toW2~w0%~=@J#0f|59ovj zxgr(0X1wL2yLmPNAs#7!Q<4@WE4Ke25Bb`!p&@m@h0y`rqq{$&=IyOU9a#rM=5yal z)?Elcx5q#ps5dhASxqzSUAOaV5KCH9vwYBPF1krg=!QUH5tin!PV^J38<(p8$+Au0 zYBiLw4PJl@Bmyvvq_d-77toJgH7b&-PLdvVWIwLP+6JZ7ZS;;hpGZWg26YXhuJekC z;Rd8MTjI;CiV*U{>D~|x*V^gRIEpz4~35%Syew2AAAR@tmQT4V-kA9Ay%#KZ4;Xts9oP|LEM9x8K+TMsU=_` z>Z2q5#H+0c>3k1At4?af&}Xu`PvP`N-~v@+s$SjnB-zH}(By02NnqLSpi#BtlwhtI z#8)4|bR8;29WYa{0ddOaENl;0ZIV+BVhSw+@xAUL(U}vUMe0H6$s4Jsr3R+XATNz6 zHw9p+>gL-k0Li`Xp)SB6B1-If&622jdz?GIw`;lsfD&x>xs#NSMk5|^Kp~MW-VWeW z+E2|lNS&0RNzOA|E^d@{i^JcF1GN@;;6CQY4bsc=N zI_0|2v>_3vZ>WZRUg4_SMh6HLnll!S4?IS3zoiiqb!W z^e-;_498r|?_T`6>(_~Y@zr}o!$K5#NW0pCz`)`>$`hj?_MW3 z+3g;@rZm>`ep5`j0ivo~`ul|;NJ3Gf2DL{FR5cKE4x&@?`xQ6GZHh6(r**5^w9idU z;^#UD%SpG6s40BDCZfVS%IVg`qI)oqA5a-Yrc?VV=nz!>M-49O8zu4?X7LImN_0PW z4Okt7%;3Vj-x6cz>70(YbDE281Gz@+=cpJv#M|i_@%24YJ`L;&;v-T;kI>8eEl z000SaNLh0L01m?d01m?e$8V@)00007bV*G`2jc?>0xLEBSA3TM00oLkL_t(Y$EB8Q zY+L0S$A9PMyM263VyAK9Bn>4^MoMU5AfO^pwNWLefe;{&Kx|xu7#HzjAS(EPwhu6g ziAkF@rfPfuY0?m=1#xKv6j~u#QMEK_(==()#<|+Zb{yNsj?X#!5SJt+WuQJEAL;r3 z-{=2--sgRVqnZWa2OeEoT6)1UEI(Hmf+S=WMfo%E7LXB+YL>rRt?r$gIQN@VgC|)? zE?g-GzD6HcZ|)-2ocKtVv&B#JX1FI@B2(Vm~O!lh^-4%1j^Yg0sqPYEXyP{KY?Yrh(sa?qPV6;&rESk zXq?@*++oS4VT!ttz!8YnP9^}-ngUVR@w!#&d?JNXh4z3+K~b1ImnRqw{)hm;5s0A+ z0;b0lL>U4*|XitLpM5@IrMMrmV(3WxxAWo8wet6 zj-X|A!ag^N1`#j;i|(jFdvKB5;65ZRNpAuiW|c$7G{VgrktFH=Iww=J&H`Ez3Gy>j z{BBpAv-1XJ!(vBwnb-gNHVuhw_}g1~`R&hXuk*6?w@;xhlyG}IAP5(o!ZJ-VnuR?i zW347JfNU*c+Ej`ejYn>ZAc=6~*cm<^ixA%u;hp_oa$zcs81%8dV;G;}XQU*7(}^Sq zc&RcyHpuLPLc+Tl!(H?A($*AF#O-oZE|*D{QiygxuBH}dQzUV!6WoJr&x*L3|r_l0{-E)J&}PI0d|vofPmIQIj*}#~yp7e^PkB6{`p?`7$I1P_E6Gt&xUA6v^SP zQI`M}sg*zU;?ScNLw1x}YKPH_ zlTceY`ie)DoW}9FaRTujNDdWrah1Go6h{-OMT5oEI4*k}(Y{O)R_*BN6!MXQ0dv!h zyT#~4k?Hh$irR3gEx6Rq+Jm;f>P}ArrzZrs0EmWQQ&$~F4(~VRk>T(Eu=%>Je|xO2 ziPPtcq_WmJAV25bR{+Oy2Y!J~aTl)21x^iq_kuj|+57KGhTOk->z4joyRRwQ!W&#y z`dg`_CX1)O|HAdvkz*fy`T2)$34nmA20Axxyt6Ud@{%_kX}waCd}@9yIX(XB(8$OK cpF07*qoM6N<$f;lv15&!@I literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/all.png b/padbuildimg/assets/lat/all.png new file mode 100644 index 0000000000000000000000000000000000000000..c26229aac22ae3d5a5b3d244e2ff383b54ab196f GIT binary patch literal 11031 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 Rt}y@r002ovPDHLkV1i8P*+&2X literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/atk+.png b/padbuildimg/assets/lat/atk+.png new file mode 100644 index 0000000000000000000000000000000000000000..c40e7fdfde6c37907b9d65880bd7c7a4c7bbcdd2 GIT binary patch literal 6741 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/atk.png b/padbuildimg/assets/lat/atk.png new file mode 100644 index 0000000000000000000000000000000000000000..b2dd72750c5d65a2960f667366e96ce7095ecc17 GIT binary patch literal 15229 zcmV-@JA%ZCP)P000>X1^@s6#OZ}&001<>dQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3^wmLxfjr2q32eFW~yak#!WH<;tkC#1QDM`UJA zS663r#bR!zrb++;5ddVH{eS<@G5_^n|K)tE*2Gk5ZaG{2i!Cxyzx8mI=^RqOW+Pd3!Oj1{k`Z- zyw*d!ukh2qUpw>t*FP5T`)_}&o$a63!|zLBZ2r6<|FK4&*IoMiwYw|yo7H}O^KU%JMfquTi3Yx#J7wSteAhGO=3VW&eFI z?tX8(-#1<5o0sFA-f=OAh#jQ>380RLEQPQ zX}arse;NC`ZN3Z}uwc5&TzSB5$GgNx?w4EBlSSoL?Sxj!uTudU`jR+GizeP8)yHC`^| z>k>|Il4LV1=A*;nMHaxJy=-Qyi_yz+vYBn3phzC8xg_H(CyQahxSdbOecSGr<^HAJ zOp5=O-TXgU&d7BC8b=IX7tmd8PeQviCJGV642yyMh%@=Njr##$M zIK+FitDNaOtGj((X~sHjTKdU3&E(w9b`ju;X@q52ofC#W$OlfU_Y!B~M*27J)WzcI zlRwWcXGi{Zltr-Aj=2z{F6~nEqHSRhr@w`xZ{y#5nrnVt`(Bx)`dk}i2qrA&KR59Y zZ~OaO=r%Ohc5SebwvHD2Z{HTUgfA-xy~q-=lo$>~#ONoooMZ0u^F8L*oAfN7XSz?M z<$cd&|9JMldzbO`Uo5O1{>QWbD+8Fnv%p^#VE+BPem=i>KJxcVVD0Y<_5D8gSKFHN zPYdyHI)V9Kh%i>@+{g~ch1_?J0NXy*YXxw3+48Q_8mn{R`7X0b3m=D!Gj8B$&AP#z zJZxSkxzWqWph8cxal7rbSbAB0L_hQPsf{^LAfW2Zp}K7du^m?(L;8LrJ}#)iMSesp!EvYnQt&kDKvR!$0Rp&*@v`xp?HD(D(Iw?blp5byq!93hYjO+W8hK~mP438ddh)f1IN=)vW^Q~F zwCVGe_kIeXaNNB}ZfQZ83BLt>S<9~vv57JkF5$jKj=*aGqCbn6h2>?iNB9_! zR$&vMK-4BYU{G~nBQs%eVGal)Jko37+$uGsv7v|_W&`H{dq``Y#m-z6D8_(OxV8^Y zw{C(kL^C{Ce@*h-GgzG6)_s8YIu4IUvjh5noM>G0Nm9~nkSPzGDJVgr+Z5+X);_PC(B@~IB2TZfm)w864gd>qFtB9Eu^Rj@!6ajPI>^H9;ivg7W6Q&Zyg z$2B4lUm@gDw(vbeN+H9=+(($mbUV%lSh9-Y6rltulj{TntP^1%0oY9r+*m>ADzGaTy|*&Q zo>;Zz8oC9-i!?o7k=uxkXeWVt!zvb8r;yXGHRfZmVp)2Bv_XTYOoHg z6~n!S={;5~_&o5nn|o0XqDaG+dr^qYLKWfm%Ra9Id@mUUHfQo^5j#6#C!fyBaX+ZI zRQb9(`1szrKu@Ohk|l1lV8Q~_QULfnLaKtUh#q3?bXEc?L>BU+yH*woY0aJG6TAo< zI9xqRuWeEs>xhG~5~vgfc?M~t^#<0NrfA$lKb}y3l4O^B6v+p>$!M9eNhsu^Pf#3NXxKs>S-Ez4Vn#1bfkTM6ufM$N+GXKg3R;Jr#~!d<{StK_JS{vaf)}frropg z5*7)ts_^JWP}D=DK(_$@foP9l1{uQu0N}ZONdu!l>sa zfWd{{02KTxsdkk?oI`R+w+`uXRdg5WfsBb0s0hnaLW{L}31sHFsRlGJh}%W@y7*{~ zP?aH8PR=#@Mi~3oeE}vST(0x&et8Gr;@)sS6$M#YI6e&IVni0!ug3ubT11r#LcKwT z_sT>Po=9RL;e%ieU-3jYOCHG#aM6?@=}^#&i>p!OpdJJy4vA(W`{yS=1vxz-n#-ep zFzXUzDwu1kom^jC4~-O(hG-Icnl%UVmqGd_>UI}bDy1~H>m45mXAZ&;fp4v`TbY9LVUxFEm@*T6+^EpeI5dPr$N43dM+ z0~5*BgoJ>{fJj`v4e6cDB3uK^ApyyQHZTBz#a-gaqyU%-04r$-w=0t*QT(lFWpyKx zND-;)GE|aUh9uw(5RBs2s3gBrEo4{pQcJZPw)!m}iOu0hh3m04Y95 z>aI%lBBSmgTlJ}O4bO`hbrW>(+%S4gbgH2H+Zfih%8ezG5*|57Ze_|LCO^ZuI&j-% zRy-anK&&%<3A&K9A{36VMRs%@M1Rj3+55EifY<_yod+Km!giZ5?@a)1eM_ zl7ePOXB3idP=>vwUP(uwJS+wQK;#kstTVum5aDWEnou~&bUzb5XDj^OfnC11 zWucK{r3{Zj#i(tM0DDU4*vUS3V&*)LatIV zY>58-mW9nt!}$BPfGG1K=ib`VC)?T&#p2>G+iqBCB*h(|M$GKh10d{97gjNnW&W9J z7i)(2o1&@2{w)}#4jY93fNDzL;A5@GF3f8lxw3KGZ|*GjeI>#PNO`rvLeW()H?mZN znM;L{t6#CT$kD(UaErWFW#Z_@rH%Rh1bl*slSJ_PsnkwheJE~qb4Or8h+kLqTURyW z5M%nv_k1Pot5aIiaqRNW~c0vb*i*+ z(H{PUUqUIQ0Qqv&C&*N2hD4Mg(fHX=%1#m3f_6wc3>B4?1&X?MNIa*)74&l58VU>v z9Lcx*rB=etgyS}v0Ij^meM}%>R9>ACZgLgen%9Cz)!v~&TfXmA#}|P|CNHC$ z-e7Xmi+)bExWFgLmL;rQ4(HsU$@Xj8TcneMj5izKrR<&u14iJOu=mo$FPrkhZ-IX! zN($>CgsD=%vC)QLu$S6F!m?Jd9!}z-7ojC2q zUn%MdDUv<2crMxoW!hoN?PT00T`|0n5H_F`-MR&PR9v?_&4^K$CSpgzJfa-Zv7dg# zRl!7-dPu^K=i^9GsnjkwM<}5q2i$aakZ-8u$!bEp5!st;&bp%l$*5#EyqY&5%Af~i z&SA3XE@?eXXq>9y+@kJv-%5Q`in*z6z|>CJYp61a`J1FY_)cO4rpY2a zEmAcX=QFMnTDm&7LyExz_!ctRBJHC!W`wY(iyb6RgRUp8;L=1<=;AdClsjVFad5j*$98ud)y!nBmnQxYeHbv`XR2f zilKF22*ILGBkY0{4vztm8f>zbpH6mZ1MH9FfcaI2Kn__6BS5!gTQqubb^WM)x@S+m z9FGv(x|dOKI3fafkkn||An(hb$ZF+$mIi`MMu(Y!lyM;WFe_FRkrcVZ{3?)>Fjdau zU-&JOi7XPu;2>#T;q?00nofYk3i0tMbb>0-Kvd(&{Sx`YH>Q=;as*!kRf{vQoF;P- z-W5{_kMIZ-o+qr8Kv0%twV+xVa!YTX8J7nn^S$w;*zL_ko(Hgp2f|<>BfG2AtH%d0 zZtl(8yp(d0(sx%SLk!96^Cc#s);3R9h?LAlj>?q5 z^A>1e2ujbBbUCt}7x5-6cimVpWq5D!qVi2N*$$3$jCn9wUKImI&tt z(^|M4EH>Cix**NHf|-|x{*}lZ)NqOcJE_j|>52s9eu)m_ zWDX0P`iLz*9}Ol0105s&>`{M39m-TG=hn2!*lbBieQ8)YH=7uBYr@L!ZBcVJI#vsg7X(fNvkYv zEGgDID3MbY8enlhYWA$`4tO`z%v7bf%da$57H+dMp&;B>;WRU+(G!kPL$k zhpbtsigp`yL%izM*05LOCyxXkl@tSZz_S){p;TTS2*_jgXi<`I|N2&xM5$gj7GYVh z&w27l>7O4eGi`opDCRlifPx}NY>&X>c0#rU;H^WZmh7j>a)Q$!VjcJf80Sa z3yMcTJAxYAauZ3!BWL``QL7}EZppA)5i9bf40lA*624e9#G^in!GmFT z?v+-a7hfgS-w)ao&F#)d9Rfm^!Hic7aNEEoI6^=fGC$b0SZz2jf=k3uu$<2gH!th}uvwV0I;aR544g8;ez3rR!Ar;R#UNxARn#b! zeh4pZdF_ci@#r;?35%V|8mhnav|KQ^S;^ZAamv$K2|Wm|`i0O=c%L0e#32gSG^6N9 zb6GP-H9No(wAC12dYu|LDBvS0K_1LQs-cWxNv__*&fQVbgFIDT(;%{@x`tB$ONBPI zh=4G&`7aaoE%VRqp&+q%v~u5f0(@(u2RXqf^onOeS^>Ke5nR7{jF4~RJ(EQ+Xo2Op z((ofdM!Fh{TWq#8;ib(7u3=Js3?w#9@Zb_r3BF96Dp~bojIZPcNUvuli+0V3+hvXT zWCsP_RWoTlDcf%tJoywxGQqmN!mKDqU|q2l4PGGWVwiJx*``6>Fh(pW$q1~}?adFZi%19-#`#5j0eG#I6g z3JW*p?R<@zR}FRrrFR$?CgPHlvAx;MF#J8>S;jG$}rm4GqbHI&FWp$%$PPtz&CpLYsSdia?~A`MT|hg zL3)@mf=OEumIeUS6)^DZ=x2xC03U;Ypnpeil~U&9%N~`Q0lfe3SOuTK)N&c`qLI ze0g@(S)+HTyoi>PxOGX}hO~B)xl=RlLvx{7;|G2StO{lU{J9w<81ipf_59$%H3Nod zJJqq#`#xidA%*5q_5hcKW+cIeS9oIu60UYh4J-9qB1cv*Xx6VB@|5u%c6sm7_DE9;2s z%-{@^4zwi0BKzQx7*`{(ntPuewdWy%8aB5o;%Wvw%Z)W>0~d#clJA^S84ih?uXA&m zD+4V8nMYm4AidZWOglEr>s4^fBqw}{WxDFpi7L*b66-Q*T{IQMiidjbtWlf}WmA7p z0Ox|&4$_7r6^#K@&@=|Pw3u#&5b!QIMKGGVR6^xPs>ST5&savK^gDv9(Ki_EUHpic z)U*Pg$bD$890Qk0Ko|n_jtJshBslR_D5QH07M{JRT~`xJGYaskO8C&tk$^nBvZ`UI zeO1jwo)uZyCTrHTPhgQ6Rebc?%bDcV2zA1#-b(5`C?Beh^uDjY+M4HE_0ne6io`2ba^P+z-8POEg-5*m=#Y1sB5`ma z#7XjArBKL3Bwh2AJHw~(Bp0G)v9(1(W-%-%Nr$LvZwrI^S*jK86UB6{PQsfV@U>jQ zLm^qUsv@bN}eunHo@3VM{QWJ@`-==cY{B{9^8z~REmxt+^V+s2# zRs1wbCs-semZ0qxd_;7s=b{M_L@lYGJcUr7ny&(8bB6ueTnxeofGS?hJgD=Z?hqcf z5jbq3aNb;oYi1zgh~duphCEAlq@AnN0|&zqF5D^?Y>p*aL{g)dHT z)rOs_Bx~fnbx~MoQ#3(yxlnk`D}YVfC_<2WZNGtIX_G9NtDy`Jy@DAL#p>@5@OZqI zi;6zAqiF+(r-bFTKcK4Bva~rtDjdoRH|SfH4S-f92z-92e+S})i~sJW9drJgiN0T% zS9UKcC2$-KlmAiMZR5*V!D9V6OE)w)34vgpl5cyuPhIbGsh`cdky&gX9;^5%3vYoX zVw4?##^KP+e@#aXb*;&v`-(|fLvK(RG$js1^srPQ9Dbnf&B;NSAxZP@qZo6d(zG%v zSrD|afQnbfl8lD7PE&{2-g#cY-IIrTOdi|mhU{q00d^C;B!afwQAZ>-2hC65iPRIq#+vmV~UDig(g3#iH z8YDj!LEzO0!2i@10A63&b5^+Zm)#8z2(q zYpesMB{7-7G+b>s7!M3~35M2*kNMZngwJ;+HmE78F_EeUqQ*q>s~p({gNEYn+tonD zK!>~PEz!Kf7eK2RfYy+GZ&e>I?4c8ycDhq#8IO%I`Ho`I<{JKw8$vY;s9|6RC`afd zVxWF@AK3C5nAX3$sFxFwh&3GfhcPm!rV8iUcy_4WSMI>?qEbp&Q_~*NZO4@~Jamiq zc-|yp{3_7cyif(i8Tp*70PA9XfnTGr(0tb((&QiI=xF5@%N@;&xrNt7JiR zBM(%3N56$TtMF!P?jyN3Rin91?Y2tVABhQwuA;M4w?>N` z+}~vq_gEU}Ci2mY7#QFqqmtdV!)fHIy4Ic0i@xG1+Fz|^89`E}TaI+p3vzmj2X)PH zYPb_H*VYN`B)h+piPfV+UPaOCqBi4a_BrPVXT(}p5X|*?J&-&=HynX%&=x>K5dP8( zS1~BLx5xDBmt-)%n_8;j07mBn)%ye0`JY=bqg=E$e^nslgHTXH-)JQw->8_rk9Tr)L(=WWUJ zrj2Yh5#$RT5%)o-vEqA*(P+P#ZxG0!9u!fw1RY6-av1|B|*BSXKM-#=LdqdKL-LkAuLhzdL7ONozuklC0ZxGJwEOch(Un2w=MaONTiL{ zP=oJwD!Khv;}5bSAt&0aSr)_hd6596vp!)w;^w(l63Wn)Bdq2%F5mXDl=_JC= z-J}}~7ou-!!{==dNfj0lQu%a6+-}+wThQB~ewQ|v*VlDGx{KMm@6$2C>Oksrhnw4Um(X+r~fTnF2nxV29L_ZukI zw~Lnm)!rX)lVAnGL5yf;t3iAEtj0LCUjR{q7C+i7n%-ap&#I%9*w0$R3(z$#D_Sfny#$5 zI`rDmd<-!}q_proljWxhu@~;%)I(B@)wQC1M$&6)NZrsT_%t+!#z%merV&cMhm?sK zrs#BZNK40!tM{Ybg5+FP60!9}^zS-ss}0!!RN=3OFLVNZP(Sgpwatjc=|U0j{y zt`msLt!^L{ST*P=0fqIC#(hCBsEX@%g3F{2=wq&$CG-?imqy;v5pw5J31a@41%6Iw zYGlXMf2tgvH+-rbkn@im@ON~*i246F==$7u)>8`Vw%t)YJdq9yP%%XO9leGqvinW+ zWX;$D>U^_HRp2{uz@e>E0(x|4Q-|_ynj}+@0;jy*ktFgm>S=3qe@}A#+x$y_eoe;0 zA*G1a(B7J7n#$L&U%B>a*7#rCt?+=u-4HE3Q&LVls`MO!|yV@)R!xp}<9$ zVSrF6$oKXive#~r>`i8L3)I5lA6X@12J@qDS8Of7mMTVff~ZuPOqAmDac!bcBY*ai zNM~NJLOf5+jA}oxB-7H4*WdO&Ms1?Mhs=<-b#x0rj9f{COViN498c3=Xc#VpxW2u- zx>Y+-6*~s)meKY^&27J(Ba5){{HV63Q*LU|c~RHO;=HV#c0y(GtN3vO_a1@(966{D#w|G}rp!Z4-Pc$iNQjwJ+ADgD`Yhk49j~ zSYsfw!q8r8?2yJRTw`eyk`9~*+T!cAuRvD7t4SXT3U%dVU1~X_SAsU_!?p}GA%TDm zPt`z__BT3QHl)MM}yYDbbYPy3_+UQalMzdqX_xR-8lof%~`%TGS7IgcvVDmMjc5 z#5RyT`)fKuni)+yKRZUihC6_lv`_OUBNagRX`i{9<5LIwbdtc8hXWYWfLsO;(6n7! zPHU8W(Ef;YJ3Ic4=C~+F#Quxu)XwH#7mXGqY!Dv&J6or-ebo2VbcH4hfMKDW(-8m= zCX7({2o8;gnL2aCcFk15ilUmkA>U#IxcbiaS?9UqOd7F?ymuy*`W(DQyL0ZPk)9fn zP^Eel->X`&O{bq7f=|1?5q?Gc$Ev2+S#Uztqc;`&PrgVr@s%RxLqq01YpYXBNu@e+ zn^nCO@8UwN75rYa(2=Zw0vjXbkXU|FeaXwh)gl2GTb{+sZnti&X+)LbxB*lznRYA{ zZY|*BRDV;^9*t78>m^t3;L=_Ev8kLt3y7s78(bl%OA+kc4ajF1S(xXY$iW1{^{S&< zvd>MYp`>@bQa{h4LtBXa<@#hIs}85L1CvVB_(;TPGg?7HXtL%Oh0eIU37fsUk1nY$ zfta#tp1HbxR%aJmYA3S}-;m65!v^pd{{j;FsoJ!t9=Znpy4aScTQ&MNquI{{Azfr*@R>#ZsE7B>Wr%tyD!NOaY=!n##_(`DAlF5$F2@63Fh`2 zrp7X07@D!!@}%1y5acGg>yCKp+rHDEc2s-iZ5mLdrQ7oPjx@NfjwFL>p^$opF_73C zfaf85o&~k)-!Jrxdao=P)G@(p^&aT4RpA8P2zn5{Rue+)2EiTV)Lb4(AXgZ!j#wDw z`Jno>RohVfa@yFYHPU=S3GJ7R;^Ou=GeW9Tcc{>A0VB^Barsz;&XxHH)*t&fXc)Zse`8J%^u%~lNz zq6INvy$#u=v z5l_G^>sm!H?x1g4M7D$L#TJJ~?wrOQjubcPNkw^<7S<}Z0+JaW^isYCJGJYmNLb;D zpUtN_0p=0Tr5n2yV0*WQsM;}Zfpq{>h9v)$7iN#24JGi9D_itvw_Uk!xZcLRD$3ZZX2OP0aS0K8|QzM^X%Nnb=BLx1nSK$QZO;&p6R1ngRZo-7dB&%s2;w_7R z>IgoSnhiQf1Dy_BW&KnfLYZjRXK%C1pp{1aMe0j7?#xEgwzCxTvRfK06hE z>{EnekrH$QA|X3Z)^B3asLA4C{X|pe6CF4}sQ*DP}(AFoFNSm4k zBIW(XgF*7*c3WM(KT6K??rBpT55+N|^G?TT{Z-1DA~b4Mv0*_sF1MHOWo3{sI_?9f6nzw{9EOY9TOp<1D6U!TQl#HLNE%i@onxH39Efpk}X*~97#UfbmUZ| z&u-~$<5Sf0Ptd0NMcL}2!JA(9`RAp5E6^GK$qjt9k9S{|A7FE)r z9CvQ%o^v_g@^)7vqAe?{?x}&qts<5{mu$Lwa5{enp%QhDm}@$K$Gp8mYJ!Ri>$CtJ zvydf8_`2;?I;b|?7VYP-p7oOayp>w{8ft|gpe133WmfTA6cGC795A7Rw z6udU^Y;8wZ$uxgTx}Ty;*`E`E-f?B{qfaf~efC05o4-!=#Pk2nJ87zAc~!uwvW#}S zR&(iQIq}wlVygEzs(YFw=~3F>fw)Z_=Q#4`q{=-%<;5RgZvKDY4o4EPjumF9qMwwH z4wfbxh>Vetc8$??bN5whEDb5BF}~TCEF!bsqXXV)A3Du7yu&b+FfsdWOkK8jf3VFP zV&Eqs>A_(n+alZ=cF#@@rCtXr=^j(A^N>G2Am97)f+Q^>cM{7uU$fToq|2;dP2~Qd zJ=xQ>cQ`3xrViyBA8TL#GQT@mxu=lbnHc}lw@t4x^7ApipThlmaP#Mr|N276ZCR}D z*gCW2dDn5m8S5dU{=?$Vn2#RY&(P%GSnQwu>z7BX`maK+aU_%XJomH}J&~U060<&3 z6<0S(iBG35{L|XS@=uGsvWnJ0n5#pFbl!#zh=mMoL)T@_Xgw*Ec8x8S*SGb72YfqX1QEiM`V^ev$i#CxEJK~ zZprFjj|9xB4W(k;nUD11w{0GgYRx7?&zAP|==g(+Qn`FKFzC>{cR(j9Z`^Z^N3uq8 zHlsYJP8`vJdT<$tm=A+N<5dRs+%Y$2vBv(%! zh>!~@J%cmm?YyySb=>s3aDmCC;M640eqded2yP7aiq_6TcMY2ITx=i2IPYs-*EE?RhV+d5?pDeMj(*CDzl;2QVjSXI#5 zr#sWS8kZWRh4QMBromi^xH~1ZdKRr<$gPKpYv}(!i#BiTzzXyK08$axk4HAV$p8QV24YJ`L;&;v-T;kI z>8eEl000SaNLh0L01m?d01m?e$8V@)00007bV*G`2jc?>0xJ_QTKQ@K00uotL_t(Y z$EB8QOq=H!hOf^@urao=4Ic*^j3GecC}~)lWmS?<+*Au`5+NmYhjwbFB|9)yt(B^3 zjZ#f%*o0Q?N3>;Xr)a9QqzN5s&Pv4+T42i{Kp`Oz5(+k8!1#b+27~QyKcF-b+mDG? zdjGsXp6k2s`*|LHLVG|0FaSpu7Z=}@;*xLck_}F>a_v_Y3dKu8dq8^I{pi8*_fDT7XedX}|9?WpyRPHAaSTBZ z3v-ZDc^Zq}ficv`4}bIuHpjX;#lphEvG%S3W(vPYEYwe|=}*MtYE}wjN?8m`Qur0Agz1>xx7?A)6QMBpEZA z{p1Aa|L`l+YBghHV}!$D{C+=kbHlW^e?(vQ%g8N7Ec(Wnn)czpbAj+^H^~|u1qI-n z0^jXlBZP-hW^82UX(x%4grGo>fm#I-h{xloR4QyX8#bG5<%@&CAltTWmxruAC*c))d3e$&_hB>ph^s(Q!JuVE}~FKORXz_AP5M8z=M7u5eJDSNE`?O zq&k?LodqBe2+-Nt$+e3Y(Ih1SP*~ug+%!g#F$Z@@qV9Bv+EXD~{}BS9yL*A^>JT+G z({y!#Xtg4f%atHSuA_65A;{#*nJg?zF&EO>dWp@OH?w>9Zj?$Tf*_#N z>1b$Zpt7=(($XzF)6~RNq=?0o?Ic;->FK$HP8Z_dJxD6tO;X0QfPhjP0EEYsNPv*W zb-#n}RDPShg#qMh6Q&{;hTQeU=U?U2 zr>AIaJkE2^x!AvdKNeBMkWBp_adp<-deAD+8Cyg~?>X z>-91?I7s(oD!$TJL8V#{`- zR=kA6VaH~(QD0xr&Ye3cE-q%`!l(Eq`yrzQl*#|yt`%tuBTUcEqEsrWDla6zq=X&o zHKgA;Nmc10PUm`bIvqZrkAZ=KmH8>kR#Lur5u>|tRVJ&H1WaGYJ=Ba=t7W6}s}O(j zlPVI41YWO~SS&_IXD8hwG9n-U9czXLQ53OQEZpt!{|`tUh%P$hVIM-zY5Dtg&csvY^g^3p(#900oX~{;EDJE0}dED;!oQ}?VMzel^ zMq`D@QZ2Jy&AfjKMO$NIqWBwoWtqbfynYY_IU4IXnUur%hF^kG0Y(wSmTJ-~4y~Sn zBt2g1vqbzG5K9iOyl0xA*qKgS>*a)S`1RvIJ@DPbr$PxUeeMWTA!*G&5@v%yu}y

zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*ra^$)dh5us}UIKDh4q}ye;N|-}U@MI@<7YpQ zYPuyhNdPyT0Jhox`sXqK;9qLho0v+?EoaNW*kbdYU#flnJ@3xO`+ENc|N6{*J#Jij zec(A1bmsGC-mmjJ$J56JCBBZ2ug6W9ulv;3jb0zT-7sim&Ej=ljqfwN$KC61(_Yv0 z_-Q$>|8w74SogZ=uYbovFjnGV@W$`JabB}V3Ctk0(0LE{wdf>{^^opsyfOMAcIN%p zUxJs%&#|-pbw>Ca3S;x@g#1g4UdKtskFmQE`jWM`U;Wn)Vs&pre2d*VtDdu;)%BQ( zR5q_g+1D-ZZ=A@&Vz`%OehU8+znA;d_^B*$lCecL2S3xe<~-3pF1qEqJ8s|i=_XT* zetW|0y`Mx}$134(e%_=+{kY=`=~*UJvNFk9P)6S8;_kTZjyGLpEQqJ^M0dqRJzV7n0ee-F)y#GVH(PuUO9_|xbuo>y79d~<~^++|CHE32Gb35Wr5v} z=N2QmkJ<`1&w=xjjmAE$;mUg5fDkcuVlpm}0h?V&E<0PiFU}FiN`5*s_aXX023$&h zIdjNO2$|$mud%tNxO=Wo{y7Y^kcbp=D5Qo4B3Use>Zim?4fPaKG%2N=N~)=)oQ)ZT96?ShWAjtC&?+TYbAtDebt^&b#co+wO;2JK@BWPCn(- z(@uZS+AFJHv*v!u+@G`NURhJd;ytf?WR1h6yxqbHPLgs)#(Z>SyeI<{v{%k-buoHn zPC2v9(-g^*HJ6l}<&-fp7`OB3xcA(BWbTi7Gb#R8dGo)^oKfojADJ^s-79n7^Y%m5 zR=im43;U^}cPTQDMzHdiV$hhCUxNnwq6>{@|YSOQpkj|B!ju4T9R>*$H^Zqe(6uwP)r*MYblk+H^Ln;?9v78)M)iQpa=jm%LQ^>Bjhs(M4XliY-TndKZ z4equ2oyjKgjOY?~{Q4o2;6u~Plf*Rp@5lThHtwu;(ir{hu^}bH4FZQaEgf{jxascKlQPr$+X)PJPhqytDHF_wgITZpK5p}V*}QU|r-2Cj)II27Hw*32Q{@Z? zGOU{~Tlw)pv z0knX3O|cyhZsLOv(6bU)M#B%Z*FG_f%zKWKO>`6swwPw*ai}_X4uq1wy%tg>fFTtG z;o82XwK`fXP$XVCK{IhaAT$wci9lCky-h^q-j+lHA6k5%R1SSIIN4&oaG{j@m?77W zWZOFcKlfK{I()pFIv3^HVTzD5C>!dUFbUAyI%6Zci0mY>JM%K5*-xnXY? zaj(XY3}mxZ$w*2$OB5oq5y=HH%3M3~q8lI~LW`fJ-$P6OfJ>|kwU^6C)v(f-7j1HD z6+Os(^W60GUf}4AXyr33zpPB853My;98^gx4dEPY8wDCR8%2!l6cl3dV3F+-bi9!M zbzbY=6Qu<}ti6Rln*yX>7_Xm{+oVfUK-Fdj^vN}lJ6CGN?zIc58+|{}+$07uc^Lr$ zOVne|E9||g#8WT>7Bw75fY~mhd3~hz6D)n~Y#C3lDnj~pxUEu_3%$I+iN<8Ke`Qy~ zVK%$S@W(=g){g`8uPPd6S%vS=k~?+iVT@ab201{H z$^wlkD0)WzxpEHSH4Ne^@9|81ml5nYJ4gLT9Am9iWXz z$#`hRT+t?=IJG(+mlL=Q80=CVw3@auG_!>RLqrrtc~b9DQi&C?@>#)!%+*=0JL1xLb5qL*Dvcz_M)kpZz!?<(b$e9;s! zByEP_obSDwgp*{r4Xv!)hjpzcG9m{;ORMA)3ya++w8+nEIL&UP4i{ z%1ywOYfo6&#>q^fPH15B*SBls?T(NO@Nsl(y}D&cu;T3I=(oLwbU+@+ zL)x->s;geL>tk~iveO)--EAPgov_xvwhOwi2Z}BoRjXz`E~G=o=zHHO5NTmFK@?u& znRoLiCwbP87C?a*T5A6&?A^=E2(Ip&;pw#?Qd~ccnQ@(Es8Ez!(n)#wjA*9$s27_1ef14UD@NN0pTG6{Ebq#_{2 z(*rD1jx+~2Mdvp{3nrjJp^{Bo%njNZ2|=_r*a#MPhgewJur0ZwSHf|y)dufFpm|SG zKHQ3cdi6jTzDA-az%}q1(STuqRU$;w48#W}PT|B9fR*C}ySXpqz5oesoPY#+h2X$g z!Us^wgf>?eOrsB`MC{m8gs{bWph~M9Ds2#l?CP_{5|6=3GZU(BW}2>aq#(95?5O?| zxhGFeBFYgtgjyiN7!VUe@5Z-==^O5-9(bi97(FeAl^rotD;{CADzM}PsgO3%1l846 z_cb;Y52zinn`DCS>B;m0Nl~tC%r*y2f$pst=|)^G>Y&7^mG!)W)7Xsd%7B6eCR7zf zD+j5)GR!%zNYn41|K*R(^Eq^l;LPE~j>V6UOt&ml8}IKp&BQIz5VazONakXbR*oV^8y^(-7t$0I(t6Ce<)j zPRO7p(YvA9M2W2DfMFd8FuRw-oVtO`A&f+>j7h78Tx8K9Jn>& z)g#h^)?{oD9SAU~(nUT{Hgp_XnI%P!`UT8b2LaOXr=_=j(@kZ|N>yO7TW#H7gW(cG zn#m%lg&6TNumIk~rB>_{hAiNnnTQ|m?m@t7KIVx>l(HS+lg05SVEf%A?{zLl`BYs2p>u zsNF8e(=8`r5z4{@ks?Pw4MM7CKPwwto}GdoZFnz*3Gs$!E$YQKwM8Hdzu*BzLwj^2 zuuuEoQ8MjT69|;(el(k0E>Qz@gF~@Jy!!-*NOJMg{-H#a+_b9GYktaG*;3p!0oBt~ zuS#!Vk~&12c77r?aWdd95lfM7mdnN=yf;#&=YDA=^QSh`6GrNUe&Tn8@YCK-0`IBn z2)FHSwN@htI+5}B?YL^Q(B(6@gy41ET@o&L;SXmArMaNl6z`eLXui2!JAq?8hFFlm zP!rH2vhwA6-YXjEvitka;=4jsg7;J)Ce|#ceZz?KLhdJ+k%jr(WO_ZL#<$Oigmw3$TeIGhqXjf5n^HKwFKNflL0Ohfj1 z&Ou?YG%O~|vlk9vx~6A5C_S0=3kld|I+q%4T&`Ur0-){LMkwTAAp&^O1dHRvk#C;b3;_Vj%@M%UcF|r^&_)$nK)aIx z!A1eOPE8CdpE`usIEs6TBr$=D*jyl-t8ujGAp#Mv{LDTym$fZ}s?xLB1|(DXcwOmd zpqK7P6{4m|nrw2G{nM2=a-<`AIMTK_M*Nkd35K?LU@cknyH(CeuwRb|Ep5`^H!K~E zKkNx_x{tPi;9i{YxYD@++A{b;Y>EZ2ssJ3OEbOGcB8#j^C4-ejxfdbmdJNT!>**~q z=J2V zI(ruNhtjUG)GJ=Hi%M_nBu6y|b44K3Mo3*u@G5i1*p=&6dF}jNhqt&uDnJ{e?!Myx z6GGWdL}QHI!TlH%OV7|UyCO*N8MIPCb~IW<^t!bLv}6_N3(O64S?-e$%q3>k1??1? z4qMCCfRWk}hKg=Ivw~f67y@K%L&$^H?I~FttkeEpg+d{hp^lqkAW6C^3kue;IRXoOtk zkXV@(bRT!?2{&xb+LK)NYd;&l2NegR&pSP%KVS+#3ao^MB}couinK)%sO0z{>8UuP zs3ANu+~Ffe)=+B>v6^MrHs+(NXRV$qOiHRYJvzh<)qFfk&3mS{$ZJZFC4}jVzS83G zw5`#$S^mpW1~{;owV&jq#1W;GJWC*ETi+ML4GV*pmmAN>MPR^#s;g&%7eiVwjnWoi zM|ky!1sz>8{p+-UHYe-lij7mm>5C&SjVo0eFJ?_!2 z2|@<|0`3ZaJQVZ5Lw|%gjyUUK!co}nt_k2kR_k+6Hu`Q^-NR@QtR5P0Q^?#%rc(QZ z+EG3#>nqeofCe^7ntmFHJ0_bOb&p(a?piZa0f0!G%N$YJ06*y!R;V?OCjK_w=wMjL zbR1rEFuaN&cRgnWn~=>(E*mHxD;n4;0Pw_14=2#_ih!QgeQoq2b&nD}zXJ#J2XrW3 z5U4@Psy8ZyW{=wTc+dq$7d^e^n+>9lA*}^SC-o9K(auSS&ef3n0B_Y}ThvU|4ix>3 zxdj4djI1%k#n^Q)^T@01sjt=~Wb)Ee1JDluYT98D?rroWg+%3qqhuqyP@U2LBiJu4guWL0cP3RZzfy#0)5O4`m~exzonA zdEq2Q;V4-9A&u>r;rS8@tS=J?r*Cv9%BD)*1Ar@;ws0vBojiBmjK!!{0JH7An9e}i zz-#oUp1?h2A~q!!IEo6C5mbEvCHGRqP?U9ml(nsr?&HIBZ|5m0ZdM?~3A<5ze%`9T zS{WOp=eAuek`ls5`^L6_Jii;e{xSm0cgL_6^?OZCR9P`8t4$|(v1kj)#n&T4d82CY zg-g#3zm9W9;g+n?)J|z?gFZ^4CG-SPf0t1{FT*|$e5pzLYGXk`Klpn(gmj>2ntAtO zOWSbBtEe5B=Z=l3Omq#Ti-rYJf$600d6J;S5@RjodU&#u&C{Sug|4ByJ0AffGL)oZKb{*!sm00!^n{@{m_)>OCfr3;^B(rduCPk^du6D z<)#npa_Rm0s(UL5^5esOP;DM0uJ&1baqZjsewfyI4g4(qX3o5?{;iI`uJJF|m-$_R zz1Nrf&AmQq=-$ixkdrU>`BtZ|Dpfez4aZZZ=I2)YZI!m?D+PW&4?rGkO0a^Ywwa29=aU2kh>uU<&fqKNSk3Oxwam8%tmuKwSf6qO!dX4?EZMUP zEKFYBHy;GHHK`0de#Pryecoe6S5n{_p<#w;aV=9`!`H2^|Irf#s7BX2eOEx=D1MwZ z+=sT3J-dHTVtE3F4RupuG#>pEgnQT(^KYzz8J9KrjUxa600v@9M??Vh0NwzNQ0b~g z00009a7bBm000id000id0mpBsWB>pF2XskIMF-;p2m&)I@zXua(SK!s&ln{`#6={SzaBT#}M*dCv!Ns^>>K&#Mt+nJ)OYU7^m+k9QuyRa?$ zQQ(0niR2gM^V++sG@s8mOK9mePVd^jy`s%}lJpUiKul~P9uyYj{)KIY*w;6MVRDEC zg%@#|H~8w>B9K$@1%E}1T;P^}4k>{G5Rw?aZKHIyF-bbhvK1@O%UV>c2h@hcRkIGcczzNi z5lJsBCv1*H)6_E25hVm~dcyrOj9!+H{f+M6pO{vV*g!XHs?X8c(}HChXi7H+4t$2c zVKp1_(^;-6?8pytXNnywS``*CG@ zS@Lo*s@qL-YcJn@d5WEGHw96J!{IQmSO#yF`RQ!>h^eC+G5Wfa_KXY=2aJRy1Su^SJu1YSzAu^;)$Rqj(=B5GTeEgCw3xj?GDqYZz1l0G^Ub;i)8f3 zU^7pe3lFgSNDZ4d9HF#RAy>c5r$LMUpoX{iI{OdZCb#Ad&=MSp=YArIX9m7evq@Rj zH0T-8MlkmLE0m-4T_{QfOxU<_27R3-JNEugaY+^0xH?4H4Q}vwvxmm)Ni}eJk^SDm zJIaVcECQAR5qM|C1aKe?=!q&A<1A{Imau;3V>AxrA&N=E|2MVtA!v4YxygtPpu6GF zH`g$XLDiIw_At3T0JtDFh&L2V-2~^kJ_ZX^sUT`Q@bZ9%U^T#a$*mcsdlFh8^ua6CMP_AMt#^Q8?6I-=Yc zdIl-j=;i=kHR0eKB%&()R+!2LG;SW(3Y{z5vbLI5Kfsa3_nw0000 zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*da@;tSMgOsiUIOM}IT#~)2fh407fi{LEW2Ee za#p1zl1Tt>xC5AG|KI-}^FRDcwPX`hsk!BB`4?MkzVl19&%fuVv+=&)f5E@bxzERq zORp22LqS(Qf9CzVzH>bN_&|xTw4yiMJaDjjUO`uB-8VX7{*z{cYOo zb3J}o&g=iV?=7r*-Sq3FtYEQb5B%&YK!;`ef2jaOxflZ-90Ie1Oun)5{axagMa?znw_PB)oi z^xGGHdOv-*$7(3P`FWEP^>W7-(z8scCw_XF z?+*UW>-|ROsC1oEF!Q*vVqRVa$TXBWy>k@_apx7&bmMzp<~^-H{wcA645l0A$^yF` z&n-rBKWZ!7JO{2zHX8f1hAZoJ146{uiOIM?25fdAx$JE5zBor5EBWcn+=u7~8E`52 z<;)>BA!L$6H!wDzDehkDlYb5aEhHj^Op{bK5Xp)$Q9mVCYN)4>qDd*`R8mbX^&E1{ zDd$|WFsheOVo4>JQfg_X*HB|kHP=#WZM8Sw0uYv3ZpF0PTI-!bJGbt9x^qSE!;diH zNF$Fj>S&`+%4f!zW}ao%*=ApUg+=?XyoyEu&R zJ?-@Oti7`OIcx4q=6;_w_sW_w7VmZCN7gu8%G)iR;3O$$WXwlL#)~pQL3`!QRu`jJ z=9DwrJWY{2S#wFrSxy-vgK;~bj(gADkIelxZzjcml{f#Z%o(NbzsQ_X>Ry@qId9)& zZS~72;U-93sF>Pt0Jg(wvIQgZXmh19bLfS6mK4^A2doUt!VMaV@*exg#jv zJrtNW^fc*}7RSi+3h9czW=LhIl6UoEu6=wOeZ_oQ&tup{o561-uU1*0b!#ls2Y^@z zO>(RI(uQv{TUHhN2*>#mv(w0aucDu)>=n+@27z1UelF7@dGA3Kn`>oJF(;R^%AAB+H+DJgy3PrALL7M} z1S|9&&iiTTDEu_#ox&MzPp+eQ4yjzc#d303Rm=D~&(qghrjT844l5N0>+0gOXKVs5U-t3f02oGOfv#!yn3y zX2|1?Hcq~?os^k=zFoj@_Y!9NTr$CIIQW<2ew)1VUNZJa#h6#ge5i<@3*tk;d{b3KDXI`x72oZe=|Gtae9qB zG5$rEG#I}#aNZ5hU3tU}&cjv>(t?E+tkX~{DM!f9nLSo!>UQ@yazl`0O|CG@5&Hh~ z&NH7|@2@&nG_<79HaG2Xi#_M^n2c@Y~0(xcUW%r3{3Mk)P(CTS57p@ zKb4K}YG>}x2nCCL%oY-cPfsZ`B*8)$tua%veS;~pHG*Vkj&{$;J@WR$<}@p^wc(C3 zDu`J`$}zXT>X?~+OR*h4-NsCx;`U0r4W#ctHtiDw-L!X(QapJt77S$r4|rf*JL#_E z=~AYZDgg|spyfb^me%UXs6df;HWN@(s z6)}*MJGh!_M{VsLDi%<`YQNz5X}%t?ZQ4Fu1~(3{Fb8!4oKSY1Yv5ip}qcxM$sDQ?*4A&(1h(V&L zrE(X|*J*tRIKvbT&4|#XxZD7%T|L^l0JBe^*IJ)65&gQ-XdMSomMiGD1L<_x0Jk~2 z?0KNc-1As%4{5iP?^&Q)H-f zS4yW=&R!0&YsR1;8I_Mar2HrZ(LsK7Zty3i&ctT2lo49HB8o$$$+7UAIXl;=lwx;m zW`gieFX75ItJkF1zB zEuB*D0Vgm!<|cQDXY&zqw&?~#+MDbFrUB$R5o=M@R)pGbJllp9D4|m`g-w08=9ynU zE5^Zj>Tb!zP2*2*6f%Q#kgCc`q$nE}PT+?&XF?_5YM~*sa(}D#>rdlw3j^*Sqb-YV zMDL7xB~-bH89@R(S~{fyR`)6jBieB(>R9c|SW;)&y2M+0qc;geOJJGP5&(n?2M23R z-xVJUt#yPNr6N>9#0-Mk52l-C$pkhNbZPvcEG6e6F~pqgyjWQ&nCmfU7)t{p74AC) zNKPCxZHFt&HRj?NDm6jeVkD|q~BR>u}X6%rvOfX1a@Y%}r z>M>0Ww?lU@Fjr~V^BbjO&GX{qm_bycJJ=h|1jQHQE?g0aC0t9fMIo;G= zNpfTeVGIT!17JEN!SQ?2PW#|NGOj*}m@;A%w36O|Ke+%|)B&dsZS}4RO-KOp*eNsF ztqKZLM8({Cz=e2N4oIy=Fw+{>Wo=`fu_jJokBoqzJtOSIj3^c_1VfgBx5$;D*Iq0# zXcK0X1^7p5qt&e04haM>fh=y~42{IRd=3a43<}J}%_&0XZ3e=j6{?qNggmsR0k8V0 zjd@CU5}n`>l8B=?I$*dq`bu}L0YoC!_nksZ;2lEc?F#etlF(xKaQ=@^n@{Hf;-yF+ zAZRL}a8~D+Hq$Q=bw>}PS<}q82ySvvqCg!&u$X+w#a`8RF|&FCj2)&Zj<)&n-(H{4jx z0LSYb3Q(GGNV@?0jyo<|G83KzwnUHPq^(h56tV$BGLk6QBDXXx0J3Ng6b58fB0`N@ z(PywSvg**94h0?w9rRfc*V?g$rHltGi5A`~cH9AIzBx&Qp(QfFRCh(JfHMFU(xf3$ z6ABetg>Vn%Z%||fJZjRpEM>xpRbYPOH7VJI3&6I)wJg9k8eq{M$gAo>O3Jaoc$#|! zGrP7lyszq!L>ypY+^Cd$RApl&E9xvL{z%1k#qKjdS_2x?!x8`t7BdWuk(tjtB1Pu% z$j)om6oNn#03CeR`wj~)CJj!Y$WUH%K~%XBS@fCNoy_o$nW3f=vyRB$sqSugHfEhe z^IJseBWgY(y7+@pkem~4_2t+NuayeDEM%3BR52oHZHIQbPZ=t_K?x(j%M5Yl41gTu zXaj$22GW?%R7z&U_fO0@Uam;Zl9vk|*}LRX*hDv8rU)WB$;0Id)dZ>F0zMkBH$V$_ zH;ATdZxLFvIU)Tb3}of#dDc{SHkW}$M*(2KJPM*wvkV4}RHyZKd0HF}aU7r)^C~DQ zLRSjlCQnQWQz%g3ioSr59Wv~9(HJ0^Q&b-KMVO2gHd|Z-mBhGik8J+%mA&~h3LEoX1&jOq#0EP}~xneY2Xnm8SnBxdC za}sN|Nwj-mrDfT8(05(CGv-d8OHX;CK+9+_66rtQQX@vPwCcXnq-3+5fHcB@Vt&AW z-!GcW$KIP_-L~~tuO&TcXv)Hz!x?yS;sAojlj7iG-Byo4SQh-6T;%%n<2=juJ1RFA zM?H9HKt3YCg4x)*Ox)qi6p|-7copP6_s5%^oNy(45J3gHc0uii5p zqzsr=mX^?#$AnT!$6)YLYQJZ>>=ntJgN@?AUf@ZRIGIGGz(<>QOAkJoi?3t?naEOB zmpj&`TVYSsjMWW*}Y)8>;?Hn@@Et9orgA!)dR+~s57D3*3vyNF(pC0C-GFFq# zK=flRAp8e1UpNJi6z*7O$kk|f6&h~`ECojfZZgU!B#I$wgB{!$ zK-|j;E{tGePnP^3-j6|YpK@hs6BV_=~IXWqrC}}5&(7-r7=;atdN$(n3 zbz>FB`_3oDkMbA*9H2|JhgXB@%y45*+)sRVNFBBI_P2Q*z!4pqxhK>pk~(6L=kbZPHExp z#I<9%I!5A1 zNM9C$X2)68@I2MlGYL`h*4$t+i@arNXcj$t2~;QknW8P)&Ql>WJMAaI94K6ENs`1Q zRB7rW6sc^d0D0SzIbjsep8O$m%I74-oGjL zhT6-T95F^#;$HmIFUTP*;4%O!sNM=fqGNbamZ?N|-^v6Q+RjGOQK3ydv&B-O{0DV+ zm@7BLjTSJ4V9T?j>nWb8ub)8kI3@o)W(oMSCXUzQC}QnE0pr^F-snv2I3@rUcnQbo znM7(hZx1rUBs8GDjz&ySNOcE^Y`hIMWtWV18}ibq5L9PDpZW!0B7#_&x7c449n|h4 z|8)tMgVAuOOyStFW2&e<`u(+mc2zej2%(RDZc*PGS!jFhPGCOaZHhMOM1u|?=$6sE$Bly28^@ti60#ckXv5FKAaF_m_ze9i63$ZBnQ>P__NwFzsbwLsx9+fas5qg zneU40Z)(eYS6u&CTj^IV5~&@a9h`=(^z{)r8cAHM|{i{DzrnzM=|T$fZgN1YSl&pHEwd=(AB`E zgHU`#U(6MDYMU8W+C=Hfzf4`jX;hlenxHlsD-gYl_QKXQ8&nDv>%|&CdTah46A$D{ zLFl#`00006VoOIv0Q3Of0F6-Tszm?*010qNS#tmY4#NNd4#NS*Z>VGd000McNliru z;{yl<8#}<}apnL31f@wtK~z}7)s}rsRdpE0Kj)nL%H?`_lS}dv7z(~28Yt-;qs?a7 z)(bP!Ij37@I?>FR+-g(XN=c1IG`B2dgqpQrQYuFMyjAC;W+pNu;e=ilKyCw8qu{+UC5{5UnqA2YB z>JVpZ8j&Q4s;Zk5Ecu?&Fbio?E0rD(i>w_iOdi4IiuGs#zcD=npaKDd%q2PsE->yosSa2IdtF03&Tw;ZSYX9dl{ZFGNjyn1A!12;2BJv zY6`j1XH3-FNKA@lRgRk<_TJ$S|0O1l9?b#u8uQX)sA%pY+G=HAmyQ+_i#Yx*qH_vB z2}QjIlokpg58h%ujm(>6r>-T5IqQOaxU!VD7fi)HB?Z?=7dLKw!a+Y=exNbT;^*^b znUh`{@p(%TL{mt)|0N;hW}gt*%IlNL__^v5;eqSSS^PX#lJ>Cqi*t;!nn-s?vu#%$ zO5Q;_@)xnH*Gzg&G9MS^kTaskFp)8ZBz?MyTK^>*tJ9f2B7qC!lIt%dGIAwV!g5Zm zc^{Lc<9lu^5#cH$tcOWR!$LZ@@71#A%wqO^la1L4`Q-_0c=kKeMvY?`)i|uZ$VwC8CKn&2vGQy)wHumD1KNv*PrQCnD(B(vlGV z?;gSC!XBUT#E#X0&{D(noH)+yPki`X&lUC`xsTnp7NQN_>HY&mF<>J0Gn_yh8Do;r zg8_sN&qJ>SV@Jo6*xp8yH_8aH9H2P;K)gNOxLtN`bQ-YlXfj*g*hzQIZ59*<@v0Ab zvP&vUW{YfpZ3heYY)3Ig7>2{`GUIKzi%?j0@YnG(UYxnJGD!U$|D#Dcs3EH=cp46% zhb7?{mH z0#z3UhQKiiLEYBZ&CRpdd|N+VnI`}uk&#)mXTG#LC4EA^WHOnD0uW7A`x>v;{doAd eBSmy|)&2t-*#L`P+>kc_0000RxP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*ra$~!ag#Tj|vjljv9N382!CU@(2~rZ}dT!r% z&1jN31}K!t%EY1B|M}lz{)c}lhiYOfHMg8C|6+^HcfM5n`1km9Hs1I9FZky<_xZSS z>3PC)DCo+^pLsv8?;KAbH+ZOHKc}0R82$FR z+j~FpxQoN2%+aoSDb<6?SH7D+%zIcr{wc752&U`i$^yF`k0C~K zAGsB7oCDV-8;yMg*_HJi02eWL!Wb8bfXyx>mz^!%7w7O}B|aU?eTaS#0hf|r&KzPB zLMAxXYi{l-?q2JYe;fu{2t*1w6hcD*5v&*!`BP#ghk6Prnv_ybCDqhY&mqU0a?T|S zqk0J?mQ->nrIuED4K>zOb1k*jR(ta;0AZ=+R!pm{wceSuGj!+f&K126Kf;J3jXcVz zqm4dEpBZPGd6rpcn|=8e7UjS4DrVKyR^M)uN;~ef^DevYw)-L1PB`(TlTSJIwA0_C z_KfQ1sJSnZ`*YOXGir)hyw{bFsByTIw;`P1Bq?S@%tuGWiy}Zmd&SIF7o%6?6f@gA zMUgyFb4kHjP7x!5aXX)mdym~mx(w0I}ej%9&@FbaZ;1hoRf|(c;;&4i{FoIyTE1 zXXeV$GVYPK8M~f=<5s!%WlALPJ@8_4tt>L;D=tlf$Z7##cK}Uu&5{biF;?&b>#bwZ(Et7@Rj4YxQf% zCh*MY5+i;-i4mMMy*z@LX8(Q8AAIA^YA21+&mNnmWEdcDh|AJ}H;lXfk6@%b<0BSC zqJE0hYoN?aq<)?k_w#p{*Ts$`KMpsKum>rtxR7n!ZwlFhahcNO%Ha=bM=|7aXB#J9 zYbR-@&$kPh?p}hmk4q+)O%MKc-XDWk-c!bYWQ=*H%u7amPl%U<`6-`nz8K~#pXa3a zbw6=a4Xjs45P;auHdu1mcXV7 z6#FhcwUGq(t>yp}Tc9TZwawLiCLSGW4*GtN*w>6q+!#p68Q#?aEdCoS%_lR7RoEE@ zNB#r%_4GU`oE_&$*wAws^qZDfY>-o>`*qia{=hId@t9LwSF4O~)8aKj#4AE|Tc z>Jdn&KUe)*(VK6m@Cq4UA>k_^+_Ge`*l$R+ygorf4(AOL-T>j3ioXS4A9!JY1B@3y zc$xPV5OA|f^A@I<|4*3k84~_c=9}MA{{s>}&-@!Gm|sxgultDkDeeDFA2DnFeIGe> zY#(!_G~jMxA+s5X#7b{jb3^$(Z<>SLj96D3u~_P?2S2FRFq!6wndZ$spMk2E-QcHf zxRoeoa(-<;D)oV2ij(CtpNB?x#u&w=iWb?D{vgVLk{t?U4ytIT9ZHUr_Gwu>#!`FK z^@$5p697?4ccMK7KD1jiU?=N^nYdcrge>8!1}r4TfkMulT+QQ`LOqiK_CfKuiT_e zVP9^s#IkdhlU$uWw^#wx?Op9Zr$ zYCE+<7i`o`8K6^+3pjzfjxpz?`<)1&EZ93a!8{wjPApbK5;mCs$>7n%JpspE8tXQZ zhsLtl?JKvIhLG&U)5Qw4ZiRCRBUr`8`Ej>Y;M72~Q5^7NL>)lU`RX_hc=d#5DZ+#2 z3IQTzXf~T()iu{#7VAz>55>-W%xI^OPi6Yk6Fq+@eMjOB&hO1a1JdZ$;0Um4uDS)p z3J5CAj`FwQv7vqr_8_HDA9xCoCT#|w30RW~1L9o|f+G@|Th!=)U1nN12|T55>=bcaQ>7p1ovXa$ql z3=bqYQgW=0ber%abY}Mp?fNKZYE=6W`on(v1iID)#@5``vKAIFX%*uE{@uAN*TtMz zur9hd<1MK~Ga&f8!Um@zG`(EBCUrwXk+n1hA@nxFokKBoX-%-6wjp#HGm+lXax`vX zILZxh3X0_9gLPI)xubq7DI2n(R0iv;3s{Dr94W*5o)(-As0SU-&sU%(Yk50u{-5)I zJl*hwn2*Q^GEK5CZ_%pgSI&SEZ3E>m+O9BsCBS;KJ-u|*OS3RG_MLKfOA!*c5)`zW+CxZQ4FKd91o6gDdbi|Dl1S_ z6~y;qte|J}$&fBIEFWPS2_>+gta-zqs^3(^Ay0a1Ifu^0)ECzDI5MdUVS)T^2hpB* z#t0}5m{(CT2W0EQmkALW^#Ly!7;aYO25mnPx}NjF7lebWkQ`31Vy=uzzA|Xk#zWfy zWs*va!C!x9PsFM16ZV|vpwWlWC|9xqbb^$1IK(nw7g~bMg67S&$^EmL5TvC+ACBR_vV{|`^+Zaki?ia!k&KDi?V z>f*4~=C-XuS#5XoA!*btfvPaaF!C$tEi$)Hl5WbobLD zgy8^-E_y;8xfTL@96 z#*zKLPHsvNAYe?*8f`U321!Qeo^cG}N)~LHp*VVLq}+>I>fOg=3jA`W;LQv0MNP)SnCjLD1n)C_A#TxRe$$wpP;U zU#E%#slC(D@oE70;ZWcsbWj&%DSIs~x+Z1?m&m=ENchsx z5OZIHs8wyEvn+|F6{LYNp-TiD1Dm3xVZ`KJmS{&uk$nA8t`uQsN)#73JMyGHLX95L zCiV=>y4Fbk9lHrqto@_X3M`?uXC?kth%cv{GzDE=5`sVIOoxW;MtqK>)xQEX z{XE1DuwgSSa$A^_IGXy2Kh;RfNLRy|sYkNq`lFFimtxH~ie=}976A1NE=B@wJ?GE{ zfut2HGO5s;`HJVq)aFOV)o{B^&ACRlh*zSm;cj?Hh3aY6o(bR2Ta_+QOZ0o}UhHy6 z?|>6T95+t-L#1I)rw2{+BjOA8BniX>WNY`HIE7tW#G&DoG zSoZM#3PaFF!;p@fAgQ72&!Hz0j84xan~OFC961MClR>_(iG^}J6^cgRn>Op_wkQ;3 z*{%bjXh^qNs5QvWM6I5kBpLX_?t)KX$s|@lo~y9|tk_PmI4dS5!GdCtYD4#TMo*Xg zK|_}Z*&8hoqBK3af+b8hzyoj&l8@H(!F9^9Ej>=o3BKnuxnO`|kTpTxpo>K+nWj&D zd2J#js8Oe%Qd0;cnu9jKAPOC40(p#VhcqRc$50O{TN+AYDYVC;tvPhvhV*c^HXqH<8oN89-61XK!#GQWuphfXqo6mn=9Z+ADL{ z1v?M2psOFxMKS|{;ElQAVmK8IKaL*KgaD=7ZPXIAd$9-Dr-yh|+l;Vl(b<@rHfvd- zi9;QcB_W!C2`|#Ng1XY2_>|bK=Qc^C;X%gf2U%NQG(F0tCuy((VNZ}17;EwQrFwFw z$++^hd?JG>x4#u0oY+iJed#nJ4G$;zEv*xkq6eWgf&~4@k}6i*2T-4(J=C20L~P9L zV#`)Ztu3)s9cWUZaCDj2YxRx7g7QMQ(kM{`pL0`a~+URTKF(s?_%FE9h8mfJhhGL>3{Z87Lmcf|m%N%GJ%lM(o z*~3vJR13L5_e|hwiBtl?h08Td`ap-Xzq_FUUs0TU+LQ*iwgA`h@ZD;tVreDFa6~20 zZPdSq$BQ75XexBSROi)T^&Gm_d6kkj9s2 zqQ?_MxKsk3a;#>aTlRgZ$!NQt9Imjyfx9WSgy;uCD-0o65cdA8;tJ_mHvLdr*6@wv z&0~{^e9Xw%rAI%*Y5%!7Xb&s8TQ~DUct=}vDkYdJJQ+#EnG9P1lanF)0V+LPr>ixR z8$}wOcdJzin<#0uOV8T+BbpYym~2GSKx~uk6m+%;J5B8n+zyR_fPx}d_QJ>dN*U_0 z_V*lwlCaRPS!G_*>i4w$2U%;7&K;_DyobX({OlUVat8piS#ig3l(4^h&Q8ghq%x_=|2e(1(H)17Vy|4gBM>EGe97NKR)AicyL2XlCMY;V`;pQoWuZSGPMiH z*12hsX`qg^H{j7t+OC+5lB^p>q5Y_>9~*4_ECIR#8>v}p52C=wx!8wbSb(tY!y}Zn zVcdaT2pYgimu0H3JWcblf1@4KnOiXMgYDw;iE-9en0*X~cGPC$);1=bHg0XRvOufn zqJS?#U5_f>1~l=GSYetV>3yP-?m*l6$Hr<%dO{QhK)n=uKwFFNzZzg(EO1{YB6shQ zGmqNIHf=Xzn7g#5D+WU z@wy80c^RzWIgOrSG11lA?)rh5^jxkhkI9c(J~_<%u#m3~d(88Tvm%vNBTGvN)Ty7Z zT`Ra;JMWkd{OK`ZqyOp0VsCDTtB)-u2l)eP87WBHIA`9T!8b$g=&SqOC5FnMg&a_NeM?STbmAh z2ikAdmRuivXMI`eH9Tz_#<^i^F!HL03BXkbYz;h88AXEXw_>zau&8<{HJ~Uk)HZ=6 zMB*0xYSRK$73AtYA;%xuE=N`W-9PtP7E0>XJQ^Z6!%F6^ES2m7^Z;xq@c#kK1Xg(q z&_&e%000JJOGiWi^Z?!fjZo>TMF0Q*32;bRa{vGi!vFvd!vV){sAK>D00(qQO+^Re z0|){pJFh`6-2eatok>JNR9M5+msxC7WfaGM_s(r+>r7{8VOr?4v{GVe!O~8P5r|+! z!6o1ljSFt6i5M_}1Vi+R#D{_pBv3V?rb>trBPwBy#6TrRNE?=73x%;m8EW@-wtHu$ za~B^_DX-jl>3RC{<;(eh|D1FF=NzG9+z^0(rfEA;a_V8jFhr(-34);8oVGTL#j*|P z5IV+<23eL*ecQ6j*&pgh*YxSY1JNQfr+f}CZ``anoz9iQ*`dky)|QsQMQsr-e=UgF z4@fdPz^#+5jCUU+uXsMi55A7KdO3m_EL|razDTX5lQ-Yl)FmkiB@p#Jg)65p3t!f8 zFv2YN2tKDmQ32#R;mF>D1j1jizCNF8@g6pvQpm5Y$70Jf5fTwsSsl?#DT*RjONMAc zu)9FLcb^4Tl`W43QKdmtd7R9oPKr2JyKd6iF-WkpoyJ0_uM#O3cB5bWgGSdzE}Tyj zORpg>@Y=oCirs}FS&*`kI&wxQH2fEF&Ji;v& zH+FgeIDBz}tv>`!VXIS#WB}QOGtB@^1G~r0!~k@jh;i=NO==xbTm&=Sci!3@;P>7> zdqSWnIi|2PQ;9G+<>p?9BqKN+IY@$)*qBUT*ujh!w{zu-t!zIU$8LiqRq*^uFQL&4 zpMIYJpf?N^6~&M?r7u&6q=A%K!OA69wGuu*KsJQntY5(>ekdzM{#PwCnI#}{w%tWzWt@@%5B^FhnGHQ{8lf9y zbnB1t$|ozZTOxGyxT((U;qsYYoP|DO-a1nya+hjpK+re`>NV+MVH zLut9e+WO@rr39C5RiNcT$pVeC2|veMJ8=fyCRPx@%yg&SrCQl&iuU{Hm$vXiqK`+G zm6K6zxZPve9bsNt;^6Ulg~Spm($OIM4#@}=YY=PRLKmd$rB=3V$hVY_T0ZC9rhQb` z%psoEiKhpNB*7ReB0id8|K3CVe%*=t**$1hSJtra$ggbPnMN+rvh_6Y{E&vK6I3+# zC@FCh8%c1bE5iBXUG!>;DO}o&sG6JarWELHB62)}FW*kjc(yhk4`S^OF8$d|+t2$+ zCuJrz2{o@2XY~j8YTf|RYHlW)V!r$wP000>X1^@s6#OZ}&001l@dQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3>tmL)l^r2lgjZwb_5Ih5zj4rclD8F?d8R8~!k z*V&cghF^CQ1Po>%=w|=l|8va${LlY57fLlTm6}`5mjA^ToA3Ok+V{Wrr?c_?{{GQE zKXbo+ZoYru`6%#bc>m1%bA9LY^y3R9em)<+es0SA+^2qS^!dTp4TGNS`OD|J`ngf? z?{oM0xA_`Sk3UW4^WWU}7S?@k`uo3Q!5Ay?VerQ9z~}s)^_0K}LJOTg!~MPJB0lRO z@~iV{`?KxL_g{ZA-uK^rvz_gq&%>{#FgAZK$iLa>^SMiZw%xVRFRS+B$-n#{4L|J9 z|NF^y=d60pezWT_W2tODllq#<{Tml@lNj#TGQSG{CH`LSug0&+6elaTSk1w&i61$S zwU3K#x$chJ_s{7jLyUfV% z^ON7$pB6T-g6W#MGQn=gyTwTEhi`?u&VlQajmEyvaAkdNfQfi^VlXbS0yevlTz0m2 zU!22^mG$Y2+=u7~E8tS{%bCO4gpgUB>h)~CQ{27QC;vVMT3Cn_awsef0mNd(nDCzx zD?ZdyNYSK}aw@5&mU<33=9F_TSs2wzD6yoHODVOq(rc)(rkZQ1wYJ)uZvhBPEw^G? zZLRgrLp!(be7kc+?>KfCaioz)8FjSLC+;)jOf%0i>uj?xzrrH?S6;=ey4vd7ZCq)` zop#=3*WGqM_}U34o^&lN+?&vaj&--sE6rG^O-nyHre4PnAKDi7aQa&~dK&-c+uZZ(-uKR!>T_?9A(*h7{|w@9F8lKk zx(&^>T^lTcU-k3+&GV5zo50#%4fTDU`|H}8^LK{$H=V$I z8zPJqIs@6^xRCqK5n$V=dRYK>*IHgXt+6^cp6@oBwD57rIO7J6)~p-c$;0M#k{i8@ z3@Y?A8^dj<#nQ|2Bl?-QPi@S30s&QL4%KZ#i0!!QAVOdSJ~AwbOYfnO>UNxaxdYH} zbz5t%R2VVTxwd{OaV_rM;y|`yS z7>~afcV@w+F)!l?@&bfbKN>633HohA96QvUMm@t8XpB#5Y~iL`aI7(Q(~Ow`IC0hr zCz-Q%>!U^o49f1YWu7ndiBED_7msyxYda4Q>dAxyB(4{Cew^$PwHZ)Ey}mrV-<~L%{?Z zOE9b`Yg72S`(-4RyWxaw<5i8ANU3A+1Ka!z>$gFf@40>r>fasG8_+iYJRq#`w?q2X zQs$ZJzqge6jO@R*l=)u2|I$+CUm4K9Vkz^pQ~l?bn(!k?k1e7LBNEN6H|_^S?zS$B zxf^BrO31axMGh~SFkK$YH1sv2X9~(|13Zf3E1>Gio=}1k!OC`kk>rr-I1rqGOi2@j zg;{WS7)J$r&jxDvHYKi-#)c54q2Zr4 znVRQ%Ap$dLXDsHh6C!v8c+KiPw!X+K6{fXM=ny*q9HtTgcb03cRQYtxY2Yg8GZ>F+ zUhJp1T_I@P2V=lvKwe}+fIyEK-AhT6XP$~a1mrz<1B@mEQPbQW|4B-M_9i0)N$M-q z65ve11EOuO$l|3YoWs@~8~2$GLdScwi>5Y zAld=_0KJ_tI}pAuE6Qs~cLn-3lJC+zk%7;gNRvP>Qu5~c>sF9YSn&HOy<7{XqK`-v zadhLl@EvOf@^l`v)*|0y*t)FY8#6uUlvCn4sm+1xP0R8m93};<+LwU|lF*q-`SFxa z1Z8qKc?+Z6v5-d^zl8(!Oot&BB=O_7(?`$Az{!s%FF-7+;8Z5Jku*G=CL?3N#DU!FiOJhmx2n86Lt@NGsy#0f==XI6~C;|dpse8T&>NT>YTT7uL?gW zEQrGFuxkn$fymm*rDo4c$K2j@VfRItBD5j*cMQ5shWBXt3fsZF=Vb8wqu+&RZvDJe zcbDSpn_V}qb99G5@q`}q7;qR3DMoNq{A1!(DG>vh*2ZE>07-GI6?Tn7x(GX`iJp#S zPK8j}@WN)Wf86}lH`Kt*@1aQTK)WvcDBB75m^tdKLktOyqC2~xY7Vev69lOmf(gjV zE5dXL%RF{XFEjx}Y=>tMngEOMcnXGv?&Q7@OZ$BND4}!=Qged|5DGxM58GIl;S$a> zdIuT912Oa!OI}NM?%@G^pSg}p_BLqGCvwc1q`T{Jm zcQh(sMrwiD7PDU6dXbz=p)xqa2I{p-JmZ+XDje!sW-)3qPc@27# z%!&gbD)&-qs2h4Od)kaw0m3QV+}?2_umrRnmg@@3yjH%MmW&CAz$FG;HG@AMhJlwoWuuH>#n0dL==!Mo3s~8iH+e~Q6xSdfYcT=gGvK*9E8#_&$pV* ztR>4@#MZ87AswMbI5v88c{E{230VNW8nGs19VunA_&_$%vCZ>Q9wUjnkeqXZTTm1T zT-6Zd(isH1o>d8638R$>);PPfWgr_%0<&if_-mKe;RXWhR53%^$eWT$7`S@`LT!RP zm8(s0Q{aHC#`FAn&T5M4Uc2W3Q$w&IY*)`STU_KoLC~Fb6%q>rl*wT?6THb5@&pT5 zCGR@KHO+Q{TqnQ4RaYln9#kozi3viQ3j#&hO_7!9ph}t=xe8BWyhk?TN#mG`0m;BI z9jPuXOT0J)?zG6`QECG{{RAB*f|eNr+n39NYk@)4fNN*2F+D_8+lvr{X@yf?ae$EO z2ydWkcM?m4vjXcjqzmOp)^9L}WfV6|cUlb4Uq$@KCM|A4?gCB8I3hi`oW+I9%QKB= z`IV3@+{-|S-%z<3X#zd_ZiYI3SZ*(r^JIQ@K`Jq*{{4?KNT1tAQ<@km2oB*%<1q`s z1~{<<7Y>2TJnl&7+&>4|pCJ%e6K?Kd2dHd|IQBV@b%XN~d>pA0QP5RIzRTS6$p{#% z7sMQCve$F7&yD}P)FjfV`~B&>M4G>8AT7-KNV<3~0 zFv7%sXLDEJT7%pUE^FRhky70*U+Iq@)U6c=mTXL%L*8!4z!~oXq-7k60tLaU?Jk3m z)HE3)Y{3>l0siusNRi;qRdso6VGqWUsgFX8ls$mQmQx)&sL$5Vfk6g?Gxl(#-VV_GtVRwM>?i~eyKbMp&6pk=}hKr6|2_|%xpXAJq>T!>a z5jWvZ3qJwcTX<;`H+T#eR}O^nJXLHJ=MqN|s|Kcvfq-GvLYRWY4XDI;$q~;*1@0C? zRN4zA&KDl|wZ3lSBRT*ptktPB#;D&DaR)CmLUzm3JuFc94&pAvvLvoBJ=oz2vMb}+ zs=gsvuOChA4Mbo~(MRz8h+`!kp7P>&BQSo8cEBb$l0N;Jz-j;sYsuYIjE~r z9ipZNpbBT{{f0ZJUqaqUwBFEtI6lg+5jGjs0r!A8hcTb?k|!40%wZc<{eb19SY7wW z&>pxD$$;1fBls5zu$GxFeh0~7hkG+-76On5SBp!v8HWBT-Rz)(9lVk$1@nzz-=O(r1aag;pg9kerY@BuGQ!f#&&yK`zpFebzRD9_f*z zJSNLZfIl3LAjY0+nI=kz4`$$ZWK%#RY z_v3*R`VWg4Ecb4^JX2mUgPAV)W+piiQ<_AbolILH1Z{vH@F?^%WE4`N{T zqF4d>xC*z%{w|8Ug^fp3qo${8aQeVe2W zowyQ;DInFLm6*BD)~9ya47VqZb*^`?WRH7*cq{_|iuuGCob!8s7*oKiI*&}R8^&f~#Th%3eZVu4_8C3blu)eGbLu83^ zyW&0xF)-{mF89?Ou8Pa_;pK^cKyPy|(}o2!E*#O!g2kBzQ<@7*WLfcQQs3W*<{-q4 z-h5FfkpVj%bqW`C@zMK#f@RN-T#hQ&{|v&LV6I#Y)DrsTXC>_*B`RD!e1;dmptNVB zpl7WrT$7NqoH)1+n0KMdk(1SQ(DuJAe zH8`VDI7&^-N4+)?IOde&XPfYi`8L7e1f-qbHV#@b5agL4lx!J~&lwW-#h?DU6ywzo z0lX|@Xu>Q&0K1_D+?SycpxAXONZ_cRpqkN2 zMVX77kIH6I9C0;kbT`UVo}|2NbB@H0is2=a z4EZEvt#zbHvZ7*Rf;^--@GPnDEmmwuIyhHqp6mrilK-l}6z;1EXclV5>&rNy>VvRc z-&G@pW$+ahAsesnYDFT1ZlnI2(&nsRRvcRow2WCPP7+QRl2<- zYyV2JR6|EWBL$MS0y-=^`8ouMaxxU1H{ zw}QZrfg)Qj|KGltpJ26ZZ6~1IXL+)Kpn*T^twt%YEd+8TWPs;aZKARuI}Mc^ZM`?* zd!#=S7Q`!IBN1y{sKFiJS^!B_>5RZtyU3TCBgFyoEOWL8luv+v)_6(pBfsD!30{5w z0_AunX>Vv`ARFHcqH_SZDoD5+F(^G`@Z1lN|G{4$>WP{FdiVB7g$sLFhe4|a418U& zmto<-gxc-W&?@~3pZ&q--=HVN5FT_sa5Jth=_Vx+xnDojXQv3D>~cFTzD-s~sJ_v; zy}ocUL7b#@Y%##e_D%{;K0081YoWT}&!Lc10fM$579UmX&s$HGtHeohua-RwoSx2CNgcfe!w1XEO4M44>8c%}cadV*6BFt~ z11q^`<8fp>5{3*|v@ar!v{(&t3IrspvIn=tCG#+qG#v={Jl(P_BHtEb6fR1#2IjO& zlY|BMEEaC^FI=JMPq^%`O{9hS7~ef1sjwv`eYsB!BDo03ppv|Bu^;W475Mf|yfHuu zp~@48!bqKTo(;q(=M1ICfz%{ax`jB-W@97S)!VqFN^rpKPkucePeL>tr4q*;=kC#T z^>30Ca30J&GefRE_(&o}@>T8Zl~oA`;Gzq73jpi1B@{3O+JV%;qph(jzO&})zhLf= z`x?bTcaN1h)u5_%haHgFgw?{e2p?iqNNQQE`2a`9Y%e9ymDd20cnLKW63W4Y?zPa? z1yRx71N1ge%&SHuvS7n?_NUNl`gw~ADYIU6c)r-QU`QQMli83YLC1GCWR6mJPUStRy#gfK`IDi)Ph_=V%7GlfVtPm>l##K@J=>RiGMOFJIB@0bXzt5L3E%-H zX#-}$WSfux@IZ6i=9uso6xP?OIy6-@z?u#F5%#d0=C_Ng&P^L6Nku$ns&M6&oT?2$ zwJ~xnaR?t`Dd1F`vm z#O{N{{wE~<*MZplGZO#HK%5M*NxrOAU6co386N54ML1m~aY<+90SH9?yM$FkPiU|` zF%F8fLN^);l1aiAL_<3Q2Bd`@KymIyKj@4izB?>pgExTWJxA!hZK#Jl(QTAnDKYF!gsrlTOxDo zRzZ0P1|UI$psj4;xMDc)&Z7orPZ{bb-vgeGO|3{`M!X-Wv|ua$CNZlgOC~xt0v{1} z=N-(?sSS1ozK*~LGmL9$FtSaWOPkwOU85Cr%1Ka+=PsbqCh)mVSfEs*I)wnX&?MDG z>GE;LJGiiu@3R!HT5TkRu7a0pk*BD!k!8U`5I#7s8Wa4qJ*fiVF&h2_pmgJHE|@u{ z%Pm@!0AXpgS*g5=I*yQImy~+ImQj@u&O-S}WaG98m>{@}_Ns!kjP}@YY^!bXRXr?6 zk?;CMNlzxnvynwe8rRfuiq25BAo{ItPSC>t`3C8x?g6WwXgrdd2S)HEZN8!jEUAF# zV;LaB%Jh-RsFC1gt3DG@&a|gHdPgicSO5cbY9V(^NkL6)KefmVz#}hsvi8DZ{t>rw zq>RAwd-akr&|I{g2yy~Hnn*+_JK^|3nnpXDL(uzR{fM%2e!l2c(9N4aO=t2f-v~-L zDpqMn4-y;Uyx_QGC;))(AWCarCQdopD0h=!w<^G?p$K)97# zdn=L5V-Q2}l@kENn1LFiF#rXs*D{urjkZ0)BneBBXnPlE2C2#Ca9Y$~m~oL`LWX0@ ztD(ANeK)a?SOvKY>99`Jet+tn1cmF`L%Nx8fyT5dgGeQ_FuADh2#QCRViFKzJTxk0 zM9_2NvL`xqjDLqF3|*Rl1t!s|mNW3er3Mnn#07qmd-)a2i@IC754V6&ZoPD|;^W5_ z|Jc0)I7lzFoK=T}_VG89v7ZK-Uy2R}-83#h|3Lbmw|)RpRxXH>xamycjHH~{!^Qd7Jo%tj09@#M?+mGm*d$<^0+05D(__(_zf{C z8l*lim67$?29Q~Md=PK*5Jy8Ikea^~S_RByKb7IER)qziopMVKo1EoO-s0sR%QtX* zfE)w1lSeYDt5q0__TVIn7u3WK!j%k-{*I~yfv|+D8V#Z}b!&S`mRcAglj)as7?*Av zO!!LM8K4VqXQrs6yp=q<_DkW}08&7KLBElx-AV_aWCdOVe^j7*eGv`!srq_Os__vD z&8n?N~{a&ECBy_79|4V!~#$E-FA@?Lqhd4y4_Nmz+oQ%jgOA#kah zTj@dWs?DFIEnUDLEKn*Ow-~S`(Fpg;Z+XOe&ASuNTvqi9MkHbxh|dn*Z2u{58uiAV zq!31PFIDuuH$K4>R(~`=6!JVj8=5NJs=`U$>D(2cTWT#ZO3*fvgrN`Wm{y%<0D7pp zCcf$h$e$lB$aqlfad*3LF$6zpPK`QynM5GkoGM8QIdr?6)}_E?r=${PPueU{-CuAa zdwErY9vwtS7j-gKgx`9_iNxbKfLj^J{{d`JOjI1C;PE6 z?0ubRudL#&o08i1M%pjDbz8Zrdo3q(_(z2abBwT6h9OJ40jF2{qIRL@3SIjKVufp( z+0|}WD}=MF!xTBzhB_};bu3bNbZKxqY~pd%s$dS>d?wf&%59@v$(r84GE_BMtXv#! z;E-^Fp-#H0vsl`@c>Pwu8)L8m)(dgfrmdiIY?ns>#*G_oanT?Tsf}Egs=y!7_Bjrq z>f}nR0w7LzXznY`Bqs&l}$Qtj#FZZ1GQ3 z9Z?GLrq{Aw5$b$KlEtQA6tGSB6uI9q3i2B%3R{w40R1Sh(h(W$6v9J>$}?NrmqeUS zP2TjSO;na4F_OVjo1ilot`$3bRi7RpN|r?eR}eLT!ObPo&{{(4S+|azsJa5(#Xc2C zUuxM}M&*H_JhC4;>x2Y&6{SiM=0o+Trt?v(Q4z*0LL%iA66o7|By~6ZEtoptaoHl% zaBX$iL_pWWV!argB&SLNIEYewD?RC3&yvTnk()Btnle{ zm};YbYYiSg%cHguDFyNj65p!`|6-yFnGmS0m3N>Qw{MF19*Zai2wGd}*`E;cxG<;f#NJEAUm=@6$Q~}Cr@!;lFTZ<#S&2ZqQl9bcT2ovr4 zWzl0OXM&#~oeFxpGdgXu_N+QnxI0EhYw8e>nirE!(vV3AMDQyK{kEE!;Un3Ke?wrk+uU?;_?SWv zDV1SsYEtj# zF?O|9A)CR$XF~*f9e<|lAsLBr=l+ha<6zA4CLeJ2NAme=?lJ#*@^RWtY)d6*vTpeh z^gVQD_bz`!0aXE(;-R~#mBlO}YgeyMUv)}St-Dn#ypFbSka9`|>2gTPVQ+jpBSPLm zL0+SRWO zxcS#Rey52R&s{r}c|J8G4!EtzMO;DKX67@8)wTcQ1=P zBUZ~(r*@%!X)uF!76=qbqyum29TOQL9Mr`mgi_ITOQ;PmXzm*r0U>{M_K+ABx84ii zcBSJry|;VHnRN>)nh?thy#V~eLe-~lZ_Y4R%DnQECA@TkxIK>Fn@Hce-ddg@< zm|pt(P$4va)G&WzcqGk$Tt`}g$)COWe+ZPjt?q8W2>`2QhYGqds)y=i^HOm6-kvA# z>BLB{MUwXuQ0i}99*3|q-ECSCK5!j_CUUn9`k7d27WP(^&+R*;qP=WZGR`G{toQdJ ztRs@NQvfm{YjF(1#NFTRhSid=bP!yJHszPiu^rhEaZY;MrHdmd(+~%0l-bD{oh@N}M6ntjf?Ca^ z1ToZKMgOdGM{p*sKDkGoFz%j{V9N5%8koyB^OGJy2O4zTCEaSx_FeszJVJUZi^PySD&~95#drfVs+d09HW*~J4oMJP z1cFz4!>LBz?shx3m624Ax}Ql-G^PNpQ9I`4hQuymJGk67y?G(1<0IexpY*zOeL^uv z!s|s5v3h7#)&V~yXp>ch*%zFI7%fAmR8JMv7*(>LR9tkpKy~=#r zELJ{oDt~=(HpFpi%fVtI`mv()xQo15x^JpyRCTUHy4BkuT)Rf-nb5tTu0;aG4${7@$txlOhDi(Y^P1REt*>og*;(GigZ ztsgowqwE4VMmFvH(h8m4(>9UzW*Fbr$sLrnnxzPQ^S0%0UMP&3!~z(uEgnOLYfAUN z<3c~xy<;o4V~oHMuhB^$z<>6v;=9Vju^(Xgwm6hf*A(BFdMA(0*r_4yzWYd8CIu3u z_OrC(jlE{|>KJdI&MdtnL4_+FcM3ez-2+`qjToQ-gs(8!pabdym&b-S4ZU*&q^QnJ zLE}KyGNG<^BfYjH>Px#77Q5%h5=2n0x369stq5x!?eOaE({UrT zVwpiZGq8f<+bu*X?*vOm14f7&$Sq?3w*G^4zD7d5>71{QBO*wk(NOl?B0~Dn_iV2N zB^JDbr!mX3vzYEs>aP;1LtfJ+q+?fENBbx8({~a9{ZQ}eTxC~@5M@9XV*_l5@vDZR zUebH;FpYs%F)i+6hbUT6XO<2D zgXI~Vf`g4ev<;}^JwUxs+3I-ImRID??}T_JFKI21keAOzbx^cVm!dX9kq+>SvellQ zf#PhZVF^Gv6%%w4iYIW3!3x7swp>jo2h{tf!*=9)q6ZsjJ)hs|j86iO8a7%w@Vj3l zl@6cY+%Y#~*QsTGJVb|k0`jU&u3N|}Y5~-{Zoqsvvon@3#6!^HA*eJ?M%R|1jz0FP zS3LlU4Z^iZDx{jK6GhrfP<}qQ5=lS|TeK+-^bDmw(BvMc)f{#DWxgUtKTlR!wETPb z$zH9lrE{VE){qMH-v}_b4%u&=nFXNT96DdCfWVFXJN0|rKD}~lJLPkh_;yB3FI^YO z{VyX7b&20s%lL=F`aye$tnvx7kr|OzQe+-JveN)rKRu({oX-j9`Z>F-QqT8+W>sOg z6Xoi!#x~XNyUKkVi+d77KbRltr=NgP?!cEj?&NGEC<)K+dn+}kCPr|jdygZ zM~ur*@Wer)SK5RwzeY-JQM|2lZ3tx&vEEoVhu53^R2jJM_8ISDbtR&&$d*_6iMEb9A|=Qu}*X zi(XswtfX#V^P)(vqY6(l>1X^Tz>M|kfb0u$1YB(S*E9Qa*zjCibvm=ZTzkAk`EN`S z0du`rKI}~SL)`o)VPpOjH~)6n01f71!*9r-PQiM|^b~ZGQW?fm+bL zycZ65-hUUe6&ev+l0zn5^2?8ykxHd!*{Bz@}H1RK2$* z)y|YvdPF36o#$(+Y+=@#rAzKxoXTxr2+w<9GEhhzZBkWrp_AmFMn#C7s+wF?2rISi zb`$fsWjAb)q`1@PLq==(fJd-FDt1mt=DOQ(du;joZeOJ1z(;!+z@$>cpV%ck7OP>d&kF!OWA_U2P{_bW;8wy{6gom3ON#~bH z>i1QF2(fZE`GfL^)Fn}ec+{IDlK!=uHb7d7Yxo^ZTwc`_IF(*mm$kF@SdV}Gw0^4W zdX-1k(aHG`W}&T+j6uyw7h#NE&V>3wA(HV`AuHO_=(DM-WWToxs@#A~6?lx6JlcUD z&b#?Jb1MPumW*UCnyI?27t3w$bmDNReOgVbQ-hf=-&S=c4jm}hTUMA-8<`vVrb9!= zETp~pe8eEl000SaNLh0L01m?d01m?e$8V@) z00007bV*G`2jc?>0xTy%ubUVE00n+YL_t(Y$E}rHXdKra#(!sKc4v3BtDRk~WNT&1 zx>SiA$yOc7j_Wv1lTc{gYufaokdTCs-oPysN(mG=0RzbF_n)V|`@Y|bf84UYciw(q`rCR3kyMud z7p%e(Cz@vY%84%oitqW7B|3?uS^*f^WuYoQE=3|vfreOtNTh<=;Goo80;)KsgBuB< zmlO;;jY$eK*h|rHxs~^klsJ)87E1{HxGxlh1Zo(}`VCI>Ow*jvDY;Adg$g!4H7YF4 z-Qw!VDy~suZ-1QLzGj-VFmMAEmAq?keR7Ve%QFPgr%4`tuFfkYlzYpZT3*JMX1HnP z@H`8b0ID8iSLV3%Q5HGVL;2$mNwnF>nhbz~LUr9{b#WDIZH|(9lD6HlztY`-fPyU3ktWjHcNTe+ebfrjs=Qr4&d7Rapj_=ovaeS~` zpfRz@cW)&@J;ERWq=5X4&g94%Rc$|&S(&&Z(SJn2*JYL#O-d6cLAZtaO6| zNC5;sDIxwF$QHXuV7N70wZiO7g<|wUkcd$N%DnNWX~f!_#}!U)1URJl>%DT%N$HyAShQj)Z)6VCf!Y4s0*->65#$vXAA3GOeu*h=w-q8wlvWKcF~O?gG39jx4o@A(@U7kurLxJbo{=B^`KI+Fr^e~*Zbk|Th!?PR9m|O0ucs*G4L&$?k+Z|2NQ0*lZO3j~v{l3d z?7KG%r2~D}kNMrNUg6kro!xy&GR-L@&!(_qadX+^$~zx(ZFvvcXHOID{`MA*Py32u zStgmJ3e(%n>i9wIo}V&2{CgtjuhQNTN0wz4mUOOP$s>0@OY@`8qG&z!l+C-(XOb$` zdLDUy|KZa;gNMW?3l2v0UKN2K$r4L<5z=Rv9n~3~k|-6jWDY(>^r2Ho(GC#umJ7Bv z!IzI~jQ#UNU&&35{-|&0@E^YS#9k)mP4dNH8&{9O;G6&R=*?(?-u4)=+D*pCuDqn2 zd;c$Q$ewbt@9?3Mj~*B>!Vck~ueY^! w{5+9q?%Jt{o?pAMxIFvoPjb1pD3uET0eduS*hS22)&Kwi07*qoM6N<$g1DBm@Bjb+ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/dek.png b/padbuildimg/assets/lat/dek.png new file mode 100644 index 0000000000000000000000000000000000000000..9f527caab0d567cf1de300df382bfa42d7c794b4 GIT binary patch literal 6629 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 j*l_OR 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/dres.png b/padbuildimg/assets/lat/dres.png new file mode 100644 index 0000000000000000000000000000000000000000..35439b8618aaf70937060ca8281591a2da68eef6 GIT binary patch literal 13506 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 w@`L(7n+a literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/drk.png b/padbuildimg/assets/lat/drk.png new file mode 100644 index 0000000000000000000000000000000000000000..4b246ad629cd320fc936e4f4c26bb704e93936e4 GIT binary patch literal 6873 zcmV;~8Ybn5P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*tb{jjEg#Tj|y#&m|axk9LJE-ON7a%2>676!m zF3X}MGMNeB#*G0{4Z_inH}bUNf8-#Czo#cdPHpD9Z9=LovaX)*J-YsI*{0LU;@IX!X|1##!Ors=Nl{W70r{racE1}d2D zGFK+p?RcDGB=?1_aO)g6F4<`8qcvPv&l9jjTstus7pQ>EE+m(oE#4RBSjS3zIwSWX z`auO;N`5(Ws7(l&;?NCToA(rVkM+qvo`zZ|L<%_+N<#xttQZscDY1f~oSIs@Eri#UTT=}XRPnYs>3MV>Asu>mY(NXcD3IJ%Y zn%U}N^s1a{W}6pPd8+1;inE+5Mg`+`J{|X2yRXXqv2G^C|EO;Mca<|L-G8HUMx}dJ z?q}WJRBiREDB%`JU8ps+;XrJM*<=exn>(btTq3?X?}KjzxUr_oo;r}aFBU9=hcR`P0<30k+t zGJQaZg{4Vubzj=>ZDz}?LLcEcKVo*evfr!d=P7%Ib2K;}69ow6%ri_ndU>74K)3Is z#j|G}F05?z+$?LHnJdqhai3|MbJsIixK-}^GA)w#9;{+>tt=38ayhHa$x`dYE~j1B zIpGe7XC4DW6?zZn^JU~H{2201;S9GY#{r&0DHjj1oE%owG`^4X^tF~LRM*?X+j;NN z(Ar|U0EWdIoNM(vl1<^cqD!3d^+g83i>8-H5!39yzvi2@ac8xY#^`5{jVKvT5O|8i z(!n>3JO8g@q{tojlu><7CvCJdv0c2Gd(8l{s0WAzJ z)0!MP`~h|}Lmu~PvK=Ui2UJzqmuX+o2%f_JJ1LgxSA82{P z!Fj0g&5sO>e~4tDHv!U8KP_~}-x zuv1L4V6=9^_xX9Ey=(MOaQuMrJXKfrg<%*jyb4!@S6HNxdT|*gC~-KrE#^M?!g>tv zJj<3~*p4Kj9;?UdQ{`9(MvR|(jPv)r^E=uxf5|%+7{4}H{e~s8?z;T{v1EQC_@80P z{I(|l7EAHvqUP7DJ}7_%6$xAlQ1E+0KCj$!$;G^9mk0g)ft`Ngp2vmF%a#A0etrm+ zd5fGfaY*Pg766BU8QD8lcXt2cATIH8UGunbT|F(~I04(TOL8q8+>ZL}+ai^hnsMe> zNkq$3Mu;28Owgt2NAIrqjQhf`G#gNE`exKVQDTpy$R(~Z$5{bDVkxpeZqPp_`7(zs zxQamYk5qc575nLBjAb{t8?DiabB$4GH@hnyXImXgdtv+97|h6I{3Whvd+c5938&MV zs~~?4l>|SVikk$q8D#;k@h#z0QqhsVaT*~-9Mh0S{KamDRt zrgsfqvrPp@<$9Sc+*QDo#WWWAcsCevxWw23cDeHkUJk6BUK?dUH6(~&E<y#V3l`17luz!J1u{tDa-HI<{{M$M)cq9M58mYdvfPfM4st zj~MGm8?VrM82f}W`w6a=*}zr~wm9$}%<|AbE_0Q(vt~Fay@hcvZM6(+0@E!rrA5&= zrrZWe!;=upW7h%P69|lBO&Mhi)Qi?&sHZy`fc5cpg_LNR6lvyL_G(E5czGglr_}&8 zGqM653^|vjw;%-^f0nbQP;RFhEaPO9fTc4$@HptFv=7;3sfFm>PKM9HjWW#)4eYV> zuBf|P-@8!tQU@eT<2v$TxP2H8*g$a@KIwp^ChXb_nL!SInnP!G7eEdQSl0lL%az`C zE0A;}fq)1khXoygYb6HIb7UPU3laJ>v~rhd*1T$}1xkGrWH^0OWbz(}o(Zc)H@$Fo zA>Egq&)>8S_W9G?Unt1lEH~2v@@POKM1(_j5RY4IIr*ck4QJeA+@s&Turlohj6wL* z1`dcI@r3z0w|jz9V$l?Ct!{6E01A^PY&ghTbTFMhSPj2b!z+*{x~zOmpKoGM9%))n z2a<|1{5B#zwKkz<1JN1^MemQtebCv;SiLE~IfPgkc>S+lhLJGr-7)u->F3}f7KkB5 zbaiR$5w>cuBs~rpdw}mCQwf6`;c8P_jj}pvKPbb3Y)_b1+i}2QjY^QUNN5fh?uvg~ zyB&FL2gra!NtnsWQ6W>)YK?;@=x;7W((It;icAA1sX0O^!H!lYGdi^>4hGQxY|;h* zdg#QsSjuP`$}{eW>9aw7jskLxo!PL)c{g}-*Fhwk5uAfFSQ{$@3s&XHJu(bfjG*6O z49*6<0qK|uta#%{)WL}(2hrOggODzsL~ zaZ6YBs$ijUU?b7ayb=IE4L64jA!=y`mMq{SS2hPyK;tXqh!Qf$i_=L?$0jT|l4_$E zdjgu>D|CRu`yMbZ1=5@`L(6etkBR%);fq${qPHa&{li3M7>{#8&hWBDVb??~Cx3>x zETxLWujw5;SPdulsSP5&OMqwCfJ#`}7Cv2Ww~?xhskX*O4Bbr&MfRP;WL@bXToT?| zDLe0o+5)%9s0u`a7%Z3<)>Hqn_!ueTMi6MOxj|uT47=+F~>5xAyX2-`f)|H5`N1`7H zv3dy`0iAT}T*gZBap+7(Hf3|NLuV*>Ohy}A5obZBRo_%x$Y_%t|;b=1ar%uYt07PNgHc|?Eqfip14ja zum#BM>{tIPeBJOxi*$rDPa{O)MmpFJ$u`LcRDwgK34w;k(3XyiMa?~|0o7>l8tAc* zn2qw-tszz}949<^JYu>IlZsi+7a8phL?*dGP7dA%E+#U4G79D}W8Ui|ySrspth(1! z8a52SL*Bx|#Oy@UoD;ZWb*B=Km3d#JRsbblI1Ys$xOw7ICS`{SdXz?%^DR?fj~v;g zy;`f}g&W3KZM-SSBH=B#dy*`*J9C=63fcg%;R`n87bf0fY$oA`MG7Ef(fwkjnaMS) z6@QFv8EAbVAf)BVD{>;Va`1*ecN^5flcaFk56prmg3}Gg1e%IF@W?p2K}sT~;A`uZ zQqZ0fMv6C9!#f~d^p6W>JWTOdn;&6^E+SsgCr6NRbEae%AxhHXj$4!18WDa1 z^TgV)3am@Demjs~VIyq}1u72Xa+SHn(6MN?Bco3JX4yMaaj603~^@P=k;6U0S!bkS?wT(8~Gq(I0zrzNH5nwXP z!(2a{b+RO~?XWeF?X@+cWtNLo)UC)%$^0w42OXJOWw*DemP3DXsKL@9V{qPe>{VBn$D#63MgK66Cdei{qeHc66{>6>QD~`wV%D9 zLe4Nf9wB43`W_&#CAbf_11FKxkL005h=!h_J(=u~o2C01^4d|id|0~5x9+@5xJ-Tv zL*|tnkr3#HJ{gB-1e+gOd9F0zGZ2kqgd){eW@_pbbP-O8Y*XR-N^+AG!hrxW`UaYY zjKDs1!-wM|CqcAy+$k!&QZtxyFoT6_v<+6Wz{^Uwy-x@q3trR`;7~S_-ew6POY%q2DblkfI7ITMppCI&ybiyIZUHhnNqmz?2k2ih4-)6> zO-&C=L#cuFKafz6@y4Wq6H?)0FAnEcrF=A&L~@HPDT;2?NwUdhWD8V6W)J@FV_I3F zYlm5R>K@=d(g3MHp4)_ILc%ciuFaZeunb~GsDSwpD@klyV@D?~(v390Epf}%5&EGq zPT&LnS!zh%MVW7~!ihYdn362SkNj#yfs15BI}qs+C^SjhR+Z9NEL=gh!6U0S1^TJL zRiN4olsy&(i6+TtpYB*-h6o!d$AEC{5;8_#)c@CTl8hwDS^FFvO~N&hAOrfx8h6lg zd@NK;aj#3@us|bY?sPYGkOPhf%4DuaZhO1&^B^MGbqluS0pEMd5OHch(}Rnq?VY{E$@JZxoExrX*>B-!3NxzJxc zD8`G6x=WrB8V_IO`Qm94jrT#IvIu-P**{Hl%KB40v%*+jNJc z-YpVsUTaJ3T0hJvp{vrS*SJ9`020?0mZGeD0Izu6(e=`RE%%4I0JaGtAqIk02Q70f zKkQzL424?~fp z>V3ok!+dxlm;`y~bj`ZkJ-Yiq!{RnK$gJ{*$f5yJTlSYTSW}F zb_Xn$9Z_{l_pL}}i%}(I0re`hkHuUbLOwE5h8PNZ(Y=XJnNmpIwZZgA2(qA1W`zY= z;-*hd)Sd?T%}0^-fpv7KcykdQk3Kn|taop&9_7`wQBf{U^Y@bAG4jmNXG0D(cL~;; zI8W{R)J3DS*dC*u8kI&=G+H!g{y^Z8n;Wkfe1V|(4F==*hI2=J|B3OooVK3v4~%Hd zXtgbWPk@afQe)CA+|ce_3Anyo%Lz%?DN&fKS)hodtpZ2b45=726Eq$M%L#FGC2NcS zU>e9R?GQpTQGZY184HlxT$I1r!!qa*91nfejZjt|8dqA*abVpnZ zHDbDglf%PfT!t*y6KQ)6i(e^V#4S<56$p|j4Ri^()P>lTYcRGTB2o)?JNj7J<9QU1 zV9fYbf7}?zlAT}Lb7wuy?qCPo0V89%)qLrB!pD6ZPFM`aV*2*D-RAGuj&=9u*C73P zN~|?SKWbS?9%_7y;dKbOkzBuB>Tw}#mZGT!Mw%-yD}G?hNsjH}z4Mr#Hx%T0jcz`^ zUP2${(G5$@vV%|%nIIN-NM=j}n-jhp&DajmKu;|JadE@p?Er@ATU7LoQhoqv{ z`=K_??kZX)Q=Q}~`>6VGXBzibD#pHUNB6Vvh+4#5M}qUwK2cmbb)?k|eN=7WD0oZI zC$o?4IG&KmH|dhv)MtkW7dALYMi`Z1!5bP)q=$U;5pJ$I$-|sc+)iVv!~= z00006VoOIv0Q3Of0F6-Tszm?*010qNS#tmY4#NNd4#NS*Z>VGd000McNliru;{yl< zBM^JVG+h7y1ky=FK~z}7)t6gLR96&+zjK)z!!XV`Tq=SLL8~-Uih!D$R&8kOwOV6I zOw?+sQd3NfLP=X=o4(+su~9U!P1UxPCarDMD%zk`Yl=4@R8*u8slgGv%>cvP4l{H5 zP+Kwa#Tj^Ty`7V@|9$@bul29Bg*vAy0J))1Xp`ULe_2&kk*6R8K?v)O`qPpml>v1^ zom0*8cswVoDz+IN?GD1B@YBEpQ4&eEC$s3iwO*sqI88WrTe-Nmq9V63l!YlJ8^oa# zq_3+D0FA+f&YTz>SU8NkqmgXs63dE;ugKm$Z*EWGY)l&4ke*=2AK#O3>=Uw+lGx+5 zkXQT-vfdPhBqF9!IrQ|W;`Mr-6ID?{FqnpnKrrY>>NthDW*wUoWVUNM*kW+dus)qY zcUSbaVlbh~5<^?qAs=~tduXp(&MvK%49U&0o*v%TI$4sM%%#dwLW+M-wvqvYfD$2q z4JUIlyzFg`N0bDLC)8t&b8)-VhU{yh@{3BADGD)hB8}cKR;`BGo&c)}bIpC3+I=PD zuG|s1;sc5F1D;4^4F@Tie2V<>PV~YEerQT#a@qxwY4jRQV~yeXONKi7_EOJHu{OsqMxIpGTa|0_@KUIU2)fCy4m zbv1Fm?K(A8ZEV~+2bar@0F1#eWuTRp0or5m8T{| zMdGop6_E)FR?z*scIWAMlzaIIS_9*@e#YX$6f}a48b>qcXCxN9nZ=TK@AJ``-&mP5 zl9W^}^QIg4szn7s7#!CM0lo-2Uvl`xMFD3hh1nyn!As|d7Ea@X}Rgbt;|at!7SqUwT=)ae5v9(Om1W&KV!r(vmrgbyZfB z;2>SxM956lv#=nR%U7Gu2=<(u{Ib&RCr9UvGx#;b2&$qmRAhpI0HP=%YUJo^LI|t+ z{%%??Hv7squbCzQA{I;Ttb& zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*bjwHL1ME|*pSpv0K4ka)u=Lu z@9Xi?a$f)EzK^i(_0aEs$3iex;$rZ|@4$7wXN?k=L1>}#Gu-b*H*u|p#7|vGzsJtJ z|N1R>dHfzb+h2Etuc0tDzi!CiV)VLBGJcKSjnJ2@z5VLHzOagW8{$Xo&RO-G{j9FX zOr)}TEy}(gaew1R78b*OTjs0qKk<9HUyZNI5+@m3WOMK}jcd*m?c<_buDj#*{W;xa ziqUURxV`t2i0fJ<{LRmsl&CLvd?7u{gi1ChSqohD?{jf?+;+#Ct}^m+-02+;bG+ln zm-+4DzxjH9&^dyxa|&i2H&)Ec3xG^RnbUXfA|dX)Vw!Gz@0WQ`>&HJOHju$|!(3Tl zx8r%lNbUn$;pRDTU$W8Ir!-tyuLlq!#!gJe1u|f>3&~|?i}%Gj;#kQ~XXZXcKgfVf z$uDOPxd|bYoa!|;?yv*jLoFmCg&Yd0p@2wMj0yadSiw+FAw`o?%BiH9TIxCE zm{ZQVWN}n4p~R9(E~V7cO0S{Dnrg14*4k=sz6B^OwcLtnwYAnegLWR>d3WcI-iIGy z#F0iGWz^9|pTK9vnP#44*4buXeuYK(ue^#`b+y&E+dyf@op#=3*WGqMVC{qxPdfRO zQ%^hnJ!`M5e$ATumbt%Y&AqawjKzCj`N$fVOL=>Q6P+aGjEwo{$aql(0JK-mY;`eu zWllM>%~KS~lQox=oaK};G8nh>>A3gYePr&hc{3^gM|tyq%A8T^{u`MyO5H1SKlAoW z)>gkACENt53Kdfu4#aj?O}21E9&N5vW)8hD&yvC#@z}Kjv#Op0ZasN0Z}wA_1YCd4@?xFR$}Vbo)M9JbTvR z!pc_HW?AFRT)A4tz0x+%u4fRqRqp#TC6f0ZM6tP67Kk~yoK@x|)OxVXY1eg5xD(>a zGa+Rv~y!U8oZLwSc zL+}RATK&#slXymSi6?%2kxB5P>E%gcn*GmX{t_E^Ry%2oe)iZjCBp*(m$)q*bi=so z|42r~^5Y2e2zvlonA1KNqTmN>=Nzz0QgQm7}UQ5Gyyy>HYMlT|1K@~YXe_Ncv%l-e>a#IfRTgnE(` zXg>TaJefB@fu}}|!9=7q4Oq7C((`WTPA_gqo>tZ9ShS!;%V|iJlp~!F+Fd5|26`23 zW%XDm>219bY@|#{>+fiOruV#Tex3d&uoHU0{eoQ<=eI0VUBu{v3*rxo{0}l|ex&lB z5af4y1XSiTJz_Lo1jzz_VZk>N{6Uh++B=1Qr^xRF`L*W06J#(>`|t=w`J*)xG1|k9KJ9k zl0z5>pD{*6!T|f7p0>4Y>Z9)LYWDDvv`t=^|cXM zsTZkjRO(nOcjxVh$!-L6%QX$IP>!5x;hUtNh+HQ>5XEQ*0yd znhOOA|G|O*;-p{;+HBvT$6Zr0Acl$iNv#b>)eIK5xI-}j1*B=zr!bJCo7fO_P(KNF zo2A`)BO}}FB^8cc9C(XJtB0l=q#WHW+xh_+xUze*TIB8v3r^#i4*Q=eT!PzBzO?M% zcFc&$0Hz~Yr&?>L5#b0ZszS&p9}5Nf1D7Y1@)#7r#8U`VD)E@ZMvWlvVWcgV8hfrW zN~WlR%s5filfl)<6bSN^tb0+MA~NM&#P!40jDGUHxH=`5Nf zrBJ92H%6#)L(B=lF$D0$4(AoUHEw#Y)sVItYR7`qQ8s@xEK|ctv7Xes@TFu;Y)oRw z#0wUCy3aZj4H*wbfKxfVf6}%)5*!fP9p*^ELKi!O=-NxuvInhzKw(olV(212H!|f+ zU81#s@Iv!o6i^^Y4HiP)SZBwL^`SHV`ELIL6i zVJ-T-7U&UU)I`2Dp75Bj-?aB<-hO$01`4sLc1q~l!9!YsV4?~QffwLe$EFMP=MK@2 z%a0e?2=&t)>=UFIt-v@i>5MOq4Vpegb!iI9$TFI=hozkYik*f?99nTkjs9slnEO;L z$Yr4x)b%ZUh}+%4pSd7_48_PUjuhQMO|9p9DaSW615*wA_gfr9^ABG(9fpRIa6fXX z`F@usNj_In5|F}vfQP-xi@YGM9x->D>y_bfs%BnH*y z#zZUz7OZBF@^n^Q=5|X1N;zsHAbX{TFbeY~J7D{5;B5qPG-W62q}k#2&oja1weW+Ckz#5brh zTDGIxBL?ZrboR^NtOy?7OpUvjgRG-9Fx_%KjMBmcLTP@j0mj{&}^if zEk*#%LH9&WJ7hO#+`%q`{K>F8Ik!~p+>#zp5u~2Ps`P^>wuJH#mj&`ct27Vb;gSXQ z9?6?a=Z$uC+czKAT*S6cF(y1)?gsOTxGDQFZF z2cntwo=4l%A(F#Yf|UVQkDl~TJ9!9>wk=_fa!Co~#Kv4GOFKdNnJqTjF~0o382rJ z9(I;L09hc3<#<3=huRKfiWTBc5I3`N>m49`w#zCP@m!Jr>I0}Vki(Fuk>!GUeUaf%# zy?TSCaS`pyJl}8nbPvb}sVH*LEnR#l_XsU#Q>B2xbXpRLT;YOs1sO=uw2mRRKVCvg zk-LGRSoGB}r8SJ~)jpPK)7`*b8xsF?^E$Celpn38Sr`_aVM1U&_^DVWQ$k?S;bk?@ zN7Ts1UBfmh;(YF+Y@oS_um|jdJIB3KF3y9mjLM=JpzIXre=OCy= z02V5$-8(SEhQKPSCrAu-MEpX5=t38v8Z?BTh>S@3S)TT9OUe#vKv`h&Cdg`amC^wPr)hHM_TA;BOcBm`H zpcc(Sj{w{B{c<|#Gl&(Pl>om1!s$?UxS+Cem?&&LYCIxN5jIHwvPVx^35}TS!4WJ4 z65g{JG(w~BD}D&v9G#5oL`}U4JWU^X0X;=y^KA=!|htai~@+$^A*P_9|w!B0JpQe5zAk zL_GGvV*DGt3lMA}jXorr9|8~fm2#zvo=3L=Lv%id95?8}5iv^fIzgFm9_T3rs~wop zdOvA4~mpG1hgUOg;FBcIO9>MEl5p9a>kS-Mlgox0W%0E#e(#}&@_;LTJRDdxL=Py z#^upjP^1}?C5oXxLFsx9f$lw!F~npvsJ;fWvgpIxgvCODZk!By2q&u?Pkdf&+9Q-d zN7A84?(P_cjl)JSG1_&DmXp^(1Llte;+K)% zPzizaZfQdnM4=ZES}>oFwP%`LrWFTEc6y{rz&{jmq#B)_$n2vmF6|2n9#J&p@$66n zLc$<*2d=XsgC;##r<4WaTc0cLM2ms_YeiE1k!~3xvxks;8F&IX@G???k;*TOR2~JD zXmd?0fY!XVh9F0>pJ^{ep@^|OvXf}COD=&hPJB`#9e{TsvmzWy>%;mKfa2Bn%%D

clqh|aX;kbe*ay5p^rgK9tSXMT^B}yaSMLR^vun>E>7YG!~yn;l6Z8ZJC|+I_#InB3@Sb-B$UXc^9N~+0K|sQtBbNn@zMThNpz9m z5nQfatJI?19dM)_?&F})2rTfk5OAv6oJXJHi&n&qX(xMEN@tvS@Se8PQe}gsUoO?7_c~aH2`Nyo6&Wqwq z5DXd?VN#Pe@@Y_2CkC@=lYxnjqP=rg0(&>L%>}W^C}2hDrcf!}Au{UqM^L|Z#J$+N zo_Y%nEuZ_atroD5R3-?1pLRBA)I^A$tg_j z*;6RIicKl%{`6?{K*nfDGp|Z_^3D-NM+HY$8YmFW%`?1!FQ){`49V zKwy=02wX^@`24QQ0#HiYV?$KTA5^n9HVQ2Q+w8B57E?n2T{infX6BD<#(XhS5Eawe zutRRI2dspjz={AXdb$pk=oi8C-3P{VaHn-+=c%I(;sIGHIf&a47p?Vj8AtE67pX`cQrrAIT`}LrB&YG&Do&k> za7rqeusbdY4f^{Gba3&E^Ne!t$7&BO*L8BMiP2|9Ee6(sGKW@b`#m%s;n_rxiO|XM z*Ac9J#vtjeGl5Uxu@^&M>I01c{{OrPcgJ}S@t={V4bRyicrekkc+Wz&k_;&g*Ftk%MvHRWBB(+ElQ-R3sK$33g{<`P^>-@hNq?ONUVF^ju0bq zXM$S2Rv9m+x3K$UsH%v52WONjE`n(_(dDTyi}pXUF0?f`?yhRd5_uoTc?>dd_R++F?%`3mT~+D-kD+I87bB0xX1I z*)VvHogNE$(2ZPh%YO^>r<9s+xFw&IOltjFqJNrCaAV%n8Na5pBY@wt`2qhQuzZ8Q zIj=$VdF7^$Ie$g|AAtWEmJj&NAFJ$Z1^iwBAM20Fk#7TE zyF*O48i%{TdokD}1hC1pK-=|}pikZ(3ziPJd>I&7cCG%uzY5#YlveZUEp zgCF&IQ-fpNtb&$mt@(dl4PShtXCsRM000JJOGiWi^Z?!fjZo>TMF0Q*32;bRa{vGi z!vFvd!vV){sAK>D00(qQO+^Re0|){%1s2por2qg0H%UZ6R9M5+msxC7RT#&A_s*Tp z*6Bh!6lhDgRG>g1T|gEw1lfHN1OyC=FM@~>6E;JXvKI^lt-=EqTmmks(y-Xnm?DB$ zmI$RFEtIl#qjcI??w$3{^#KHYWM*FYk}oGYCprJ~<@^8tUyg9zt_eVqs;Z@4+557l zX(G=-2!aqYT8zKzbh(M2^MYx-~yqu-E;<}Zm$F5;`R769}nD~9-#$Sw_qaNWJga9tgXmn^>` zDQ>06k@6~5eR4$i<|Vb0niS$hqm2OiCkGGi;?Qd8zVZwD@^cLDlR>$XMDg;kk&M;| zS|Vc2D8LcukD|zfB~8>Jn5+tSp*l^HMAa$xas@^d`2 zx7}dbTjhKnVSeEpz-bW=n{Rib}5-J%2iVECq_Y>Zv`jGHh%=PlzM|1OPcKMrqJ-mXetP zcAOr9q2B;%ZuMf&)n>*Pl*3`;WBbKuP}^dFeaQPt0g zahX(J=pZI8mT|M^Ar72DlP?nYdWu^3040p{2pAKxXwTnC)!&WGpFWo6P8WyvT}KTD z$xC-LJ#!B=SC&#Cz^R)qZr9&o%Poc4fXK%8`*H2Z4?*#Shp#+(`{Ry8Dzq3A-y9oF zA7cg6CJ)8#?m-T?ut;Ey)$!e-1_l=9a>PA^>aGS#_7<`5jgc%XX(Kpl8+uF;zac7x z2!hC@34JNqk-!%_8yJ?AimTg&4@mE8V8@1H+`wl*tT2++R+*KnzUPd#nZ%s*sJNE& zpKC>Af~t@CBkGu(o6b9%?Uc-IV*L2tcw`?&w}Do72O1iJUcedB*t*+IrM8LqjA2A- zzW;MA74Sqd4mCJl^?neA`2uQzO>k!{jsIQ~lh$+C%Fdk-W47%)Jcw?{!l8$~l}u zJA$i;O*NxwN}V2^ff#8xTJBRg;yyvU19QMhW2c+j_uBCG(x|Au!Mz3-vzF9x@k|>P z2OAmC_X-6`Rb&pnh3xc2MI%q_R}MejaT%OGY$Gn)j9`Jnf*5`}6=2HjJItGvO<`^# zug}cqqr)~v&&)w`DOeITS}%oYPo5AF_G9~%T|K14n`p2{;95EZYwITmT71;HOac1pY+xxEFU^uW*6 zLtdIXaZgoBNyggsg&s5o=a`T9aEm$mG>05Y$8=s=q2l0aXRF z8Fadbmg)5(N_s>|7yjH3LYgtq!}W9jc(!eSZ@d7ABqkP3pEzwr*1(}Nb&{li9=^J2 nFc@gQcDdr%ug7=M-F^OFidt$cDis3{00000NkvXXu0mjffC?7T literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/evk.png b/padbuildimg/assets/lat/evk.png new file mode 100644 index 0000000000000000000000000000000000000000..6adc6928463c9afa14a0ab79ef2b76dff1a98475 GIT binary patch literal 6633 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/gok.png b/padbuildimg/assets/lat/gok.png new file mode 100644 index 0000000000000000000000000000000000000000..b601ab6e30d3b17b1abd4f20e533af8fadb5793a GIT binary patch literal 7058 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/gres+.png b/padbuildimg/assets/lat/gres+.png new file mode 100644 index 0000000000000000000000000000000000000000..8ef447dcc04aad491b284357feccaa7adc2fd1b3 GIT binary patch literal 6572 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 tJh5e?Bzhq~lU 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 P00000NkvXXu0mjf3f~sZ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/lres+.png b/padbuildimg/assets/lat/lres+.png new file mode 100644 index 0000000000000000000000000000000000000000..ff93af27bfa639305aaecd67d6413ca3e94cf683 GIT binary patch literal 6584 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/rek.png b/padbuildimg/assets/lat/rek.png new file mode 100644 index 0000000000000000000000000000000000000000..6e3d7ea953d41457c25c3fa0d4542cdbfa9cbfc7 GIT binary patch literal 6923 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 R%8dX3002ovPDHLkV1f-zAJPB- literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/rres+.png b/padbuildimg/assets/lat/rres+.png new file mode 100644 index 0000000000000000000000000000000000000000..3063e59d9af1e5deead40a37f3f979cc8ed67737 GIT binary patch literal 6567 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/rres.png b/padbuildimg/assets/lat/rres.png new file mode 100644 index 0000000000000000000000000000000000000000..74992c27fe201687c55a68a5d0e73141edd7f0bb GIT binary patch literal 12762 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/sdr.png b/padbuildimg/assets/lat/sdr.png new file mode 100644 index 0000000000000000000000000000000000000000..f1c9450dff248d377a26303df0ef01302ca25f9a GIT binary patch literal 12451 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/te+.png b/padbuildimg/assets/lat/te+.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8aefb3a71ab1dd154d50439491fa84f7c1101e GIT binary patch literal 6320 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 B(@X#W literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/star.png b/padbuildimg/assets/star.png new file mode 100644 index 0000000000000000000000000000000000000000..468d8ffc5550b95bf6c217a44a16e114d0b8ba5a GIT binary patch literal 1951 zcmV;Q2VnS#P)P000{Z1^@s6i6JCt000MONklvFCbV)p>CdSb(_}M6X0EB+ao@9?nK(l{_!T4D2Z;71i4N`{+<$ve4@Sm7#3A-O zDdoqCqO`vSflFIJc@Eg!tQT0G|A=z>3Nvmy9$BnJ3)^5o@{3 zdeNXeO>)_W(EjwhZgZy~xwv|npy>2UkfZ?^<(#V?*LJ=ef!tDRi zI_hMK?vB>Vgu(=8$H+LIuct25B;&=puj_$w|XSI2j-$6+9*F*+J& z&(2;9wawL~Dy2pQUn;a(UMZc&i3I1 zYX4efY=Ckgh8ImB;%m8e`+8zMLHeQ^LDi$MTxNDYkEcXYRh@dhk(!#CEs4X2-!uz_ z%A#%<^z?0D?#l|ZvpEVmlYF*_=en#*M)1&?yi(&2M`n5J-#MyHg|28PeLV)bY>9l< zWOg>k+?N%4`Zk~&2BukBJbd^qQ=6Fh!ajd~`pb3e64u5|TSLLf2L3bo1+G;iop%tx znl7F9Po+3=Y>v#b&D3;(r5iOKf9O`$4|Fi`=?ywmm$@aIaQ_ZAZrY0Dw5;>zuPjVV zOxgl)4;>o+ES1WpVtsMO9(tHe$>8MaG#95$x+4ZJzcJ6Bj(pCIY!yxMsMl;>e|LfB z|9l?X_L#j|=EKuz(nX!I2OlQZ7blgc*}pGPDm$Dw@dn5&9Dec8m;Dty<0T@z-Zw zeDSpl`FwE^C;}~Ib%Z;BA>giXIJEoGM}IQ>)Kfp(91eGeY`dw4LjiPM#dBTS?KY~a zVgv#xLZw=>DV1t1+qSLa$KP9c?X?ds)$7(I@Ch&v*f;-p0dxaffjx?%jP~_)Cl4MR zOYGnO!=9m`TRH~@;(bZ5(Q@ + +Generates an image representing a team based on a string. + +Format: + card name(assist)[latent,latent]*repeat|Stats + Card name must be first, otherwise the order does not matter + Separate each card with / + Separate each team with ; + To use / in card name, put quote around the entire team slot (e.g. "g/l medjed(g/x zela)"/...) + sdr is a special card name for dummy assists/skill delay buffers +Latent Acronyms: + Separate each latent with a , + Killers: bak(balanced), phk(physical), hek(healer), drk(dragon), gok(god), aak(attacker, dek(devil), mak(machine) + evk(evo mat), rek(redeemable), awk(awoken mat), enk(enhance) + Stats (+ for 2 slot): hp, atk, rcv, all(all stat), hp+, atk+, rcv+ + Resists (+ for 2 slot): rres, bres, gres, lres, dres, rres+, bres+, gres+, lres+, dres+ + Others: sdr, ah(autoheal) +Repeat: + *# defines number of times to repeat this particular card + e.g. whaledor(plutus)*3/whaledor(carat)*2 creates a team of 3 whaledor(plutus) followed by 2 whaledor(carat) + Latents can also be repeated, e.g. whaledor[sdr*5] for 5 sdr latents +Stats Format: + | LV### SLV## AW# SA# +H## +A## +R## +(0 or 297) + | indicates end of card name and start of stats + LV: level, 1 to 110 + SLV: skill level, 1 to 99 or MAX + AW: awakenings, 0 to 9 + SA: super awakening, 0 to 9 + +H: HP plus, 0 to 99 + +A: ATK plus, 0 to 99 + +R: RCV plus, 0 to 99 + +: total plus (+0 or +297 only) + Case insensitive, order does not matter +""" +EXAMPLE_MSG = "Examples:\n1P{}\n2P{}\n3P{}\nLatent Validation{}\nStats Validation{}".format( + box("bj(weld)lv110/baldin[gok *3](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian)"), + box("amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf)"), + box("zela(assist amen) *3/base raizer * 2/zela; zela(assist amen) *4/base valeria/zela; zela * 6"), + box("eir[drk,drk,sdr]/eir[bak,bak,sdr]/eir[sdr *4, dek]/eir[sdr *8, dek]"), + box("dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15") +) + +""" +Examples: +1P: + bj(weld)lv110/baldin[gok, gok, gok](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian) +2P: + amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf) +3P: + zela(assist amen) *3/base raizer * 2/zela; zela(assist amen) *4/base valeria/zela; zela * 6 +Latent Validation: + eir[drk,drk,sdr]/eir[bak,bak,sdr] +Stats Validation: + dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15 + +""" + +MAX_LATENTS = 8 +LATENTS_MAP = { + 1: 'bak', + 2: 'phk', + 3: 'hek', + 4: 'drk', + 5: 'gok', + 6: 'aak', + 7: 'dek', + 8: 'mak', + 9: 'evk', + 10: 'rek', + 11: 'awk', + 12: 'enk', + 13: 'all', + 14: 'hp+', + 15: 'atk+', + 16: 'rcv+', + 17: 'rres+', + 18: 'bres+', + 19: 'gres+', + 20: 'lres+', + 21: 'dres+', + 22: 'hp', + 23: 'atk', + 24: 'rcv', + 25: 'rres', + 26: 'bres', + 27: 'gres', + 28: 'lres', + 29: 'dres', + 30: 'ah', + 31: 'sdr', + 32: 'te', + 33: 'te+' +} +REVERSE_LATENTS_MAP = {v: k for k, v in LATENTS_MAP.items()} +TYPE_TO_KILLERS_MAP = { + 'God': [7], # devil + 'Devil': [5], # god + 'Machine': [5, 1], # god balanced + 'Dragon': [8, 3], # machine healer + 'Physical': [8, 3], # machine healer + 'Attacker': [7, 2], # devil physical + 'Healer': [4, 6], # dragon attacker +} + +AWK_CIRCLE = 'circle' +AWK_STAR = 'star' +DELAY_BUFFER = 'delay_buffer' +REMOTE_ASSET_URL = 'https://github.com/Mushymato/pdchu-cog/raw/master/assets/' +REMOTE_AWK_URL = 'https://f002.backblazeb2.com/file/dadguide-data/media/awakenings/{0:03d}.png' +# REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' + +class DictWithAttributeAccess(dict): + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + self[key] = value + + +class PadBuildImgSettings(CogSettings): + def make_default_build_img_params(self): + build_img_params = DictWithAttributeAccess({ + 'ASSETS_DIR': './assets/', + 'PORTRAIT_DIR': 'https://f002.backblazeb2.com/file/dadguide-data/media/icons/{monster_id:05d}.png', + # 'OUTPUT_DIR': './data/padbuildimg/output/', + 'PORTRAIT_WIDTH': 100, + 'PADDING': 10, + 'LATENTS_WIDTH': 25, + 'FONT_NAME': './assets/OpenSans-ExtraBold.ttf' + }) + if not os.path.exists(build_img_params.ASSETS_DIR): + os.mkdir(build_img_params.ASSETS_DIR) + # if not os.path.exists(build_img_params.OUTPUT_DIR): + # os.mkdir(build_img_params.OUTPUT_DIR) + return build_img_params + + def buildImgParams(self): + if 'build_img_params' not in self.bot_settings: + self.bot_settings['build_img_params'] = self.make_default_build_img_params() + self.save_settings() + return DictWithAttributeAccess(self.bot_settings['build_img_params']) + + def setBuildImgParamsByKey(self, key, value): + if 'build_img_params' not in self.bot_settings: + self.bot_settings['build_img_params'] = self.make_default_build_img_params() + if key in self.bot_settings['build_img_params']: + self.bot_settings['build_img_params'][key] = value + self.save_settings() + + async def downloadAssets(self, source, target): + async with aiohttp.ClientSession() as session: + async with session.get(source) as resp: + data = await resp.read() + with open(target, "wb") as f: + f.write(data) + + async def downloadAllAssets(self, awk_ids): + params = self.buildImgParams() + if os.path.exists(params.ASSETS_DIR): + rmtree(params.ASSETS_DIR) + os.mkdir(params.ASSETS_DIR) + os.mkdir(params.ASSETS_DIR + 'lat/') + os.mkdir(params.ASSETS_DIR + 'awk/') + for lat in LATENTS_MAP.values(): + await self.downloadAssets(REMOTE_ASSET_URL + 'lat/' + lat + '.png', params.ASSETS_DIR + 'lat/' + lat + '.png') + for awk in awk_ids: + await self.downloadAssets(REMOTE_AWK_URL.format(awk), params.ASSETS_DIR + 'awk/' + str(awk) + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + AWK_CIRCLE + '.png', params.ASSETS_DIR + AWK_CIRCLE + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + AWK_STAR + '.png', params.ASSETS_DIR + AWK_STAR + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + DELAY_BUFFER + '.png', params.ASSETS_DIR + DELAY_BUFFER + '.png') + font_name = os.path.basename(params.FONT_NAME) + await self.downloadAssets(REMOTE_ASSET_URL + font_name, params.ASSETS_DIR + font_name) + + def dmOnly(self, server_id): + if 'dm_only' not in self.bot_settings: + self.bot_settings['dm_only'] = [] + self.save_settings() + return server_id in self.bot_settings['dm_only'] + + def toggleDmOnly(self, server_id): + if 'dm_only' not in self.bot_settings: + self.bot_settings['dm_only'] = [] + else: + if server_id in self.bot_settings['dm_only']: + self.bot_settings['dm_only'].remove(server_id) + else: + self.bot_settings['dm_only'].append(server_id) + self.save_settings() + +def lstripalpha(s): + while s and not s[0].isdigit(): + s=s[1:] + return s + +class PaDTeamLexer(object): + tokens = [ + 'ID', + 'ASSIST', + 'LATENT', + 'STATS', + 'SPACES', + 'LV', + 'SLV', + 'AWAKE', + 'SUPER', + 'P_HP', + 'P_ATK', + 'P_RCV', + 'P_ALL', + 'REPEAT', + ] + + def t_ID(self, t): + r'^.+?(?=[\(\|\[\*])|^(?!.*[\(\|\[\*].*).+' + # first word before ( or [ or | or * entire word if those characters are not in string + t.value = t.value.strip() + return t + + def t_ASSIST(self, t): + r'\(.*?\)' + # words in () + t.value = t.value.strip('()') + return t + + def t_LATENT(self, t): + r'\[.+?\]' + # words in [] + t.value = [l.strip().lower() for l in t.value.strip('[]').split(',')] + for v in t.value.copy(): + if '*' not in v: + continue + tmp = [l.strip() for l in v.split('*')] + if len(tmp[0]) == 1 and tmp[0].isdigit(): + count = int(tmp[0]) + latent = tmp[1] + elif len(tmp[1]) == 1 and tmp[1].isdigit(): + count = int(tmp[1]) + latent = tmp[0] + else: + continue + idx = t.value.index(v) + t.value.remove(v) + for i in range(count): + t.value.insert(idx, latent) + t.value = t.value[0:MAX_LATENTS] + t.value = [REVERSE_LATENTS_MAP[l] for l in t.value if l in REVERSE_LATENTS_MAP] + return t + + def t_STATS(self, t): + r'\|' + pass + + def t_SPACES(self, t): + r'\s' + # spaces must be checked after ID + pass + + def t_LV(self, t): + r'[lL][vV][lL]?\s?\d{1,3}' + # LV followed by 1~3 digit number + t.value = int(lstripalpha(t.value[2:])) + return t + + def t_SLV(self, t): + r'[sS][lL][vV]\s?(\d{1,2}|[mM][aA][xX])' + # SL followed by 1~2 digit number or max + t.value = t.value[3:] + if t.value.isdigit(): + t.value = int(t.value) + else: + t.value = 99 + return t + + def t_AWAKE(self, t): + r'[aA][wW]\s?\d' + # AW followed by 1 digit number + t.value = int(t.value[2:]) + return t + + def t_SUPER(self, t): + r'[sS][aA]\s?\d' + # SA followed by 1 digit number + t.value = int(t.value[2:]) + return t + + def t_P_ALL(self, t): + r'\+\s?\d{1,3}' + # + followed by 0 or 297 + t.value = min(int(t.value[1:]), 297) + return t + + def t_P_HP(self, t): + r'\+[hH]\s?\d{1,2}' + # +H followed by 1~2 digit number + t.value = int(t.value[2:]) + return t + + def t_P_ATK(self, t): + r'\+[aA]\s?\d{1,2}' + # +A followed by 1~2 digit number + t.value = int(t.value[2:]) + return t + + def t_P_RCV(self, t): + r'\+[rR]\s?\d{1,2}' + # +R followed by 1~2 digit number + t.value = int(t.value[2:]) + return t + + def t_REPEAT(self, t): + r'\*\s?\d' + # * followed by a number + t.value = min(int(t.value[1:]), MAX_LATENTS) + return t + + t_ignore = '\t\n' + + def t_error(self, t): + raise commands.UserFeedbackCheckFailure("Parse Error: Unknown text '{}' at position {}".format(t.value, t.lexpos)) + + def build(self, **kwargs): + # pass debug=1 to enable verbose output + self.lexer = lex.lex(module=self) + return self.lexer + + +def validate_latents(latents, card_types): + if latents is None: + return None + if card_types is None: + return None + if 'Balance' in card_types: + return latents + for idx, l in enumerate(latents): + if 0 < l < 9: + if not any([l in TYPE_TO_KILLERS_MAP[t] for t in card_types if t is not None]): + latents[idx] = None + latents = [l for l in latents if l is not None] + return latents if len(latents) > 0 else None + + +def outline_text(draw, x, y, font, text_color, text, thickness=1): + shadow_color = 'black' + draw.text((x - thickness, y - thickness), text, font=font, fill=shadow_color) + draw.text((x + thickness, y - thickness), text, font=font, fill=shadow_color) + draw.text((x - thickness, y + thickness), text, font=font, fill=shadow_color) + draw.text((x + thickness, y + thickness), text, font=font, fill=shadow_color) + draw.text((x, y), text, font=font, fill=text_color) + + +def trim(im): + bg = Image.new(im.mode, im.size, (255, 255, 255, 0)) + diff = ImageChops.difference(im, bg) + diff = ImageChops.add(diff, diff, 2.0, -100) + bbox = diff.getbbox() + if bbox: + return im.crop(bbox) + + +def text_center_pad(font_size, line_height): + return math.floor((line_height - font_size) / 3) + + +def idx_to_xy(idx): + return idx // 2, - (idx % 2) + + +class PadBuildImageGenerator(object): + def __init__(self, params, padinfo_cog, build_name='pad_build'): + self.params = params + self.padinfo_cog = padinfo_cog + self.lexer = PaDTeamLexer().build() + self.build = { + 'NAME': build_name, + 'TEAM': [], + 'INSTRUCTION': None + } + self.build_img = None + + def process_build(self, input_str): + team_strings = [row for row in csv.reader(re.split('[;\n]', input_str), delimiter='/') if len(row) > 0] + if len(team_strings) > 3: + team_strings = team_strings[0:3] + for team in team_strings: + team_sublist = [] + for slot in team: + try: + team_sublist.extend(self.process_card(slot)) + except Exception as ex: + self.build['TEAM'] = [] + raise ex + self.build['TEAM'].append(team_sublist) + + def process_card(self, card_str, is_assist=False): + if not is_assist: + result_card = { + '+ATK': 99, + '+HP': 99, + '+RCV': 99, + 'AWAKE': 9, + 'SUPER': 0, + 'MAX_AWAKE': 9, + 'GOLD_STAR': True, + 'ID': 0, + 'LATENT': None, + 'LV': 99, + 'SLV': 0, + 'ON_COLOR': True + } + else: + result_card = { + '+ATK': 0, + '+HP': 0, + '+RCV': 0, + 'AWAKE': 0, + 'SUPER': 0, + 'MAX_AWAKE': 0, + 'GOLD_STAR': True, + 'ID': 0, + 'LATENT': None, + 'LV': 1, + 'SLV': 0, + 'MAX_SLV': 0, + 'ON_COLOR': False + } + if len(card_str) == 0: + if is_assist: + result_card['ID'] = DELAY_BUFFER + return result_card, None + else: + return [] + self.lexer.input(card_str) + assist_str = None + card = None + repeat = 1 + for tok in iter(self.lexer.token, None): + # print('{} - {}'.format(tok.type, tok.value)) + if tok.type == 'ASSIST': + assist_str = tok.value + elif tok.type == 'REPEAT': + repeat = min(tok.value, MAX_LATENTS) + elif tok.type == 'ID': + if tok.value.lower() == 'sdr': + result_card['ID'] = DELAY_BUFFER + card = DELAY_BUFFER + else: + card, err, debug_info = self.padinfo_cog.findMonster(tok.value) + if card is None: + raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) + if not card.is_inheritable: + if is_assist: + return None, None + else: + result_card['GOLD_STAR'] = False + result_card['MNO'] = card.monster_no_na if card.monster_no_na != card.monster_id else card.monster_no_jp + result_card['ID'] = card.monster_id + elif tok.type == 'P_ALL': + if tok.value >= 297: + result_card['+HP'] = 99 + result_card['+ATK'] = 99 + result_card['+RCV'] = 99 + else: + result_card['+HP'] = 0 + result_card['+ATK'] = 0 + result_card['+RCV'] = 0 + elif tok.type != 'STATS': + result_card[tok.type.replace('P_', '+')] = tok.value + card_att = None + if card is None: + return [] + elif card != DELAY_BUFFER: + result_card['LATENT'] = validate_latents( + result_card['LATENT'], + [t.name for t in card.types] + ) + result_card['LV'] = min( + result_card['LV'], + 110 if card.limit_mult is not None and card.limit_mult > 1 else card.level + ) + if card.active_skill: + result_card['MAX_SLV'] = card.active_skill.turn_max - card.active_skill.turn_min + 1 + else: + result_card['MAX_SLV'] = 0 + result_card['MAX_AWAKE'] = len(card.awakenings) - card.superawakening_count + if is_assist: + result_card['MAX_AWAKE'] = result_card['MAX_AWAKE'] if result_card['AWAKE'] > 0 else 0 + result_card['AWAKE'] = result_card['MAX_AWAKE'] + result_card['SUPER'] = 0 + else: + result_card['SUPER'] = min(result_card['SUPER'], card.superawakening_count) + if result_card['SUPER'] > 0: + super_awakes = [x.awoken_skill_id for x in card.awakenings[-card.superawakening_count:]] + result_card['SUPER'] = super_awakes[result_card['SUPER'] - 1] + result_card['LV'] = max(100, result_card['LV']) + card_att = card.attr1 + if is_assist: + return result_card, card_att + else: + parsed_cards = [result_card] + if isinstance(assist_str, str): + assist_card, assist_att = self.process_card(assist_str, is_assist=True) + if card_att is not None and assist_att is not None: + assist_card['ON_COLOR'] = card_att == assist_att + parsed_cards.append(assist_card) + else: + parsed_cards.append(None) + parsed_cards = parsed_cards * repeat + return parsed_cards + + def combine_latents(self, latents): + if not latents: + return False + if len(latents) > MAX_LATENTS: + latents = latents[0:MAX_LATENTS] + latents_bar = Image.new('RGBA', + (self.params.PORTRAIT_WIDTH, self.params.LATENTS_WIDTH * 2), + (255, 255, 255, 0)) + x_offset = 0 + y_offset = 0 + row_count = 0 + one_slot, two_slot = [], [] + for l in latents: + if l < 22: + two_slot.append(l) + else: + one_slot.append(l) + sorted_latents = [] + if len(one_slot) > len(two_slot): + sorted_latents.extend(one_slot) + sorted_latents.extend(two_slot) + else: + sorted_latents.extend(two_slot) + sorted_latents.extend(one_slot) + last_height = 0 + for l in sorted_latents: + latent_icon = Image.open(self.params.ASSETS_DIR + 'lat/' + LATENTS_MAP[l] + '.png') + if x_offset + latent_icon.size[0] > self.params.PORTRAIT_WIDTH: + row_count += 1 + x_offset = 0 + y_offset += last_height + if row_count >= MAX_LATENTS//4 and x_offset + latent_icon.size[0] >= self.params.LATENTS_WIDTH * (MAX_LATENTS%4): + break + latents_bar.paste(latent_icon, (x_offset, y_offset)) + last_height = latent_icon.size[1] + x_offset += latent_icon.size[0] + + return latents_bar + + def combine_portrait(self, card, show_stats=True, show_supers=False): + if card['ID'] == DELAY_BUFFER: + return Image.open(self.params.ASSETS_DIR + DELAY_BUFFER + '.png') + if 'http' in self.params.PORTRAIT_DIR: + portrait = Image.open(urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))) + else: + portrait = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])) + draw = ImageDraw.Draw(portrait) + slv_offset = 80 + if show_stats: + # + eggsinclude_instructions + sum_plus = card['+HP'] + card['+ATK'] + card['+RCV'] + if 0 < sum_plus: + if sum_plus < 297: + font = ImageFont.truetype(self.params.FONT_NAME, 14) + outline_text(draw, 5, 2, font, 'yellow', '+{:d} HP'.format(card['+HP'])) + outline_text(draw, 5, 14, font, 'yellow', '+{:d} ATK'.format(card['+ATK'])) + outline_text(draw, 5, 26, font, 'yellow', '+{:d} RCV'.format(card['+RCV'])) + else: + font = ImageFont.truetype(self.params.FONT_NAME, 18) + outline_text(draw, 5, 0, font, 'yellow', '+297') + # level + if card['LV'] > 0: + outline_text(draw, 5, 75, ImageFont.truetype(self.params.FONT_NAME, 18), + 'white', 'Lv.{:d}'.format(card['LV'])) + slv_offset = 65 + # skill level + if card['MAX_SLV'] > 0 and card['SLV'] > 0: + slv_txt = 'SLv.max' if card['SLV'] >= card['MAX_SLV'] else 'SLv.{:d}'.format(card['SLV']) + outline_text(draw, 5, slv_offset, + ImageFont.truetype(self.params.FONT_NAME, 12), 'pink', slv_txt) + # ID + outline_text(draw, 67, 82, ImageFont.truetype(self.params.FONT_NAME, 12), 'lightblue', str(card['MNO'])) + del draw + if card['MAX_AWAKE'] > 0: + # awakening + if card['AWAKE'] >= card['MAX_AWAKE']: + awake = Image.open(self.params.ASSETS_DIR + AWK_STAR + '.png') + else: + awake = Image.open(self.params.ASSETS_DIR + AWK_CIRCLE + '.png') + draw = ImageDraw.Draw(awake) + draw.text((8, -2), str(card['AWAKE']), + font=ImageFont.truetype(self.params.FONT_NAME, 18), fill='yellow') + del draw + portrait.paste(awake, (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, 5), awake) + awake.close() + if show_supers and card['SUPER'] > 0: + # SA + awake = Image.open(self.params.ASSETS_DIR + 'awk/' + str(card['SUPER']) + '.png') + portrait.paste(awake, + (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, + (self.params.PORTRAIT_WIDTH - awake.size[0]) // 2), + awake) + awake.close() + return portrait + + def generate_build_image(self, include_instructions=False): + if self.build is None: + return + team_size = max([len(x) for x in self.build['TEAM']]) + p_w = self.params.PORTRAIT_WIDTH * math.ceil(team_size / 2) + \ + self.params.PADDING * math.ceil(team_size / 10) + p_h = (self.params.PORTRAIT_WIDTH + self.params.LATENTS_WIDTH + self.params.PADDING) * \ + 2 * len(self.build['TEAM']) + include_instructions &= self.build['INSTRUCTION'] is not None + if include_instructions: + p_h += len(self.build['INSTRUCTION']) * (self.params.PORTRAIT_WIDTH // 2 + self.params.PADDING) + self.build_img = Image.new('RGBA', + (p_w, p_h), + (255, 255, 255, 0)) + y_offset = 0 + for team in self.build['TEAM']: + has_assist = any([card is not None for idx, card in enumerate(team) if idx % 2 == 1]) + has_latents = any([card['LATENT'] is not None for idx, card in enumerate(team) + if idx % 2 == 0 and card is not None]) + if has_assist: + y_offset += self.params.PORTRAIT_WIDTH + for idx, card in enumerate(team): + if idx > 11 or idx > 9 and len(self.build['TEAM']) % 2 == 0: + break + if card is not None: + x, y = idx_to_xy(idx) + portrait = self.combine_portrait( + card, + show_stats=card['ON_COLOR'], + show_supers=len(self.build['TEAM']) != 2) + if portrait is None: + continue + x_offset = self.params.PADDING * math.ceil(x / 4) + self.build_img.paste( + portrait, + (x_offset + x * self.params.PORTRAIT_WIDTH, + y_offset + y * self.params.PORTRAIT_WIDTH)) + if has_latents and idx % 2 == 0 and card['LATENT'] is not None: + latents = self.combine_latents(card['LATENT']) + self.build_img.paste( + latents, + (x_offset + x * self.params.PORTRAIT_WIDTH, + y_offset + (y + 1) * self.params.PORTRAIT_WIDTH)) + latents.close() + portrait.close() + y_offset += self.params.PORTRAIT_WIDTH + self.params.PADDING * 2 + if has_latents: + y_offset += self.params.LATENTS_WIDTH * 2 + + if include_instructions: + y_offset -= self.params.PADDING * 2 + draw = ImageDraw.Draw(self.build_img) + font = ImageFont.truetype(self.params.FONT_NAME, 24) + text_padding = text_center_pad(25, self.params.PORTRAIT_WIDTH // 2) + for step in self.build['INSTRUCTION']: + x_offset = self.params.PADDING + outline_text(draw, x_offset, y_offset + text_padding, + font, 'white', 'F{:d} - P{:d} '.format(step['FLOOR'], step['PLAYER'] + 1)) + x_offset += self.params.PORTRAIT_WIDTH + if step['ACTIVE'] is not None: + actives_used = [self.build['TEAM'][idx][ids] + for idx, side in enumerate(step['ACTIVE']) + for ids in side] + for card in actives_used: + if 'http' in self.params.PORTRAIT_DIR: + p_small = Image.open(urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) + else: + p_small = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) + self.build_img.paste(p_small, (x_offset, y_offset)) + x_offset += self.params.PORTRAIT_WIDTH // 2 + x_offset += self.params.PADDING + outline_text(draw, x_offset, y_offset + text_padding, font, 'white', step['ACTION']) + y_offset += self.params.PORTRAIT_WIDTH // 2 + del draw + + self.build_img = trim(self.build_img) + + +class PadBuildImage(commands.Cog): + """PAD Build Image Generator.""" + + def __init__(self, bot): + self.bot = bot + self.settings = PadBuildImgSettings("padbuildimg") + + @commands.command() + async def helpbuildimg(self, ctx): + """Help info for the buildimage command.""" + await ctx.author.send(box(HELP_MSG)) + if checks.admin_or_permissions(manage_guild=True): + await ctx.author.send(box('For Server Admins: Output location can be changed between current channel and ' + 'direct messages via ^togglebuildimgoutput')) + await ctx.author.send(EXAMPLE_MSG) + + @commands.command(aliases=['buildimg', 'pdchu']) + async def padbuildimg(self, ctx, *, build_str: str): + """Create a build image based on input. + Use ^helpbuildimg for more info. + """ + # print('BUILD_STR: {}'.format(build_str)) + + # start = time.perf_counter() + params = self.settings.buildImgParams() + try: + pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) + # print('PARSE: {}'.format(time.perf_counter() - start)) + pbg.process_build(build_str) + # start = time.perf_counter() + pbg.generate_build_image() + # print('DRAW: {}'.format(time.perf_counter() - start)) + except commands.UserFeedbackCheckFailure as ex: + await ctx.send(box(str(ex) + '\nSee ^helpbuildimg for syntax')) + return -1 + + # start = time.perf_counter() + if pbg.build_img is not None: + with io.BytesIO() as build_io: + pbg.build_img.save(build_io, format='PNG') + build_io.seek(0) + if ctx.guild and self.settings.dmOnly(ctx.guild.id): + try: + await ctx.author.send(file=discord.File(build_io,'pad_build.png')) + await ctx.send(inline('Sent build to {}'.format(ctx.author))) + except discord.errors.Forbidden as ex: + await ctx.send(inline('Failed to send build to {}'.format(ctx.author))) + else: + await ctx.send(file=discord.File(build_io,'pad_build.png')) + else: + await ctx.send(box('Invalid build, see ^helpbuildimg')) + return 0 + + @commands.command() + @checks.is_owner() + async def configbuildimg(self, ctx, param_key: str, param_value: str): + """ + Configure PadBuildImageGenerator parameters: + ASSETS_DIR - directory for storing assets (use ^refreshassets to update) + PORTRAIT_DIR - path pattern to where portraits are stored, {monster_id} must be present + PORTRAIT_WIDTH - width of portraits, default 100 + PADDING - padding between various things, default 10 + LATENTS_WIDTH - width of 1 slot latent, default 25 + FONT_NAME - path to font + """ + if param_key in ['ASSETS_DIR', 'PORTRAIT_DIR', 'PORTRAIT_WIDTH', 'PADDING', 'LATENTS_WIDTH', 'FONT_NAME']: + if param_key in ['PORTRAIT_WIDTH', 'PADDING', 'LATENTS_WIDTH']: + param_value = int(param_value) + if param_key in ['ASSETS_DIR'] and param_value[-1] not in ['/', '\\']: + param_value += '/' + self.settings.setBuildImgParamsByKey(param_key, param_value) + await ctx.send(box('Set {} to {}'.format(param_key, param_value))) + else: + await ctx.send(box('Invaalid parameter {}'.format(param_key))) + + @commands.command() + @checks.is_owner() + async def refreshassets(self, ctx): + """ + Refresh assets folder + """ + await ctx.send('Downloading assets to {}'.format(self.settings.buildImgParams().ASSETS_DIR)) + awk_ids = self.bot.get_cog('Dadguide').database.get_awoken_skill_ids() + await self.settings.downloadAllAssets(awk_ids) + await ctx.send('Done') + + @commands.command() + @commands.guild_only() + @checks.admin_or_permissions(manage_guild=True) + async def togglebuildimgoutput(self, ctx): + """ + Toggles between sending result to server vs sending result to direct message + """ + self.settings.toggleDmOnly(ctx.guild.id) + if self.settings.dmOnly(ctx.guild.id): + await ctx.send('Response mode set to direct message') + else: + await ctx.send('Response mode set to current channel') From 8bab8465dc2d5bff0f9bdf0375c3d9b0df85c733 Mon Sep 17 00:00:00 2001 From: droon Date: Thu, 18 Jun 2020 22:15:56 -0700 Subject: [PATCH 0002/1220] gh --- dadguide/dadguide.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index d607f674..6ed7049f 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -1217,6 +1217,7 @@ def __init__(self, monster_database, nickname_overrides, basename_overrides, pan 149: ['wedding', 'bride'], 154: ['padr'], 175: ['valentines', 'vday', 'v'], + 183: ['gh', 'gungho'], } monster_id_to_nicknames = defaultdict(set) From 7bb1e225c7c883d7e1baea146899dbdf4bfd8973 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 20 Jun 2020 06:17:45 -0700 Subject: [PATCH 0003/1220] Support for [p]idjp bc nobody likes power rangers (#27) * jpid * await jp --- padinfo/padinfo.py | 64 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0d4cb347..1b8e34c5 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -5,6 +5,7 @@ import traceback import urllib.parse from collections import OrderedDict +from enum import Enum import discord import prettytable @@ -53,6 +54,11 @@ YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' +class ServerFilter(Enum): + any = 0 + na = 1 + jp = 2 + def get_pdx_url(m): return INFO_PDX_TEMPLATE.format(rpadutils.get_pdx_id(m)) @@ -125,6 +131,8 @@ def __init__(self, bot: Red, *args, **kwargs): self.index_all = None self.index_na = None + self.index_jp = None + self.menu = Menu(bot) @@ -153,6 +161,7 @@ def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.index_all = None self.index_na = None + self.index_jp = None self.historic_lookups = {} self.historic_lookups_id2 = {} @@ -190,6 +199,9 @@ async def refresh_index(self, ctx=None): self.index_na = dg_cog.create_index(lambda m: m.on_na) await asyncio.sleep(1) + logger.info('Loading JP index') + self.index_jp = await dg_cog.create_index(lambda m: m.on_jp) + logger.info('Done refreshing indexes') def get_monster_by_id(self, monster_id: int): @@ -216,10 +228,15 @@ async def _id(self, ctx, *, query: str): @commands.command() async def idna(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" - await self._do_id(ctx, query, na_only=True) + await self._do_id(ctx, query, server_filter=ServerFilter.na) + + @commands.command() + async def idjp(self, ctx, *, query: str): + """Monster info (limited to JP monsters ONLY)""" + await self._do_id(ctx, query, server_filter=ServerFilter.jp) - async def _do_id(self, ctx, query: str, na_only=False): - m, err, debug_info = self.findMonster(query, na_only=na_only) + async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): + m, err, debug_info = self.findMonster(query, server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -233,10 +250,15 @@ async def id2(self, ctx, *, query: str): @commands.command() async def id2na(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" - await self._do_id2(ctx, query, na_only=True) + await self._do_id2(ctx, query, server_filter=ServerFilter.na) - async def _do_id2(self, ctx, query: str, na_only=False): - m, err, debug_info = self.findMonster2(query, na_only=na_only) + @commands.command() + async def id2jp(self, ctx, *, query: str): + """Monster info (limited to JP monsters ONLY)""" + await self._do_id2(ctx, query, server_filter=ServerFilter.jp) + + async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): + m, err, debug_info = self.findMonster2(query, server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -535,9 +557,9 @@ def makeFailureMsg(self, err): 'Unexpected results? Use ^helpid for more info.').format(err) return box(msg) - def findMonster(self, query, na_only=False): + def findMonster(self, query, server_filter=ServerFilter.any): query = rmdiacritics(query) - nm, err, debug_info = self._findMonster(query, na_only) + nm, err, debug_info = self._findMonster(query, server_filter) monster_no = nm.monster_id if nm else -1 self.historic_lookups[query] = monster_no @@ -547,13 +569,20 @@ def findMonster(self, query, na_only=False): return m, err, debug_info - def _findMonster(self, query, na_only=False): - monster_index = self.index_na if na_only else self.index_all + def _findMonster(self, query, server_filter=ServerFilter.any): + if server_filter == ServerFilter.any: + monster_index = self.index_all + elif server_filter == ServerFilter.na: + monster_index = self.index_na + elif server_filter == ServerFilter.jp: + monster_index = self.index_jp + else: + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster(query) - def findMonster2(self, query, na_only=False): + def findMonster2(self, query, server_filter=ServerFilter.any): query = rmdiacritics(query) - nm, err, debug_info = self._findMonster2(query, na_only) + nm, err, debug_info = self._findMonster2(query, server_filter) monster_no = nm.monster_id if nm else -1 self.historic_lookups_id2[query] = monster_no @@ -563,8 +592,15 @@ def findMonster2(self, query, na_only=False): return m, err, debug_info - def _findMonster2(self, query, na_only=False): - monster_index = self.index_na if na_only else self.index_all + def _findMonster2(self, query, server_filter=ServerFilter.any): + if server_filter == ServerFilter.any: + monster_index = self.index_all + elif server_filter == ServerFilter.na: + monster_index = self.index_na + elif server_filter == ServerFilter.jp: + monster_index = self.index_jp + else: + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster2(query) From 53eef050571827ff83a49f9088a8faaf68c3d0ca Mon Sep 17 00:00:00 2001 From: nachoapps Date: Sat, 20 Jun 2020 09:37:53 -0400 Subject: [PATCH 0004/1220] Bugfix for idjp; create_index is not async. Other changes are autoformatting. --- padinfo/padinfo.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1b8e34c5..2dd55ec0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -54,6 +54,7 @@ YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' + class ServerFilter(Enum): any = 0 na = 1 @@ -133,7 +134,6 @@ def __init__(self, bot: Red, *args, **kwargs): self.index_na = None self.index_jp = None - self.menu = Menu(bot) # These emojis are the keys into the idmenu submenus @@ -200,7 +200,7 @@ async def refresh_index(self, ctx=None): await asyncio.sleep(1) logger.info('Loading JP index') - self.index_jp = await dg_cog.create_index(lambda m: m.on_jp) + self.index_jp = dg_cog.create_index(lambda m: m.on_jp) logger.info('Done refreshing indexes') @@ -833,12 +833,16 @@ def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): lls = left_m.leader_skill rls = right_m.leader_skill - if lls: lhp, latk, lrcv, lresist = lls.data - else: lhp, latk, lrcv, lresist = 1, 1, 1, 0 + if lls: + lhp, latk, lrcv, lresist = lls.data + else: + lhp, latk, lrcv, lresist = 1, 1, 1, 0 + + if rls: + rhp, ratk, rrcv, rresist = rls.data + else: + rhp, ratk, rrcv, rresist = 1, 1, 1, 0 - if rls: rhp, ratk, rrcv, rresist = rls.data - else: rhp, ratk, rrcv, rresist = 1, 1, 1, 0 - multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) embed = discord.Embed() From 0c5d0149ad6a0860c73c2e0639e8a34ad909b002 Mon Sep 17 00:00:00 2001 From: droon Date: Sat, 27 Jun 2020 23:13:44 -0700 Subject: [PATCH 0005/1220] lil changes --- padguidedb/padguidedb.py | 13 +++++++------ trutils/trutils.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index c17a5ad6..ae928e19 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -84,10 +84,11 @@ async def setconfigfile(self, ctx, *, config_file): async def searchdungeon(self, ctx, *, search_text): """Search""" search_text = re.sub(r"[@#\$%\^&\*;\/<>?\\|`~\-\=]", " ", search_text) - with self.get_connection() as cursor: + conn = self.get_connection() + with conn as cursor: sql = ("select dungeon_id, name_na, name_jp, visible from dungeons" - " where (lower(name_na) like '%{}%' or lower(name_jp) like '%{}%')" - " order by dungeon_id desc limit 20".format(search_text, search_text)) + " where (lower(name_na) like {0} or lower(name_jp) like {0})" + " order by dungeon_id desc limit 20".format(conn.escape("%"+search_text+"%")) cursor.execute(sql) results = list(cursor.fetchall()) msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) @@ -167,7 +168,7 @@ async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): " FROM wave_data" " WHERE dungeon_id = {} AND floor_id = {}" " GROUP BY 1, 2" - " ORDER BY 1, 2".format(dungeon_id, dungeon_floor_id)) + " ORDER BY 1, 2").format(int(dungeon_id), int(dungeon_floor_id)) cursor.execute(sql) results = list(cursor.fetchall()) msg = 'stage,drop_monster_id,count' @@ -180,7 +181,7 @@ async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): @checks.is_owner() async def cleardungeon(self, ctx, dungeon_id: int): with self.get_connection() as cursor: - sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(dungeon_id) + sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) await ctx.send(inline("Done")) @@ -188,7 +189,7 @@ async def cleardungeon(self, ctx, dungeon_id: int): @is_padguidedb_admin() async def dungeondata(self, ctx, dungeon_id: int): with ctx.typing(), self.get_connection() as cursor: - sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(dungeon_id) + sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) results = list(cursor.fetchall()) order = sorted(results[0]) diff --git a/trutils/trutils.py b/trutils/trutils.py index 5a0cdb97..36b7fc10 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -1,6 +1,7 @@ import asyncio import datetime import re +import sys import discord from redbot.core.bot import Red @@ -462,10 +463,38 @@ async def freload(self, ctx, cmd, *, args=""): if cmd is None: await ctx.send("Invalid Command: {}".format(full_cmd)) return - await self.bot.get_cog("Core").reload(ctx, cmd.cog.__module__.split('.')[-1]) + await self.bot.get_cog("Core").reload(ctx, cmd.cog.__module__.split('.')[0]) ctx.message.content = full_cmd await self.bot.process_commands(ctx.message) + @commands.command() + @checks.is_owner() + async def pipupdate(self, ctx, module="Red-DiscordBot", updatepip=True): + async with ctx.typing(): + process = await asyncio.create_subprocess_exec( + sys.executable, "-m", "pip", "install", "-U", module, + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = (bts.decode() if bts else "" for bts in await process.communicate()) + + if stderr.startswith("WARNING: You are using pip version"): + if updatepip: + await ctx.send(stderr.split('You should consider')[0]+'\n\nUpdating pip...') + await self.pipupdate(ctx, 'pip', False) + stderr = "" + + if stderr: + await ctx.author.send("Error updating:\n" + stderr) + await ctx.send(inline("Error (sent via DM)")) + else: + await ctx.send(inline('Done')) + + @commands.command(hidden=True, aliases=["make_aa", "makearadia"]) + async def makeaa(self, ctx, *, task): + await self.bot.get_user(144250811315257344).send(task) class TrUtilsSettings(CogSettings): From 5c70f4d945909a846d5e1260a81619d1a9c2d6f8 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 5 Jul 2020 13:54:27 -0700 Subject: [PATCH 0006/1220] Some Little Changes (#42) * gh * lil changes * translate update * channelmod * zhen --- channelmod/channelmod.py | 8 ++--- dadguide/dadguide.py | 7 ---- translate/__init__.py | 4 ++- translate/translate.py | 72 +++++++++++++++++++++++----------------- 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 443980dc..f53276d4 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -152,11 +152,11 @@ async def mirror_msg(self, message): continue if attribution_required: - msg = 'Posted by **{}** in *{} - #{}*:'.format(message.author.name, - message.guild.name, - message.channel.name) + msg = 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name, + message.guild.name, + message.channel.name, + message.jump_url) await dest_channel.send(msg) - await dest_channel.send(message.jump_url) fmessage = await self.mformat(message.content, message.channel, dest_channel) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 6ed7049f..5b06e21c 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -1179,13 +1179,6 @@ def make_roma_subname(name_jp): return adjusted_subname.strip() -def int_or_none(maybe_int: str): - return int(maybe_int) if maybe_int else None - - -def float_or_none(maybe_float: str): - return float(maybe_float) if maybe_float else None - class MonsterIndex(object): def __init__(self, monster_database, nickname_overrides, basename_overrides, panthname_overrides, diff --git a/translate/__init__.py b/translate/__init__.py index 3363b07e..c1ce592f 100644 --- a/translate/__init__.py +++ b/translate/__init__.py @@ -2,4 +2,6 @@ def setup(bot): - bot.add_cog(Translate(bot)) + n=Translate(bot) + bot.add_cog(n) + bot.loop.create_task(n.build_service()) diff --git a/translate/translate.py b/translate/translate.py index 0eb1462a..d1c9b2f0 100644 --- a/translate/translate.py +++ b/translate/translate.py @@ -1,7 +1,8 @@ import discord +import romkan + from googleapiclient.discovery import build -from redbot.core import checks -from redbot.core import commands +from redbot.core import commands, Config, checks from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline @@ -14,21 +15,28 @@ class Translate(commands.Cog): def __init__(self, bot: Red, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.settings = TranslateSettings("translate") + self.config = Config.get_conf(self, identifier=724757473) + self.config.register_global(api_key=None) self.service = None - self.trySetupService() - def trySetupService(self): - api_key = self.settings.get_key() - if api_key: - self.service = build('translate', 'v2', developerKey=api_key) @commands.group() @checks.is_owner() async def translate(self, context): """Translation utilities.""" + + @translate.command() + async def build_service(self): + api_key = await self.config.api_key() + try: + assert api_key + self.service = build('translate', 'v2', developerKey=api_key) + except: + print("Google API key not found or invalid") + + @commands.command(aliases=['jaus', 'jpen', 'jpus']) async def jaen(self, ctx, *, query): """Translates from Japanese to English""" @@ -36,35 +44,39 @@ async def jaen(self, ctx, *, query): await ctx.send(inline('Set up an API key first!')) return - em = self.translate_to_embed(query) + em = self.translate_to_embed("ja", "en", query) + await ctx.send(embed=em) + + @commands.command(aliases=['zhus']) + async def zhen(self, ctx, *, query): + """Translates from Chinese to English""" + if not self.service: + await ctx.send(inline('Set up an API key first!')) + return + + em = self.translate_to_embed("zh", "en", query) await ctx.send(embed=em) - def translate_jp_en(self, query): - result = self.service.translations().list(source='ja', target='en', format='text', q=query).execute() + @commands.command() + async def kanrom(self, ctx, *, query): + """Transliterates Kanji to Romanji""" + await ctx.send(romkan.to_roma(query)) + + + def translate_lang(self, source, target, query): + result = self.service.translations().list(source=source, target=target, format='text', q=query).execute() return result.get('translations')[0].get('translatedText') - def translate_to_embed(self, query): - translation = self.translate_jp_en(query) + def translate_to_embed(self, source, target, query): + translation = self.translate_lang(source, target, query) return discord.Embed(description='**Original**\n`{}`\n\n**Translation**\n`{}`'.format(query, translation)) @translate.command() - @checks.is_owner() async def setkey(self, ctx, api_key): """Sets the google api key.""" - self.settings.set_key(api_key) - await ctx.send("done") - - -class TranslateSettings(CogSettings): - def make_default_settings(self): - config = { - 'google_api_key': '' - } - return config + await self.config.api_key.set(api_key) + await ctx.send(inline("done")) - def get_key(self): - return self.bot_settings.get('google_api_key') - - def set_key(self, api_key): - self.bot_settings['google_api_key'] = api_key - self.save_settings() + @translate.command() + async def getkey(self, ctx): + await ctx.author.send(await self.config.api_key()) From 9fbf71074122f56b57d89aa89493a7076337c7f0 Mon Sep 17 00:00:00 2001 From: nachoapps Date: Sun, 5 Jul 2020 17:01:06 -0400 Subject: [PATCH 0007/1220] Fix bad code --- padguidedb/padguidedb.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index ae928e19..6d5b3541 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -83,12 +83,11 @@ async def setconfigfile(self, ctx, *, config_file): @is_padguidedb_admin() async def searchdungeon(self, ctx, *, search_text): """Search""" - search_text = re.sub(r"[@#\$%\^&\*;\/<>?\\|`~\-\=]", " ", search_text) - conn = self.get_connection() - with conn as cursor: - sql = ("select dungeon_id, name_na, name_jp, visible from dungeons" - " where (lower(name_na) like {0} or lower(name_jp) like {0})" - " order by dungeon_id desc limit 20".format(conn.escape("%"+search_text+"%")) + search_text = re.sub(r"[@#$%^&*;/<>?\\|`~\-=]", ' ', search_text) + with self.get_connection() as cursor: + sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' + ' where (lower(name_na) like {0} or lower(name_jp) like {0})' + ' order by dungeon_id desc limit 20'.format(cursor.escape('%' + search_text + '%'))) cursor.execute(sql) results = list(cursor.fetchall()) msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) From 8e30071d47b8fc9283309b7e2ddf2fdcb9b8d572 Mon Sep 17 00:00:00 2001 From: nachoapps Date: Sun, 5 Jul 2020 17:09:31 -0400 Subject: [PATCH 0008/1220] Simplify to bound parameters instead of escaping --- padguidedb/padguidedb.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 6d5b3541..a0f55552 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -2,7 +2,6 @@ import csv import io import json -import re import discord import pymysql @@ -83,12 +82,12 @@ async def setconfigfile(self, ctx, *, config_file): @is_padguidedb_admin() async def searchdungeon(self, ctx, *, search_text): """Search""" - search_text = re.sub(r"[@#$%^&*;/<>?\\|`~\-=]", ' ', search_text) + search_text = '%{}%'.format(search_text) with self.get_connection() as cursor: sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' - ' where (lower(name_na) like {0} or lower(name_jp) like {0})' - ' order by dungeon_id desc limit 20'.format(cursor.escape('%' + search_text + '%'))) - cursor.execute(sql) + ' where lower(name_na) like %s or lower(name_jp) like %s' + ' order by dungeon_id desc limit 20') + cursor.execute(sql, [search_text, search_text]) results = list(cursor.fetchall()) msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) await ctx.send(inline(sql)) From 903fe124db64831730a61fc1e060e58f8e11ccd6 Mon Sep 17 00:00:00 2001 From: nachoapps Date: Sun, 5 Jul 2020 17:12:16 -0400 Subject: [PATCH 0009/1220] Remove unnecessary command --- translate/translate.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/translate/translate.py b/translate/translate.py index d1c9b2f0..15726179 100644 --- a/translate/translate.py +++ b/translate/translate.py @@ -1,13 +1,10 @@ import discord import romkan -from googleapiclient.discovery import build from redbot.core import commands, Config, checks from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline -from rpadutils import CogSettings - class Translate(commands.Cog): """Translation utilities.""" @@ -20,23 +17,11 @@ def __init__(self, bot: Red, *args, **kwargs): self.service = None - @commands.group() @checks.is_owner() async def translate(self, context): """Translation utilities.""" - - @translate.command() - async def build_service(self): - api_key = await self.config.api_key() - try: - assert api_key - self.service = build('translate', 'v2', developerKey=api_key) - except: - print("Google API key not found or invalid") - - @commands.command(aliases=['jaus', 'jpen', 'jpus']) async def jaen(self, ctx, *, query): """Translates from Japanese to English""" @@ -62,7 +47,6 @@ async def kanrom(self, ctx, *, query): """Transliterates Kanji to Romanji""" await ctx.send(romkan.to_roma(query)) - def translate_lang(self, source, target, query): result = self.service.translations().list(source=source, target=target, format='text', q=query).execute() return result.get('translations')[0].get('translatedText') From 994fe2d73d23ab8ca0e4bb6148beb7f11b29d116 Mon Sep 17 00:00:00 2001 From: nachoapps Date: Sun, 5 Jul 2020 17:13:15 -0400 Subject: [PATCH 0010/1220] Oops deleted too much --- translate/translate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/translate/translate.py b/translate/translate.py index 15726179..9ee41549 100644 --- a/translate/translate.py +++ b/translate/translate.py @@ -1,6 +1,7 @@ import discord import romkan +from googleapiclient.discovery import build from redbot.core import commands, Config, checks from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline @@ -22,6 +23,14 @@ def __init__(self, bot: Red, *args, **kwargs): async def translate(self, context): """Translation utilities.""" + async def build_service(self): + api_key = await self.config.api_key() + try: + assert api_key + self.service = build('translate', 'v2', developerKey=api_key) + except: + print("Google API key not found or invalid") + @commands.command(aliases=['jaus', 'jpen', 'jpus']) async def jaen(self, ctx, *, query): """Translates from Japanese to English""" From bfec990aa53c681eb9072616e66e268aa777ff91 Mon Sep 17 00:00:00 2001 From: droon Date: Thu, 9 Jul 2020 03:43:44 -0700 Subject: [PATCH 0011/1220] River Requests --- channelmod/channelmod.py | 10 ++++++---- dadguide/dadguide.py | 3 +++ padglobal/padglobal.py | 3 ++- padinfo/padinfo.py | 28 ++++++++++++++++++++++++---- selfroleoverride/selfroleoverride.py | 2 +- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 443980dc..2c34cb0f 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -152,11 +152,8 @@ async def mirror_msg(self, message): continue if attribution_required: - msg = 'Posted by **{}** in *{} - #{}*:'.format(message.author.name, - message.guild.name, - message.channel.name) + msg = self.makeheader(message) await dest_channel.send(msg) - await dest_channel.send(message.jump_url) fmessage = await self.mformat(message.content, message.channel, dest_channel) @@ -235,6 +232,11 @@ async def mirror_msg_mod(self, message, print('Failed to mirror message edit from ', channel.id, 'to', dest_channel_id, ':', ex) + def makeheader(self, message): + return 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name, + message.guild.name, + message.channel.name, + message.jump_url) async def mformat(self, text, from_channel, dest_channel): # LINKS diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 6ed7049f..b56b343f 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -1353,6 +1353,9 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('uuvo') prefixes.add('uuevo') + if m.farmable: + prefixes.add('farmable') + # If any monster in the group is a pixel, add 'nonpixel' to all the versions # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. def is_pixel(n): diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 368a2234..601c6277 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -859,7 +859,8 @@ async def getwhich(self, ctx): for page in pagify(msg): await ctx.send(box(page)) - @padglobal.command() + @commands.command(aliases=['lookupdebug']) + @is_padglobal_admin() async def debuglookup(self, ctx, *, term: str): """Shows why a query matches to a monster""" term = term.lower().replace('?', '') diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2dd55ec0..04cd7ae0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -53,6 +53,7 @@ YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' +ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' class ServerFilter(Enum): @@ -386,6 +387,23 @@ async def pic(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) + @commands.command() + async def links(self, ctx, *, query: str): + """Monster links""" + m, err, debug_info = self.findMonster(query) + if m is not None: + embed = monsterToBaseEmbed(m) + embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + embed.set_footer(text='') + await ctx.send(embed=embed) + + else: + await ctx.send(self.makeFailureMsg(err)) + @commands.command(aliases=['stats']) async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" @@ -995,10 +1013,12 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): tbl.add_row([row_name.format(plus), hp, atk, rcv]) body_text += box(tbl.get_string()) - search_text = YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)) - skyozora_text = SKYOZORA_TEMPLATE.format(m.monster_no_jp) - body_text += "\n**JP Name**: {} | [YouTube]({}) | [Skyozora]({})".format( - m.name_jp, search_text, skyozora_text) + body_text += "\n**JP Name**: {}".format(m.name_jp) + body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) if m.history_us: body_text += '\n**History:** {}'.format(m.history_us) diff --git a/selfroleoverride/selfroleoverride.py b/selfroleoverride/selfroleoverride.py index aaddc258..8ab56260 100644 --- a/selfroleoverride/selfroleoverride.py +++ b/selfroleoverride/selfroleoverride.py @@ -110,5 +110,5 @@ async def selfrole_list(self, ctx: commands.Context): await ctx.send("There are currently no selfroles.") return - msg = _("Available Selfroles:\n{selfroles}").format(selfroles=fmt_selfroles) + msg = "Available Selfroles:\n{selfroles}".format(selfroles=fmt_selfroles) await ctx.send(box(msg, "diff")) From fc42dea622f2f6bdac2190a3b97d4c42dd53a4f1 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 27 Jul 2020 20:27:02 -0700 Subject: [PATCH 0012/1220] River Requests (#43) --- channelmod/channelmod.py | 10 ++++++---- dadguide/dadguide.py | 3 +++ padglobal/padglobal.py | 3 ++- padinfo/padinfo.py | 28 ++++++++++++++++++++++++---- selfroleoverride/selfroleoverride.py | 2 +- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index f53276d4..2c34cb0f 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -152,10 +152,7 @@ async def mirror_msg(self, message): continue if attribution_required: - msg = 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name, - message.guild.name, - message.channel.name, - message.jump_url) + msg = self.makeheader(message) await dest_channel.send(msg) fmessage = await self.mformat(message.content, message.channel, dest_channel) @@ -235,6 +232,11 @@ async def mirror_msg_mod(self, message, print('Failed to mirror message edit from ', channel.id, 'to', dest_channel_id, ':', ex) + def makeheader(self, message): + return 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name, + message.guild.name, + message.channel.name, + message.jump_url) async def mformat(self, text, from_channel, dest_channel): # LINKS diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 5b06e21c..02687faf 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -1346,6 +1346,9 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('uuvo') prefixes.add('uuevo') + if m.farmable: + prefixes.add('farmable') + # If any monster in the group is a pixel, add 'nonpixel' to all the versions # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. def is_pixel(n): diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 368a2234..601c6277 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -859,7 +859,8 @@ async def getwhich(self, ctx): for page in pagify(msg): await ctx.send(box(page)) - @padglobal.command() + @commands.command(aliases=['lookupdebug']) + @is_padglobal_admin() async def debuglookup(self, ctx, *, term: str): """Shows why a query matches to a monster""" term = term.lower().replace('?', '') diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2dd55ec0..04cd7ae0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -53,6 +53,7 @@ YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' +ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' class ServerFilter(Enum): @@ -386,6 +387,23 @@ async def pic(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) + @commands.command() + async def links(self, ctx, *, query: str): + """Monster links""" + m, err, debug_info = self.findMonster(query) + if m is not None: + embed = monsterToBaseEmbed(m) + embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + embed.set_footer(text='') + await ctx.send(embed=embed) + + else: + await ctx.send(self.makeFailureMsg(err)) + @commands.command(aliases=['stats']) async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" @@ -995,10 +1013,12 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): tbl.add_row([row_name.format(plus), hp, atk, rcv]) body_text += box(tbl.get_string()) - search_text = YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)) - skyozora_text = SKYOZORA_TEMPLATE.format(m.monster_no_jp) - body_text += "\n**JP Name**: {} | [YouTube]({}) | [Skyozora]({})".format( - m.name_jp, search_text, skyozora_text) + body_text += "\n**JP Name**: {}".format(m.name_jp) + body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) if m.history_us: body_text += '\n**History:** {}'.format(m.history_us) diff --git a/selfroleoverride/selfroleoverride.py b/selfroleoverride/selfroleoverride.py index aaddc258..8ab56260 100644 --- a/selfroleoverride/selfroleoverride.py +++ b/selfroleoverride/selfroleoverride.py @@ -110,5 +110,5 @@ async def selfrole_list(self, ctx: commands.Context): await ctx.send("There are currently no selfroles.") return - msg = _("Available Selfroles:\n{selfroles}").format(selfroles=fmt_selfroles) + msg = "Available Selfroles:\n{selfroles}".format(selfroles=fmt_selfroles) await ctx.send(box(msg, "diff")) From 5941d4b19f31347a6d69d047e75d26e60fdb04fe Mon Sep 17 00:00:00 2001 From: droonkid Date: Thu, 13 Aug 2020 02:45:36 -0700 Subject: [PATCH 0013/1220] PadEvents --- dadguide/dadguide.py | 3 -- padevents/padevents.py | 57 ++++++++++++---------------- padglobal/padglobal.py | 15 ++++++-- selfroleoverride/selfroleoverride.py | 2 +- 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 02687faf..e9bcc97c 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -665,9 +665,6 @@ def get_all_events(self, as_generator=True): def get_dungeon_by_id(self, dungeon_id: int): return self._select_one_entry_by_pk(dungeon_id, DgDungeon) - def get_dungeon_by_id(self, dungeon_id: int): - return self._select_one_entry_by_pk(dungeon_id, DgDungeon) - def enum_or_none(enum, value, default=None): diff --git a/padevents/padevents.py b/padevents/padevents.py index 86dadb87..5c9de8d4 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -76,9 +76,9 @@ async def check_started(self): self.started_events.add(e.key) if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: for gr in list(self.settings.listGuerrillaReg()): - if SUPPORTED_SERVERS[e.server_id] == gr['server']: + if e.server == gr['server']: try: - message = box("Server " + SUPPORTED_SERVERS[e.server_id] + ", group " + + message = box("Server " + e.server + ", group " + e.groupLongName() + " : " + e.name_and_modifier) channel = self.bot.get_channel(int(gr['channel_id'])) @@ -96,9 +96,8 @@ async def check_started(self): except Exception as ex: # deregister gr traceback.print_exc() - # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) - print( - "caught exception while sending guerrilla msg" + str(ex)) + # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) + print("caught exception while sending guerrilla msg:\n" + str(ex)) else: if not e.dungeon_type in [DungeonType.Normal]: @@ -112,47 +111,42 @@ async def check_started(self): await self.pageOutput(ctx, msg, channel_id=daily_registration['channel_id']) except Exception as ex: traceback.print_exc() - # self.settings.removeDailyReg( - # daily_registration['channel_id'], daily_registration['server']) - print("caught exception while sending daily msg " + str(ex)) + # self.settings.removeDailyReg( + # daily_registration['channel_id'], daily_registration['server']) + print("caught exception while sending daily msg:\n" + str(ex)) except Exception as ex: traceback.print_exc() - print("caught exception while checking guerrillas " + str(ex)) + print("caught exception while checking guerrillas:\n" + str(ex)) - try: - await asyncio.sleep(10) - except Exception as ex: - traceback.print_exc() - print("check event loop caught exception " + str(ex)) - raise ex + await asyncio.sleep(10) print("done check_started") - @commands.group() + @commands.group(aliases=['pde']) @checks.mod_or_permissions(manage_guild=True) async def padevents(self, ctx): """PAD event tracking""" @padevents.command() @checks.is_owner() - async def testevent(self, ctx, server): - # FIXME: (it's VERY borked) + async def testevent(self, ctx, server, seconds: int = 0): server = normalizeServer(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return - te = dadguide.DgScheduledEvent([], None) - te.server = server + dg_cog = self.bot.get_cog('Dadguide') + await dg_cog.wait_until_ready() + te = dadguide.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) - te.dungeon_code = 1 - te.event_type = EventType.Guerrilla + te.server = server + te.event_type_id = EventType.Guerrilla.value te.event_seq = 0 - self.fake_uid = self.fake_uid - 1 - te.key = self.fake_uid - te.group = 'F' + fuid = self.fake_uid = self.fake_uid - 1 + te.key = lambda: fuid + te.group_name = 'Yellow' - te.open_datetime = datetime.datetime.now(pytz.utc) + te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) te.close_datetime = te.open_datetime + timedelta(minutes=1) te.dungeon_name = 'fake_dungeon_name' te.event_modifier = 'fake_event_modifier' @@ -488,7 +482,7 @@ def removeDailyReg(self, channel_id, server): class Event: def __init__(self, scheduled_event: dadguide.DgScheduledEvent): self.key = scheduled_event.key() - self.server = SUPPORTED_SERVERS[scheduled_event.server_id] + self.server = scheduled_event.server self.open_datetime = scheduled_event.open_datetime self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name @@ -665,11 +659,9 @@ def fmtHrsMins(seconds): def fmtDaysHrsMinsShort(sec): - days = sec // 86400 - sec -= 86400 * days - hours = sec // 3600 - sec -= 3600 * hours - minutes = sec // 60 + days, sec = divmod(sec, 86400) + hours, sec = divmod(sec, 3600) + minutes, sec = divmod(sec, 60) if days > 0: return '{:2}d {:2}h'.format(int(days), int(hours)) @@ -694,6 +686,7 @@ def isEventWanted(event): def cleanDungeonNames(name): + # TODO: Make this info internally stored if 'tamadra invades in some tech' in name.lower(): return 'Latents invades some Techs & 20x +Eggs' if '1.5x Bonus Pal Point in multiplay' in name: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 601c6277..7615b9be 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -452,10 +452,9 @@ async def print_cmdlist(self, ctx, cmdlist, inline=False): return prefix = ctx.prefix - cmds = list(cmdlist.keys()) prefixes = defaultdict(int) - for c in cmds: + for c in cmdlist: m = re.match(r'^([a-zA-Z]+)\d+$', c) if m: grp = m.group(1) @@ -616,7 +615,8 @@ async def rmglossary(self, ctx, *, term): if term not in self.settings.glossary(): await ctx.send("Glossary item doesn't exist.") return - + if not await confirm_message(ctx, "Are you sure you want to globally remove the glossary data for {}?".format(term)): + return self.settings.rmGlossary(term) await ctx.send("done") @@ -695,6 +695,8 @@ async def rmboss(self, ctx, *, term): if term not in self.settings.boss(): await ctx.send("Boss mechanics item doesn't exist.") return + if not await confirm_message(ctx, "Are you sure you want to globally remove the boss data for {}?".format(term)): + return self.settings.rmBoss(term) await ctx.send("done") @@ -821,6 +823,8 @@ async def rmwhich(self, ctx, *, monster_id: int): if m != m.base_monster: m = m.base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format(m.name_na)): + return name = m.monster_id if name not in self.settings.which(): @@ -1143,7 +1147,8 @@ async def rmdungeonguide(self, ctx, term: str): if term not in self.settings.dungeonGuide(): await ctx.send("DungeonGuide doesn't exist.") return - + if not await confirm_message(ctx, "Are you sure you want to globally remove the dungeonguide data for {}?".format(term)): + return self.settings.rmDungeonGuide(term) await ctx.send("done") @@ -1167,6 +1172,8 @@ async def rmleaderguide(self, ctx, monster_id: int): if m != m.base_monster: m = m.base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format(m.name_na)): + return name = m.monster_id if name not in self.settings.leaderGuide(): diff --git a/selfroleoverride/selfroleoverride.py b/selfroleoverride/selfroleoverride.py index 8ab56260..f9f30145 100644 --- a/selfroleoverride/selfroleoverride.py +++ b/selfroleoverride/selfroleoverride.py @@ -30,7 +30,7 @@ async def convert(self, ctx: commands.Context, arg: str) -> discord.Role: else: if len(pool) > 1: await ctx.send("This selfrole has more than one capitalization" - "possibilities. Please inform a moderator.") + "possibilities. Please inform a moderator.") role = pool[0] From 873785fd4731a71577205075f1245b19de6aeb03 Mon Sep 17 00:00:00 2001 From: tejstead <49261835+tejstead@users.noreply.github.com> Date: Thu, 13 Aug 2020 13:51:22 -0400 Subject: [PATCH 0014/1220] Updated readme so that romkan stuff actually works, also added stuff for Windows users --- README.md | 36 +++++++++++++++++------------------- requirements.txt | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 05dedece..9d6e07ae 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ wget https://raw.githubusercontent.com/nachoapps/miru-v3cogs/master/requirements ``` ## Set up a virtualenv +If using Windows, just follow the Redbot setup guide's instructions to install dependencies (which includes Python 3.8). One way to install python 3.8: @@ -108,8 +109,12 @@ virtualenv --python=/usr/bin/python3.8 venv python3.8 -m venv venv # Activate your venv +Linux/OS X: source venv/bin/activate +Windows: +venv\Scripts\activate.bat + # Install python deps, including Red. pip install -r requirements.txt ``` @@ -118,8 +123,12 @@ pip install -r requirements.txt ``` # You will use the output from pwd in the next steps, so copy it +Linux/OS X: pwd +Windows: +echo %cd% + # Initial setup of bot redbot-setup @@ -128,6 +137,7 @@ bot name> miru_bot_test # Replace current_dir with output from pwd data location> (current_dir)/bot_data +replace / with \ if on Windows # Pick json for now storage backend> 1 @@ -151,28 +161,13 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type - - - - - - -## TODOs - -fork romkan and fix this so people don't keep having to do this - * For Romanji/Kana conversion you will use Romkan. This is a bit tricky to install so we'll go over it first. * If you want to skip this, you can locally comment out all calls to it, but this might be annoying when syncing your code if you're editing files that import it, so you should probably just do it. But if figuring this out is a barrier to entry to start actually coding, feel free to skip at least at the start. - * git clone [this repo](https://github.com/soimort/python-romkan) into any folder you want. - * Open setup.py in the text editor or IDE of your choice. Make the following replacement (basically add `encoding='utf8'` in 3 places): - ```python - here = os.path.abspath(os.path.dirname(__file__)) - proj_info = json.loads(open(os.path.join(here, PROJ_METADATA), encoding='utf8').read()) - README = open(os.path.join(here, 'README.rst'), encoding='utf8').read() - CHANGELOG = open(os.path.join(here, 'CHANGELOG.rst'), encoding='utf8').read() - VERSION = imp.load_source('version', os.path.join(here, 'src/%s/version.py' % PACKAGE_NAME)).__version__ - ``` + * git clone [this repo](https://github.com/tejstead/python-romkan) into any folder you want. * Open a command line in this same directory and run `py setup.py install` + * Locate the folder named romkan-0.2.1-py3.8.egg + * Copy the folder inside that folder, named `romkan`, and copy it to your virtual environment's lib folder (on Windows this is named `venv\lib\site-packages`) + ## Setting up the bot @@ -186,6 +181,9 @@ fork romkan and fix this so people don't keep having to do this ## Other +#TODO: update this lol + + ### Emoji * If you want emojis in `^id` commands, and you are setting this up for DEV PURPOSES ONLY, you can talk to tactical_retreat about getting your bot invited to the emoji servers. If you want to make your own separate Miru instance though, you're on your own for that. * Give t_r your bot's invite link & ask him for the server IDs diff --git a/requirements.txt b/requirements.txt index c4de0d58..41caffe8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ wheel Red-DiscordBot[postgres] backoff prettytable -romkan +# romkan - commented out because this has to be manually installed google-api google-api-python-client google-cloud From 2463c62809e2f06699876c93b1183b3a4c09f965 Mon Sep 17 00:00:00 2001 From: droonkid Date: Mon, 17 Aug 2020 17:18:28 -0700 Subject: [PATCH 0015/1220] update --- padevents/padevents.py | 1442 +++++++++++------------ padguidedb/padguidedb.py | 628 +++++----- padinfo/padinfo.py | 2333 ++++++++++++++++++++------------------ 3 files changed, 2238 insertions(+), 2165 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 5c9de8d4..a95091e1 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,721 +1,721 @@ -import traceback -import asyncio -import re -import datetime -import pytz -from collections import defaultdict -from datetime import timedelta -from enum import Enum - -import prettytable -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box, pagify - -from dadguide import dadguide -from rpadutils import CogSettings - -SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] - - -class PadEvents(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - self.settings = PadEventSettings("padevents") - - # Load event data - self.events = list() - self.started_events = set() - - self.fake_uid = -999 - - def cog_unload(self): - # Manually nulling out database because the GC for cogs seems to be pretty shitty - self.events = list() - self.started_events = set() - - async def reload_padevents(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadEvents'): - try: - await self.refresh_data() - print('Done refreshing PadEvents') - except Exception as ex: - print("reload padevents loop caught exception " + str(ex)) - traceback.print_exc() - - await asyncio.sleep(60 * 60 * 1) - - async def refresh_data(self): - dg_cog = self.bot.get_cog('Dadguide') - await dg_cog.wait_until_ready() - scheduled_events = dg_cog.database.get_all_events() - - new_events = [] - for se in scheduled_events: - try: - new_events.append(Event(se)) - except Exception as ex: - print(ex) - - self.events = new_events - self.started_events = {ev.key for ev in new_events if ev.is_started()} - - async def check_started(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadEvents'): - try: - events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) - - daily_refresh_servers = set() - for e in events: - self.started_events.add(e.key) - if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: - for gr in list(self.settings.listGuerrillaReg()): - if e.server == gr['server']: - try: - message = box("Server " + e.server + ", group " + - e.groupLongName() + " : " + e.name_and_modifier) - channel = self.bot.get_channel(int(gr['channel_id'])) - - try: - role_name = '{}_group_{}'.format( - e.server, e.groupLongName()) - role = get_role(channel.guild.roles, role_name) - if role and role.mentionable: - message = "{} `: {} is starting`".format( - role.mention, e.name_and_modifier) - except: - pass # do nothing if role is missing - - await channel.send(message) - except Exception as ex: - # deregister gr - traceback.print_exc() - # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) - print("caught exception while sending guerrilla msg:\n" + str(ex)) - - else: - if not e.dungeon_type in [DungeonType.Normal]: - daily_refresh_servers.add(e.server) - - for server in daily_refresh_servers: - msg = self.makeActiveText(server) - for daily_registration in list(self.settings.listDailyReg()): - try: - if server == daily_registration['server']: - await self.pageOutput(ctx, msg, channel_id=daily_registration['channel_id']) - except Exception as ex: - traceback.print_exc() - # self.settings.removeDailyReg( - # daily_registration['channel_id'], daily_registration['server']) - print("caught exception while sending daily msg:\n" + str(ex)) - - except Exception as ex: - traceback.print_exc() - print("caught exception while checking guerrillas:\n" + str(ex)) - - await asyncio.sleep(10) - print("done check_started") - - @commands.group(aliases=['pde']) - @checks.mod_or_permissions(manage_guild=True) - async def padevents(self, ctx): - """PAD event tracking""" - - @padevents.command() - @checks.is_owner() - async def testevent(self, ctx, server, seconds: int = 0): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - dg_cog = self.bot.get_cog('Dadguide') - await dg_cog.wait_until_ready() - te = dadguide.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) - - te.server = server - te.event_type_id = EventType.Guerrilla.value - te.event_seq = 0 - fuid = self.fake_uid = self.fake_uid - 1 - te.key = lambda: fuid - te.group_name = 'Yellow' - - te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) - te.close_datetime = te.open_datetime + timedelta(minutes=1) - te.dungeon_name = 'fake_dungeon_name' - te.event_modifier = 'fake_event_modifier' - self.events.append(Event(te)) - - await ctx.send("Fake event injected.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def addchannel(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - channel_id = ctx.channel.id - if self.settings.checkGuerrillaReg(channel_id, server): - await ctx.send("Channel already active.") - return - - self.settings.addGuerrillaReg(channel_id, server) - await ctx.send("Channel now active.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmchannel(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") - return - - channel_id = ctx.channel.id - if not self.settings.checkGuerrillaReg(channel_id, server): - await ctx.send("Channel is not active.") - return - - self.settings.removeGuerrillaReg(channel_id, server) - await ctx.send("Channel deactivated.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def addchanneldaily(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") - return - - channel_id = ctx.channel.id - if self.settings.checkDailyReg(channel_id, server): - await ctx.send("Channel already active.") - return - - self.settings.addDailyReg(channel_id, server) - await ctx.send("Channel now active.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmchanneldaily(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") - return - - channel_id = ctx.channel.id - if not self.settings.checkDailyReg(channel_id, server): - await ctx.send("Channel is not active.") - return - - self.settings.removeDailyReg(channel_id, server) - await ctx.send("Channel deactivated.") - - @padevents.command(name="listchannels") - @checks.mod_or_permissions(manage_guild=True) - async def _listchannel(self, ctx): - msg = 'Following daily channels are registered:\n' - msg += self.makeChannelList(self.settings.listDailyReg()) - msg += "\n" - msg += 'Following guerilla channels are registered:\n' - msg += self.makeChannelList(self.settings.listGuerrillaReg()) - await self.pageOutput(ctx, msg) - - def makeChannelList(self, reg_list): - msg = "" - for cr in reg_list: - reg_channel_id = cr['channel_id'] - channel = self.bot.get_channel(int(reg_channel_id)) - channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' - server_name = channel.guild.name if channel else 'Unknown server' - msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' - return msg - - @padevents.command() - @checks.mod_or_permissions(manage_guild=True) - async def active(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - msg = self.makeActiveText(server) - await self.pageOutput(ctx, msg) - - def makeActiveText(self, server): - server_events = EventList(self.events).withServer(server) - active_events = server_events.activeOnly() - pending_events = server_events.pendingOnly() - available_events = server_events.availableOnly() - - msg = "Listing all events for " + server - - special_events = active_events.withType( - EventType.Special).itemsByCloseTime() - if len(special_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Special Events', special_events) - - all_etc_events = active_events.withType(EventType.Etc) - - etc_events = all_etc_events.withDungeonType( - DungeonType.Etc).excludeUnwantedEvents().itemsByCloseTime() - if len(etc_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Etc Events', etc_events) - - # Old-style guerrillas - active_guerrilla_events = active_events.withType(EventType.Guerrilla).items() - if len(active_guerrilla_events) > 0: - msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) - - guerrilla_events = pending_events.withType(EventType.Guerrilla).items() - if len(guerrilla_events) > 0: - msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', guerrilla_events) - - # New-style guerrillas - active_guerrilla_events = active_events.withType(EventType.SpecialWeek).items() - if len(active_guerrilla_events) > 0: - msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) - - guerrilla_events = pending_events.withType(EventType.SpecialWeek).items() - if len(guerrilla_events) > 0: - msg += "\n\n" + \ - self.makeFullGuerrillaOutput( - 'Guerrilla Events', guerrilla_events, starter_guerilla=True) - - # clean up long headers - msg = msg.replace('-------------------------------------', '-----------------------') - - return msg - - async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): - msg = msg.strip() - msg = pagify(msg, ["\n"], shorten_by=20) - for page in msg: - try: - if channel_id is None: - await ctx.send(format_type(page)) - else: - await self.bot.get_channel(int(channel_id)).send(format_type(page)) - except Exception as e: - print("page output failed " + str(e)) - print("tried to print: " + page) - - def makeActiveOutput(self, table_name, event_list): - tbl = prettytable.PrettyTable(["Time", table_name]) - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.NONE - tbl.align[table_name] = "l" - tbl.align["Time"] = "r" - for e in event_list: - tbl.add_row([e.endFromNowFullMin().strip(), e.name_and_modifier]) - return tbl.get_string() - - def makeActiveGuerrillaOutput(self, table_name, event_list): - tbl = prettytable.PrettyTable([table_name, "Group", "Time"]) - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.NONE - tbl.align[table_name] = "l" - tbl.align["Time"] = "r" - for e in event_list: - tbl.add_row([e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) - return tbl.get_string() - - def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False): - events_by_name = defaultdict(list) - for e in event_list: - events_by_name[e.name_and_modifier].append(e) - - rows = list() - grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] - for name, events in events_by_name.items(): - events = sorted(events, key=lambda e: e.open_datetime) - events_by_group = defaultdict(list) - for e in events: - events_by_group[e.group].append(e) - - done = False - while not done: - did_work = False - row = list() - row.append(name) - for g in grps: - grp_list = events_by_group[g] - if len(grp_list) == 0: - row.append("") - else: - did_work = True - e = grp_list.pop(0) - row.append(e.toGuerrillaStr()) - if did_work: - rows.append(row) - else: - done = True - - col1 = "Pending" - tbl = prettytable.PrettyTable([col1] + grps) - tbl.align[col1] = "l" - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.ALL - - for r in rows: - tbl.add_row(r) - - header = "Times are PT below\n\n" - return header + tbl.get_string() + "\n" - - @commands.command(aliases=['events']) - async def eventsna(self, ctx): - """Print upcoming daily events for NA.""" - await self.doPartial(ctx, 'NA') - - @commands.command() - async def eventsjp(self, ctx): - """Print upcoming daily events for JP.""" - await self.doPartial(ctx, 'JP') - - @commands.command() - async def eventskr(self, ctx): - """Print upcoming daily events for KR.""" - await self.doPartial(ctx, 'KR') - - async def doPartial(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - events = EventList(self.events) - events = events.withServer(server) - events = events.inType([EventType.Guerrilla, EventType.SpecialWeek]) - - active_events = events.activeOnly().itemsByOpenTime(reverse=True) - pending_events = events.pendingOnly().itemsByOpenTime(reverse=True) - - group_to_active_event = {e.group: e for e in active_events} - group_to_pending_event = {e.group: e for e in pending_events} - - active_events = list(group_to_active_event.values()) - pending_events = list(group_to_pending_event.values()) - - active_events.sort(key=lambda e: e.group, reverse=True) - pending_events.sort(key=lambda e: e.group, reverse=True) - - if len(active_events) == 0 and len(pending_events) == 0: - await ctx.send("No events available for " + server) - return - - output = "Events for {}".format(server) - - if len(active_events) > 0: - output += "\n\n" + "G Remaining Dungeon" - for e in active_events: - output += "\n" + e.toPartialEvent(self) - - if len(pending_events) > 0: - output += "\n\n" + "G PT ET ETA Dungeon" - for e in pending_events: - output += "\n" + e.toPartialEvent(self) - - await ctx.send(box(output)) - - -def makeChannelReg(channel_id, server): - server = normalizeServer(server) - return { - "channel_id": channel_id, - "server": server - } - - -class PadEventSettings(CogSettings): - def make_default_settings(self): - config = { - 'guerrilla_regs': [], - 'daily_regs': [], - } - return config - - def listGuerrillaReg(self): - return self.bot_settings['guerrilla_regs'] - - def addGuerrillaReg(self, channel_id, server): - self.listGuerrillaReg().append(makeChannelReg(channel_id, server)) - self.save_settings() - - def checkGuerrillaReg(self, channel_id, server): - return makeChannelReg(channel_id, server) in self.listGuerrillaReg() - - def removeGuerrillaReg(self, channel_id, server): - if self.checkGuerrillaReg(channel_id, server): - self.listGuerrillaReg().remove(makeChannelReg(channel_id, server)) - self.save_settings() - - def listDailyReg(self): - return self.bot_settings['daily_regs'] - - def addDailyReg(self, channel_id, server): - self.listDailyReg().append(makeChannelReg(channel_id, server)) - self.save_settings() - - def checkDailyReg(self, channel_id, server): - return makeChannelReg(channel_id, server) in self.listDailyReg() - - def removeDailyReg(self, channel_id, server): - if self.checkDailyReg(channel_id, server): - self.listDailyReg().remove(makeChannelReg(channel_id, server)) - self.save_settings() - - -class Event: - def __init__(self, scheduled_event: dadguide.DgScheduledEvent): - self.key = scheduled_event.key() - self.server = scheduled_event.server - self.open_datetime = scheduled_event.open_datetime - self.close_datetime = scheduled_event.close_datetime - self.group = scheduled_event.group_name - self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' - self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' - - self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) - self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') - - self.name_and_modifier = self.clean_dungeon_name - if self.clean_event_name != '': - self.name_and_modifier += ', ' + self.clean_event_name - - self.event_type = EventType(scheduled_event.event_type_id) - self.dungeon_type = DungeonType( - scheduled_event.dungeon.dungeon_type) if scheduled_event.dungeon else DungeonType.Unknown - - def start_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.open_datetime - now).total_seconds() - - def end_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.close_datetime - now).total_seconds() - - def is_started(self): - """True if past the open time for the event.""" - return self.start_from_now_sec() <= 0 - - def is_finished(self): - """True if past the close time for the event.""" - return self.end_from_now_sec() <= 0 - - def is_active(self): - """True if between open and close time for the event.""" - return self.is_started() and not self.is_finished() - - def is_pending(self): - """True if event has not started.""" - return not self.is_started() - - def is_available(self): - """True if event has not finished.""" - return not self.is_finished() - - def tostr(self): - return fmtTime(self.open_datetime) + "," + fmtTime( - self.close_datetime) + "," + self.group + "," + self.dungeon_code + "," + self.event_type + "," + self.event_seq - - def startPst(self): - tz = pytz.timezone('US/Pacific') - return self.open_datetime.astimezone(tz) - - def startEst(self): - tz = pytz.timezone('US/Eastern') - return self.open_datetime.astimezone(tz) - - def startFromNow(self): - return fmtHrsMins(self.start_from_now_sec()) - - def endFromNow(self): - return fmtHrsMins(self.end_from_now_sec()) - - def endFromNowFullMin(self): - return fmtDaysHrsMinsShort(self.end_from_now_sec()) - - def toGuerrillaStr(self): - return fmtTimeShort(self.startPst()) - - def toDateStr(self): - return self.server + "," + self.group + "," + fmtTime(self.startPst()) + "," + fmtTime( - self.startEst()) + "," + self.startFromNow() - - def groupShortName(self): - return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') - - def groupLongName(self): - return self.group.upper() - - def toPartialEvent(self, pe): - group = self.groupShortName() - if self.is_started(): - return group + " " + self.endFromNow() + " " + self.name_and_modifier - else: - return group + " " + fmtTimeShort(self.startPst()) + " " + fmtTimeShort( - self.startEst()) + " " + self.startFromNow() + " " + self.name_and_modifier - - -class EventList: - def __init__(self, event_list): - self.event_list = event_list - - def withFunc(self, func, exclude=False): - if exclude: - return EventList(list(itertools.filterfalse(func, self.event_list))) - else: - return EventList(list(filter(func, self.event_list))) - - def withServer(self, server): - return self.withFunc(lambda e: e.server == normalizeServer(server)) - - def withType(self, event_type): - return self.withFunc(lambda e: e.event_type == event_type) - - def inType(self, event_types): - return self.withFunc(lambda e: e.event_type in event_types) - - def withDungeonType(self, dungeon_type, exclude=False): - return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) - - def withNameContains(self, name, exclude=False): - return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) - - def excludeUnwantedEvents(self): - return self.withFunc(isEventWanted) - - def items(self): - return self.event_list - - def startedOnly(self): - return self.withFunc(lambda e: e.is_started()) - - def pendingOnly(self): - return self.withFunc(lambda e: e.is_pending()) - - def activeOnly(self): - return self.withFunc(lambda e: e.is_active()) - - def availableOnly(self): - return self.withFunc(lambda e: e.is_available()) - - def itemsByOpenTime(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) - - def itemsByCloseTime(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) - - -# TIME_FMT = """%a %b %d %H:%M:%S %Y""" - - -class EventType(Enum): - Week = 0 - Special = 1 - SpecialWeek = 2 - Guerrilla = 3 - GuerrillaNew = 4 - Etc = -100 - - -class DungeonType(Enum): - Unknown = -1 - Normal = 0 - CoinDailyOther = 1 - Technical = 2 - Etc = 3 - Unknown4 = 4 - Unknown7 = 7 - Unknown9 = 9 - - -def fmtTime(dt): - return dt.strftime("%Y-%m-%d %H:%M") - - -def fmtTimeShort(dt): - return dt.strftime("%H:%M") - - -def fmtHrsMins(seconds): - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - return '{:2}h {:2}m'.format(int(hours), int(minutes)) - - -def fmtDaysHrsMinsShort(sec): - days, sec = divmod(sec, 86400) - hours, sec = divmod(sec, 3600) - minutes, sec = divmod(sec, 60) - - if days > 0: - return '{:2}d {:2}h'.format(int(days), int(hours)) - elif hours > 0: - return '{:2}h {:2}m'.format(int(hours), int(minutes)) - else: - return '{:2}m'.format(int(minutes)) - - -def normalizeServer(server): - server = server.upper() - return 'NA' if server == 'US' else server - - -def isEventWanted(event): - name = event.name_and_modifier.lower() - if 'castle of satan' in name: - # eliminate things like : TAMADRA Invades in [Castle of Satan][Castle of Satan in the Abyss] - return False - - return True - - -def cleanDungeonNames(name): - # TODO: Make this info internally stored - if 'tamadra invades in some tech' in name.lower(): - return 'Latents invades some Techs & 20x +Eggs' - if '1.5x Bonus Pal Point in multiplay' in name: - name = '[Descends] 1.5x Pal Points in multiplay' - name = name.replace('No Continues', 'No Cont') - name = name.replace('No Continue', 'No Cont') - name = name.replace('Some Limited Time Dungeons', 'Some Guerrillas') - name = name.replace('are added in', 'in') - name = name.replace('!', '') - name = name.replace('Dragon Infestation', 'Dragons') - name = name.replace(' Infestation', 's') - name = name.replace('Daily Descended Dungeon', 'Daily Descends') - name = name.replace('Chance for ', '') - name = name.replace('Jewel of the Spirit', 'Spirit Jewel') - name = name.replace(' & ', '/') - name = name.replace(' / ', '/') - name = name.replace('PAD Radar', 'PADR') - name = name.replace('in normal dungeons', 'in normals') - name = name.replace('Selected ', 'Some ') - name = name.replace('Enhanced ', 'Enh ') - name = name.replace('All Att. Req.', 'All Att.') - name = name.replace('Extreme King Metal Dragon', 'Extreme KMD') - name = name.replace('Golden Mound-Tricolor [Fr/Wt/Wd Only]', 'Golden Mound') - name = name.replace('Gods-Awakening Materials Descended', "Awoken Mats") - name = name.replace('Orb move time 4 sec', '4s move time') - name = name.replace('Awakening Materials Descended', 'Awkn Mats') - name = name.replace('Awakening Materials', 'Awkn Mats') - name = name.replace("Star Treasure Thieves' Den", 'STTD') - name = name.replace('Ruins of the Star Vault', 'Star Vault') - name = name.replace('-★6 or lower Enhanced', '') - - return name +import traceback +import asyncio +import re +import datetime +import pytz +from collections import defaultdict +from datetime import timedelta +from enum import Enum + +import prettytable +import discord +from redbot.core import checks +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import inline, box, pagify + +from dadguide import dadguide +from rpadutils import CogSettings + +SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] + + +class PadEvents(commands.Cog): + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + self.settings = PadEventSettings("padevents") + + # Load event data + self.events = list() + self.started_events = set() + + self.fake_uid = -999 + + def cog_unload(self): + # Manually nulling out database because the GC for cogs seems to be pretty shitty + self.events = list() + self.started_events = set() + + async def reload_padevents(self): + await self.bot.wait_until_ready() + while self == self.bot.get_cog('PadEvents'): + try: + await self.refresh_data() + print('Done refreshing PadEvents') + except Exception as ex: + print("reload padevents loop caught exception " + str(ex)) + traceback.print_exc() + + await asyncio.sleep(60 * 60 * 1) + + async def refresh_data(self): + dg_cog = self.bot.get_cog('Dadguide') + await dg_cog.wait_until_ready() + scheduled_events = dg_cog.database.get_all_events() + + new_events = [] + for se in scheduled_events: + try: + new_events.append(Event(se)) + except Exception as ex: + print(ex) + + self.events = new_events + self.started_events = {ev.key for ev in new_events if ev.is_started()} + + async def check_started(self): + await self.bot.wait_until_ready() + while self == self.bot.get_cog('PadEvents'): + try: + events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) + + daily_refresh_servers = set() + for e in events: + self.started_events.add(e.key) + if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: + for gr in list(self.settings.listGuerrillaReg()): + if e.server == gr['server']: + try: + message = box("Server " + e.server + ", group " + + e.groupLongName() + " : " + e.name_and_modifier) + channel = self.bot.get_channel(int(gr['channel_id'])) + + try: + role_name = '{}_group_{}'.format( + e.server, e.groupLongName()) + role = get_role(channel.guild.roles, role_name) + if role and role.mentionable: + message = "{} `: {} is starting`".format( + role.mention, e.name_and_modifier) + except: + pass # do nothing if role is missing + + await channel.send(message) + except Exception as ex: + # deregister gr + traceback.print_exc() + # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) + print("caught exception while sending guerrilla msg:\n" + str(ex)) + + else: + if not e.dungeon_type in [DungeonType.Normal]: + daily_refresh_servers.add(e.server) + + for server in daily_refresh_servers: + msg = self.makeActiveText(server) + for daily_registration in list(self.settings.listDailyReg()): + try: + if server == daily_registration['server']: + await self.pageOutput(ctx, msg, channel_id=daily_registration['channel_id']) + except Exception as ex: + traceback.print_exc() + # self.settings.removeDailyReg( + # daily_registration['channel_id'], daily_registration['server']) + print("caught exception while sending daily msg:\n" + str(ex)) + + except Exception as ex: + traceback.print_exc() + print("caught exception while checking guerrillas:\n" + str(ex)) + + await asyncio.sleep(10) + print("done check_started") + + @commands.group(aliases=['pde']) + @checks.mod_or_permissions(manage_guild=True) + async def padevents(self, ctx): + """PAD event tracking""" + + @padevents.command() + @checks.is_owner() + async def testevent(self, ctx, server, seconds: int = 0): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + dg_cog = self.bot.get_cog('Dadguide') + await dg_cog.wait_until_ready() + te = dadguide.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) + + te.server = server + te.event_type_id = EventType.Guerrilla.value + te.event_seq = 0 + fuid = self.fake_uid = self.fake_uid - 1 + te.key = lambda: fuid + te.group_name = 'Yellow' + + te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) + te.close_datetime = te.open_datetime + timedelta(minutes=1) + te.dungeon_name = 'fake_dungeon_name' + te.event_modifier = 'fake_event_modifier' + self.events.append(Event(te)) + + await ctx.send("Fake event injected.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def addchannel(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + channel_id = ctx.channel.id + if self.settings.checkGuerrillaReg(channel_id, server): + await ctx.send("Channel already active.") + return + + self.settings.addGuerrillaReg(channel_id, server) + await ctx.send("Channel now active.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def rmchannel(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + return + + channel_id = ctx.channel.id + if not self.settings.checkGuerrillaReg(channel_id, server): + await ctx.send("Channel is not active.") + return + + self.settings.removeGuerrillaReg(channel_id, server) + await ctx.send("Channel deactivated.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def addchanneldaily(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + return + + channel_id = ctx.channel.id + if self.settings.checkDailyReg(channel_id, server): + await ctx.send("Channel already active.") + return + + self.settings.addDailyReg(channel_id, server) + await ctx.send("Channel now active.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def rmchanneldaily(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + return + + channel_id = ctx.channel.id + if not self.settings.checkDailyReg(channel_id, server): + await ctx.send("Channel is not active.") + return + + self.settings.removeDailyReg(channel_id, server) + await ctx.send("Channel deactivated.") + + @padevents.command(name="listchannels") + @checks.mod_or_permissions(manage_guild=True) + async def _listchannel(self, ctx): + msg = 'Following daily channels are registered:\n' + msg += self.makeChannelList(self.settings.listDailyReg()) + msg += "\n" + msg += 'Following guerilla channels are registered:\n' + msg += self.makeChannelList(self.settings.listGuerrillaReg()) + await self.pageOutput(ctx, msg) + + def makeChannelList(self, reg_list): + msg = "" + for cr in reg_list: + reg_channel_id = cr['channel_id'] + channel = self.bot.get_channel(int(reg_channel_id)) + channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' + server_name = channel.guild.name if channel else 'Unknown server' + msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' + return msg + + @padevents.command() + @checks.mod_or_permissions(manage_guild=True) + async def active(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + msg = self.makeActiveText(server) + await self.pageOutput(ctx, msg) + + def makeActiveText(self, server): + server_events = EventList(self.events).withServer(server) + active_events = server_events.activeOnly() + pending_events = server_events.pendingOnly() + available_events = server_events.availableOnly() + + msg = "Listing all events for " + server + + special_events = active_events.withType( + EventType.Special).itemsByCloseTime() + if len(special_events) > 0: + msg += "\n\n" + self.makeActiveOutput('Special Events', special_events) + + all_etc_events = active_events.withType(EventType.Etc) + + etc_events = all_etc_events.withDungeonType( + DungeonType.Etc).excludeUnwantedEvents().itemsByCloseTime() + if len(etc_events) > 0: + msg += "\n\n" + self.makeActiveOutput('Etc Events', etc_events) + + # Old-style guerrillas + active_guerrilla_events = active_events.withType(EventType.Guerrilla).items() + if len(active_guerrilla_events) > 0: + msg += "\n\n" + \ + self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + + guerrilla_events = pending_events.withType(EventType.Guerrilla).items() + if len(guerrilla_events) > 0: + msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', guerrilla_events) + + # New-style guerrillas + active_guerrilla_events = active_events.withType(EventType.SpecialWeek).items() + if len(active_guerrilla_events) > 0: + msg += "\n\n" + \ + self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + + guerrilla_events = pending_events.withType(EventType.SpecialWeek).items() + if len(guerrilla_events) > 0: + msg += "\n\n" + \ + self.makeFullGuerrillaOutput( + 'Guerrilla Events', guerrilla_events, starter_guerilla=True) + + # clean up long headers + msg = msg.replace('-------------------------------------', '-----------------------') + + return msg + + async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): + msg = msg.strip() + msg = pagify(msg, ["\n"], shorten_by=20) + for page in msg: + try: + if channel_id is None: + await ctx.send(format_type(page)) + else: + await self.bot.get_channel(int(channel_id)).send(format_type(page)) + except Exception as e: + print("page output failed " + str(e)) + print("tried to print: " + page) + + def makeActiveOutput(self, table_name, event_list): + tbl = prettytable.PrettyTable(["Time", table_name]) + tbl.hrules = prettytable.HEADER + tbl.vrules = prettytable.NONE + tbl.align[table_name] = "l" + tbl.align["Time"] = "r" + for e in event_list: + tbl.add_row([e.endFromNowFullMin().strip(), e.name_and_modifier]) + return tbl.get_string() + + def makeActiveGuerrillaOutput(self, table_name, event_list): + tbl = prettytable.PrettyTable([table_name, "Group", "Time"]) + tbl.hrules = prettytable.HEADER + tbl.vrules = prettytable.NONE + tbl.align[table_name] = "l" + tbl.align["Time"] = "r" + for e in event_list: + tbl.add_row([e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) + return tbl.get_string() + + def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False): + events_by_name = defaultdict(list) + for e in event_list: + events_by_name[e.name_and_modifier].append(e) + + rows = list() + grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] + for name, events in events_by_name.items(): + events = sorted(events, key=lambda e: e.open_datetime) + events_by_group = defaultdict(list) + for e in events: + events_by_group[e.group].append(e) + + done = False + while not done: + did_work = False + row = list() + row.append(name) + for g in grps: + grp_list = events_by_group[g] + if len(grp_list) == 0: + row.append("") + else: + did_work = True + e = grp_list.pop(0) + row.append(e.toGuerrillaStr()) + if did_work: + rows.append(row) + else: + done = True + + col1 = "Pending" + tbl = prettytable.PrettyTable([col1] + grps) + tbl.align[col1] = "l" + tbl.hrules = prettytable.HEADER + tbl.vrules = prettytable.ALL + + for r in rows: + tbl.add_row(r) + + header = "Times are PT below\n\n" + return header + tbl.get_string() + "\n" + + @commands.command(aliases=['events']) + async def eventsna(self, ctx): + """Print upcoming daily events for NA.""" + await self.doPartial(ctx, 'NA') + + @commands.command() + async def eventsjp(self, ctx): + """Print upcoming daily events for JP.""" + await self.doPartial(ctx, 'JP') + + @commands.command() + async def eventskr(self, ctx): + """Print upcoming daily events for KR.""" + await self.doPartial(ctx, 'KR') + + async def doPartial(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + events = EventList(self.events) + events = events.withServer(server) + events = events.inType([EventType.Guerrilla, EventType.SpecialWeek]) + + active_events = events.activeOnly().itemsByOpenTime(reverse=True) + pending_events = events.pendingOnly().itemsByOpenTime(reverse=True) + + group_to_active_event = {e.group: e for e in active_events} + group_to_pending_event = {e.group: e for e in pending_events} + + active_events = list(group_to_active_event.values()) + pending_events = list(group_to_pending_event.values()) + + active_events.sort(key=lambda e: e.group, reverse=True) + pending_events.sort(key=lambda e: e.group, reverse=True) + + if len(active_events) == 0 and len(pending_events) == 0: + await ctx.send("No events available for " + server) + return + + output = "Events for {}".format(server) + + if len(active_events) > 0: + output += "\n\n" + "G Remaining Dungeon" + for e in active_events: + output += "\n" + e.toPartialEvent(self) + + if len(pending_events) > 0: + output += "\n\n" + "G PT ET ETA Dungeon" + for e in pending_events: + output += "\n" + e.toPartialEvent(self) + + await ctx.send(box(output)) + + +def makeChannelReg(channel_id, server): + server = normalizeServer(server) + return { + "channel_id": channel_id, + "server": server + } + + +class PadEventSettings(CogSettings): + def make_default_settings(self): + config = { + 'guerrilla_regs': [], + 'daily_regs': [], + } + return config + + def listGuerrillaReg(self): + return self.bot_settings['guerrilla_regs'] + + def addGuerrillaReg(self, channel_id, server): + self.listGuerrillaReg().append(makeChannelReg(channel_id, server)) + self.save_settings() + + def checkGuerrillaReg(self, channel_id, server): + return makeChannelReg(channel_id, server) in self.listGuerrillaReg() + + def removeGuerrillaReg(self, channel_id, server): + if self.checkGuerrillaReg(channel_id, server): + self.listGuerrillaReg().remove(makeChannelReg(channel_id, server)) + self.save_settings() + + def listDailyReg(self): + return self.bot_settings['daily_regs'] + + def addDailyReg(self, channel_id, server): + self.listDailyReg().append(makeChannelReg(channel_id, server)) + self.save_settings() + + def checkDailyReg(self, channel_id, server): + return makeChannelReg(channel_id, server) in self.listDailyReg() + + def removeDailyReg(self, channel_id, server): + if self.checkDailyReg(channel_id, server): + self.listDailyReg().remove(makeChannelReg(channel_id, server)) + self.save_settings() + + +class Event: + def __init__(self, scheduled_event: dadguide.DgScheduledEvent): + self.key = scheduled_event.key() + self.server = SUPPORTED_SERVERS[scheduled_event.server_id] + self.open_datetime = scheduled_event.open_datetime + self.close_datetime = scheduled_event.close_datetime + self.group = scheduled_event.group_name + self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' + self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' + + self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) + self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') + + self.name_and_modifier = self.clean_dungeon_name + if self.clean_event_name != '': + self.name_and_modifier += ', ' + self.clean_event_name + + self.event_type = EventType(scheduled_event.event_type_id) + self.dungeon_type = DungeonType( + scheduled_event.dungeon.dungeon_type) if scheduled_event.dungeon else DungeonType.Unknown + + def start_from_now_sec(self): + now = datetime.datetime.now(pytz.utc) + return (self.open_datetime - now).total_seconds() + + def end_from_now_sec(self): + now = datetime.datetime.now(pytz.utc) + return (self.close_datetime - now).total_seconds() + + def is_started(self): + """True if past the open time for the event.""" + return self.start_from_now_sec() <= 0 + + def is_finished(self): + """True if past the close time for the event.""" + return self.end_from_now_sec() <= 0 + + def is_active(self): + """True if between open and close time for the event.""" + return self.is_started() and not self.is_finished() + + def is_pending(self): + """True if event has not started.""" + return not self.is_started() + + def is_available(self): + """True if event has not finished.""" + return not self.is_finished() + + def tostr(self): + return fmtTime(self.open_datetime) + "," + fmtTime( + self.close_datetime) + "," + self.group + "," + self.dungeon_code + "," + self.event_type + "," + self.event_seq + + def startPst(self): + tz = pytz.timezone('US/Pacific') + return self.open_datetime.astimezone(tz) + + def startEst(self): + tz = pytz.timezone('US/Eastern') + return self.open_datetime.astimezone(tz) + + def startFromNow(self): + return fmtHrsMins(self.start_from_now_sec()) + + def endFromNow(self): + return fmtHrsMins(self.end_from_now_sec()) + + def endFromNowFullMin(self): + return fmtDaysHrsMinsShort(self.end_from_now_sec()) + + def toGuerrillaStr(self): + return fmtTimeShort(self.startPst()) + + def toDateStr(self): + return self.server + "," + self.group + "," + fmtTime(self.startPst()) + "," + fmtTime( + self.startEst()) + "," + self.startFromNow() + + def groupShortName(self): + return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') + + def groupLongName(self): + return self.group.upper() + + def toPartialEvent(self, pe): + group = self.groupShortName() + if self.is_started(): + return group + " " + self.endFromNow() + " " + self.name_and_modifier + else: + return group + " " + fmtTimeShort(self.startPst()) + " " + fmtTimeShort( + self.startEst()) + " " + self.startFromNow() + " " + self.name_and_modifier + + +class EventList: + def __init__(self, event_list): + self.event_list = event_list + + def withFunc(self, func, exclude=False): + if exclude: + return EventList(list(itertools.filterfalse(func, self.event_list))) + else: + return EventList(list(filter(func, self.event_list))) + + def withServer(self, server): + return self.withFunc(lambda e: e.server == normalizeServer(server)) + + def withType(self, event_type): + return self.withFunc(lambda e: e.event_type == event_type) + + def inType(self, event_types): + return self.withFunc(lambda e: e.event_type in event_types) + + def withDungeonType(self, dungeon_type, exclude=False): + return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) + + def withNameContains(self, name, exclude=False): + return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) + + def excludeUnwantedEvents(self): + return self.withFunc(isEventWanted) + + def items(self): + return self.event_list + + def startedOnly(self): + return self.withFunc(lambda e: e.is_started()) + + def pendingOnly(self): + return self.withFunc(lambda e: e.is_pending()) + + def activeOnly(self): + return self.withFunc(lambda e: e.is_active()) + + def availableOnly(self): + return self.withFunc(lambda e: e.is_available()) + + def itemsByOpenTime(self, reverse=False): + return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) + + def itemsByCloseTime(self, reverse=False): + return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) + + +# TIME_FMT = """%a %b %d %H:%M:%S %Y""" + + +class EventType(Enum): + Week = 0 + Special = 1 + SpecialWeek = 2 + Guerrilla = 3 + GuerrillaNew = 4 + Etc = -100 + + +class DungeonType(Enum): + Unknown = -1 + Normal = 0 + CoinDailyOther = 1 + Technical = 2 + Etc = 3 + Unknown4 = 4 + Unknown7 = 7 + Unknown9 = 9 + + +def fmtTime(dt): + return dt.strftime("%Y-%m-%d %H:%M") + + +def fmtTimeShort(dt): + return dt.strftime("%H:%M") + + +def fmtHrsMins(seconds): + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return '{:2}h {:2}m'.format(int(hours), int(minutes)) + + +def fmtDaysHrsMinsShort(sec): + days, sec = divmod(sec, 86400) + hours, sec = divmod(sec, 3600) + minutes, sec = divmod(sec, 60) + + if days > 0: + return '{:2}d {:2}h'.format(int(days), int(hours)) + elif hours > 0: + return '{:2}h {:2}m'.format(int(hours), int(minutes)) + else: + return '{:2}m'.format(int(minutes)) + + +def normalizeServer(server): + server = server.upper() + return 'NA' if server == 'US' else server + + +def isEventWanted(event): + name = event.name_and_modifier.lower() + if 'castle of satan' in name: + # eliminate things like : TAMADRA Invades in [Castle of Satan][Castle of Satan in the Abyss] + return False + + return True + + +def cleanDungeonNames(name): + # TODO: Make this info internally stored + if 'tamadra invades in some tech' in name.lower(): + return 'Latents invades some Techs & 20x +Eggs' + if '1.5x Bonus Pal Point in multiplay' in name: + name = '[Descends] 1.5x Pal Points in multiplay' + name = name.replace('No Continues', 'No Cont') + name = name.replace('No Continue', 'No Cont') + name = name.replace('Some Limited Time Dungeons', 'Some Guerrillas') + name = name.replace('are added in', 'in') + name = name.replace('!', '') + name = name.replace('Dragon Infestation', 'Dragons') + name = name.replace(' Infestation', 's') + name = name.replace('Daily Descended Dungeon', 'Daily Descends') + name = name.replace('Chance for ', '') + name = name.replace('Jewel of the Spirit', 'Spirit Jewel') + name = name.replace(' & ', '/') + name = name.replace(' / ', '/') + name = name.replace('PAD Radar', 'PADR') + name = name.replace('in normal dungeons', 'in normals') + name = name.replace('Selected ', 'Some ') + name = name.replace('Enhanced ', 'Enh ') + name = name.replace('All Att. Req.', 'All Att.') + name = name.replace('Extreme King Metal Dragon', 'Extreme KMD') + name = name.replace('Golden Mound-Tricolor [Fr/Wt/Wd Only]', 'Golden Mound') + name = name.replace('Gods-Awakening Materials Descended', "Awoken Mats") + name = name.replace('Orb move time 4 sec', '4s move time') + name = name.replace('Awakening Materials Descended', 'Awkn Mats') + name = name.replace('Awakening Materials', 'Awkn Mats') + name = name.replace("Star Treasure Thieves' Den", 'STTD') + name = name.replace('Ruins of the Star Vault', 'Star Vault') + name = name.replace('-★6 or lower Enhanced', '') + + return name diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index a0f55552..01bff9b1 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -1,314 +1,314 @@ -import asyncio -import csv -import io -import json - -import discord -import pymysql -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box, pagify - -from rpadutils import CogSettings, rpadutils - -PADGUIDEDB_COG = None - - -def is_padguidedb_admin_check(ctx): - is_owner = PADGUIDEDB_COG.bot.owner_id == ctx.author.id - return is_owner or PADGUIDEDB_COG.settings.checkAdmin(ctx.author.id) - - -def is_padguidedb_admin(): - return commands.check(is_padguidedb_admin_check) - - -class PadGuideDb(commands.Cog): - """PadGuide Database manipulator""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = PadGuideDbSettings("padguidedb") - - self.queue_size = 0 - self.full_etl_lock = asyncio.Lock() - self.extract_images_lock = asyncio.Lock() - self.dungeon_load_lock = asyncio.Lock() - global PADGUIDEDB_COG - PADGUIDEDB_COG = self - - def get_connection(self): - with open(self.settings.configFile(), 'r') as db_config: - return self.connect(json.load(db_config)) - - def connect(self, db_config): - return pymysql.connect(host=db_config['host'], - user=db_config['user'], - password=db_config['password'], - db=db_config['db'], - charset=db_config['charset'], - cursorclass=pymysql.cursors.DictCursor, - autocommit=True) - - @commands.group(aliases=['pdb']) - @is_padguidedb_admin() - async def padguidedb(self, context): - """PadGuide database manipulation.""" - - @padguidedb.command() - @checks.is_owner() - async def addadmin(self, ctx, user: discord.Member): - """Adds a user to the padguide db admin""" - self.settings.addAdmin(user.id) - await ctx.send("done") - - @padguidedb.command() - @checks.is_owner() - async def rmadmin(self, ctx, user: discord.Member): - """Removes a user from the padguide db admin""" - self.settings.rmAdmin(user.id) - await ctx.send("done") - - @padguidedb.command() - @checks.is_owner() - async def setconfigfile(self, ctx, *, config_file): - """Set the database config file.""" - self.settings.setConfigFile(config_file) - await ctx.send(inline('Done')) - - @padguidedb.command() - @is_padguidedb_admin() - async def searchdungeon(self, ctx, *, search_text): - """Search""" - search_text = '%{}%'.format(search_text) - with self.get_connection() as cursor: - sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' - ' where lower(name_na) like %s or lower(name_jp) like %s' - ' order by dungeon_id desc limit 20') - cursor.execute(sql, [search_text, search_text]) - results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) - await ctx.send(inline(sql)) - for page in pagify(msg): - await ctx.send(box(page)) - - @padguidedb.command() - @checks.is_owner() - async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): - """Set the dungeon script.""" - self.settings.setDungeonScriptFile(dungeon_script_file) - await ctx.send(inline('Done')) - - @padguidedb.command() - @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): - """Set the dungeon script.""" - self.settings.setUserInfo(server, user_uuid, user_intid) - await ctx.send(inline('Done')) - - @padguidedb.command() - @is_padguidedb_admin() - async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): - if queues > 5: - await ctx.send("You must send less than 5 queues.") - return - elif self.queue_size + queues > 60: - await ctx.send("The size of the queue cannot exceed 60. It is currently {}.".format(self.queue_size)) - return - elif not self.settings.hasUserInfo(server): - await ctx.send("There is no account associated with server '{}'.".format(server.upper())) - return - - self.queue_size += queues - if queues == 1: - await ctx.send(inline('Queueing load in slot {}'.format(self.queue_size))) - else: - await ctx.send( - inline('Queueing loads in slots {}-{}'.format(self.queue_size - queues + 1, self.queue_size))) - - event_loop = asyncio.get_event_loop() - for queue in range(queues): - event_loop.create_task(self.do_dungeon_load(ctx, server.upper(), dungeon_id, dungeon_floor_id)) - - async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): - async with self.dungeon_load_lock: - process = await asyncio.create_subprocess_exec( - '/usr/bin/python3', - self.settings.dungeonScriptFile(), - '--db_config={}'.format(self.settings.configFile()), - '--server={}'.format(server), - '--dungeon_id={}'.format(dungeon_id), - '--floor_id={}'.format(dungeon_floor_id), - '--user_uuid={}'.format(self.settings.userUuidFor(server)), - '--user_intid={}'.format(self.settings.userIntidFor(server)), - - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - - if stderr: - print("Dungeon Load Error:\n" + stderr.decode()) - await rpadutils.doubleup(ctx, inline( - 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) - else: - await rpadutils.doubleup(ctx, inline( - 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) - self.queue_size -= 1 - - @padguidedb.command() - @is_padguidedb_admin() - async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): - with self.get_connection() as cursor: - sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" - " FROM wave_data" - " WHERE dungeon_id = {} AND floor_id = {}" - " GROUP BY 1, 2" - " ORDER BY 1, 2").format(int(dungeon_id), int(dungeon_floor_id)) - cursor.execute(sql) - results = list(cursor.fetchall()) - msg = 'stage,drop_monster_id,count' - for row in results: - msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], row['count']) - for page in pagify(msg): - await ctx.send(box(page)) - - @padguidedb.command() - @checks.is_owner() - async def cleardungeon(self, ctx, dungeon_id: int): - with self.get_connection() as cursor: - sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) - cursor.execute(sql) - await ctx.send(inline("Done")) - - # @padguidedb.command() - @is_padguidedb_admin() - async def dungeondata(self, ctx, dungeon_id: int): - with ctx.typing(), self.get_connection() as cursor: - sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) - cursor.execute(sql) - results = list(cursor.fetchall()) - order = sorted(results[0]) - rows = [list(map(lambda x: row[x], order)) for row in results] - fauxfile = io.StringIO() - writer = csv.writer(fauxfile) - writer.writerow(order) - writer.writerows(rows) - fauxfile.seek(0) - m = await ctx.send(file=discord.File(fauxfile, filename="dungeondata.csv")) - await asyncio.sleep(10) - await m.delete() - - @padguidedb.group() - @is_padguidedb_admin() - async def pipeline(self, context): - """PadGuide pipeline utilities.""" - - @pipeline.command() - @is_padguidedb_admin() - async def fulletl(self, ctx): - """Runs a job which downloads pad data, and updates the padguide database.""" - if self.full_etl_lock.locked(): - await ctx.send(inline('Full ETL already running')) - return - - async with self.full_etl_lock: - await ctx.send(inline('Running full ETL pipeline: this could take a while')) - process = await asyncio.create_subprocess_exec( - 'bash', - '/home/tactical0retreat/dadguide/dadguide-jobs/run_loader.sh', - - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - - if stderr: - print("Full ETL Error:\n" + stderr.decode()) - await ctx.send(inline('Full ETL failed')) - return - await ctx.send(inline('Full ETL finished')) - - @pipeline.command() - @is_padguidedb_admin() - async def extractimages(self, ctx): - """Runs a job which downloads image updates, generates full images, and portraits.""" - if self.extract_images_lock.locked(): - await ctx.send(inline('Extract images already running')) - return - - async with self.extract_images_lock: - await ctx.send(inline('Running image extract pipeline: this could take a while')) - process = await asyncio.create_subprocess_exec( - 'bash', - '/home/tactical0retreat/dadguide/dadguide-jobs/media/update_image_files.sh', - - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - - if stderr: - print("Image Extract Error:\n" + stderr.decode()) - await ctx.send(inline('Image extract failed')) - return - await ctx.send(inline('Image extract finished')) - - -class PadGuideDbSettings(CogSettings): - def make_default_settings(self): - config = { - 'admins': [], - 'config_file': '', - 'dungeon_script_file': '', - 'users': {} - } - return config - - def admins(self): - return self.bot_settings['admins'] - - def checkAdmin(self, user_id): - admins = self.admins() - return user_id in admins - - def addAdmin(self, user_id): - admins = self.admins() - if user_id not in admins: - admins.append(user_id) - self.save_settings() - - def rmAdmin(self, user_id): - admins = self.admins() - if user_id in admins: - admins.remove(user_id) - self.save_settings() - - def configFile(self): - return self.bot_settings.get('config_file', '') - - def setConfigFile(self, config_file): - self.bot_settings['config_file'] = config_file - self.save_settings() - - def dungeonScriptFile(self): - return self.bot_settings.get('dungeon_script_file', '') - - def setDungeonScriptFile(self, dungeon_script_file): - self.bot_settings['dungeon_script_file'] = dungeon_script_file - self.save_settings() - - def setUserInfo(self, server, user_uuid, user_intid): - self.bot_settings['users'][server.upper()] = [user_uuid, user_intid] - self.save_settings() - - def hasUserInfo(self, server): - return server.upper() in self.bot_settings['users'] - - def userUuidFor(self, server): - return self.bot_settings['users'][server.upper()][0] - - def userIntidFor(self, server): - return self.bot_settings['users'][server.upper()][1] +import asyncio +import csv +import io +import json + +import discord +import pymysql +from redbot.core import checks +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import inline, box, pagify + +from rpadutils import CogSettings, rpadutils + +PADGUIDEDB_COG = None + + +def is_padguidedb_admin_check(ctx): + is_owner = ctx.author.id in PADGUIDEDB_COG.bot.owner_ids + return is_owner or PADGUIDEDB_COG.settings.checkAdmin(ctx.author.id) + + +def is_padguidedb_admin(): + return commands.check(is_padguidedb_admin_check) + + +class PadGuideDb(commands.Cog): + """PadGuide Database manipulator""" + + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.settings = PadGuideDbSettings("padguidedb") + + self.queue_size = 0 + self.full_etl_lock = asyncio.Lock() + self.extract_images_lock = asyncio.Lock() + self.dungeon_load_lock = asyncio.Lock() + global PADGUIDEDB_COG + PADGUIDEDB_COG = self + + def get_connection(self): + with open(self.settings.configFile(), 'r') as db_config: + return self.connect(json.load(db_config)) + + def connect(self, db_config): + return pymysql.connect(host=db_config['host'], + user=db_config['user'], + password=db_config['password'], + db=db_config['db'], + charset=db_config['charset'], + cursorclass=pymysql.cursors.DictCursor, + autocommit=True) + + @commands.group(aliases=['pdb']) + @is_padguidedb_admin() + async def padguidedb(self, context): + """PadGuide database manipulation.""" + + @padguidedb.command() + @checks.is_owner() + async def addadmin(self, ctx, user: discord.Member): + """Adds a user to the padguide db admin""" + self.settings.addAdmin(user.id) + await ctx.send("done") + + @padguidedb.command() + @checks.is_owner() + async def rmadmin(self, ctx, user: discord.Member): + """Removes a user from the padguide db admin""" + self.settings.rmAdmin(user.id) + await ctx.send("done") + + @padguidedb.command() + @checks.is_owner() + async def setconfigfile(self, ctx, *, config_file): + """Set the database config file.""" + self.settings.setConfigFile(config_file) + await ctx.send(inline('Done')) + + @padguidedb.command() + @is_padguidedb_admin() + async def searchdungeon(self, ctx, *, search_text): + """Search""" + search_text = '%{}%'.format(search_text) + with self.get_connection() as cursor: + sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' + ' where lower(name_na) like %s or lower(name_jp) like %s' + ' order by dungeon_id desc limit 20') + cursor.execute(sql, [search_text, search_text]) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(sql)) + for page in pagify(msg): + await ctx.send(box(page)) + + @padguidedb.command() + @checks.is_owner() + async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): + """Set the dungeon script.""" + self.settings.setDungeonScriptFile(dungeon_script_file) + await ctx.send(inline('Done')) + + @padguidedb.command() + @checks.is_owner() + async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): + """Set the dungeon script.""" + self.settings.setUserInfo(server, user_uuid, user_intid) + await ctx.send(inline('Done')) + + @padguidedb.command() + @is_padguidedb_admin() + async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): + if queues > 5: + await ctx.send("You must send less than 5 queues.") + return + elif self.queue_size + queues > 60: + await ctx.send("The size of the queue cannot exceed 60. It is currently {}.".format(self.queue_size)) + return + elif not self.settings.hasUserInfo(server): + await ctx.send("There is no account associated with server '{}'.".format(server.upper())) + return + + self.queue_size += queues + if queues == 1: + await ctx.send(inline('Queueing load in slot {}'.format(self.queue_size))) + else: + await ctx.send( + inline('Queueing loads in slots {}-{}'.format(self.queue_size - queues + 1, self.queue_size))) + + event_loop = asyncio.get_event_loop() + for queue in range(queues): + event_loop.create_task(self.do_dungeon_load(ctx, server.upper(), dungeon_id, dungeon_floor_id)) + + async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): + async with self.dungeon_load_lock: + process = await asyncio.create_subprocess_exec( + '/usr/bin/python3', + self.settings.dungeonScriptFile(), + '--db_config={}'.format(self.settings.configFile()), + '--server={}'.format(server), + '--dungeon_id={}'.format(dungeon_id), + '--floor_id={}'.format(dungeon_floor_id), + '--user_uuid={}'.format(self.settings.userUuidFor(server)), + '--user_intid={}'.format(self.settings.userIntidFor(server)), + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + if stderr: + print("Dungeon Load Error:\n" + stderr.decode()) + await rpadutils.doubleup(ctx, inline( + 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) + else: + await rpadutils.doubleup(ctx, inline( + 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) + self.queue_size -= 1 + + @padguidedb.command() + @is_padguidedb_admin() + async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): + with self.get_connection() as cursor: + sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" + " FROM wave_data" + " WHERE dungeon_id = {} AND floor_id = {}" + " GROUP BY 1, 2" + " ORDER BY 1, 2").format(int(dungeon_id), int(dungeon_floor_id)) + cursor.execute(sql) + results = list(cursor.fetchall()) + msg = 'stage,drop_monster_id,count' + for row in results: + msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], row['count']) + for page in pagify(msg): + await ctx.send(box(page)) + + @padguidedb.command() + @checks.is_owner() + async def cleardungeon(self, ctx, dungeon_id: int): + with self.get_connection() as cursor: + sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) + cursor.execute(sql) + await ctx.send(inline("Done")) + + # @padguidedb.command() + @is_padguidedb_admin() + async def dungeondata(self, ctx, dungeon_id: int): + with ctx.typing(), self.get_connection() as cursor: + sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) + cursor.execute(sql) + results = list(cursor.fetchall()) + order = sorted(results[0]) + rows = [list(map(lambda x: row[x], order)) for row in results] + fauxfile = io.StringIO() + writer = csv.writer(fauxfile) + writer.writerow(order) + writer.writerows(rows) + fauxfile.seek(0) + m = await ctx.send(file=discord.File(fauxfile, filename="dungeondata.csv")) + await asyncio.sleep(10) + await m.delete() + + @padguidedb.group() + @is_padguidedb_admin() + async def pipeline(self, context): + """PadGuide pipeline utilities.""" + + @pipeline.command() + @is_padguidedb_admin() + async def fulletl(self, ctx): + """Runs a job which downloads pad data, and updates the padguide database.""" + if self.full_etl_lock.locked(): + await ctx.send(inline('Full ETL already running')) + return + + async with self.full_etl_lock: + await ctx.send(inline('Running full ETL pipeline: this could take a while')) + process = await asyncio.create_subprocess_exec( + 'bash', + '/home/tactical0retreat/dadguide/dadguide-jobs/run_loader.sh', + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + if stderr: + print("Full ETL Error:\n" + stderr.decode()) + await ctx.send(inline('Full ETL failed')) + return + await ctx.send(inline('Full ETL finished')) + + @pipeline.command() + @is_padguidedb_admin() + async def extractimages(self, ctx): + """Runs a job which downloads image updates, generates full images, and portraits.""" + if self.extract_images_lock.locked(): + await ctx.send(inline('Extract images already running')) + return + + async with self.extract_images_lock: + await ctx.send(inline('Running image extract pipeline: this could take a while')) + process = await asyncio.create_subprocess_exec( + 'bash', + '/home/tactical0retreat/dadguide/dadguide-jobs/media/update_image_files.sh', + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + if stderr: + print("Image Extract Error:\n" + stderr.decode()) + await ctx.send(inline('Image extract failed')) + return + await ctx.send(inline('Image extract finished')) + + +class PadGuideDbSettings(CogSettings): + def make_default_settings(self): + config = { + 'admins': [], + 'config_file': '', + 'dungeon_script_file': '', + 'users': {} + } + return config + + def admins(self): + return self.bot_settings['admins'] + + def checkAdmin(self, user_id): + admins = self.admins() + return user_id in admins + + def addAdmin(self, user_id): + admins = self.admins() + if user_id not in admins: + admins.append(user_id) + self.save_settings() + + def rmAdmin(self, user_id): + admins = self.admins() + if user_id in admins: + admins.remove(user_id) + self.save_settings() + + def configFile(self): + return self.bot_settings.get('config_file', '') + + def setConfigFile(self, config_file): + self.bot_settings['config_file'] = config_file + self.save_settings() + + def dungeonScriptFile(self): + return self.bot_settings.get('dungeon_script_file', '') + + def setDungeonScriptFile(self, dungeon_script_file): + self.bot_settings['dungeon_script_file'] = dungeon_script_file + self.save_settings() + + def setUserInfo(self, server, user_uuid, user_intid): + self.bot_settings['users'][server.upper()] = [user_uuid, user_intid] + self.save_settings() + + def hasUserInfo(self, server): + return server.upper() in self.bot_settings['users'] + + def userUuidFor(self, server): + return self.bot_settings['users'][server.upper()][0] + + def userIntidFor(self, server): + return self.bot_settings['users'][server.upper()][1] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 04cd7ae0..1406089d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,1130 +1,1203 @@ -import asyncio -import json -import logging -import os -import traceback -import urllib.parse -from collections import OrderedDict -from enum import Enum - -import discord -import prettytable -from redbot.core import checks, data_manager -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box - -import rpadutils -from rpadutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics - -logger = logging.getLogger('red.miruv3.padinfo') - -HELP_MSG = """ -{0.prefix}helpid : shows this message -{0.prefix}id : look up a monster and print a link to puzzledragonx -{0.prefix}pic : Look up a monster and display its image inline - -Options for - : Find a monster by ID - {0.prefix}id 1234 (picks sun quan) - : Take the best guess for a monster, picks the most recent monster - {0.prefix}id kali (picks mega awoken d kali) - : Limit by element or awoken, e.g. - {0.prefix}id ares (selects the most recent, revo ares) - {0.prefix}id aares (explicitly selects awoken ares) - {0.prefix}id a ares (spaces work too) - {0.prefix}id rd ares (select a specific evo for ares, the red/dark one) - {0.prefix}id r/d ares (slashes, spaces work too) - -computed nickname list and overrides: https://docs.google.com/spreadsheets/d/1EyzMjvf8ZCQ4K-gJYnNkiZlCEsT9YYI9dUd-T5qCirc/pubhtml -submit an override suggestion: https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" - -EMBED_NOT_GENERATED = -1 - -INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' - -MEDIA_PATH = 'https://f002.backblazeb2.com/file/dadguide-data/media/' -RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png' -RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' -VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' -GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}cb.png' - -YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' -SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' -ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' - - -class ServerFilter(Enum): - any = 0 - na = 1 - jp = 2 - - -def get_pdx_url(m): - return INFO_PDX_TEMPLATE.format(rpadutils.get_pdx_id(m)) - - -def get_portrait_url(m): - return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) - - -def get_pic_url(m): - return RPAD_PIC_TEMPLATE.format(m.monster_id) - - -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) - - -class IdEmojiUpdater(EmojiUpdater): - def __init__(self, emoji_to_embed, m: "DgMonster" = None, - pad_info=None, selected_emoji=None, bot=None): - self.emoji_dict = emoji_to_embed - self.m = m - self.pad_info = pad_info - self.selected_emoji = selected_emoji - self.bot = bot - - def on_update(self, ctx, selected_emoji): - if self.pad_info.settings.checkEvoID(ctx.author.id): - DGCOG = self.bot.get_cog('Dadguide') - evos = list(map(lambda x: [x['from_id'], x['to_id']], - DGCOG.database.get_all_evolutions_by_monster(self.m.monster_id))) - evos = list(set(sum(evos, []))) - evos.sort() - index = evos.index(self.m.monster_id) - if selected_emoji == self.pad_info.previous_monster_emoji: - if index == 0: - self.m = DGCOG.get_monster_by_id(evos[-1]) - else: - self.m = DGCOG.get_monster_by_id(evos[index - 1]) - elif selected_emoji == self.pad_info.next_monster_emoji: - if index == len(evos) - 1: - self.m = DGCOG.get_monster_by_id(evos[0]) - else: - self.m = DGCOG.get_monster_by_id(evos[index + 1]) - else: - self.selected_emoji = selected_emoji - return True - else: - if selected_emoji == self.pad_info.previous_monster_emoji: - if self.m.prev_monster is None: - return False - self.m = self.m.prev_monster - elif selected_emoji == self.pad_info.next_monster_emoji: - if self.m.next_monster is None: - return False - self.m = self.m.next_monster - else: - self.selected_emoji = selected_emoji - return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m) - return True - - -class PadInfo(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - self.settings = PadInfoSettings("padinfo") - - self.index_all = None - self.index_na = None - self.index_jp = None - - self.menu = Menu(bot) - - # These emojis are the keys into the idmenu submenus - self.id_emoji = '\N{INFORMATION SOURCE}' - self.evo_emoji = char_to_emoji('e') - self.mats_emoji = char_to_emoji('m') - self.ls_emoji = '\N{INFORMATION SOURCE}' - self.left_emoji = char_to_emoji('l') - self.right_emoji = char_to_emoji('r') - self.pantheon_emoji = '\N{CLASSICAL BUILDING}' - self.skillups_emoji = '\N{MEAT ON BONE}' - self.pic_emoji = '\N{FRAME WITH PICTURE}' - self.other_info_emoji = '\N{SCROLL}' - self.previous_monster_emoji = '\N{HEAVY MINUS SIGN}' - self.next_monster_emoji = '\N{HEAVY PLUS SIGN}' - self.remove_emoji = self.menu.emoji['no'] - - self.historic_lookups_file_path = _data_file('historic_lookups.json') - self.historic_lookups = safe_read_json(self.historic_lookups_file_path) - - self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') - self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) - - def cog_unload(self): - # Manually nulling out database because the GC for cogs seems to be pretty shitty - self.index_all = None - self.index_na = None - self.index_jp = None - self.historic_lookups = {} - self.historic_lookups_id2 = {} - - async def reload_nicknames(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadInfo'): - try: - await self.refresh_index() - print('Done refreshing PadInfo') - except Exception as ex: - print("reload padinfo loop caught exception " + str(ex)) - traceback.print_exc() - - await asyncio.sleep(60 * 60 * 1) - - @commands.command() - @checks.is_owner() - async def refresh_index(self, ctx=None): - """Refresh the monster indexes.""" - dg_cog = self.bot.get_cog('Dadguide') - if not dg_cog: - print("Cog 'Dadguide' not loaded") - return - logger.info('Waiting until DG is ready') - await dg_cog.wait_until_ready() - - # Putting some sleeps in here to give the bot time to respond to Discord. - await asyncio.sleep(1) - - logger.info('Loading ALL index') - self.index_all = dg_cog.create_index() - await asyncio.sleep(1) - - logger.info('Loading NA index') - self.index_na = dg_cog.create_index(lambda m: m.on_na) - await asyncio.sleep(1) - - logger.info('Loading JP index') - self.index_jp = dg_cog.create_index(lambda m: m.on_jp) - - logger.info('Done refreshing indexes') - - def get_monster_by_id(self, monster_id: int): - dg_cog = self.bot.get_cog('Dadguide') - return dg_cog.get_monster_by_id(monster_id) - - @commands.command() - async def jpname(self, ctx, *, query: str): - """Print the Japanese name of a monster""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await ctx.send(monsterToHeader(m)) - await ctx.send(box(m.name_jp)) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(name="id", aliases="iD Id ID".split()) - async def _id(self, ctx, *, query: str): - """Monster info (main tab)""" - prefix = ctx.prefix + "id" - query = prefix.join(filter(None, query.split(prefix))) - await self._do_id(ctx, query) - - @commands.command() - async def idna(self, ctx, *, query: str): - """Monster info (limited to NA monsters ONLY)""" - await self._do_id(ctx, query, server_filter=ServerFilter.na) - - @commands.command() - async def idjp(self, ctx, *, query: str): - """Monster info (limited to JP monsters ONLY)""" - await self._do_id(ctx, query, server_filter=ServerFilter.jp) - - async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = self.findMonster(query, server_filter=server_filter) - if m is not None: - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def id2(self, ctx, *, query: str): - """Monster info (main tab)""" - await self._do_id2(ctx, query) - - @commands.command() - async def id2na(self, ctx, *, query: str): - """Monster info (limited to NA monsters ONLY)""" - await self._do_id2(ctx, query, server_filter=ServerFilter.na) - - @commands.command() - async def id2jp(self, ctx, *, query: str): - """Monster info (limited to JP monsters ONLY)""" - await self._do_id2(ctx, query, server_filter=ServerFilter.jp) - - async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = self.findMonster2(query, server_filter=server_filter) - if m is not None: - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(name="evos") - async def evos(self, ctx, *, query: str): - """Monster info (evolutions tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.evo_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(name="mats", aliases=['evomats', 'evomat']) - async def evomats(self, ctx, *, query: str): - """Monster info (evo materials tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.mats_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def pantheon(self, ctx, *, query: str): - """Monster info (pantheon tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) - if menu == EMBED_NOT_GENERATED: - await ctx.send(inline('Not a pantheon monster')) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def skillups(self, ctx, *, query: str): - """Monster info (evolutions tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - menu = await self._do_idmenu(ctx, m, self.skillups_emoji) - if menu == EMBED_NOT_GENERATED: - await ctx.send(inline('No skillups available')) - else: - await ctx.send(self.makeFailureMsg(err)) - - async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m) - return await self._do_menu( - ctx, - starting_menu_emoji, - IdEmojiUpdater(emoji_to_embed, pad_info=self, - m=m, selected_emoji=starting_menu_emoji, bot=self.bot) - ) - - def get_id_emoji_options(self, m=None): - id_embed = monsterToEmbed(m, self.get_emojis()) - evo_embed = monsterToEvoEmbed(m) - mats_embed = monsterToEvoMatsEmbed(m) - animated = m.has_animation - pic_embed = monsterToPicEmbed(m, animated=animated) - other_info_embed = monsterToOtherInfoEmbed(m) - - emoji_to_embed = OrderedDict() - emoji_to_embed[self.id_emoji] = id_embed - emoji_to_embed[self.evo_emoji] = evo_embed - emoji_to_embed[self.mats_emoji] = mats_embed - emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = monsterToPantheonEmbed(m) - if pantheon_embed: - emoji_to_embed[self.pantheon_emoji] = pantheon_embed - - skillups_embed = monsterToSkillupsEmbed(m) - if skillups_embed: - emoji_to_embed[self.skillups_emoji] = skillups_embed - - emoji_to_embed[self.other_info_emoji] = other_info_embed - - # it's impossible for the previous/next ones to be accessed because - # IdEmojiUpdater won't allow it, however they have to be defined - # so that the buttons print in the first place - - emoji_to_embed[self.previous_monster_emoji] = None - emoji_to_embed[self.next_monster_emoji] = None - - # remove emoji needs to be last - emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message - return emoji_to_embed - - async def _do_evolistmenu(self, ctx, sm): - monsters = sm.alt_evos - monsters.sort(key=lambda m: m.monster_id) - - emoji_to_embed = OrderedDict() - for idx, m in enumerate(monsters): - emoji = char_to_emoji(str(idx)) - emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis()) - if m.monster_id == sm.monster_id: - starting_menu_emoji = emoji - - return await self._do_menu(ctx, starting_menu_emoji, EmojiUpdater(emoji_to_embed), timeout=60) - - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): - if starting_menu_emoji not in emoji_to_embed.emoji_dict: - # Selected menu wasn't generated for this monster - return EMBED_NOT_GENERATED - - emoji_to_embed.emoji_dict[self.remove_emoji] = self.menu.reaction_delete_message - - try: - result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, - starting_menu_emoji, timeout=timeout) - if result_msg and result_embed: - # Message is finished but not deleted, clear the footer - result_embed.set_footer(text=discord.Embed.Empty) - await result_msg.edit(embed=result_embed) - except Exception as ex: - print('Menu failure', ex) - - @commands.command(aliases=['img']) - async def pic(self, ctx, *, query: str): - """Monster info (full image tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.pic_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def links(self, ctx, *, query: str): - """Monster links""" - m, err, debug_info = self.findMonster(query) - if m is not None: - embed = monsterToBaseEmbed(m) - embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - embed.set_footer(text='') - await ctx.send(embed=embed) - - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(aliases=['stats']) - async def otherinfo(self, ctx, *, query: str): - """Monster info (misc info tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.other_info_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def lookup(self, ctx, *, query: str): - """Short info results for a monster query""" - m, err, debug_info = self.findMonster(query) - if m is not None: - embed = monsterToHeaderEmbed(m) - await ctx.send(embed=embed) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def evolist(self, ctx, *, query): - """Monster info (for all monsters in the evo tree)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_evolistmenu(ctx, m) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(aliases=['leaders', 'leaderskills', 'ls']) - async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): - """Display the multiplier and leaderskills for two monsters - - If either your left or right query contains spaces, wrap in quotes. - e.g.: [p]leaderskill "r sonia" "b sonia" - """ - if bad: - await ctx.send(inline('Too many inputs. Try wrapping your queries in quotes.')) - return - - # Handle a very specific failure case, user typing something like "uuvo ragdra" - if ' ' not in left_query and right_query is not None and ' ' not in right_query and bad is None: - combined_query = left_query + ' ' + right_query - nm, err, debug_info = self._findMonster(combined_query) - if nm and left_query in nm.prefixes: - left_query = combined_query - right_query = None - - left_m, left_err, _ = self.findMonster(left_query) - if right_query: - right_m, right_err, _ = self.findMonster(right_query) - else: - right_m, right_err, = left_m, left_err - - err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, wrap it in quotes.' - if left_err: - await ctx.send(inline(err_msg.format('Left', left_query))) - return - if right_err: - await ctx.send(inline(err_msg.format('Right', right_query))) - return - - emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis()) - emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis()) - - await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) - - @commands.command(aliases=['helppic', 'helpimg']) - async def helpid(self, ctx): - """Whispers you info on how to craft monster queries for [p]id""" - await ctx.author.send(box(HELP_MSG.format(ctx))) - - @commands.command() - async def padsay(self, ctx, server, *, query: str = None): - """Speak the voice line of a monster into your current chat""" - voice = ctx.author.voice - if not voice: - await ctx.send(inline('You must be in a voice channel to use this command')) - return - channel = voice.channel - - speech_cog = self.bot.get_cog('Speech') - if not speech_cog: - await ctx.send(inline('Speech seems to be offline')) - return - - if server.lower() not in ['na', 'jp']: - query = server + ' ' + (query or '') - server = 'na' - query = query.strip().lower() - - m, err, debug_info = self.findMonster(query) - if m is not None: - voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na - base_dir = '/home/tactical0retreat/dadguide/data/media/voices' - voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) - header = '{} ({})'.format(monsterToHeader(m), server) - if not os.path.exists(voice_file): - await ctx.send(inline('Could not find voice for ' + header)) - return - await ctx.send('Speaking for ' + header) - await speech_cog.play_path(channel, voice_file) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def idmode(self, ctx, id_type): - """Switch between number mode and evo mode""" - if id_type in ['evo']: - if self.settings.setEvoID(ctx.author.id): - await ctx.send(inline("Done")) - else: - await ctx.send(inline("You're already using evo mode")) - elif id_type in ['number', 'default']: - if self.settings.rmEvoID(ctx.author.id): - await ctx.send(inline("Done")) - else: - await ctx.send(inline("You're already using number mode")) - else: - await ctx.send(inline("id_type must be 'number' or 'evo'")) - - @commands.group() - @checks.is_owner() - async def padinfo(self, ctx): - """PAD info management""" - - @padinfo.command() - @checks.is_owner() - async def setemojiservers(self, ctx, *, emoji_servers=''): - """Set the emoji servers by ID (csv)""" - self.settings.emojiServers().clear() - if emoji_servers: - self.settings.setEmojiServers(emoji_servers.split(',')) - await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) - - @checks.is_owner() - @padinfo.command() - async def iddiff(self, ctx): - """Runs the diff checker for id and id2""" - await ctx.send("Running diff checker...") - hist_aggreg = list(self.historic_lookups) + list(self.historic_lookups_id2) - s = 0 - f = [] - for query in hist_aggreg: - m1, err1, debug_info1 = self.findMonster(query) - m2, err2, debug_info2 = self.findMonster2(query) - if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): - s += 1 - continue - - f.append((query, - [m1.monster_id if m1 else None, m2.monster_id if m2 else None], - [err1, err2], - [debug_info1, debug_info2] - )) - if m1 and m2: - ctx.send("Major Discrepency: {} -> {}/{}".format(query, m1.name_na, m2.name_na)) - await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) - print(f) - - def get_emojis(self): - server_ids = [int(sid) for sid in self.settings.emojiServers()] - return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] - - def makeFailureMsg(self, err): - msg = ('Lookup failed: {}.\n' - 'Try one of , , [argbld]/[rgbld] . ' - 'Unexpected results? Use ^helpid for more info.').format(err) - return box(msg) - - def findMonster(self, query, server_filter=ServerFilter.any): - query = rmdiacritics(query) - nm, err, debug_info = self._findMonster(query, server_filter) - - monster_no = nm.monster_id if nm else -1 - self.historic_lookups[query] = monster_no - json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) - - m = self.get_monster_by_id(nm.monster_id) if nm else None - - return m, err, debug_info - - def _findMonster(self, query, server_filter=ServerFilter.any): - if server_filter == ServerFilter.any: - monster_index = self.index_all - elif server_filter == ServerFilter.na: - monster_index = self.index_na - elif server_filter == ServerFilter.jp: - monster_index = self.index_jp - else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) - return monster_index.find_monster(query) - - def findMonster2(self, query, server_filter=ServerFilter.any): - query = rmdiacritics(query) - nm, err, debug_info = self._findMonster2(query, server_filter) - - monster_no = nm.monster_id if nm else -1 - self.historic_lookups_id2[query] = monster_no - json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) - - m = self.get_monster_by_id(nm.monster_id) if nm else None - - return m, err, debug_info - - def _findMonster2(self, query, server_filter=ServerFilter.any): - if server_filter == ServerFilter.any: - monster_index = self.index_all - elif server_filter == ServerFilter.na: - monster_index = self.index_na - elif server_filter == ServerFilter.jp: - monster_index = self.index_jp - else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) - return monster_index.find_monster2(query) - - -class PadInfoSettings(CogSettings): - def make_default_settings(self): - config = { - 'animation_dir': '', - 'evo_id_users': [], - } - return config - - def emojiServers(self): - key = 'emoji_servers' - if key not in self.bot_settings: - self.bot_settings[key] = [] - return self.bot_settings[key] - - def setEmojiServers(self, emoji_servers): - es = self.emojiServers() - es.clear() - es.extend(emoji_servers) - self.save_settings() - - def setEvoID(self, user_id): - if user_id not in self.bot_settings['evo_id_users']: - self.bot_settings['evo_id_users'].append(user_id) - self.save_settings() - return True - return False - - def rmEvoID(self, user_id): - if user_id in self.bot_settings['evo_id_users']: - self.bot_settings['evo_id_users'].remove(user_id) - self.save_settings() - return True - return False - - def checkEvoID(self, user_id): - return user_id in self.bot_settings['evo_id_users'] - - -def monsterToHeader(m: "DgMonster", link=False): - msg = 'No. {} {}'.format(m.monster_no_na, m.name_na) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - -def monsterToJpSuffix(m: "DgMonster"): - suffix = "" - if m.roma_subname: - suffix += ' [{}]'.format(m.roma_subname) - if not m.on_na: - suffix += ' (JP only)' - return suffix - - -def monsterToLongHeader(m: "DgMonster", link=False): - msg = monsterToHeader(m) + monsterToJpSuffix(m) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - -def monsterToEvoText(m: "DgMonster"): - output = monsterToLongHeader(m) - for ae in sorted(m.alt_evos, key=lambda x: int(x.monster_id)): - output += "\n\t- {}".format(monsterToLongHeader(ae)) - return output - - -def monsterToThumbnailUrl(m: "DgMonster"): - return get_portrait_url(m) - - -def monsterToBaseEmbed(m: "DgMonster"): - header = monsterToLongHeader(m) - embed = discord.Embed() - embed.set_thumbnail(url=monsterToThumbnailUrl(m)) - embed.title = header - embed.url = get_pdx_url(m) - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed - - -def printEvoListFields(list_of_monsters, embed, name): - if not len(list_of_monsters): - return - field_name = name.format(len(list_of_monsters)) - field_data = '' - for ae in sorted(list_of_monsters, key=lambda x: int(x.monster_id)): - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - - embed.add_field(name=field_name, value=field_data) - - -def monsterToEvoEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - - if not len(m.alt_evos) and not m.evo_gem: - embed.description = 'No alternate evos or evo gem' - return embed - - printEvoListFields(m.alt_evos, embed, '{} alternate evo(s)') - if not m.evo_gem: - return embed - printEvoListFields([m.evo_gem], embed, '{} evo gem(s)') - - return embed - - -def printMonsterEvoOfList(monster_list, embed, field_name): - if not len(monster_list): - return - field_data = '' - if len(monster_list) > 5: - field_data = '{} monsters'.format(len(monster_list)) - else: - item_count = min(len(monster_list), 5) - for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - embed.add_field(name=field_name, value=field_data) - - -def monsterToEvoMatsEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - - mats_for_evo = m.mats_for_evo - - field_name = 'Evo materials' - field_data = '' - if len(mats_for_evo) > 0: - for ae in m.mats_for_evo: - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - else: - field_data = 'None' - embed.add_field(name=field_name, value=field_data) - - printMonsterEvoOfList(m.material_of, embed, 'Material for') - if not m.evo_gem: - return embed - printMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") - return embed - - -def monsterToPantheonEmbed(m: "DgMonster"): - full_pantheon = m.series.monsters - pantheon_list = list(filter(lambda x: x.evo_from is None, full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 6: - return None - - embed = monsterToBaseEmbed(m) - - field_name = 'Pantheon: ' + m.series.name - field_data = '' - for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): - field_data += '\n' + monsterToHeader(monster, link=True) - embed.add_field(name=field_name, value=field_data) - - return embed - - -def monsterToSkillupsEmbed(m: "DgMonster"): - skillups_list = m.active_skill.skillups if m.active_skill else [] - if len(skillups_list) == 0: - return None - - embed = monsterToBaseEmbed(m) - - field_name = 'Skillups' - field_data = '' - - # Prevent huge skillup lists - if len(skillups_list) > 8: - field_data = '({} skillups omitted)'.format(len(skillups_list) - 8) - skillups_list = skillups_list[0:8] - - for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): - field_data += '\n' + monsterToHeader(monster, link=True) - - if len(field_data.strip()): - embed.add_field(name=field_name, value=field_data) - - return embed - - -def monsterToPicUrl(m: "DgMonster"): - return get_pic_url(m) - - -def monsterToPicEmbed(m: "DgMonster", animated=False): - embed = monsterToBaseEmbed(m) - url = monsterToPicUrl(m) - embed.set_image(url=url) - # Clear the thumbnail, don't need it on pic - embed.set_thumbnail(url='') - extra_links = [] - if animated: - extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), monsterToGifUrl(m))) - if m.orb_skin_id is not None: - extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), monsterToOrbSkinCBUrl(m))) - if len(extra_links) > 0: - embed.add_field(name='Extra Links', value='\n'.join(extra_links)) - - return embed - - -def monsterToVideoUrl(m: "DgMonster", link_text='(MP4)'): - return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) - - -def monsterToGifUrl(m: "DgMonster", link_text='(GIF)'): - return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) - - -def monsterToOrbSkinUrl(m: "DgMonster", link_text='Regular'): - return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) - - -def monsterToOrbSkinCBUrl(m: "DgMonster", link_text='Color Blind'): - return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) - - -def monsterToGifEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - url = monsterToGifUrl(m) - embed.set_image(url=url) - # Clear the thumbnail, don't need it on pic - embed.set_thumbnail(url='') - return embed - - -def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): - lls = left_m.leader_skill - rls = right_m.leader_skill - - if lls: - lhp, latk, lrcv, lresist = lls.data - else: - lhp, latk, lrcv, lresist = 1, 1, 1, 0 - - if rls: - rhp, ratk, rrcv, rresist = rls.data - else: - rhp, ratk, rrcv, rresist = 1, 1, 1, 0 - - multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) - - embed = discord.Embed() - embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) - description = '' - description += '\n**{}**\n{}'.format( - monsterToHeader(left_m, link=True), - left_m.leader_skill.desc if left_m.leader_skill else 'None') - description += '\n**{}**\n{}'.format( - monsterToHeader(right_m, link=True), - right_m.leader_skill.desc if right_m.leader_skill else 'None') - embed.description = description - - return embed - - -def monsterToHeaderEmbed(m: "DgMonster"): - header = monsterToLongHeader(m, link=True) - embed = discord.Embed() - embed.description = header - return embed - - -def monsterToTypeString(m: "DgMonster"): - return '/'.join([t.name for t in m.types]) - - -def monsterToAcquireString(m: "DgMonster"): - acquire_text = None - if m.farmable and not m.mp_evo: - # Some MP shop monsters 'drop' in PADR - acquire_text = 'Farmable' - elif m.farmable_evo and not m.mp_evo: - acquire_text = 'Farmable Evo' - elif m.in_pem: - acquire_text = 'In PEM' - elif m.pem_evo: - acquire_text = 'PEM Evo' - elif m.in_rem: - acquire_text = 'In REM' - elif m.rem_evo: - acquire_text = 'REM Evo' - elif m.in_mpshop: - acquire_text = 'MP Shop' - elif m.mp_evo: - acquire_text = 'MP Shop Evo' - return acquire_text - - -def match_emoji(emoji_list, name): - for e in emoji_list: - if e.name == name: - return e - return name - - -def monsterToEmbed(m: "DgMonster", emoji_list): - embed = monsterToBaseEmbed(m) - - info_row_1 = monsterToTypeString(m) - acquire_text = monsterToAcquireString(m) - tet_text = m.true_evo_type.value - - info_row_2 = '**Rarity** {}\n**Cost** {}'.format(m.rarity, m.cost) - if acquire_text: - info_row_2 += '\n**{}**'.format(acquire_text) - if m.is_inheritable: - info_row_2 += '\n**Inheritable**' - else: - info_row_2 += '\n**Not inheritable**' - if tet_text: - info_row_2 += '\n**{}**'.format(tet_text) - - embed.add_field(name=info_row_1, value=info_row_2) - - hp, atk, rcv, weighted = m.stats() - if m.limit_mult > 0: - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, m.limit_mult) - stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( - hp, lb_hp, atk, lb_atk, rcv, lb_rcv) - else: - stats_row_1 = 'Weighted {}'.format(weighted) - stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) - embed.add_field(name=stats_row_1, value=stats_row_2) - - awakenings_row = '' - for idx, a in enumerate(m.awakenings): - as_id = a.awoken_skill_id - as_name = a.name - mapped_awakening = AWAKENING_MAP.get(as_id, as_name) - mapped_awakening = match_emoji(emoji_list, mapped_awakening) - - # Wrap superawakenings to the next line - if len(m.awakenings) - idx == m.superawakening_count: - awakenings_row += '\n{}'.format(mapped_awakening) - else: - awakenings_row += ' {}'.format(mapped_awakening) - - awakenings_row = awakenings_row.strip() - - if not len(awakenings_row): - awakenings_row = 'No Awakenings' - - killers_row = '**Available Killers:** {}'.format(' '.join(m.killers)) - - embed.description = '{}\n{}'.format(awakenings_row, killers_row) - - active_header = 'Active Skill' - active_body = 'None' - active_skill = m.active_skill - if active_skill: - active_header = 'Active Skill ({} -> {})'.format(active_skill.turn_max, - active_skill.turn_min) - active_body = active_skill.desc - embed.add_field(name=active_header, value=active_body, inline=False) - - leader_skill = m.leader_skill - ls_row = m.leader_skill.desc if leader_skill else 'None' - ls_header = 'Leader Skill' - if leader_skill: - hp, atk, rcv, resist = m.leader_skill.data - multiplier_text = createMultiplierText(hp, atk, rcv, resist) - ls_header += " [ {} ]".format(multiplier_text) - embed.add_field(name=ls_header, value=ls_row, inline=False) - - return embed - - -def monsterToOtherInfoEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - # Clear the thumbnail, takes up too much space - embed.set_thumbnail(url='') - - body_text = '\n' - stat_cols = ['', 'HP', 'ATK', 'RCV'] - for plus in (0, 297): - body_text += '**Stats at +{}:**'.format(plus) - tbl = prettytable.PrettyTable(stat_cols) - tbl.hrules = prettytable.NONE - tbl.vrules = prettytable.NONE - tbl.align = "l" - levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) - for lv in levels: - for inh in (False, True): - hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) - row_name = 'Lv{}'.format(lv) - if inh: - row_name = '(Inh)' - tbl.add_row([row_name.format(plus), hp, atk, rcv]) - body_text += box(tbl.get_string()) - - body_text += "\n**JP Name**: {}".format(m.name_jp) - body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - - if m.history_us: - body_text += '\n**History:** {}'.format(m.history_us) - - body_text += '\n**Series:** {}'.format(m.series.name) - body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) - if m.buy_mp is not None: - body_text += " **Buy MP:** {:,}".format(m.buy_mp) - - if m.exp < 1000000: - xp_text = '{:,}'.format(m.exp) - else: - xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' - body_text += '\n**XP to Max:** {}'.format(xp_text) - body_text += ' **Max Level:**: {}'.format(m.level) - body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) - - embed.description = body_text - - return embed - - -AWAKENING_MAP = { - 1: 'boost_hp', - 2: 'boost_atk', - 3: 'boost_rcv', - 4: 'reduce_fire', - 5: 'reduce_water', - 6: 'reduce_wood', - 7: 'reduce_light', - 8: 'reduce_dark', - 9: 'misc_autoheal', - 10: 'res_bind', - 11: 'res_blind', - 12: 'res_jammer', - 13: 'res_poison', - 14: 'oe_fire', - 15: 'oe_water', - 16: 'oe_wood', - 17: 'oe_light', - 18: 'oe_dark', - 19: 'misc_te', - 20: 'misc_bindclear', - 21: 'misc_sb', - 22: 'row_fire', - 23: 'row_water', - 24: 'row_wood', - 25: 'row_light', - 26: 'row_dark', - 27: 'misc_tpa', - 28: 'res_skillbind', - 29: 'oe_heart', - 30: 'misc_multiboost', - 31: 'killer_dragon', - 32: 'killer_god', - 33: 'killer_devil', - 34: 'killer_machine', - 35: 'killer_balance', - 36: 'killer_attacker', - 37: 'killer_physical', - 38: 'killer_healer', - 39: 'killer_evomat', - 40: 'killer_awoken', - 41: 'killer_enhancemat', - 42: 'killer_vendor', - 43: 'misc_comboboost', - 44: 'misc_guardbreak', - 45: 'misc_extraattack', - 46: 'teamboost_hp', - 47: 'teamboost_rcv', - 48: 'misc_voidshield', - 49: 'misc_assist', - 50: 'misc_super_extraattack', - 51: 'misc_skillcharge', - 52: 'res_bind_super', - 53: 'misc_te_super', - 54: 'res_cloud', - 55: 'res_seal', - 56: 'misc_sb_super', - 57: 'attack_boost_high', - 58: 'attack_boost_low', - 59: 'l_shield', - 60: 'l_attack', - 61: 'misc_super_comboboost', - 62: 'orb_combo', - 63: 'misc_voice', - 64: 'misc_dungeonbonus', - 65: 'reduce_hp', - 66: 'reduce_atk', - 67: 'reduce_rcv', - 68: 'res_blind_super', - 69: 'res_jammer_super', - 70: 'res_poison_super', - 71: 'misc_jammerboost', - 72: 'misc_poisonboost', -} - - -def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=None, resist2=None): - if all([x is None for x in (hp2, atk2, rcv2, resist2)]): - hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 - - def fmtNum(val): - return ('{:.2f}').format(val).strip('0').rstrip('.') - - text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) - if resist1 > 0 or resist2 > 0: - text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) - return text +import asyncio +import json +import logging +import os +import traceback +import urllib.parse +from collections import OrderedDict +from enum import Enum + +import discord +import prettytable +from redbot.core import checks, data_manager +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import inline, box + +import rpadutils +from rpadutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics + +logger = logging.getLogger('red.miruv3.padinfo') + +HELP_MSG = """ +{0.prefix}helpid : shows this message +{0.prefix}id : look up a monster and print a link to puzzledragonx +{0.prefix}pic : Look up a monster and display its image inline + +Options for + : Find a monster by ID + {0.prefix}id 1234 (picks sun quan) + : Take the best guess for a monster, picks the most recent monster + {0.prefix}id kali (picks mega awoken d kali) + : Limit by element or awoken, e.g. + {0.prefix}id ares (selects the most recent, revo ares) + {0.prefix}id aares (explicitly selects awoken ares) + {0.prefix}id a ares (spaces work too) + {0.prefix}id rd ares (select a specific evo for ares, the red/dark one) + {0.prefix}id r/d ares (slashes, spaces work too) + +computed nickname list and overrides: https://docs.google.com/spreadsheets/d/1EyzMjvf8ZCQ4K-gJYnNkiZlCEsT9YYI9dUd-T5qCirc/pubhtml +submit an override suggestion: https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" + +EMBED_NOT_GENERATED = -1 + +INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' + +MEDIA_PATH = 'https://f002.backblazeb2.com/file/dadguide-data/media/' +RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png' +RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' +VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' +GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' +ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}.png' +ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}cb.png' + +YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' +SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' +ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' + + +class ServerFilter(Enum): + any = 0 + na = 1 + jp = 2 + + +def get_pdx_url(m): + return INFO_PDX_TEMPLATE.format(rpadutils.get_pdx_id(m)) + + +def get_portrait_url(m): + return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) + + +def get_pic_url(m): + return RPAD_PIC_TEMPLATE.format(m.monster_id) + + +def _data_file(file_name: str) -> str: + return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) + + +class IdEmojiUpdater(EmojiUpdater): + def __init__(self, emoji_to_embed, m: "DgMonster" = None, + pad_info=None, selected_emoji=None, bot=None): + self.emoji_dict = emoji_to_embed + self.m = m + self.pad_info = pad_info + self.selected_emoji = selected_emoji + self.bot = bot + + def on_update(self, ctx, selected_emoji): + if self.pad_info.settings.checkEvoID(ctx.author.id): + DGCOG = self.bot.get_cog('Dadguide') + evos = list(map(lambda x: [x['from_id'], x['to_id']], + DGCOG.database.get_all_evolutions_by_monster(self.m.monster_id))) + evos = list(set(sum(evos, []))) + evos.sort() + index = evos.index(self.m.monster_id) + if selected_emoji == self.pad_info.previous_monster_emoji: + if index == 0: + self.m = DGCOG.get_monster_by_id(evos[-1]) + else: + self.m = DGCOG.get_monster_by_id(evos[index - 1]) + elif selected_emoji == self.pad_info.next_monster_emoji: + if index == len(evos) - 1: + self.m = DGCOG.get_monster_by_id(evos[0]) + else: + self.m = DGCOG.get_monster_by_id(evos[index + 1]) + else: + self.selected_emoji = selected_emoji + return True + else: + if selected_emoji == self.pad_info.previous_monster_emoji: + if self.m.prev_monster is None: + return False + self.m = self.m.prev_monster + elif selected_emoji == self.pad_info.next_monster_emoji: + if self.m.next_monster is None: + return False + self.m = self.m.next_monster + else: + self.selected_emoji = selected_emoji + return True + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m) + return True + + +class ScrollEmojiUpdater(EmojiUpdater): + def __init__(self, emoji_to_embed, m: "DgMonster" = None, + ms: "List[int]" = None, selected_emoji=None, pad_info=None, bot=None): + self.emoji_dict = emoji_to_embed + self.m = m + self.ms = ms + self.pad_info = pad_info + self.selected_emoji = selected_emoji + self.bot = bot + + def on_update(self, ctx, selected_emoji): + DGCOG = self.bot.get_cog('Dadguide') + index = self.ms.index(self.m.monster_id) + + if selected_emoji == self.pad_info.first_monster_emoji: + self.m = DGCOG.get_monster_by_id(self.ms[0]) + elif selected_emoji == self.pad_info.previous_monster_emoji: + self.m = DGCOG.get_monster_by_id(self.ms[index - 1]) + elif selected_emoji == self.pad_info.next_monster_emoji: + if index == len(self.ms) - 1: + self.m = DGCOG.get_monster_by_id(self.ms[0]) + else: + self.m = DGCOG.get_monster_by_id(self.ms[index + 1]) + elif selected_emoji == self.pad_info.last_monster_emoji: + self.m = DGCOG.get_monster_by_id(self.ms[-1]) + else: + self.selected_emoji = selected_emoji + return True + + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=True) + return True + + +class PadInfo(commands.Cog): + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + self.settings = PadInfoSettings("padinfo") + + self.index_all = None + self.index_na = None + self.index_jp = None + + self.menu = Menu(bot) + + # These emojis are the keys into the idmenu submenus + self.id_emoji = '\N{INFORMATION SOURCE}' + self.evo_emoji = char_to_emoji('e') + self.mats_emoji = char_to_emoji('m') + self.ls_emoji = '\N{INFORMATION SOURCE}' + self.left_emoji = char_to_emoji('l') + self.right_emoji = char_to_emoji('r') + self.pantheon_emoji = '\N{CLASSICAL BUILDING}' + self.skillups_emoji = '\N{MEAT ON BONE}' + self.pic_emoji = '\N{FRAME WITH PICTURE}' + self.other_info_emoji = '\N{SCROLL}' + self.first_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + self.previous_monster_emoji = '\N{BLACK LEFT-POINTING TRIANGLE}' + self.next_monster_emoji = '\N{BLACK RIGHT-POINTING TRIANGLE}' + self.last_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + self.remove_emoji = self.menu.emoji['no'] + + self.historic_lookups_file_path = _data_file('historic_lookups.json') + self.historic_lookups = safe_read_json(self.historic_lookups_file_path) + + self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') + self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) + + def cog_unload(self): + # Manually nulling out database because the GC for cogs seems to be pretty shitty + self.index_all = None + self.index_na = None + self.index_jp = None + self.historic_lookups = {} + self.historic_lookups_id2 = {} + + async def reload_nicknames(self): + await self.bot.wait_until_ready() + while self == self.bot.get_cog('PadInfo'): + try: + await self.refresh_index() + print('Done refreshing PadInfo') + except Exception as ex: + print("reload padinfo loop caught exception " + str(ex)) + traceback.print_exc() + + await asyncio.sleep(60 * 60 * 1) + + @commands.command() + @checks.is_owner() + async def refresh_index(self, ctx=None): + """Refresh the monster indexes.""" + dg_cog = self.bot.get_cog('Dadguide') + if not dg_cog: + print("Cog 'Dadguide' not loaded") + return + logger.info('Waiting until DG is ready') + await dg_cog.wait_until_ready() + + # Putting some sleeps in here to give the bot time to respond to Discord. + await asyncio.sleep(1) + + logger.info('Loading ALL index') + self.index_all = dg_cog.create_index() + await asyncio.sleep(1) + + logger.info('Loading NA index') + self.index_na = dg_cog.create_index(lambda m: m.on_na) + await asyncio.sleep(1) + + logger.info('Loading JP index') + self.index_jp = dg_cog.create_index(lambda m: m.on_jp) + + logger.info('Done refreshing indexes') + + def get_monster_by_id(self, monster_id: int): + dg_cog = self.bot.get_cog('Dadguide') + return dg_cog.get_monster_by_id(monster_id) + + @commands.command() + async def jpname(self, ctx, *, query: str): + """Print the Japanese name of a monster""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await ctx.send(monsterToHeader(m)) + await ctx.send(box(m.name_jp)) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(name="id", aliases="iD Id ID".split()) + async def _id(self, ctx, *, query: str): + """Monster info (main tab)""" + prefix = ctx.prefix + "id" + query = prefix.join(filter(None, query.split(prefix))) + await self._do_id(ctx, query) + + @commands.command() + async def idna(self, ctx, *, query: str): + """Monster info (limited to NA monsters ONLY)""" + await self._do_id(ctx, query, server_filter=ServerFilter.na) + + @commands.command() + async def idjp(self, ctx, *, query: str): + """Monster info (limited to JP monsters ONLY)""" + await self._do_id(ctx, query, server_filter=ServerFilter.jp) + + async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): + m, err, debug_info = self.findMonster(query, server_filter=server_filter) + if m is not None: + await self._do_idmenu(ctx, m, self.id_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def id2(self, ctx, *, query: str): + """Monster info (main tab)""" + await self._do_id2(ctx, query) + + @commands.command() + async def id2na(self, ctx, *, query: str): + """Monster info (limited to NA monsters ONLY)""" + await self._do_id2(ctx, query, server_filter=ServerFilter.na) + + @commands.command() + async def id2jp(self, ctx, *, query: str): + """Monster info (limited to JP monsters ONLY)""" + await self._do_id2(ctx, query, server_filter=ServerFilter.jp) + + async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): + m, err, debug_info = self.findMonster2(query, server_filter=server_filter) + if m is not None: + await self._do_idmenu(ctx, m, self.id_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(name="evos") + async def evos(self, ctx, *, query: str): + """Monster info (evolutions tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.evo_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(name="mats", aliases=['evomats', 'evomat']) + async def evomats(self, ctx, *, query: str): + """Monster info (evo materials tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.mats_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def pantheon(self, ctx, *, query: str): + """Monster info (pantheon tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) + if menu == EMBED_NOT_GENERATED: + await ctx.send(inline('Not a pantheon monster')) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def skillups(self, ctx, *, query: str): + """Monster info (evolutions tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + menu = await self._do_idmenu(ctx, m, self.skillups_emoji) + if menu == EMBED_NOT_GENERATED: + await ctx.send(inline('No skillups available')) + else: + await ctx.send(self.makeFailureMsg(err)) + + async def _do_idmenu(self, ctx, m, starting_menu_emoji): + emoji_to_embed = self.get_id_emoji_options(m=m) + return await self._do_menu( + ctx, + starting_menu_emoji, + IdEmojiUpdater(emoji_to_embed, pad_info=self, + m=m, selected_emoji=starting_menu_emoji, bot=self.bot) + ) + + async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=True) + return await self._do_menu( + ctx, + starting_menu_emoji, + ScrollEmojiUpdater(emoji_to_embed, pad_info=self, bot=self.bot, + m=m, ms=ms, selected_emoji=starting_menu_emoji) + ) + + def get_id_emoji_options(self, m=None, scroll=False): + id_embed = monsterToEmbed(m, self.get_emojis()) + evo_embed = monsterToEvoEmbed(m) + mats_embed = monsterToEvoMatsEmbed(m) + animated = m.has_animation + pic_embed = monsterToPicEmbed(m, animated=animated) + other_info_embed = monsterToOtherInfoEmbed(m) + + emoji_to_embed = OrderedDict() + emoji_to_embed[self.id_emoji] = id_embed + emoji_to_embed[self.evo_emoji] = evo_embed + emoji_to_embed[self.mats_emoji] = mats_embed + emoji_to_embed[self.pic_emoji] = pic_embed + pantheon_embed = monsterToPantheonEmbed(m) + if pantheon_embed: + emoji_to_embed[self.pantheon_emoji] = pantheon_embed + + skillups_embed = monsterToSkillupsEmbed(m) + if skillups_embed: + emoji_to_embed[self.skillups_emoji] = skillups_embed + + emoji_to_embed[self.other_info_emoji] = other_info_embed + + # it's impossible for the previous/next ones to be accessed because + # IdEmojiUpdater won't allow it, however they have to be defined + # so that the buttons print in the first place + + if scroll: emoji_to_embed[self.first_monster_emoji] = None + emoji_to_embed[self.previous_monster_emoji] = None + emoji_to_embed[self.next_monster_emoji] = None + if scroll: emoji_to_embed[self.last_monster_emoji] = None + + # remove emoji needs to be last + emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message + return emoji_to_embed + + + async def _do_evolistmenu(self, ctx, sm): + monsters = sm.alt_evos + monsters.sort(key=lambda m: m.monster_id) + + emoji_to_embed = OrderedDict() + for idx, m in enumerate(monsters): + emoji = char_to_emoji(str(idx)) + emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis()) + if m.monster_id == sm.monster_id: + starting_menu_emoji = emoji + + return await self._do_menu(ctx, starting_menu_emoji, EmojiUpdater(emoji_to_embed), timeout=60) + + async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): + if starting_menu_emoji not in emoji_to_embed.emoji_dict: + # Selected menu wasn't generated for this monster + return EMBED_NOT_GENERATED + + emoji_to_embed.emoji_dict[self.remove_emoji] = self.menu.reaction_delete_message + + try: + result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, + starting_menu_emoji, timeout=timeout) + if result_msg and result_embed: + # Message is finished but not deleted, clear the footer + result_embed.set_footer(text=discord.Embed.Empty) + await result_msg.edit(embed=result_embed) + except Exception as ex: + print('Menu failure', ex) + + @commands.command(aliases=['img']) + async def pic(self, ctx, *, query: str): + """Monster info (full image tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.pic_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def links(self, ctx, *, query: str): + """Monster links""" + m, err, debug_info = self.findMonster(query) + if m is not None: + embed = monsterToBaseEmbed(m) + embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + embed.set_footer(text='') + await ctx.send(embed=embed) + + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(aliases=['stats']) + async def otherinfo(self, ctx, *, query: str): + """Monster info (misc info tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.other_info_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def lookup(self, ctx, *, query: str): + """Short info results for a monster query""" + m, err, debug_info = self.findMonster(query) + if m is not None: + embed = monsterToHeaderEmbed(m) + await ctx.send(embed=embed) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def evolist(self, ctx, *, query): + """Monster info (for all monsters in the evo tree)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_evolistmenu(ctx, m) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def collabscroll(self, ctx, *, query: str): + """Scroll through the monsters in a collab""" + DGCOG = self.bot.get_cog("Dadguide") + m, err, debug_info = self.findMonster(query) + ms = DGCOG.database.get_monsters_by_series(m.series.series_id) + + ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) + ms = [m._alt_evo_id_list for m in ms + if m._base_monster_id == m.monster_id + and m.sell_mp >= 3000] + ms = [m for ml in ms for m in ml] + + if m._base_monster_id != m.monster_id: + m = self.get_monster_by_id(m._base_monster_id) + if m.monster_id not in ms: + m = m if m.monster_id in ms else self.get_monster_by_id(ms[0]) + + if m is not None: + await self._do_scrollmenu(ctx, m, ms, self.id_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + + @commands.command(aliases=['leaders', 'leaderskills', 'ls']) + async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): + """Display the multiplier and leaderskills for two monsters + + If either your left or right query contains spaces, wrap in quotes. + e.g.: [p]leaderskill "r sonia" "b sonia" + """ + if bad: + await ctx.send(inline('Too many inputs. Try wrapping your queries in quotes.')) + return + + # Handle a very specific failure case, user typing something like "uuvo ragdra" + if ' ' not in left_query and right_query is not None and ' ' not in right_query and bad is None: + combined_query = left_query + ' ' + right_query + nm, err, debug_info = self._findMonster(combined_query) + if nm and left_query in nm.prefixes: + left_query = combined_query + right_query = None + + left_m, left_err, _ = self.findMonster(left_query) + if right_query: + right_m, right_err, _ = self.findMonster(right_query) + else: + right_m, right_err, = left_m, left_err + + err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, wrap it in quotes.' + if left_err: + await ctx.send(inline(err_msg.format('Left', left_query))) + return + if right_err: + await ctx.send(inline(err_msg.format('Right', right_query))) + return + + emoji_to_embed = OrderedDict() + emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) + emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis()) + emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis()) + + await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) + + @commands.command(aliases=['helppic', 'helpimg']) + async def helpid(self, ctx): + """Whispers you info on how to craft monster queries for [p]id""" + await ctx.author.send(box(HELP_MSG.format(ctx))) + + @commands.command() + async def padsay(self, ctx, server, *, query: str = None): + """Speak the voice line of a monster into your current chat""" + voice = ctx.author.voice + if not voice: + await ctx.send(inline('You must be in a voice channel to use this command')) + return + channel = voice.channel + + speech_cog = self.bot.get_cog('Speech') + if not speech_cog: + await ctx.send(inline('Speech seems to be offline')) + return + + if server.lower() not in ['na', 'jp']: + query = server + ' ' + (query or '') + server = 'na' + query = query.strip().lower() + + m, err, debug_info = self.findMonster(query) + if m is not None: + voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na + base_dir = '/home/tactical0retreat/dadguide/data/media/voices' + voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) + header = '{} ({})'.format(monsterToHeader(m), server) + if not os.path.exists(voice_file): + await ctx.send(inline('Could not find voice for ' + header)) + return + await ctx.send('Speaking for ' + header) + await speech_cog.play_path(channel, voice_file) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def idmode(self, ctx, id_type): + """Switch between number mode and evo mode""" + if id_type in ['evo']: + if self.settings.setEvoID(ctx.author.id): + await ctx.send(inline("Done")) + else: + await ctx.send(inline("You're already using evo mode")) + elif id_type in ['number', 'default']: + if self.settings.rmEvoID(ctx.author.id): + await ctx.send(inline("Done")) + else: + await ctx.send(inline("You're already using number mode")) + else: + await ctx.send(inline("id_type must be 'number' or 'evo'")) + + @commands.group() + @checks.is_owner() + async def padinfo(self, ctx): + """PAD info management""" + + @padinfo.command() + @checks.is_owner() + async def setemojiservers(self, ctx, *, emoji_servers=''): + """Set the emoji servers by ID (csv)""" + self.settings.emojiServers().clear() + if emoji_servers: + self.settings.setEmojiServers(emoji_servers.split(',')) + await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) + + @checks.is_owner() + @padinfo.command() + async def iddiff(self, ctx): + """Runs the diff checker for id and id2""" + await ctx.send("Running diff checker...") + hist_aggreg = list(self.historic_lookups) + list(self.historic_lookups_id2) + s = 0 + f = [] + for query in hist_aggreg: + m1, err1, debug_info1 = self.findMonster(query) + m2, err2, debug_info2 = self.findMonster2(query) + if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): + s += 1 + continue + + f.append((query, + [m1.monster_id if m1 else None, m2.monster_id if m2 else None], + [err1, err2], + [debug_info1, debug_info2] + )) + if m1 and m2: + ctx.send("Major Discrepency: {} -> {}/{}".format(query, m1.name_na, m2.name_na)) + await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) + print(f) + + def get_emojis(self): + server_ids = [int(sid) for sid in self.settings.emojiServers()] + return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] + + def makeFailureMsg(self, err): + msg = ('Lookup failed: {}.\n' + 'Try one of , , [argbld]/[rgbld] . ' + 'Unexpected results? Use ^helpid for more info.').format(err) + return box(msg) + + def findMonster(self, query, server_filter=ServerFilter.any): + query = rmdiacritics(query) + nm, err, debug_info = self._findMonster(query, server_filter) + + monster_no = nm.monster_id if nm else -1 + self.historic_lookups[query] = monster_no + json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) + + m = self.get_monster_by_id(nm.monster_id) if nm else None + + return m, err, debug_info + + def _findMonster(self, query, server_filter=ServerFilter.any): + if server_filter == ServerFilter.any: + monster_index = self.index_all + elif server_filter == ServerFilter.na: + monster_index = self.index_na + elif server_filter == ServerFilter.jp: + monster_index = self.index_jp + else: + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) + return monster_index.find_monster(query) + + def findMonster2(self, query, server_filter=ServerFilter.any): + query = rmdiacritics(query) + nm, err, debug_info = self._findMonster2(query, server_filter) + + monster_no = nm.monster_id if nm else -1 + self.historic_lookups_id2[query] = monster_no + json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) + + m = self.get_monster_by_id(nm.monster_id) if nm else None + + return m, err, debug_info + + def _findMonster2(self, query, server_filter=ServerFilter.any): + if server_filter == ServerFilter.any: + monster_index = self.index_all + elif server_filter == ServerFilter.na: + monster_index = self.index_na + elif server_filter == ServerFilter.jp: + monster_index = self.index_jp + else: + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) + return monster_index.find_monster2(query) + + +class PadInfoSettings(CogSettings): + def make_default_settings(self): + config = { + 'animation_dir': '', + 'evo_id_users': [], + } + return config + + def emojiServers(self): + key = 'emoji_servers' + if key not in self.bot_settings: + self.bot_settings[key] = [] + return self.bot_settings[key] + + def setEmojiServers(self, emoji_servers): + es = self.emojiServers() + es.clear() + es.extend(emoji_servers) + self.save_settings() + + def setEvoID(self, user_id): + if user_id not in self.bot_settings['evo_id_users']: + self.bot_settings['evo_id_users'].append(user_id) + self.save_settings() + return True + return False + + def rmEvoID(self, user_id): + if user_id in self.bot_settings['evo_id_users']: + self.bot_settings['evo_id_users'].remove(user_id) + self.save_settings() + return True + return False + + def checkEvoID(self, user_id): + return user_id in self.bot_settings['evo_id_users'] + + +def monsterToHeader(m: "DgMonster", link=False): + msg = 'No. {} {}'.format(m.monster_no_na, m.name_na) + return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg + + +def monsterToJpSuffix(m: "DgMonster"): + suffix = "" + if m.roma_subname: + suffix += ' [{}]'.format(m.roma_subname) + if not m.on_na: + suffix += ' (JP only)' + return suffix + + +def monsterToLongHeader(m: "DgMonster", link=False): + msg = monsterToHeader(m) + monsterToJpSuffix(m) + return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg + + +def monsterToEvoText(m: "DgMonster"): + output = monsterToLongHeader(m) + for ae in sorted(m.alt_evos, key=lambda x: int(x.monster_id)): + output += "\n\t- {}".format(monsterToLongHeader(ae)) + return output + + +def monsterToThumbnailUrl(m: "DgMonster"): + return get_portrait_url(m) + + +def monsterToBaseEmbed(m: "DgMonster"): + header = monsterToLongHeader(m) + embed = discord.Embed() + embed.set_thumbnail(url=monsterToThumbnailUrl(m)) + embed.title = header + embed.url = get_pdx_url(m) + embed.set_footer(text='Requester may click the reactions below to switch tabs') + return embed + + +def printEvoListFields(list_of_monsters, embed, name): + if not len(list_of_monsters): + return + field_name = name.format(len(list_of_monsters)) + field_data = '' + for ae in sorted(list_of_monsters, key=lambda x: int(x.monster_id)): + field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) + + embed.add_field(name=field_name, value=field_data) + + +def monsterToEvoEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + + if not len(m.alt_evos) and not m.evo_gem: + embed.description = 'No alternate evos or evo gem' + return embed + + printEvoListFields(m.alt_evos, embed, '{} alternate evo(s)') + if not m.evo_gem: + return embed + printEvoListFields([m.evo_gem], embed, '{} evo gem(s)') + + return embed + + +def printMonsterEvoOfList(monster_list, embed, field_name): + if not len(monster_list): + return + field_data = '' + if len(monster_list) > 5: + field_data = '{} monsters'.format(len(monster_list)) + else: + item_count = min(len(monster_list), 5) + for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: + field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) + embed.add_field(name=field_name, value=field_data) + + +def monsterToEvoMatsEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + + mats_for_evo = m.mats_for_evo + + field_name = 'Evo materials' + field_data = '' + if len(mats_for_evo) > 0: + for ae in m.mats_for_evo: + field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) + else: + field_data = 'None' + embed.add_field(name=field_name, value=field_data) + + printMonsterEvoOfList(m.material_of, embed, 'Material for') + if not m.evo_gem: + return embed + printMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") + return embed + + +def monsterToPantheonEmbed(m: "DgMonster"): + full_pantheon = m.series.monsters + pantheon_list = list(filter(lambda x: x.evo_from is None, full_pantheon)) + if len(pantheon_list) == 0 or len(pantheon_list) > 6: + return None + + embed = monsterToBaseEmbed(m) + + field_name = 'Pantheon: ' + m.series.name + field_data = '' + for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): + field_data += '\n' + monsterToHeader(monster, link=True) + embed.add_field(name=field_name, value=field_data) + + return embed + + +def monsterToSkillupsEmbed(m: "DgMonster"): + skillups_list = m.active_skill.skillups if m.active_skill else [] + if len(skillups_list) == 0: + return None + + embed = monsterToBaseEmbed(m) + + field_name = 'Skillups' + field_data = '' + + # Prevent huge skillup lists + if len(skillups_list) > 8: + field_data = '({} skillups omitted)'.format(len(skillups_list) - 8) + skillups_list = skillups_list[0:8] + + for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): + field_data += '\n' + monsterToHeader(monster, link=True) + + if len(field_data.strip()): + embed.add_field(name=field_name, value=field_data) + + return embed + + +def monsterToPicUrl(m: "DgMonster"): + return get_pic_url(m) + + +def monsterToPicEmbed(m: "DgMonster", animated=False): + embed = monsterToBaseEmbed(m) + url = monsterToPicUrl(m) + embed.set_image(url=url) + # Clear the thumbnail, don't need it on pic + embed.set_thumbnail(url='') + extra_links = [] + if animated: + extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), monsterToGifUrl(m))) + if m.orb_skin_id is not None: + extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), monsterToOrbSkinCBUrl(m))) + if len(extra_links) > 0: + embed.add_field(name='Extra Links', value='\n'.join(extra_links)) + + return embed + + +def monsterToVideoUrl(m: "DgMonster", link_text='(MP4)'): + return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) + + +def monsterToGifUrl(m: "DgMonster", link_text='(GIF)'): + return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) + + +def monsterToOrbSkinUrl(m: "DgMonster", link_text='Regular'): + return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) + + +def monsterToOrbSkinCBUrl(m: "DgMonster", link_text='Color Blind'): + return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) + + +def monsterToGifEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + url = monsterToGifUrl(m) + embed.set_image(url=url) + # Clear the thumbnail, don't need it on pic + embed.set_thumbnail(url='') + return embed + + +def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): + lls = left_m.leader_skill + rls = right_m.leader_skill + + if lls: + lhp, latk, lrcv, lresist = lls.data + else: + lhp, latk, lrcv, lresist = 1, 1, 1, 0 + + if rls: + rhp, ratk, rrcv, rresist = rls.data + else: + rhp, ratk, rrcv, rresist = 1, 1, 1, 0 + + multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) + + embed = discord.Embed() + embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) + description = '' + description += '\n**{}**\n{}'.format( + monsterToHeader(left_m, link=True), + left_m.leader_skill.desc if left_m.leader_skill else 'None') + description += '\n**{}**\n{}'.format( + monsterToHeader(right_m, link=True), + right_m.leader_skill.desc if right_m.leader_skill else 'None') + embed.description = description + + return embed + + +def monsterToHeaderEmbed(m: "DgMonster"): + header = monsterToLongHeader(m, link=True) + embed = discord.Embed() + embed.description = header + return embed + + +def monsterToTypeString(m: "DgMonster"): + return '/'.join([t.name for t in m.types]) + + +def monsterToAcquireString(m: "DgMonster"): + acquire_text = None + if m.farmable and not m.mp_evo: + # Some MP shop monsters 'drop' in PADR + acquire_text = 'Farmable' + elif m.farmable_evo and not m.mp_evo: + acquire_text = 'Farmable Evo' + elif m.in_pem: + acquire_text = 'In PEM' + elif m.pem_evo: + acquire_text = 'PEM Evo' + elif m.in_rem: + acquire_text = 'In REM' + elif m.rem_evo: + acquire_text = 'REM Evo' + elif m.in_mpshop: + acquire_text = 'MP Shop' + elif m.mp_evo: + acquire_text = 'MP Shop Evo' + return acquire_text + + +def match_emoji(emoji_list, name): + for e in emoji_list: + if e.name == name: + return e + return name + + +def monsterToEmbed(m: "DgMonster", emoji_list): + embed = monsterToBaseEmbed(m) + + info_row_1 = monsterToTypeString(m) + acquire_text = monsterToAcquireString(m) + tet_text = m.true_evo_type.value + + os = "" if m.orb_skin_id is None else " (Orb Skin)" + + info_row_2 = '**Rarity** {}{}\n**Cost** {}'.format(m.rarity, os, m.cost) + if acquire_text: + info_row_2 += '\n**{}**'.format(acquire_text) + if m.is_inheritable: + info_row_2 += '\n**Inheritable**' + else: + info_row_2 += '\n**Not inheritable**' + if tet_text: + info_row_2 += '\n**{}**'.format(tet_text) + + embed.add_field(name=info_row_1, value=info_row_2) + + hp, atk, rcv, weighted = m.stats() + if m.limit_mult > 0: + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) + stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, m.limit_mult) + stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( + hp, lb_hp, atk, lb_atk, rcv, lb_rcv) + else: + stats_row_1 = 'Weighted {}'.format(weighted) + stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) + embed.add_field(name=stats_row_1, value=stats_row_2) + + awakenings_row = '' + for idx, a in enumerate(m.awakenings): + as_id = a.awoken_skill_id + as_name = a.name + mapped_awakening = AWAKENING_MAP.get(as_id, as_name) + mapped_awakening = match_emoji(emoji_list, mapped_awakening) + + # Wrap superawakenings to the next line + if len(m.awakenings) - idx == m.superawakening_count: + awakenings_row += '\n{}'.format(mapped_awakening) + else: + awakenings_row += ' {}'.format(mapped_awakening) + + awakenings_row = awakenings_row.strip() + + if not len(awakenings_row): + awakenings_row = 'No Awakenings' + + killers_row = '**Available Killers:** {}'.format(' '.join(m.killers)) + + embed.description = '{}\n{}'.format(awakenings_row, killers_row) + + active_header = 'Active Skill' + active_body = 'None' + active_skill = m.active_skill + if active_skill: + active_header = 'Active Skill ({} -> {})'.format(active_skill.turn_max, + active_skill.turn_min) + active_body = active_skill.desc + embed.add_field(name=active_header, value=active_body, inline=False) + + leader_skill = m.leader_skill + ls_row = m.leader_skill.desc if leader_skill else 'None' + ls_header = 'Leader Skill' + if leader_skill: + hp, atk, rcv, resist = m.leader_skill.data + multiplier_text = createMultiplierText(hp, atk, rcv, resist) + ls_header += " [ {} ]".format(multiplier_text) + embed.add_field(name=ls_header, value=ls_row, inline=False) + + return embed + + +def monsterToOtherInfoEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + # Clear the thumbnail, takes up too much space + embed.set_thumbnail(url='') + + body_text = '\n' + stat_cols = ['', 'HP', 'ATK', 'RCV'] + for plus in (0, 297): + body_text += '**Stats at +{}:**'.format(plus) + tbl = prettytable.PrettyTable(stat_cols) + tbl.hrules = prettytable.NONE + tbl.vrules = prettytable.NONE + tbl.align = "l" + levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) + for lv in levels: + for inh in (False, True): + hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) + row_name = 'Lv{}'.format(lv) + if inh: + row_name = '(Inh)' + tbl.add_row([row_name.format(plus), hp, atk, rcv]) + body_text += box(tbl.get_string()) + + body_text += "\n**JP Name**: {}".format(m.name_jp) + body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + + if m.history_us: + body_text += '\n**History:** {}'.format(m.history_us) + + body_text += '\n**Series:** {}'.format(m.series.name) + body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) + if m.buy_mp is not None: + body_text += " **Buy MP:** {:,}".format(m.buy_mp) + + if m.exp < 1000000: + xp_text = '{:,}'.format(m.exp) + else: + xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' + body_text += '\n**XP to Max:** {}'.format(xp_text) + body_text += ' **Max Level:**: {}'.format(m.level) + body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) + + embed.description = body_text + + return embed + + +AWAKENING_MAP = { + 1: 'boost_hp', + 2: 'boost_atk', + 3: 'boost_rcv', + 4: 'reduce_fire', + 5: 'reduce_water', + 6: 'reduce_wood', + 7: 'reduce_light', + 8: 'reduce_dark', + 9: 'misc_autoheal', + 10: 'res_bind', + 11: 'res_blind', + 12: 'res_jammer', + 13: 'res_poison', + 14: 'oe_fire', + 15: 'oe_water', + 16: 'oe_wood', + 17: 'oe_light', + 18: 'oe_dark', + 19: 'misc_te', + 20: 'misc_bindclear', + 21: 'misc_sb', + 22: 'row_fire', + 23: 'row_water', + 24: 'row_wood', + 25: 'row_light', + 26: 'row_dark', + 27: 'misc_tpa', + 28: 'res_skillbind', + 29: 'oe_heart', + 30: 'misc_multiboost', + 31: 'killer_dragon', + 32: 'killer_god', + 33: 'killer_devil', + 34: 'killer_machine', + 35: 'killer_balance', + 36: 'killer_attacker', + 37: 'killer_physical', + 38: 'killer_healer', + 39: 'killer_evomat', + 40: 'killer_awoken', + 41: 'killer_enhancemat', + 42: 'killer_vendor', + 43: 'misc_comboboost', + 44: 'misc_guardbreak', + 45: 'misc_extraattack', + 46: 'teamboost_hp', + 47: 'teamboost_rcv', + 48: 'misc_voidshield', + 49: 'misc_assist', + 50: 'misc_super_extraattack', + 51: 'misc_skillcharge', + 52: 'res_bind_super', + 53: 'misc_te_super', + 54: 'res_cloud', + 55: 'res_seal', + 56: 'misc_sb_super', + 57: 'attack_boost_high', + 58: 'attack_boost_low', + 59: 'l_shield', + 60: 'l_attack', + 61: 'misc_super_comboboost', + 62: 'orb_combo', + 63: 'misc_voice', + 64: 'misc_dungeonbonus', + 65: 'reduce_hp', + 66: 'reduce_atk', + 67: 'reduce_rcv', + 68: 'res_blind_super', + 69: 'res_jammer_super', + 70: 'res_poison_super', + 71: 'misc_jammerboost', + 72: 'misc_poisonboost', +} + + +def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=None, resist2=None): + if all([x is None for x in (hp2, atk2, rcv2, resist2)]): + hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 + + def fmtNum(val): + return ('{:.2f}').format(val).strip('0').rstrip('.') + + text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) + if resist1 > 0 or resist2 > 0: + text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) + return text From 50965b67967000a7c4852b20d4a8b23fb76d668e Mon Sep 17 00:00:00 2001 From: droonkid Date: Mon, 17 Aug 2020 18:40:13 -0700 Subject: [PATCH 0016/1220] crlf remove --- padevents/padevents.py | 1442 +++++++++++------------ padguidedb/padguidedb.py | 628 +++++----- padinfo/padinfo.py | 2406 +++++++++++++++++++------------------- 3 files changed, 2238 insertions(+), 2238 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index a95091e1..29908289 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,721 +1,721 @@ -import traceback -import asyncio -import re -import datetime -import pytz -from collections import defaultdict -from datetime import timedelta -from enum import Enum - -import prettytable -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box, pagify - -from dadguide import dadguide -from rpadutils import CogSettings - -SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] - - -class PadEvents(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - self.settings = PadEventSettings("padevents") - - # Load event data - self.events = list() - self.started_events = set() - - self.fake_uid = -999 - - def cog_unload(self): - # Manually nulling out database because the GC for cogs seems to be pretty shitty - self.events = list() - self.started_events = set() - - async def reload_padevents(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadEvents'): - try: - await self.refresh_data() - print('Done refreshing PadEvents') - except Exception as ex: - print("reload padevents loop caught exception " + str(ex)) - traceback.print_exc() - - await asyncio.sleep(60 * 60 * 1) - - async def refresh_data(self): - dg_cog = self.bot.get_cog('Dadguide') - await dg_cog.wait_until_ready() - scheduled_events = dg_cog.database.get_all_events() - - new_events = [] - for se in scheduled_events: - try: - new_events.append(Event(se)) - except Exception as ex: - print(ex) - - self.events = new_events - self.started_events = {ev.key for ev in new_events if ev.is_started()} - - async def check_started(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadEvents'): - try: - events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) - - daily_refresh_servers = set() - for e in events: - self.started_events.add(e.key) - if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: - for gr in list(self.settings.listGuerrillaReg()): - if e.server == gr['server']: - try: - message = box("Server " + e.server + ", group " + - e.groupLongName() + " : " + e.name_and_modifier) - channel = self.bot.get_channel(int(gr['channel_id'])) - - try: - role_name = '{}_group_{}'.format( - e.server, e.groupLongName()) - role = get_role(channel.guild.roles, role_name) - if role and role.mentionable: - message = "{} `: {} is starting`".format( - role.mention, e.name_and_modifier) - except: - pass # do nothing if role is missing - - await channel.send(message) - except Exception as ex: - # deregister gr - traceback.print_exc() - # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) - print("caught exception while sending guerrilla msg:\n" + str(ex)) - - else: - if not e.dungeon_type in [DungeonType.Normal]: - daily_refresh_servers.add(e.server) - - for server in daily_refresh_servers: - msg = self.makeActiveText(server) - for daily_registration in list(self.settings.listDailyReg()): - try: - if server == daily_registration['server']: - await self.pageOutput(ctx, msg, channel_id=daily_registration['channel_id']) - except Exception as ex: - traceback.print_exc() - # self.settings.removeDailyReg( - # daily_registration['channel_id'], daily_registration['server']) - print("caught exception while sending daily msg:\n" + str(ex)) - - except Exception as ex: - traceback.print_exc() - print("caught exception while checking guerrillas:\n" + str(ex)) - - await asyncio.sleep(10) - print("done check_started") - - @commands.group(aliases=['pde']) - @checks.mod_or_permissions(manage_guild=True) - async def padevents(self, ctx): - """PAD event tracking""" - - @padevents.command() - @checks.is_owner() - async def testevent(self, ctx, server, seconds: int = 0): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - dg_cog = self.bot.get_cog('Dadguide') - await dg_cog.wait_until_ready() - te = dadguide.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) - - te.server = server - te.event_type_id = EventType.Guerrilla.value - te.event_seq = 0 - fuid = self.fake_uid = self.fake_uid - 1 - te.key = lambda: fuid - te.group_name = 'Yellow' - - te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) - te.close_datetime = te.open_datetime + timedelta(minutes=1) - te.dungeon_name = 'fake_dungeon_name' - te.event_modifier = 'fake_event_modifier' - self.events.append(Event(te)) - - await ctx.send("Fake event injected.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def addchannel(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - channel_id = ctx.channel.id - if self.settings.checkGuerrillaReg(channel_id, server): - await ctx.send("Channel already active.") - return - - self.settings.addGuerrillaReg(channel_id, server) - await ctx.send("Channel now active.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmchannel(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") - return - - channel_id = ctx.channel.id - if not self.settings.checkGuerrillaReg(channel_id, server): - await ctx.send("Channel is not active.") - return - - self.settings.removeGuerrillaReg(channel_id, server) - await ctx.send("Channel deactivated.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def addchanneldaily(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") - return - - channel_id = ctx.channel.id - if self.settings.checkDailyReg(channel_id, server): - await ctx.send("Channel already active.") - return - - self.settings.addDailyReg(channel_id, server) - await ctx.send("Channel now active.") - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmchanneldaily(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") - return - - channel_id = ctx.channel.id - if not self.settings.checkDailyReg(channel_id, server): - await ctx.send("Channel is not active.") - return - - self.settings.removeDailyReg(channel_id, server) - await ctx.send("Channel deactivated.") - - @padevents.command(name="listchannels") - @checks.mod_or_permissions(manage_guild=True) - async def _listchannel(self, ctx): - msg = 'Following daily channels are registered:\n' - msg += self.makeChannelList(self.settings.listDailyReg()) - msg += "\n" - msg += 'Following guerilla channels are registered:\n' - msg += self.makeChannelList(self.settings.listGuerrillaReg()) - await self.pageOutput(ctx, msg) - - def makeChannelList(self, reg_list): - msg = "" - for cr in reg_list: - reg_channel_id = cr['channel_id'] - channel = self.bot.get_channel(int(reg_channel_id)) - channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' - server_name = channel.guild.name if channel else 'Unknown server' - msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' - return msg - - @padevents.command() - @checks.mod_or_permissions(manage_guild=True) - async def active(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - msg = self.makeActiveText(server) - await self.pageOutput(ctx, msg) - - def makeActiveText(self, server): - server_events = EventList(self.events).withServer(server) - active_events = server_events.activeOnly() - pending_events = server_events.pendingOnly() - available_events = server_events.availableOnly() - - msg = "Listing all events for " + server - - special_events = active_events.withType( - EventType.Special).itemsByCloseTime() - if len(special_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Special Events', special_events) - - all_etc_events = active_events.withType(EventType.Etc) - - etc_events = all_etc_events.withDungeonType( - DungeonType.Etc).excludeUnwantedEvents().itemsByCloseTime() - if len(etc_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Etc Events', etc_events) - - # Old-style guerrillas - active_guerrilla_events = active_events.withType(EventType.Guerrilla).items() - if len(active_guerrilla_events) > 0: - msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) - - guerrilla_events = pending_events.withType(EventType.Guerrilla).items() - if len(guerrilla_events) > 0: - msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', guerrilla_events) - - # New-style guerrillas - active_guerrilla_events = active_events.withType(EventType.SpecialWeek).items() - if len(active_guerrilla_events) > 0: - msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) - - guerrilla_events = pending_events.withType(EventType.SpecialWeek).items() - if len(guerrilla_events) > 0: - msg += "\n\n" + \ - self.makeFullGuerrillaOutput( - 'Guerrilla Events', guerrilla_events, starter_guerilla=True) - - # clean up long headers - msg = msg.replace('-------------------------------------', '-----------------------') - - return msg - - async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): - msg = msg.strip() - msg = pagify(msg, ["\n"], shorten_by=20) - for page in msg: - try: - if channel_id is None: - await ctx.send(format_type(page)) - else: - await self.bot.get_channel(int(channel_id)).send(format_type(page)) - except Exception as e: - print("page output failed " + str(e)) - print("tried to print: " + page) - - def makeActiveOutput(self, table_name, event_list): - tbl = prettytable.PrettyTable(["Time", table_name]) - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.NONE - tbl.align[table_name] = "l" - tbl.align["Time"] = "r" - for e in event_list: - tbl.add_row([e.endFromNowFullMin().strip(), e.name_and_modifier]) - return tbl.get_string() - - def makeActiveGuerrillaOutput(self, table_name, event_list): - tbl = prettytable.PrettyTable([table_name, "Group", "Time"]) - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.NONE - tbl.align[table_name] = "l" - tbl.align["Time"] = "r" - for e in event_list: - tbl.add_row([e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) - return tbl.get_string() - - def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False): - events_by_name = defaultdict(list) - for e in event_list: - events_by_name[e.name_and_modifier].append(e) - - rows = list() - grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] - for name, events in events_by_name.items(): - events = sorted(events, key=lambda e: e.open_datetime) - events_by_group = defaultdict(list) - for e in events: - events_by_group[e.group].append(e) - - done = False - while not done: - did_work = False - row = list() - row.append(name) - for g in grps: - grp_list = events_by_group[g] - if len(grp_list) == 0: - row.append("") - else: - did_work = True - e = grp_list.pop(0) - row.append(e.toGuerrillaStr()) - if did_work: - rows.append(row) - else: - done = True - - col1 = "Pending" - tbl = prettytable.PrettyTable([col1] + grps) - tbl.align[col1] = "l" - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.ALL - - for r in rows: - tbl.add_row(r) - - header = "Times are PT below\n\n" - return header + tbl.get_string() + "\n" - - @commands.command(aliases=['events']) - async def eventsna(self, ctx): - """Print upcoming daily events for NA.""" - await self.doPartial(ctx, 'NA') - - @commands.command() - async def eventsjp(self, ctx): - """Print upcoming daily events for JP.""" - await self.doPartial(ctx, 'JP') - - @commands.command() - async def eventskr(self, ctx): - """Print upcoming daily events for KR.""" - await self.doPartial(ctx, 'KR') - - async def doPartial(self, ctx, server): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - events = EventList(self.events) - events = events.withServer(server) - events = events.inType([EventType.Guerrilla, EventType.SpecialWeek]) - - active_events = events.activeOnly().itemsByOpenTime(reverse=True) - pending_events = events.pendingOnly().itemsByOpenTime(reverse=True) - - group_to_active_event = {e.group: e for e in active_events} - group_to_pending_event = {e.group: e for e in pending_events} - - active_events = list(group_to_active_event.values()) - pending_events = list(group_to_pending_event.values()) - - active_events.sort(key=lambda e: e.group, reverse=True) - pending_events.sort(key=lambda e: e.group, reverse=True) - - if len(active_events) == 0 and len(pending_events) == 0: - await ctx.send("No events available for " + server) - return - - output = "Events for {}".format(server) - - if len(active_events) > 0: - output += "\n\n" + "G Remaining Dungeon" - for e in active_events: - output += "\n" + e.toPartialEvent(self) - - if len(pending_events) > 0: - output += "\n\n" + "G PT ET ETA Dungeon" - for e in pending_events: - output += "\n" + e.toPartialEvent(self) - - await ctx.send(box(output)) - - -def makeChannelReg(channel_id, server): - server = normalizeServer(server) - return { - "channel_id": channel_id, - "server": server - } - - -class PadEventSettings(CogSettings): - def make_default_settings(self): - config = { - 'guerrilla_regs': [], - 'daily_regs': [], - } - return config - - def listGuerrillaReg(self): - return self.bot_settings['guerrilla_regs'] - - def addGuerrillaReg(self, channel_id, server): - self.listGuerrillaReg().append(makeChannelReg(channel_id, server)) - self.save_settings() - - def checkGuerrillaReg(self, channel_id, server): - return makeChannelReg(channel_id, server) in self.listGuerrillaReg() - - def removeGuerrillaReg(self, channel_id, server): - if self.checkGuerrillaReg(channel_id, server): - self.listGuerrillaReg().remove(makeChannelReg(channel_id, server)) - self.save_settings() - - def listDailyReg(self): - return self.bot_settings['daily_regs'] - - def addDailyReg(self, channel_id, server): - self.listDailyReg().append(makeChannelReg(channel_id, server)) - self.save_settings() - - def checkDailyReg(self, channel_id, server): - return makeChannelReg(channel_id, server) in self.listDailyReg() - - def removeDailyReg(self, channel_id, server): - if self.checkDailyReg(channel_id, server): - self.listDailyReg().remove(makeChannelReg(channel_id, server)) - self.save_settings() - - -class Event: - def __init__(self, scheduled_event: dadguide.DgScheduledEvent): - self.key = scheduled_event.key() - self.server = SUPPORTED_SERVERS[scheduled_event.server_id] - self.open_datetime = scheduled_event.open_datetime - self.close_datetime = scheduled_event.close_datetime - self.group = scheduled_event.group_name - self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' - self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' - - self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) - self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') - - self.name_and_modifier = self.clean_dungeon_name - if self.clean_event_name != '': - self.name_and_modifier += ', ' + self.clean_event_name - - self.event_type = EventType(scheduled_event.event_type_id) - self.dungeon_type = DungeonType( - scheduled_event.dungeon.dungeon_type) if scheduled_event.dungeon else DungeonType.Unknown - - def start_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.open_datetime - now).total_seconds() - - def end_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.close_datetime - now).total_seconds() - - def is_started(self): - """True if past the open time for the event.""" - return self.start_from_now_sec() <= 0 - - def is_finished(self): - """True if past the close time for the event.""" - return self.end_from_now_sec() <= 0 - - def is_active(self): - """True if between open and close time for the event.""" - return self.is_started() and not self.is_finished() - - def is_pending(self): - """True if event has not started.""" - return not self.is_started() - - def is_available(self): - """True if event has not finished.""" - return not self.is_finished() - - def tostr(self): - return fmtTime(self.open_datetime) + "," + fmtTime( - self.close_datetime) + "," + self.group + "," + self.dungeon_code + "," + self.event_type + "," + self.event_seq - - def startPst(self): - tz = pytz.timezone('US/Pacific') - return self.open_datetime.astimezone(tz) - - def startEst(self): - tz = pytz.timezone('US/Eastern') - return self.open_datetime.astimezone(tz) - - def startFromNow(self): - return fmtHrsMins(self.start_from_now_sec()) - - def endFromNow(self): - return fmtHrsMins(self.end_from_now_sec()) - - def endFromNowFullMin(self): - return fmtDaysHrsMinsShort(self.end_from_now_sec()) - - def toGuerrillaStr(self): - return fmtTimeShort(self.startPst()) - - def toDateStr(self): - return self.server + "," + self.group + "," + fmtTime(self.startPst()) + "," + fmtTime( - self.startEst()) + "," + self.startFromNow() - - def groupShortName(self): - return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') - - def groupLongName(self): - return self.group.upper() - - def toPartialEvent(self, pe): - group = self.groupShortName() - if self.is_started(): - return group + " " + self.endFromNow() + " " + self.name_and_modifier - else: - return group + " " + fmtTimeShort(self.startPst()) + " " + fmtTimeShort( - self.startEst()) + " " + self.startFromNow() + " " + self.name_and_modifier - - -class EventList: - def __init__(self, event_list): - self.event_list = event_list - - def withFunc(self, func, exclude=False): - if exclude: - return EventList(list(itertools.filterfalse(func, self.event_list))) - else: - return EventList(list(filter(func, self.event_list))) - - def withServer(self, server): - return self.withFunc(lambda e: e.server == normalizeServer(server)) - - def withType(self, event_type): - return self.withFunc(lambda e: e.event_type == event_type) - - def inType(self, event_types): - return self.withFunc(lambda e: e.event_type in event_types) - - def withDungeonType(self, dungeon_type, exclude=False): - return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) - - def withNameContains(self, name, exclude=False): - return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) - - def excludeUnwantedEvents(self): - return self.withFunc(isEventWanted) - - def items(self): - return self.event_list - - def startedOnly(self): - return self.withFunc(lambda e: e.is_started()) - - def pendingOnly(self): - return self.withFunc(lambda e: e.is_pending()) - - def activeOnly(self): - return self.withFunc(lambda e: e.is_active()) - - def availableOnly(self): - return self.withFunc(lambda e: e.is_available()) - - def itemsByOpenTime(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) - - def itemsByCloseTime(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) - - -# TIME_FMT = """%a %b %d %H:%M:%S %Y""" - - -class EventType(Enum): - Week = 0 - Special = 1 - SpecialWeek = 2 - Guerrilla = 3 - GuerrillaNew = 4 - Etc = -100 - - -class DungeonType(Enum): - Unknown = -1 - Normal = 0 - CoinDailyOther = 1 - Technical = 2 - Etc = 3 - Unknown4 = 4 - Unknown7 = 7 - Unknown9 = 9 - - -def fmtTime(dt): - return dt.strftime("%Y-%m-%d %H:%M") - - -def fmtTimeShort(dt): - return dt.strftime("%H:%M") - - -def fmtHrsMins(seconds): - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - return '{:2}h {:2}m'.format(int(hours), int(minutes)) - - -def fmtDaysHrsMinsShort(sec): - days, sec = divmod(sec, 86400) - hours, sec = divmod(sec, 3600) - minutes, sec = divmod(sec, 60) - - if days > 0: - return '{:2}d {:2}h'.format(int(days), int(hours)) - elif hours > 0: - return '{:2}h {:2}m'.format(int(hours), int(minutes)) - else: - return '{:2}m'.format(int(minutes)) - - -def normalizeServer(server): - server = server.upper() - return 'NA' if server == 'US' else server - - -def isEventWanted(event): - name = event.name_and_modifier.lower() - if 'castle of satan' in name: - # eliminate things like : TAMADRA Invades in [Castle of Satan][Castle of Satan in the Abyss] - return False - - return True - - -def cleanDungeonNames(name): - # TODO: Make this info internally stored - if 'tamadra invades in some tech' in name.lower(): - return 'Latents invades some Techs & 20x +Eggs' - if '1.5x Bonus Pal Point in multiplay' in name: - name = '[Descends] 1.5x Pal Points in multiplay' - name = name.replace('No Continues', 'No Cont') - name = name.replace('No Continue', 'No Cont') - name = name.replace('Some Limited Time Dungeons', 'Some Guerrillas') - name = name.replace('are added in', 'in') - name = name.replace('!', '') - name = name.replace('Dragon Infestation', 'Dragons') - name = name.replace(' Infestation', 's') - name = name.replace('Daily Descended Dungeon', 'Daily Descends') - name = name.replace('Chance for ', '') - name = name.replace('Jewel of the Spirit', 'Spirit Jewel') - name = name.replace(' & ', '/') - name = name.replace(' / ', '/') - name = name.replace('PAD Radar', 'PADR') - name = name.replace('in normal dungeons', 'in normals') - name = name.replace('Selected ', 'Some ') - name = name.replace('Enhanced ', 'Enh ') - name = name.replace('All Att. Req.', 'All Att.') - name = name.replace('Extreme King Metal Dragon', 'Extreme KMD') - name = name.replace('Golden Mound-Tricolor [Fr/Wt/Wd Only]', 'Golden Mound') - name = name.replace('Gods-Awakening Materials Descended', "Awoken Mats") - name = name.replace('Orb move time 4 sec', '4s move time') - name = name.replace('Awakening Materials Descended', 'Awkn Mats') - name = name.replace('Awakening Materials', 'Awkn Mats') - name = name.replace("Star Treasure Thieves' Den", 'STTD') - name = name.replace('Ruins of the Star Vault', 'Star Vault') - name = name.replace('-★6 or lower Enhanced', '') - - return name +import traceback +import asyncio +import re +import datetime +import pytz +from collections import defaultdict +from datetime import timedelta +from enum import Enum + +import prettytable +import discord +from redbot.core import checks +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import inline, box, pagify + +from dadguide import dadguide +from rpadutils import CogSettings + +SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] + + +class PadEvents(commands.Cog): + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + self.settings = PadEventSettings("padevents") + + # Load event data + self.events = list() + self.started_events = set() + + self.fake_uid = -999 + + def cog_unload(self): + # Manually nulling out database because the GC for cogs seems to be pretty shitty + self.events = list() + self.started_events = set() + + async def reload_padevents(self): + await self.bot.wait_until_ready() + while self == self.bot.get_cog('PadEvents'): + try: + await self.refresh_data() + print('Done refreshing PadEvents') + except Exception as ex: + print("reload padevents loop caught exception " + str(ex)) + traceback.print_exc() + + await asyncio.sleep(60 * 60 * 1) + + async def refresh_data(self): + dg_cog = self.bot.get_cog('Dadguide') + await dg_cog.wait_until_ready() + scheduled_events = dg_cog.database.get_all_events() + + new_events = [] + for se in scheduled_events: + try: + new_events.append(Event(se)) + except Exception as ex: + print(ex) + + self.events = new_events + self.started_events = {ev.key for ev in new_events if ev.is_started()} + + async def check_started(self): + await self.bot.wait_until_ready() + while self == self.bot.get_cog('PadEvents'): + try: + events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) + + daily_refresh_servers = set() + for e in events: + self.started_events.add(e.key) + if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: + for gr in list(self.settings.listGuerrillaReg()): + if e.server == gr['server']: + try: + message = box("Server " + e.server + ", group " + + e.groupLongName() + " : " + e.name_and_modifier) + channel = self.bot.get_channel(int(gr['channel_id'])) + + try: + role_name = '{}_group_{}'.format( + e.server, e.groupLongName()) + role = get_role(channel.guild.roles, role_name) + if role and role.mentionable: + message = "{} `: {} is starting`".format( + role.mention, e.name_and_modifier) + except: + pass # do nothing if role is missing + + await channel.send(message) + except Exception as ex: + # deregister gr + traceback.print_exc() + # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) + print("caught exception while sending guerrilla msg:\n" + str(ex)) + + else: + if not e.dungeon_type in [DungeonType.Normal]: + daily_refresh_servers.add(e.server) + + for server in daily_refresh_servers: + msg = self.makeActiveText(server) + for daily_registration in list(self.settings.listDailyReg()): + try: + if server == daily_registration['server']: + await self.pageOutput(ctx, msg, channel_id=daily_registration['channel_id']) + except Exception as ex: + traceback.print_exc() + # self.settings.removeDailyReg( + # daily_registration['channel_id'], daily_registration['server']) + print("caught exception while sending daily msg:\n" + str(ex)) + + except Exception as ex: + traceback.print_exc() + print("caught exception while checking guerrillas:\n" + str(ex)) + + await asyncio.sleep(10) + print("done check_started") + + @commands.group(aliases=['pde']) + @checks.mod_or_permissions(manage_guild=True) + async def padevents(self, ctx): + """PAD event tracking""" + + @padevents.command() + @checks.is_owner() + async def testevent(self, ctx, server, seconds: int = 0): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + dg_cog = self.bot.get_cog('Dadguide') + await dg_cog.wait_until_ready() + te = dadguide.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) + + te.server = server + te.event_type_id = EventType.Guerrilla.value + te.event_seq = 0 + fuid = self.fake_uid = self.fake_uid - 1 + te.key = lambda: fuid + te.group_name = 'Yellow' + + te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) + te.close_datetime = te.open_datetime + timedelta(minutes=1) + te.dungeon_name = 'fake_dungeon_name' + te.event_modifier = 'fake_event_modifier' + self.events.append(Event(te)) + + await ctx.send("Fake event injected.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def addchannel(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + channel_id = ctx.channel.id + if self.settings.checkGuerrillaReg(channel_id, server): + await ctx.send("Channel already active.") + return + + self.settings.addGuerrillaReg(channel_id, server) + await ctx.send("Channel now active.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def rmchannel(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + return + + channel_id = ctx.channel.id + if not self.settings.checkGuerrillaReg(channel_id, server): + await ctx.send("Channel is not active.") + return + + self.settings.removeGuerrillaReg(channel_id, server) + await ctx.send("Channel deactivated.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def addchanneldaily(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + return + + channel_id = ctx.channel.id + if self.settings.checkDailyReg(channel_id, server): + await ctx.send("Channel already active.") + return + + self.settings.addDailyReg(channel_id, server) + await ctx.send("Channel now active.") + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def rmchanneldaily(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + return + + channel_id = ctx.channel.id + if not self.settings.checkDailyReg(channel_id, server): + await ctx.send("Channel is not active.") + return + + self.settings.removeDailyReg(channel_id, server) + await ctx.send("Channel deactivated.") + + @padevents.command(name="listchannels") + @checks.mod_or_permissions(manage_guild=True) + async def _listchannel(self, ctx): + msg = 'Following daily channels are registered:\n' + msg += self.makeChannelList(self.settings.listDailyReg()) + msg += "\n" + msg += 'Following guerilla channels are registered:\n' + msg += self.makeChannelList(self.settings.listGuerrillaReg()) + await self.pageOutput(ctx, msg) + + def makeChannelList(self, reg_list): + msg = "" + for cr in reg_list: + reg_channel_id = cr['channel_id'] + channel = self.bot.get_channel(int(reg_channel_id)) + channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' + server_name = channel.guild.name if channel else 'Unknown server' + msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' + return msg + + @padevents.command() + @checks.mod_or_permissions(manage_guild=True) + async def active(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + msg = self.makeActiveText(server) + await self.pageOutput(ctx, msg) + + def makeActiveText(self, server): + server_events = EventList(self.events).withServer(server) + active_events = server_events.activeOnly() + pending_events = server_events.pendingOnly() + available_events = server_events.availableOnly() + + msg = "Listing all events for " + server + + special_events = active_events.withType( + EventType.Special).itemsByCloseTime() + if len(special_events) > 0: + msg += "\n\n" + self.makeActiveOutput('Special Events', special_events) + + all_etc_events = active_events.withType(EventType.Etc) + + etc_events = all_etc_events.withDungeonType( + DungeonType.Etc).excludeUnwantedEvents().itemsByCloseTime() + if len(etc_events) > 0: + msg += "\n\n" + self.makeActiveOutput('Etc Events', etc_events) + + # Old-style guerrillas + active_guerrilla_events = active_events.withType(EventType.Guerrilla).items() + if len(active_guerrilla_events) > 0: + msg += "\n\n" + \ + self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + + guerrilla_events = pending_events.withType(EventType.Guerrilla).items() + if len(guerrilla_events) > 0: + msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', guerrilla_events) + + # New-style guerrillas + active_guerrilla_events = active_events.withType(EventType.SpecialWeek).items() + if len(active_guerrilla_events) > 0: + msg += "\n\n" + \ + self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + + guerrilla_events = pending_events.withType(EventType.SpecialWeek).items() + if len(guerrilla_events) > 0: + msg += "\n\n" + \ + self.makeFullGuerrillaOutput( + 'Guerrilla Events', guerrilla_events, starter_guerilla=True) + + # clean up long headers + msg = msg.replace('-------------------------------------', '-----------------------') + + return msg + + async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): + msg = msg.strip() + msg = pagify(msg, ["\n"], shorten_by=20) + for page in msg: + try: + if channel_id is None: + await ctx.send(format_type(page)) + else: + await self.bot.get_channel(int(channel_id)).send(format_type(page)) + except Exception as e: + print("page output failed " + str(e)) + print("tried to print: " + page) + + def makeActiveOutput(self, table_name, event_list): + tbl = prettytable.PrettyTable(["Time", table_name]) + tbl.hrules = prettytable.HEADER + tbl.vrules = prettytable.NONE + tbl.align[table_name] = "l" + tbl.align["Time"] = "r" + for e in event_list: + tbl.add_row([e.endFromNowFullMin().strip(), e.name_and_modifier]) + return tbl.get_string() + + def makeActiveGuerrillaOutput(self, table_name, event_list): + tbl = prettytable.PrettyTable([table_name, "Group", "Time"]) + tbl.hrules = prettytable.HEADER + tbl.vrules = prettytable.NONE + tbl.align[table_name] = "l" + tbl.align["Time"] = "r" + for e in event_list: + tbl.add_row([e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) + return tbl.get_string() + + def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False): + events_by_name = defaultdict(list) + for e in event_list: + events_by_name[e.name_and_modifier].append(e) + + rows = list() + grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] + for name, events in events_by_name.items(): + events = sorted(events, key=lambda e: e.open_datetime) + events_by_group = defaultdict(list) + for e in events: + events_by_group[e.group].append(e) + + done = False + while not done: + did_work = False + row = list() + row.append(name) + for g in grps: + grp_list = events_by_group[g] + if len(grp_list) == 0: + row.append("") + else: + did_work = True + e = grp_list.pop(0) + row.append(e.toGuerrillaStr()) + if did_work: + rows.append(row) + else: + done = True + + col1 = "Pending" + tbl = prettytable.PrettyTable([col1] + grps) + tbl.align[col1] = "l" + tbl.hrules = prettytable.HEADER + tbl.vrules = prettytable.ALL + + for r in rows: + tbl.add_row(r) + + header = "Times are PT below\n\n" + return header + tbl.get_string() + "\n" + + @commands.command(aliases=['events']) + async def eventsna(self, ctx): + """Print upcoming daily events for NA.""" + await self.doPartial(ctx, 'NA') + + @commands.command() + async def eventsjp(self, ctx): + """Print upcoming daily events for JP.""" + await self.doPartial(ctx, 'JP') + + @commands.command() + async def eventskr(self, ctx): + """Print upcoming daily events for KR.""" + await self.doPartial(ctx, 'KR') + + async def doPartial(self, ctx, server): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + + events = EventList(self.events) + events = events.withServer(server) + events = events.inType([EventType.Guerrilla, EventType.SpecialWeek]) + + active_events = events.activeOnly().itemsByOpenTime(reverse=True) + pending_events = events.pendingOnly().itemsByOpenTime(reverse=True) + + group_to_active_event = {e.group: e for e in active_events} + group_to_pending_event = {e.group: e for e in pending_events} + + active_events = list(group_to_active_event.values()) + pending_events = list(group_to_pending_event.values()) + + active_events.sort(key=lambda e: e.group, reverse=True) + pending_events.sort(key=lambda e: e.group, reverse=True) + + if len(active_events) == 0 and len(pending_events) == 0: + await ctx.send("No events available for " + server) + return + + output = "Events for {}".format(server) + + if len(active_events) > 0: + output += "\n\n" + "G Remaining Dungeon" + for e in active_events: + output += "\n" + e.toPartialEvent(self) + + if len(pending_events) > 0: + output += "\n\n" + "G PT ET ETA Dungeon" + for e in pending_events: + output += "\n" + e.toPartialEvent(self) + + await ctx.send(box(output)) + + +def makeChannelReg(channel_id, server): + server = normalizeServer(server) + return { + "channel_id": channel_id, + "server": server + } + + +class PadEventSettings(CogSettings): + def make_default_settings(self): + config = { + 'guerrilla_regs': [], + 'daily_regs': [], + } + return config + + def listGuerrillaReg(self): + return self.bot_settings['guerrilla_regs'] + + def addGuerrillaReg(self, channel_id, server): + self.listGuerrillaReg().append(makeChannelReg(channel_id, server)) + self.save_settings() + + def checkGuerrillaReg(self, channel_id, server): + return makeChannelReg(channel_id, server) in self.listGuerrillaReg() + + def removeGuerrillaReg(self, channel_id, server): + if self.checkGuerrillaReg(channel_id, server): + self.listGuerrillaReg().remove(makeChannelReg(channel_id, server)) + self.save_settings() + + def listDailyReg(self): + return self.bot_settings['daily_regs'] + + def addDailyReg(self, channel_id, server): + self.listDailyReg().append(makeChannelReg(channel_id, server)) + self.save_settings() + + def checkDailyReg(self, channel_id, server): + return makeChannelReg(channel_id, server) in self.listDailyReg() + + def removeDailyReg(self, channel_id, server): + if self.checkDailyReg(channel_id, server): + self.listDailyReg().remove(makeChannelReg(channel_id, server)) + self.save_settings() + + +class Event: + def __init__(self, scheduled_event: dadguide.DgScheduledEvent): + self.key = scheduled_event.key() + self.server = SUPPORTED_SERVERS[scheduled_event.server_id] + self.open_datetime = scheduled_event.open_datetime + self.close_datetime = scheduled_event.close_datetime + self.group = scheduled_event.group_name + self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' + self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' + + self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) + self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') + + self.name_and_modifier = self.clean_dungeon_name + if self.clean_event_name != '': + self.name_and_modifier += ', ' + self.clean_event_name + + self.event_type = EventType(scheduled_event.event_type_id) + self.dungeon_type = DungeonType( + scheduled_event.dungeon.dungeon_type) if scheduled_event.dungeon else DungeonType.Unknown + + def start_from_now_sec(self): + now = datetime.datetime.now(pytz.utc) + return (self.open_datetime - now).total_seconds() + + def end_from_now_sec(self): + now = datetime.datetime.now(pytz.utc) + return (self.close_datetime - now).total_seconds() + + def is_started(self): + """True if past the open time for the event.""" + return self.start_from_now_sec() <= 0 + + def is_finished(self): + """True if past the close time for the event.""" + return self.end_from_now_sec() <= 0 + + def is_active(self): + """True if between open and close time for the event.""" + return self.is_started() and not self.is_finished() + + def is_pending(self): + """True if event has not started.""" + return not self.is_started() + + def is_available(self): + """True if event has not finished.""" + return not self.is_finished() + + def tostr(self): + return fmtTime(self.open_datetime) + "," + fmtTime( + self.close_datetime) + "," + self.group + "," + self.dungeon_code + "," + self.event_type + "," + self.event_seq + + def startPst(self): + tz = pytz.timezone('US/Pacific') + return self.open_datetime.astimezone(tz) + + def startEst(self): + tz = pytz.timezone('US/Eastern') + return self.open_datetime.astimezone(tz) + + def startFromNow(self): + return fmtHrsMins(self.start_from_now_sec()) + + def endFromNow(self): + return fmtHrsMins(self.end_from_now_sec()) + + def endFromNowFullMin(self): + return fmtDaysHrsMinsShort(self.end_from_now_sec()) + + def toGuerrillaStr(self): + return fmtTimeShort(self.startPst()) + + def toDateStr(self): + return self.server + "," + self.group + "," + fmtTime(self.startPst()) + "," + fmtTime( + self.startEst()) + "," + self.startFromNow() + + def groupShortName(self): + return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') + + def groupLongName(self): + return self.group.upper() + + def toPartialEvent(self, pe): + group = self.groupShortName() + if self.is_started(): + return group + " " + self.endFromNow() + " " + self.name_and_modifier + else: + return group + " " + fmtTimeShort(self.startPst()) + " " + fmtTimeShort( + self.startEst()) + " " + self.startFromNow() + " " + self.name_and_modifier + + +class EventList: + def __init__(self, event_list): + self.event_list = event_list + + def withFunc(self, func, exclude=False): + if exclude: + return EventList(list(itertools.filterfalse(func, self.event_list))) + else: + return EventList(list(filter(func, self.event_list))) + + def withServer(self, server): + return self.withFunc(lambda e: e.server == normalizeServer(server)) + + def withType(self, event_type): + return self.withFunc(lambda e: e.event_type == event_type) + + def inType(self, event_types): + return self.withFunc(lambda e: e.event_type in event_types) + + def withDungeonType(self, dungeon_type, exclude=False): + return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) + + def withNameContains(self, name, exclude=False): + return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) + + def excludeUnwantedEvents(self): + return self.withFunc(isEventWanted) + + def items(self): + return self.event_list + + def startedOnly(self): + return self.withFunc(lambda e: e.is_started()) + + def pendingOnly(self): + return self.withFunc(lambda e: e.is_pending()) + + def activeOnly(self): + return self.withFunc(lambda e: e.is_active()) + + def availableOnly(self): + return self.withFunc(lambda e: e.is_available()) + + def itemsByOpenTime(self, reverse=False): + return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) + + def itemsByCloseTime(self, reverse=False): + return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) + + +# TIME_FMT = """%a %b %d %H:%M:%S %Y""" + + +class EventType(Enum): + Week = 0 + Special = 1 + SpecialWeek = 2 + Guerrilla = 3 + GuerrillaNew = 4 + Etc = -100 + + +class DungeonType(Enum): + Unknown = -1 + Normal = 0 + CoinDailyOther = 1 + Technical = 2 + Etc = 3 + Unknown4 = 4 + Unknown7 = 7 + Unknown9 = 9 + + +def fmtTime(dt): + return dt.strftime("%Y-%m-%d %H:%M") + + +def fmtTimeShort(dt): + return dt.strftime("%H:%M") + + +def fmtHrsMins(seconds): + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return '{:2}h {:2}m'.format(int(hours), int(minutes)) + + +def fmtDaysHrsMinsShort(sec): + days, sec = divmod(sec, 86400) + hours, sec = divmod(sec, 3600) + minutes, sec = divmod(sec, 60) + + if days > 0: + return '{:2}d {:2}h'.format(int(days), int(hours)) + elif hours > 0: + return '{:2}h {:2}m'.format(int(hours), int(minutes)) + else: + return '{:2}m'.format(int(minutes)) + + +def normalizeServer(server): + server = server.upper() + return 'NA' if server == 'US' else server + + +def isEventWanted(event): + name = event.name_and_modifier.lower() + if 'castle of satan' in name: + # eliminate things like : TAMADRA Invades in [Castle of Satan][Castle of Satan in the Abyss] + return False + + return True + + +def cleanDungeonNames(name): + # TODO: Make this info internally stored + if 'tamadra invades in some tech' in name.lower(): + return 'Latents invades some Techs & 20x +Eggs' + if '1.5x Bonus Pal Point in multiplay' in name: + name = '[Descends] 1.5x Pal Points in multiplay' + name = name.replace('No Continues', 'No Cont') + name = name.replace('No Continue', 'No Cont') + name = name.replace('Some Limited Time Dungeons', 'Some Guerrillas') + name = name.replace('are added in', 'in') + name = name.replace('!', '') + name = name.replace('Dragon Infestation', 'Dragons') + name = name.replace(' Infestation', 's') + name = name.replace('Daily Descended Dungeon', 'Daily Descends') + name = name.replace('Chance for ', '') + name = name.replace('Jewel of the Spirit', 'Spirit Jewel') + name = name.replace(' & ', '/') + name = name.replace(' / ', '/') + name = name.replace('PAD Radar', 'PADR') + name = name.replace('in normal dungeons', 'in normals') + name = name.replace('Selected ', 'Some ') + name = name.replace('Enhanced ', 'Enh ') + name = name.replace('All Att. Req.', 'All Att.') + name = name.replace('Extreme King Metal Dragon', 'Extreme KMD') + name = name.replace('Golden Mound-Tricolor [Fr/Wt/Wd Only]', 'Golden Mound') + name = name.replace('Gods-Awakening Materials Descended', "Awoken Mats") + name = name.replace('Orb move time 4 sec', '4s move time') + name = name.replace('Awakening Materials Descended', 'Awkn Mats') + name = name.replace('Awakening Materials', 'Awkn Mats') + name = name.replace("Star Treasure Thieves' Den", 'STTD') + name = name.replace('Ruins of the Star Vault', 'Star Vault') + name = name.replace('-★6 or lower Enhanced', '') + + return name diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 01bff9b1..04b2ea1f 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -1,314 +1,314 @@ -import asyncio -import csv -import io -import json - -import discord -import pymysql -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box, pagify - -from rpadutils import CogSettings, rpadutils - -PADGUIDEDB_COG = None - - -def is_padguidedb_admin_check(ctx): - is_owner = ctx.author.id in PADGUIDEDB_COG.bot.owner_ids - return is_owner or PADGUIDEDB_COG.settings.checkAdmin(ctx.author.id) - - -def is_padguidedb_admin(): - return commands.check(is_padguidedb_admin_check) - - -class PadGuideDb(commands.Cog): - """PadGuide Database manipulator""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = PadGuideDbSettings("padguidedb") - - self.queue_size = 0 - self.full_etl_lock = asyncio.Lock() - self.extract_images_lock = asyncio.Lock() - self.dungeon_load_lock = asyncio.Lock() - global PADGUIDEDB_COG - PADGUIDEDB_COG = self - - def get_connection(self): - with open(self.settings.configFile(), 'r') as db_config: - return self.connect(json.load(db_config)) - - def connect(self, db_config): - return pymysql.connect(host=db_config['host'], - user=db_config['user'], - password=db_config['password'], - db=db_config['db'], - charset=db_config['charset'], - cursorclass=pymysql.cursors.DictCursor, - autocommit=True) - - @commands.group(aliases=['pdb']) - @is_padguidedb_admin() - async def padguidedb(self, context): - """PadGuide database manipulation.""" - - @padguidedb.command() - @checks.is_owner() - async def addadmin(self, ctx, user: discord.Member): - """Adds a user to the padguide db admin""" - self.settings.addAdmin(user.id) - await ctx.send("done") - - @padguidedb.command() - @checks.is_owner() - async def rmadmin(self, ctx, user: discord.Member): - """Removes a user from the padguide db admin""" - self.settings.rmAdmin(user.id) - await ctx.send("done") - - @padguidedb.command() - @checks.is_owner() - async def setconfigfile(self, ctx, *, config_file): - """Set the database config file.""" - self.settings.setConfigFile(config_file) - await ctx.send(inline('Done')) - - @padguidedb.command() - @is_padguidedb_admin() - async def searchdungeon(self, ctx, *, search_text): - """Search""" - search_text = '%{}%'.format(search_text) - with self.get_connection() as cursor: - sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' - ' where lower(name_na) like %s or lower(name_jp) like %s' - ' order by dungeon_id desc limit 20') - cursor.execute(sql, [search_text, search_text]) - results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) - await ctx.send(inline(sql)) - for page in pagify(msg): - await ctx.send(box(page)) - - @padguidedb.command() - @checks.is_owner() - async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): - """Set the dungeon script.""" - self.settings.setDungeonScriptFile(dungeon_script_file) - await ctx.send(inline('Done')) - - @padguidedb.command() - @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): - """Set the dungeon script.""" - self.settings.setUserInfo(server, user_uuid, user_intid) - await ctx.send(inline('Done')) - - @padguidedb.command() - @is_padguidedb_admin() - async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): - if queues > 5: - await ctx.send("You must send less than 5 queues.") - return - elif self.queue_size + queues > 60: - await ctx.send("The size of the queue cannot exceed 60. It is currently {}.".format(self.queue_size)) - return - elif not self.settings.hasUserInfo(server): - await ctx.send("There is no account associated with server '{}'.".format(server.upper())) - return - - self.queue_size += queues - if queues == 1: - await ctx.send(inline('Queueing load in slot {}'.format(self.queue_size))) - else: - await ctx.send( - inline('Queueing loads in slots {}-{}'.format(self.queue_size - queues + 1, self.queue_size))) - - event_loop = asyncio.get_event_loop() - for queue in range(queues): - event_loop.create_task(self.do_dungeon_load(ctx, server.upper(), dungeon_id, dungeon_floor_id)) - - async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): - async with self.dungeon_load_lock: - process = await asyncio.create_subprocess_exec( - '/usr/bin/python3', - self.settings.dungeonScriptFile(), - '--db_config={}'.format(self.settings.configFile()), - '--server={}'.format(server), - '--dungeon_id={}'.format(dungeon_id), - '--floor_id={}'.format(dungeon_floor_id), - '--user_uuid={}'.format(self.settings.userUuidFor(server)), - '--user_intid={}'.format(self.settings.userIntidFor(server)), - - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - - if stderr: - print("Dungeon Load Error:\n" + stderr.decode()) - await rpadutils.doubleup(ctx, inline( - 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) - else: - await rpadutils.doubleup(ctx, inline( - 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) - self.queue_size -= 1 - - @padguidedb.command() - @is_padguidedb_admin() - async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): - with self.get_connection() as cursor: - sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" - " FROM wave_data" - " WHERE dungeon_id = {} AND floor_id = {}" - " GROUP BY 1, 2" - " ORDER BY 1, 2").format(int(dungeon_id), int(dungeon_floor_id)) - cursor.execute(sql) - results = list(cursor.fetchall()) - msg = 'stage,drop_monster_id,count' - for row in results: - msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], row['count']) - for page in pagify(msg): - await ctx.send(box(page)) - - @padguidedb.command() - @checks.is_owner() - async def cleardungeon(self, ctx, dungeon_id: int): - with self.get_connection() as cursor: - sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) - cursor.execute(sql) - await ctx.send(inline("Done")) - - # @padguidedb.command() - @is_padguidedb_admin() - async def dungeondata(self, ctx, dungeon_id: int): - with ctx.typing(), self.get_connection() as cursor: - sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) - cursor.execute(sql) - results = list(cursor.fetchall()) - order = sorted(results[0]) - rows = [list(map(lambda x: row[x], order)) for row in results] - fauxfile = io.StringIO() - writer = csv.writer(fauxfile) - writer.writerow(order) - writer.writerows(rows) - fauxfile.seek(0) - m = await ctx.send(file=discord.File(fauxfile, filename="dungeondata.csv")) - await asyncio.sleep(10) - await m.delete() - - @padguidedb.group() - @is_padguidedb_admin() - async def pipeline(self, context): - """PadGuide pipeline utilities.""" - - @pipeline.command() - @is_padguidedb_admin() - async def fulletl(self, ctx): - """Runs a job which downloads pad data, and updates the padguide database.""" - if self.full_etl_lock.locked(): - await ctx.send(inline('Full ETL already running')) - return - - async with self.full_etl_lock: - await ctx.send(inline('Running full ETL pipeline: this could take a while')) - process = await asyncio.create_subprocess_exec( - 'bash', - '/home/tactical0retreat/dadguide/dadguide-jobs/run_loader.sh', - - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - - if stderr: - print("Full ETL Error:\n" + stderr.decode()) - await ctx.send(inline('Full ETL failed')) - return - await ctx.send(inline('Full ETL finished')) - - @pipeline.command() - @is_padguidedb_admin() - async def extractimages(self, ctx): - """Runs a job which downloads image updates, generates full images, and portraits.""" - if self.extract_images_lock.locked(): - await ctx.send(inline('Extract images already running')) - return - - async with self.extract_images_lock: - await ctx.send(inline('Running image extract pipeline: this could take a while')) - process = await asyncio.create_subprocess_exec( - 'bash', - '/home/tactical0retreat/dadguide/dadguide-jobs/media/update_image_files.sh', - - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - - if stderr: - print("Image Extract Error:\n" + stderr.decode()) - await ctx.send(inline('Image extract failed')) - return - await ctx.send(inline('Image extract finished')) - - -class PadGuideDbSettings(CogSettings): - def make_default_settings(self): - config = { - 'admins': [], - 'config_file': '', - 'dungeon_script_file': '', - 'users': {} - } - return config - - def admins(self): - return self.bot_settings['admins'] - - def checkAdmin(self, user_id): - admins = self.admins() - return user_id in admins - - def addAdmin(self, user_id): - admins = self.admins() - if user_id not in admins: - admins.append(user_id) - self.save_settings() - - def rmAdmin(self, user_id): - admins = self.admins() - if user_id in admins: - admins.remove(user_id) - self.save_settings() - - def configFile(self): - return self.bot_settings.get('config_file', '') - - def setConfigFile(self, config_file): - self.bot_settings['config_file'] = config_file - self.save_settings() - - def dungeonScriptFile(self): - return self.bot_settings.get('dungeon_script_file', '') - - def setDungeonScriptFile(self, dungeon_script_file): - self.bot_settings['dungeon_script_file'] = dungeon_script_file - self.save_settings() - - def setUserInfo(self, server, user_uuid, user_intid): - self.bot_settings['users'][server.upper()] = [user_uuid, user_intid] - self.save_settings() - - def hasUserInfo(self, server): - return server.upper() in self.bot_settings['users'] - - def userUuidFor(self, server): - return self.bot_settings['users'][server.upper()][0] - - def userIntidFor(self, server): - return self.bot_settings['users'][server.upper()][1] +import asyncio +import csv +import io +import json + +import discord +import pymysql +from redbot.core import checks +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import inline, box, pagify + +from rpadutils import CogSettings, rpadutils + +PADGUIDEDB_COG = None + + +def is_padguidedb_admin_check(ctx): + is_owner = ctx.author.id in PADGUIDEDB_COG.bot.owner_ids + return is_owner or PADGUIDEDB_COG.settings.checkAdmin(ctx.author.id) + + +def is_padguidedb_admin(): + return commands.check(is_padguidedb_admin_check) + + +class PadGuideDb(commands.Cog): + """PadGuide Database manipulator""" + + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.settings = PadGuideDbSettings("padguidedb") + + self.queue_size = 0 + self.full_etl_lock = asyncio.Lock() + self.extract_images_lock = asyncio.Lock() + self.dungeon_load_lock = asyncio.Lock() + global PADGUIDEDB_COG + PADGUIDEDB_COG = self + + def get_connection(self): + with open(self.settings.configFile(), 'r') as db_config: + return self.connect(json.load(db_config)) + + def connect(self, db_config): + return pymysql.connect(host=db_config['host'], + user=db_config['user'], + password=db_config['password'], + db=db_config['db'], + charset=db_config['charset'], + cursorclass=pymysql.cursors.DictCursor, + autocommit=True) + + @commands.group(aliases=['pdb']) + @is_padguidedb_admin() + async def padguidedb(self, context): + """PadGuide database manipulation.""" + + @padguidedb.command() + @checks.is_owner() + async def addadmin(self, ctx, user: discord.Member): + """Adds a user to the padguide db admin""" + self.settings.addAdmin(user.id) + await ctx.send("done") + + @padguidedb.command() + @checks.is_owner() + async def rmadmin(self, ctx, user: discord.Member): + """Removes a user from the padguide db admin""" + self.settings.rmAdmin(user.id) + await ctx.send("done") + + @padguidedb.command() + @checks.is_owner() + async def setconfigfile(self, ctx, *, config_file): + """Set the database config file.""" + self.settings.setConfigFile(config_file) + await ctx.send(inline('Done')) + + @padguidedb.command() + @is_padguidedb_admin() + async def searchdungeon(self, ctx, *, search_text): + """Search""" + search_text = '%{}%'.format(search_text) + with self.get_connection() as cursor: + sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' + ' where lower(name_na) like %s or lower(name_jp) like %s' + ' order by dungeon_id desc limit 20') + cursor.execute(sql, [search_text, search_text]) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(sql)) + for page in pagify(msg): + await ctx.send(box(page)) + + @padguidedb.command() + @checks.is_owner() + async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): + """Set the dungeon script.""" + self.settings.setDungeonScriptFile(dungeon_script_file) + await ctx.send(inline('Done')) + + @padguidedb.command() + @checks.is_owner() + async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): + """Set the dungeon script.""" + self.settings.setUserInfo(server, user_uuid, user_intid) + await ctx.send(inline('Done')) + + @padguidedb.command() + @is_padguidedb_admin() + async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): + if queues > 5: + await ctx.send("You must send less than 5 queues.") + return + elif self.queue_size + queues > 60: + await ctx.send("The size of the queue cannot exceed 60. It is currently {}.".format(self.queue_size)) + return + elif not self.settings.hasUserInfo(server): + await ctx.send("There is no account associated with server '{}'.".format(server.upper())) + return + + self.queue_size += queues + if queues == 1: + await ctx.send(inline('Queueing load in slot {}'.format(self.queue_size))) + else: + await ctx.send( + inline('Queueing loads in slots {}-{}'.format(self.queue_size - queues + 1, self.queue_size))) + + event_loop = asyncio.get_event_loop() + for queue in range(queues): + event_loop.create_task(self.do_dungeon_load(ctx, server.upper(), dungeon_id, dungeon_floor_id)) + + async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): + async with self.dungeon_load_lock: + process = await asyncio.create_subprocess_exec( + '/usr/bin/python3', + self.settings.dungeonScriptFile(), + '--db_config={}'.format(self.settings.configFile()), + '--server={}'.format(server), + '--dungeon_id={}'.format(dungeon_id), + '--floor_id={}'.format(dungeon_floor_id), + '--user_uuid={}'.format(self.settings.userUuidFor(server)), + '--user_intid={}'.format(self.settings.userIntidFor(server)), + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + if stderr: + print("Dungeon Load Error:\n" + stderr.decode()) + await rpadutils.doubleup(ctx, inline( + 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) + else: + await rpadutils.doubleup(ctx, inline( + 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) + self.queue_size -= 1 + + @padguidedb.command() + @is_padguidedb_admin() + async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): + with self.get_connection() as cursor: + sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" + " FROM wave_data" + " WHERE dungeon_id = {} AND floor_id = {}" + " GROUP BY 1, 2" + " ORDER BY 1, 2").format(int(dungeon_id), int(dungeon_floor_id)) + cursor.execute(sql) + results = list(cursor.fetchall()) + msg = 'stage,drop_monster_id,count' + for row in results: + msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], row['count']) + for page in pagify(msg): + await ctx.send(box(page)) + + @padguidedb.command() + @checks.is_owner() + async def cleardungeon(self, ctx, dungeon_id: int): + with self.get_connection() as cursor: + sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) + cursor.execute(sql) + await ctx.send(inline("Done")) + + # @padguidedb.command() + @is_padguidedb_admin() + async def dungeondata(self, ctx, dungeon_id: int): + with ctx.typing(), self.get_connection() as cursor: + sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) + cursor.execute(sql) + results = list(cursor.fetchall()) + order = sorted(results[0]) + rows = [list(map(lambda x: row[x], order)) for row in results] + fauxfile = io.StringIO() + writer = csv.writer(fauxfile) + writer.writerow(order) + writer.writerows(rows) + fauxfile.seek(0) + m = await ctx.send(file=discord.File(fauxfile, filename="dungeondata.csv")) + await asyncio.sleep(10) + await m.delete() + + @padguidedb.group() + @is_padguidedb_admin() + async def pipeline(self, context): + """PadGuide pipeline utilities.""" + + @pipeline.command() + @is_padguidedb_admin() + async def fulletl(self, ctx): + """Runs a job which downloads pad data, and updates the padguide database.""" + if self.full_etl_lock.locked(): + await ctx.send(inline('Full ETL already running')) + return + + async with self.full_etl_lock: + await ctx.send(inline('Running full ETL pipeline: this could take a while')) + process = await asyncio.create_subprocess_exec( + 'bash', + '/home/tactical0retreat/dadguide/dadguide-jobs/run_loader.sh', + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + if stderr: + print("Full ETL Error:\n" + stderr.decode()) + await ctx.send(inline('Full ETL failed')) + return + await ctx.send(inline('Full ETL finished')) + + @pipeline.command() + @is_padguidedb_admin() + async def extractimages(self, ctx): + """Runs a job which downloads image updates, generates full images, and portraits.""" + if self.extract_images_lock.locked(): + await ctx.send(inline('Extract images already running')) + return + + async with self.extract_images_lock: + await ctx.send(inline('Running image extract pipeline: this could take a while')) + process = await asyncio.create_subprocess_exec( + 'bash', + '/home/tactical0retreat/dadguide/dadguide-jobs/media/update_image_files.sh', + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + if stderr: + print("Image Extract Error:\n" + stderr.decode()) + await ctx.send(inline('Image extract failed')) + return + await ctx.send(inline('Image extract finished')) + + +class PadGuideDbSettings(CogSettings): + def make_default_settings(self): + config = { + 'admins': [], + 'config_file': '', + 'dungeon_script_file': '', + 'users': {} + } + return config + + def admins(self): + return self.bot_settings['admins'] + + def checkAdmin(self, user_id): + admins = self.admins() + return user_id in admins + + def addAdmin(self, user_id): + admins = self.admins() + if user_id not in admins: + admins.append(user_id) + self.save_settings() + + def rmAdmin(self, user_id): + admins = self.admins() + if user_id in admins: + admins.remove(user_id) + self.save_settings() + + def configFile(self): + return self.bot_settings.get('config_file', '') + + def setConfigFile(self, config_file): + self.bot_settings['config_file'] = config_file + self.save_settings() + + def dungeonScriptFile(self): + return self.bot_settings.get('dungeon_script_file', '') + + def setDungeonScriptFile(self, dungeon_script_file): + self.bot_settings['dungeon_script_file'] = dungeon_script_file + self.save_settings() + + def setUserInfo(self, server, user_uuid, user_intid): + self.bot_settings['users'][server.upper()] = [user_uuid, user_intid] + self.save_settings() + + def hasUserInfo(self, server): + return server.upper() in self.bot_settings['users'] + + def userUuidFor(self, server): + return self.bot_settings['users'][server.upper()][0] + + def userIntidFor(self, server): + return self.bot_settings['users'][server.upper()][1] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1406089d..b7d2df23 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,1203 +1,1203 @@ -import asyncio -import json -import logging -import os -import traceback -import urllib.parse -from collections import OrderedDict -from enum import Enum - -import discord -import prettytable -from redbot.core import checks, data_manager -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box - -import rpadutils -from rpadutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics - -logger = logging.getLogger('red.miruv3.padinfo') - -HELP_MSG = """ -{0.prefix}helpid : shows this message -{0.prefix}id : look up a monster and print a link to puzzledragonx -{0.prefix}pic : Look up a monster and display its image inline - -Options for - : Find a monster by ID - {0.prefix}id 1234 (picks sun quan) - : Take the best guess for a monster, picks the most recent monster - {0.prefix}id kali (picks mega awoken d kali) - : Limit by element or awoken, e.g. - {0.prefix}id ares (selects the most recent, revo ares) - {0.prefix}id aares (explicitly selects awoken ares) - {0.prefix}id a ares (spaces work too) - {0.prefix}id rd ares (select a specific evo for ares, the red/dark one) - {0.prefix}id r/d ares (slashes, spaces work too) - -computed nickname list and overrides: https://docs.google.com/spreadsheets/d/1EyzMjvf8ZCQ4K-gJYnNkiZlCEsT9YYI9dUd-T5qCirc/pubhtml -submit an override suggestion: https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" - -EMBED_NOT_GENERATED = -1 - -INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' - -MEDIA_PATH = 'https://f002.backblazeb2.com/file/dadguide-data/media/' -RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png' -RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' -VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' -GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}cb.png' - -YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' -SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' -ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' - - -class ServerFilter(Enum): - any = 0 - na = 1 - jp = 2 - - -def get_pdx_url(m): - return INFO_PDX_TEMPLATE.format(rpadutils.get_pdx_id(m)) - - -def get_portrait_url(m): - return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) - - -def get_pic_url(m): - return RPAD_PIC_TEMPLATE.format(m.monster_id) - - -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) - - -class IdEmojiUpdater(EmojiUpdater): - def __init__(self, emoji_to_embed, m: "DgMonster" = None, - pad_info=None, selected_emoji=None, bot=None): - self.emoji_dict = emoji_to_embed - self.m = m - self.pad_info = pad_info - self.selected_emoji = selected_emoji - self.bot = bot - - def on_update(self, ctx, selected_emoji): - if self.pad_info.settings.checkEvoID(ctx.author.id): - DGCOG = self.bot.get_cog('Dadguide') - evos = list(map(lambda x: [x['from_id'], x['to_id']], - DGCOG.database.get_all_evolutions_by_monster(self.m.monster_id))) - evos = list(set(sum(evos, []))) - evos.sort() - index = evos.index(self.m.monster_id) - if selected_emoji == self.pad_info.previous_monster_emoji: - if index == 0: - self.m = DGCOG.get_monster_by_id(evos[-1]) - else: - self.m = DGCOG.get_monster_by_id(evos[index - 1]) - elif selected_emoji == self.pad_info.next_monster_emoji: - if index == len(evos) - 1: - self.m = DGCOG.get_monster_by_id(evos[0]) - else: - self.m = DGCOG.get_monster_by_id(evos[index + 1]) - else: - self.selected_emoji = selected_emoji - return True - else: - if selected_emoji == self.pad_info.previous_monster_emoji: - if self.m.prev_monster is None: - return False - self.m = self.m.prev_monster - elif selected_emoji == self.pad_info.next_monster_emoji: - if self.m.next_monster is None: - return False - self.m = self.m.next_monster - else: - self.selected_emoji = selected_emoji - return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m) - return True - - -class ScrollEmojiUpdater(EmojiUpdater): - def __init__(self, emoji_to_embed, m: "DgMonster" = None, - ms: "List[int]" = None, selected_emoji=None, pad_info=None, bot=None): - self.emoji_dict = emoji_to_embed - self.m = m - self.ms = ms - self.pad_info = pad_info - self.selected_emoji = selected_emoji - self.bot = bot - - def on_update(self, ctx, selected_emoji): - DGCOG = self.bot.get_cog('Dadguide') - index = self.ms.index(self.m.monster_id) - - if selected_emoji == self.pad_info.first_monster_emoji: - self.m = DGCOG.get_monster_by_id(self.ms[0]) - elif selected_emoji == self.pad_info.previous_monster_emoji: - self.m = DGCOG.get_monster_by_id(self.ms[index - 1]) - elif selected_emoji == self.pad_info.next_monster_emoji: - if index == len(self.ms) - 1: - self.m = DGCOG.get_monster_by_id(self.ms[0]) - else: - self.m = DGCOG.get_monster_by_id(self.ms[index + 1]) - elif selected_emoji == self.pad_info.last_monster_emoji: - self.m = DGCOG.get_monster_by_id(self.ms[-1]) - else: - self.selected_emoji = selected_emoji - return True - - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=True) - return True - - -class PadInfo(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - self.settings = PadInfoSettings("padinfo") - - self.index_all = None - self.index_na = None - self.index_jp = None - - self.menu = Menu(bot) - - # These emojis are the keys into the idmenu submenus - self.id_emoji = '\N{INFORMATION SOURCE}' - self.evo_emoji = char_to_emoji('e') - self.mats_emoji = char_to_emoji('m') - self.ls_emoji = '\N{INFORMATION SOURCE}' - self.left_emoji = char_to_emoji('l') - self.right_emoji = char_to_emoji('r') - self.pantheon_emoji = '\N{CLASSICAL BUILDING}' - self.skillups_emoji = '\N{MEAT ON BONE}' - self.pic_emoji = '\N{FRAME WITH PICTURE}' - self.other_info_emoji = '\N{SCROLL}' - self.first_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' - self.previous_monster_emoji = '\N{BLACK LEFT-POINTING TRIANGLE}' - self.next_monster_emoji = '\N{BLACK RIGHT-POINTING TRIANGLE}' - self.last_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' - self.remove_emoji = self.menu.emoji['no'] - - self.historic_lookups_file_path = _data_file('historic_lookups.json') - self.historic_lookups = safe_read_json(self.historic_lookups_file_path) - - self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') - self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) - - def cog_unload(self): - # Manually nulling out database because the GC for cogs seems to be pretty shitty - self.index_all = None - self.index_na = None - self.index_jp = None - self.historic_lookups = {} - self.historic_lookups_id2 = {} - - async def reload_nicknames(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadInfo'): - try: - await self.refresh_index() - print('Done refreshing PadInfo') - except Exception as ex: - print("reload padinfo loop caught exception " + str(ex)) - traceback.print_exc() - - await asyncio.sleep(60 * 60 * 1) - - @commands.command() - @checks.is_owner() - async def refresh_index(self, ctx=None): - """Refresh the monster indexes.""" - dg_cog = self.bot.get_cog('Dadguide') - if not dg_cog: - print("Cog 'Dadguide' not loaded") - return - logger.info('Waiting until DG is ready') - await dg_cog.wait_until_ready() - - # Putting some sleeps in here to give the bot time to respond to Discord. - await asyncio.sleep(1) - - logger.info('Loading ALL index') - self.index_all = dg_cog.create_index() - await asyncio.sleep(1) - - logger.info('Loading NA index') - self.index_na = dg_cog.create_index(lambda m: m.on_na) - await asyncio.sleep(1) - - logger.info('Loading JP index') - self.index_jp = dg_cog.create_index(lambda m: m.on_jp) - - logger.info('Done refreshing indexes') - - def get_monster_by_id(self, monster_id: int): - dg_cog = self.bot.get_cog('Dadguide') - return dg_cog.get_monster_by_id(monster_id) - - @commands.command() - async def jpname(self, ctx, *, query: str): - """Print the Japanese name of a monster""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await ctx.send(monsterToHeader(m)) - await ctx.send(box(m.name_jp)) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(name="id", aliases="iD Id ID".split()) - async def _id(self, ctx, *, query: str): - """Monster info (main tab)""" - prefix = ctx.prefix + "id" - query = prefix.join(filter(None, query.split(prefix))) - await self._do_id(ctx, query) - - @commands.command() - async def idna(self, ctx, *, query: str): - """Monster info (limited to NA monsters ONLY)""" - await self._do_id(ctx, query, server_filter=ServerFilter.na) - - @commands.command() - async def idjp(self, ctx, *, query: str): - """Monster info (limited to JP monsters ONLY)""" - await self._do_id(ctx, query, server_filter=ServerFilter.jp) - - async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = self.findMonster(query, server_filter=server_filter) - if m is not None: - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def id2(self, ctx, *, query: str): - """Monster info (main tab)""" - await self._do_id2(ctx, query) - - @commands.command() - async def id2na(self, ctx, *, query: str): - """Monster info (limited to NA monsters ONLY)""" - await self._do_id2(ctx, query, server_filter=ServerFilter.na) - - @commands.command() - async def id2jp(self, ctx, *, query: str): - """Monster info (limited to JP monsters ONLY)""" - await self._do_id2(ctx, query, server_filter=ServerFilter.jp) - - async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = self.findMonster2(query, server_filter=server_filter) - if m is not None: - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(name="evos") - async def evos(self, ctx, *, query: str): - """Monster info (evolutions tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.evo_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(name="mats", aliases=['evomats', 'evomat']) - async def evomats(self, ctx, *, query: str): - """Monster info (evo materials tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.mats_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def pantheon(self, ctx, *, query: str): - """Monster info (pantheon tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) - if menu == EMBED_NOT_GENERATED: - await ctx.send(inline('Not a pantheon monster')) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def skillups(self, ctx, *, query: str): - """Monster info (evolutions tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - menu = await self._do_idmenu(ctx, m, self.skillups_emoji) - if menu == EMBED_NOT_GENERATED: - await ctx.send(inline('No skillups available')) - else: - await ctx.send(self.makeFailureMsg(err)) - - async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m) - return await self._do_menu( - ctx, - starting_menu_emoji, - IdEmojiUpdater(emoji_to_embed, pad_info=self, - m=m, selected_emoji=starting_menu_emoji, bot=self.bot) - ) - - async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=True) - return await self._do_menu( - ctx, - starting_menu_emoji, - ScrollEmojiUpdater(emoji_to_embed, pad_info=self, bot=self.bot, - m=m, ms=ms, selected_emoji=starting_menu_emoji) - ) - - def get_id_emoji_options(self, m=None, scroll=False): - id_embed = monsterToEmbed(m, self.get_emojis()) - evo_embed = monsterToEvoEmbed(m) - mats_embed = monsterToEvoMatsEmbed(m) - animated = m.has_animation - pic_embed = monsterToPicEmbed(m, animated=animated) - other_info_embed = monsterToOtherInfoEmbed(m) - - emoji_to_embed = OrderedDict() - emoji_to_embed[self.id_emoji] = id_embed - emoji_to_embed[self.evo_emoji] = evo_embed - emoji_to_embed[self.mats_emoji] = mats_embed - emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = monsterToPantheonEmbed(m) - if pantheon_embed: - emoji_to_embed[self.pantheon_emoji] = pantheon_embed - - skillups_embed = monsterToSkillupsEmbed(m) - if skillups_embed: - emoji_to_embed[self.skillups_emoji] = skillups_embed - - emoji_to_embed[self.other_info_emoji] = other_info_embed - - # it's impossible for the previous/next ones to be accessed because - # IdEmojiUpdater won't allow it, however they have to be defined - # so that the buttons print in the first place - - if scroll: emoji_to_embed[self.first_monster_emoji] = None - emoji_to_embed[self.previous_monster_emoji] = None - emoji_to_embed[self.next_monster_emoji] = None - if scroll: emoji_to_embed[self.last_monster_emoji] = None - - # remove emoji needs to be last - emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message - return emoji_to_embed - - - async def _do_evolistmenu(self, ctx, sm): - monsters = sm.alt_evos - monsters.sort(key=lambda m: m.monster_id) - - emoji_to_embed = OrderedDict() - for idx, m in enumerate(monsters): - emoji = char_to_emoji(str(idx)) - emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis()) - if m.monster_id == sm.monster_id: - starting_menu_emoji = emoji - - return await self._do_menu(ctx, starting_menu_emoji, EmojiUpdater(emoji_to_embed), timeout=60) - - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): - if starting_menu_emoji not in emoji_to_embed.emoji_dict: - # Selected menu wasn't generated for this monster - return EMBED_NOT_GENERATED - - emoji_to_embed.emoji_dict[self.remove_emoji] = self.menu.reaction_delete_message - - try: - result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, - starting_menu_emoji, timeout=timeout) - if result_msg and result_embed: - # Message is finished but not deleted, clear the footer - result_embed.set_footer(text=discord.Embed.Empty) - await result_msg.edit(embed=result_embed) - except Exception as ex: - print('Menu failure', ex) - - @commands.command(aliases=['img']) - async def pic(self, ctx, *, query: str): - """Monster info (full image tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.pic_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def links(self, ctx, *, query: str): - """Monster links""" - m, err, debug_info = self.findMonster(query) - if m is not None: - embed = monsterToBaseEmbed(m) - embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - embed.set_footer(text='') - await ctx.send(embed=embed) - - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command(aliases=['stats']) - async def otherinfo(self, ctx, *, query: str): - """Monster info (misc info tab)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_idmenu(ctx, m, self.other_info_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def lookup(self, ctx, *, query: str): - """Short info results for a monster query""" - m, err, debug_info = self.findMonster(query) - if m is not None: - embed = monsterToHeaderEmbed(m) - await ctx.send(embed=embed) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def evolist(self, ctx, *, query): - """Monster info (for all monsters in the evo tree)""" - m, err, debug_info = self.findMonster(query) - if m is not None: - await self._do_evolistmenu(ctx, m) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def collabscroll(self, ctx, *, query: str): - """Scroll through the monsters in a collab""" - DGCOG = self.bot.get_cog("Dadguide") - m, err, debug_info = self.findMonster(query) - ms = DGCOG.database.get_monsters_by_series(m.series.series_id) - - ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) - ms = [m._alt_evo_id_list for m in ms - if m._base_monster_id == m.monster_id - and m.sell_mp >= 3000] - ms = [m for ml in ms for m in ml] - - if m._base_monster_id != m.monster_id: - m = self.get_monster_by_id(m._base_monster_id) - if m.monster_id not in ms: - m = m if m.monster_id in ms else self.get_monster_by_id(ms[0]) - - if m is not None: - await self._do_scrollmenu(ctx, m, ms, self.id_emoji) - else: - await ctx.send(self.makeFailureMsg(err)) - - - @commands.command(aliases=['leaders', 'leaderskills', 'ls']) - async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): - """Display the multiplier and leaderskills for two monsters - - If either your left or right query contains spaces, wrap in quotes. - e.g.: [p]leaderskill "r sonia" "b sonia" - """ - if bad: - await ctx.send(inline('Too many inputs. Try wrapping your queries in quotes.')) - return - - # Handle a very specific failure case, user typing something like "uuvo ragdra" - if ' ' not in left_query and right_query is not None and ' ' not in right_query and bad is None: - combined_query = left_query + ' ' + right_query - nm, err, debug_info = self._findMonster(combined_query) - if nm and left_query in nm.prefixes: - left_query = combined_query - right_query = None - - left_m, left_err, _ = self.findMonster(left_query) - if right_query: - right_m, right_err, _ = self.findMonster(right_query) - else: - right_m, right_err, = left_m, left_err - - err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, wrap it in quotes.' - if left_err: - await ctx.send(inline(err_msg.format('Left', left_query))) - return - if right_err: - await ctx.send(inline(err_msg.format('Right', right_query))) - return - - emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis()) - emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis()) - - await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) - - @commands.command(aliases=['helppic', 'helpimg']) - async def helpid(self, ctx): - """Whispers you info on how to craft monster queries for [p]id""" - await ctx.author.send(box(HELP_MSG.format(ctx))) - - @commands.command() - async def padsay(self, ctx, server, *, query: str = None): - """Speak the voice line of a monster into your current chat""" - voice = ctx.author.voice - if not voice: - await ctx.send(inline('You must be in a voice channel to use this command')) - return - channel = voice.channel - - speech_cog = self.bot.get_cog('Speech') - if not speech_cog: - await ctx.send(inline('Speech seems to be offline')) - return - - if server.lower() not in ['na', 'jp']: - query = server + ' ' + (query or '') - server = 'na' - query = query.strip().lower() - - m, err, debug_info = self.findMonster(query) - if m is not None: - voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na - base_dir = '/home/tactical0retreat/dadguide/data/media/voices' - voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) - header = '{} ({})'.format(monsterToHeader(m), server) - if not os.path.exists(voice_file): - await ctx.send(inline('Could not find voice for ' + header)) - return - await ctx.send('Speaking for ' + header) - await speech_cog.play_path(channel, voice_file) - else: - await ctx.send(self.makeFailureMsg(err)) - - @commands.command() - async def idmode(self, ctx, id_type): - """Switch between number mode and evo mode""" - if id_type in ['evo']: - if self.settings.setEvoID(ctx.author.id): - await ctx.send(inline("Done")) - else: - await ctx.send(inline("You're already using evo mode")) - elif id_type in ['number', 'default']: - if self.settings.rmEvoID(ctx.author.id): - await ctx.send(inline("Done")) - else: - await ctx.send(inline("You're already using number mode")) - else: - await ctx.send(inline("id_type must be 'number' or 'evo'")) - - @commands.group() - @checks.is_owner() - async def padinfo(self, ctx): - """PAD info management""" - - @padinfo.command() - @checks.is_owner() - async def setemojiservers(self, ctx, *, emoji_servers=''): - """Set the emoji servers by ID (csv)""" - self.settings.emojiServers().clear() - if emoji_servers: - self.settings.setEmojiServers(emoji_servers.split(',')) - await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) - - @checks.is_owner() - @padinfo.command() - async def iddiff(self, ctx): - """Runs the diff checker for id and id2""" - await ctx.send("Running diff checker...") - hist_aggreg = list(self.historic_lookups) + list(self.historic_lookups_id2) - s = 0 - f = [] - for query in hist_aggreg: - m1, err1, debug_info1 = self.findMonster(query) - m2, err2, debug_info2 = self.findMonster2(query) - if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): - s += 1 - continue - - f.append((query, - [m1.monster_id if m1 else None, m2.monster_id if m2 else None], - [err1, err2], - [debug_info1, debug_info2] - )) - if m1 and m2: - ctx.send("Major Discrepency: {} -> {}/{}".format(query, m1.name_na, m2.name_na)) - await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) - print(f) - - def get_emojis(self): - server_ids = [int(sid) for sid in self.settings.emojiServers()] - return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] - - def makeFailureMsg(self, err): - msg = ('Lookup failed: {}.\n' - 'Try one of , , [argbld]/[rgbld] . ' - 'Unexpected results? Use ^helpid for more info.').format(err) - return box(msg) - - def findMonster(self, query, server_filter=ServerFilter.any): - query = rmdiacritics(query) - nm, err, debug_info = self._findMonster(query, server_filter) - - monster_no = nm.monster_id if nm else -1 - self.historic_lookups[query] = monster_no - json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) - - m = self.get_monster_by_id(nm.monster_id) if nm else None - - return m, err, debug_info - - def _findMonster(self, query, server_filter=ServerFilter.any): - if server_filter == ServerFilter.any: - monster_index = self.index_all - elif server_filter == ServerFilter.na: - monster_index = self.index_na - elif server_filter == ServerFilter.jp: - monster_index = self.index_jp - else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) - return monster_index.find_monster(query) - - def findMonster2(self, query, server_filter=ServerFilter.any): - query = rmdiacritics(query) - nm, err, debug_info = self._findMonster2(query, server_filter) - - monster_no = nm.monster_id if nm else -1 - self.historic_lookups_id2[query] = monster_no - json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) - - m = self.get_monster_by_id(nm.monster_id) if nm else None - - return m, err, debug_info - - def _findMonster2(self, query, server_filter=ServerFilter.any): - if server_filter == ServerFilter.any: - monster_index = self.index_all - elif server_filter == ServerFilter.na: - monster_index = self.index_na - elif server_filter == ServerFilter.jp: - monster_index = self.index_jp - else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) - return monster_index.find_monster2(query) - - -class PadInfoSettings(CogSettings): - def make_default_settings(self): - config = { - 'animation_dir': '', - 'evo_id_users': [], - } - return config - - def emojiServers(self): - key = 'emoji_servers' - if key not in self.bot_settings: - self.bot_settings[key] = [] - return self.bot_settings[key] - - def setEmojiServers(self, emoji_servers): - es = self.emojiServers() - es.clear() - es.extend(emoji_servers) - self.save_settings() - - def setEvoID(self, user_id): - if user_id not in self.bot_settings['evo_id_users']: - self.bot_settings['evo_id_users'].append(user_id) - self.save_settings() - return True - return False - - def rmEvoID(self, user_id): - if user_id in self.bot_settings['evo_id_users']: - self.bot_settings['evo_id_users'].remove(user_id) - self.save_settings() - return True - return False - - def checkEvoID(self, user_id): - return user_id in self.bot_settings['evo_id_users'] - - -def monsterToHeader(m: "DgMonster", link=False): - msg = 'No. {} {}'.format(m.monster_no_na, m.name_na) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - -def monsterToJpSuffix(m: "DgMonster"): - suffix = "" - if m.roma_subname: - suffix += ' [{}]'.format(m.roma_subname) - if not m.on_na: - suffix += ' (JP only)' - return suffix - - -def monsterToLongHeader(m: "DgMonster", link=False): - msg = monsterToHeader(m) + monsterToJpSuffix(m) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - -def monsterToEvoText(m: "DgMonster"): - output = monsterToLongHeader(m) - for ae in sorted(m.alt_evos, key=lambda x: int(x.monster_id)): - output += "\n\t- {}".format(monsterToLongHeader(ae)) - return output - - -def monsterToThumbnailUrl(m: "DgMonster"): - return get_portrait_url(m) - - -def monsterToBaseEmbed(m: "DgMonster"): - header = monsterToLongHeader(m) - embed = discord.Embed() - embed.set_thumbnail(url=monsterToThumbnailUrl(m)) - embed.title = header - embed.url = get_pdx_url(m) - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed - - -def printEvoListFields(list_of_monsters, embed, name): - if not len(list_of_monsters): - return - field_name = name.format(len(list_of_monsters)) - field_data = '' - for ae in sorted(list_of_monsters, key=lambda x: int(x.monster_id)): - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - - embed.add_field(name=field_name, value=field_data) - - -def monsterToEvoEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - - if not len(m.alt_evos) and not m.evo_gem: - embed.description = 'No alternate evos or evo gem' - return embed - - printEvoListFields(m.alt_evos, embed, '{} alternate evo(s)') - if not m.evo_gem: - return embed - printEvoListFields([m.evo_gem], embed, '{} evo gem(s)') - - return embed - - -def printMonsterEvoOfList(monster_list, embed, field_name): - if not len(monster_list): - return - field_data = '' - if len(monster_list) > 5: - field_data = '{} monsters'.format(len(monster_list)) - else: - item_count = min(len(monster_list), 5) - for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - embed.add_field(name=field_name, value=field_data) - - -def monsterToEvoMatsEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - - mats_for_evo = m.mats_for_evo - - field_name = 'Evo materials' - field_data = '' - if len(mats_for_evo) > 0: - for ae in m.mats_for_evo: - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - else: - field_data = 'None' - embed.add_field(name=field_name, value=field_data) - - printMonsterEvoOfList(m.material_of, embed, 'Material for') - if not m.evo_gem: - return embed - printMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") - return embed - - -def monsterToPantheonEmbed(m: "DgMonster"): - full_pantheon = m.series.monsters - pantheon_list = list(filter(lambda x: x.evo_from is None, full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 6: - return None - - embed = monsterToBaseEmbed(m) - - field_name = 'Pantheon: ' + m.series.name - field_data = '' - for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): - field_data += '\n' + monsterToHeader(monster, link=True) - embed.add_field(name=field_name, value=field_data) - - return embed - - -def monsterToSkillupsEmbed(m: "DgMonster"): - skillups_list = m.active_skill.skillups if m.active_skill else [] - if len(skillups_list) == 0: - return None - - embed = monsterToBaseEmbed(m) - - field_name = 'Skillups' - field_data = '' - - # Prevent huge skillup lists - if len(skillups_list) > 8: - field_data = '({} skillups omitted)'.format(len(skillups_list) - 8) - skillups_list = skillups_list[0:8] - - for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): - field_data += '\n' + monsterToHeader(monster, link=True) - - if len(field_data.strip()): - embed.add_field(name=field_name, value=field_data) - - return embed - - -def monsterToPicUrl(m: "DgMonster"): - return get_pic_url(m) - - -def monsterToPicEmbed(m: "DgMonster", animated=False): - embed = monsterToBaseEmbed(m) - url = monsterToPicUrl(m) - embed.set_image(url=url) - # Clear the thumbnail, don't need it on pic - embed.set_thumbnail(url='') - extra_links = [] - if animated: - extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), monsterToGifUrl(m))) - if m.orb_skin_id is not None: - extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), monsterToOrbSkinCBUrl(m))) - if len(extra_links) > 0: - embed.add_field(name='Extra Links', value='\n'.join(extra_links)) - - return embed - - -def monsterToVideoUrl(m: "DgMonster", link_text='(MP4)'): - return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) - - -def monsterToGifUrl(m: "DgMonster", link_text='(GIF)'): - return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) - - -def monsterToOrbSkinUrl(m: "DgMonster", link_text='Regular'): - return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) - - -def monsterToOrbSkinCBUrl(m: "DgMonster", link_text='Color Blind'): - return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) - - -def monsterToGifEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - url = monsterToGifUrl(m) - embed.set_image(url=url) - # Clear the thumbnail, don't need it on pic - embed.set_thumbnail(url='') - return embed - - -def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): - lls = left_m.leader_skill - rls = right_m.leader_skill - - if lls: - lhp, latk, lrcv, lresist = lls.data - else: - lhp, latk, lrcv, lresist = 1, 1, 1, 0 - - if rls: - rhp, ratk, rrcv, rresist = rls.data - else: - rhp, ratk, rrcv, rresist = 1, 1, 1, 0 - - multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) - - embed = discord.Embed() - embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) - description = '' - description += '\n**{}**\n{}'.format( - monsterToHeader(left_m, link=True), - left_m.leader_skill.desc if left_m.leader_skill else 'None') - description += '\n**{}**\n{}'.format( - monsterToHeader(right_m, link=True), - right_m.leader_skill.desc if right_m.leader_skill else 'None') - embed.description = description - - return embed - - -def monsterToHeaderEmbed(m: "DgMonster"): - header = monsterToLongHeader(m, link=True) - embed = discord.Embed() - embed.description = header - return embed - - -def monsterToTypeString(m: "DgMonster"): - return '/'.join([t.name for t in m.types]) - - -def monsterToAcquireString(m: "DgMonster"): - acquire_text = None - if m.farmable and not m.mp_evo: - # Some MP shop monsters 'drop' in PADR - acquire_text = 'Farmable' - elif m.farmable_evo and not m.mp_evo: - acquire_text = 'Farmable Evo' - elif m.in_pem: - acquire_text = 'In PEM' - elif m.pem_evo: - acquire_text = 'PEM Evo' - elif m.in_rem: - acquire_text = 'In REM' - elif m.rem_evo: - acquire_text = 'REM Evo' - elif m.in_mpshop: - acquire_text = 'MP Shop' - elif m.mp_evo: - acquire_text = 'MP Shop Evo' - return acquire_text - - -def match_emoji(emoji_list, name): - for e in emoji_list: - if e.name == name: - return e - return name - - -def monsterToEmbed(m: "DgMonster", emoji_list): - embed = monsterToBaseEmbed(m) - - info_row_1 = monsterToTypeString(m) - acquire_text = monsterToAcquireString(m) - tet_text = m.true_evo_type.value - - os = "" if m.orb_skin_id is None else " (Orb Skin)" - - info_row_2 = '**Rarity** {}{}\n**Cost** {}'.format(m.rarity, os, m.cost) - if acquire_text: - info_row_2 += '\n**{}**'.format(acquire_text) - if m.is_inheritable: - info_row_2 += '\n**Inheritable**' - else: - info_row_2 += '\n**Not inheritable**' - if tet_text: - info_row_2 += '\n**{}**'.format(tet_text) - - embed.add_field(name=info_row_1, value=info_row_2) - - hp, atk, rcv, weighted = m.stats() - if m.limit_mult > 0: - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, m.limit_mult) - stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( - hp, lb_hp, atk, lb_atk, rcv, lb_rcv) - else: - stats_row_1 = 'Weighted {}'.format(weighted) - stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) - embed.add_field(name=stats_row_1, value=stats_row_2) - - awakenings_row = '' - for idx, a in enumerate(m.awakenings): - as_id = a.awoken_skill_id - as_name = a.name - mapped_awakening = AWAKENING_MAP.get(as_id, as_name) - mapped_awakening = match_emoji(emoji_list, mapped_awakening) - - # Wrap superawakenings to the next line - if len(m.awakenings) - idx == m.superawakening_count: - awakenings_row += '\n{}'.format(mapped_awakening) - else: - awakenings_row += ' {}'.format(mapped_awakening) - - awakenings_row = awakenings_row.strip() - - if not len(awakenings_row): - awakenings_row = 'No Awakenings' - - killers_row = '**Available Killers:** {}'.format(' '.join(m.killers)) - - embed.description = '{}\n{}'.format(awakenings_row, killers_row) - - active_header = 'Active Skill' - active_body = 'None' - active_skill = m.active_skill - if active_skill: - active_header = 'Active Skill ({} -> {})'.format(active_skill.turn_max, - active_skill.turn_min) - active_body = active_skill.desc - embed.add_field(name=active_header, value=active_body, inline=False) - - leader_skill = m.leader_skill - ls_row = m.leader_skill.desc if leader_skill else 'None' - ls_header = 'Leader Skill' - if leader_skill: - hp, atk, rcv, resist = m.leader_skill.data - multiplier_text = createMultiplierText(hp, atk, rcv, resist) - ls_header += " [ {} ]".format(multiplier_text) - embed.add_field(name=ls_header, value=ls_row, inline=False) - - return embed - - -def monsterToOtherInfoEmbed(m: "DgMonster"): - embed = monsterToBaseEmbed(m) - # Clear the thumbnail, takes up too much space - embed.set_thumbnail(url='') - - body_text = '\n' - stat_cols = ['', 'HP', 'ATK', 'RCV'] - for plus in (0, 297): - body_text += '**Stats at +{}:**'.format(plus) - tbl = prettytable.PrettyTable(stat_cols) - tbl.hrules = prettytable.NONE - tbl.vrules = prettytable.NONE - tbl.align = "l" - levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) - for lv in levels: - for inh in (False, True): - hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) - row_name = 'Lv{}'.format(lv) - if inh: - row_name = '(Inh)' - tbl.add_row([row_name.format(plus), hp, atk, rcv]) - body_text += box(tbl.get_string()) - - body_text += "\n**JP Name**: {}".format(m.name_jp) - body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - - if m.history_us: - body_text += '\n**History:** {}'.format(m.history_us) - - body_text += '\n**Series:** {}'.format(m.series.name) - body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) - if m.buy_mp is not None: - body_text += " **Buy MP:** {:,}".format(m.buy_mp) - - if m.exp < 1000000: - xp_text = '{:,}'.format(m.exp) - else: - xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' - body_text += '\n**XP to Max:** {}'.format(xp_text) - body_text += ' **Max Level:**: {}'.format(m.level) - body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) - - embed.description = body_text - - return embed - - -AWAKENING_MAP = { - 1: 'boost_hp', - 2: 'boost_atk', - 3: 'boost_rcv', - 4: 'reduce_fire', - 5: 'reduce_water', - 6: 'reduce_wood', - 7: 'reduce_light', - 8: 'reduce_dark', - 9: 'misc_autoheal', - 10: 'res_bind', - 11: 'res_blind', - 12: 'res_jammer', - 13: 'res_poison', - 14: 'oe_fire', - 15: 'oe_water', - 16: 'oe_wood', - 17: 'oe_light', - 18: 'oe_dark', - 19: 'misc_te', - 20: 'misc_bindclear', - 21: 'misc_sb', - 22: 'row_fire', - 23: 'row_water', - 24: 'row_wood', - 25: 'row_light', - 26: 'row_dark', - 27: 'misc_tpa', - 28: 'res_skillbind', - 29: 'oe_heart', - 30: 'misc_multiboost', - 31: 'killer_dragon', - 32: 'killer_god', - 33: 'killer_devil', - 34: 'killer_machine', - 35: 'killer_balance', - 36: 'killer_attacker', - 37: 'killer_physical', - 38: 'killer_healer', - 39: 'killer_evomat', - 40: 'killer_awoken', - 41: 'killer_enhancemat', - 42: 'killer_vendor', - 43: 'misc_comboboost', - 44: 'misc_guardbreak', - 45: 'misc_extraattack', - 46: 'teamboost_hp', - 47: 'teamboost_rcv', - 48: 'misc_voidshield', - 49: 'misc_assist', - 50: 'misc_super_extraattack', - 51: 'misc_skillcharge', - 52: 'res_bind_super', - 53: 'misc_te_super', - 54: 'res_cloud', - 55: 'res_seal', - 56: 'misc_sb_super', - 57: 'attack_boost_high', - 58: 'attack_boost_low', - 59: 'l_shield', - 60: 'l_attack', - 61: 'misc_super_comboboost', - 62: 'orb_combo', - 63: 'misc_voice', - 64: 'misc_dungeonbonus', - 65: 'reduce_hp', - 66: 'reduce_atk', - 67: 'reduce_rcv', - 68: 'res_blind_super', - 69: 'res_jammer_super', - 70: 'res_poison_super', - 71: 'misc_jammerboost', - 72: 'misc_poisonboost', -} - - -def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=None, resist2=None): - if all([x is None for x in (hp2, atk2, rcv2, resist2)]): - hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 - - def fmtNum(val): - return ('{:.2f}').format(val).strip('0').rstrip('.') - - text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) - if resist1 > 0 or resist2 > 0: - text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) - return text +import asyncio +import json +import logging +import os +import traceback +import urllib.parse +from collections import OrderedDict +from enum import Enum + +import discord +import prettytable +from redbot.core import checks, data_manager +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import inline, box + +import rpadutils +from rpadutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics + +logger = logging.getLogger('red.miruv3.padinfo') + +HELP_MSG = """ +{0.prefix}helpid : shows this message +{0.prefix}id : look up a monster and print a link to puzzledragonx +{0.prefix}pic : Look up a monster and display its image inline + +Options for + : Find a monster by ID + {0.prefix}id 1234 (picks sun quan) + : Take the best guess for a monster, picks the most recent monster + {0.prefix}id kali (picks mega awoken d kali) + : Limit by element or awoken, e.g. + {0.prefix}id ares (selects the most recent, revo ares) + {0.prefix}id aares (explicitly selects awoken ares) + {0.prefix}id a ares (spaces work too) + {0.prefix}id rd ares (select a specific evo for ares, the red/dark one) + {0.prefix}id r/d ares (slashes, spaces work too) + +computed nickname list and overrides: https://docs.google.com/spreadsheets/d/1EyzMjvf8ZCQ4K-gJYnNkiZlCEsT9YYI9dUd-T5qCirc/pubhtml +submit an override suggestion: https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" + +EMBED_NOT_GENERATED = -1 + +INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' + +MEDIA_PATH = 'https://f002.backblazeb2.com/file/dadguide-data/media/' +RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png' +RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' +VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' +GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' +ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}.png' +ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}cb.png' + +YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' +SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' +ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' + + +class ServerFilter(Enum): + any = 0 + na = 1 + jp = 2 + + +def get_pdx_url(m): + return INFO_PDX_TEMPLATE.format(rpadutils.get_pdx_id(m)) + + +def get_portrait_url(m): + return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) + + +def get_pic_url(m): + return RPAD_PIC_TEMPLATE.format(m.monster_id) + + +def _data_file(file_name: str) -> str: + return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) + + +class IdEmojiUpdater(EmojiUpdater): + def __init__(self, emoji_to_embed, m: "DgMonster" = None, + pad_info=None, selected_emoji=None, bot=None): + self.emoji_dict = emoji_to_embed + self.m = m + self.pad_info = pad_info + self.selected_emoji = selected_emoji + self.bot = bot + + def on_update(self, ctx, selected_emoji): + if self.pad_info.settings.checkEvoID(ctx.author.id): + DGCOG = self.bot.get_cog('Dadguide') + evos = list(map(lambda x: [x['from_id'], x['to_id']], + DGCOG.database.get_all_evolutions_by_monster(self.m.monster_id))) + evos = list(set(sum(evos, []))) + evos.sort() + index = evos.index(self.m.monster_id) + if selected_emoji == self.pad_info.previous_monster_emoji: + if index == 0: + self.m = DGCOG.get_monster_by_id(evos[-1]) + else: + self.m = DGCOG.get_monster_by_id(evos[index - 1]) + elif selected_emoji == self.pad_info.next_monster_emoji: + if index == len(evos) - 1: + self.m = DGCOG.get_monster_by_id(evos[0]) + else: + self.m = DGCOG.get_monster_by_id(evos[index + 1]) + else: + self.selected_emoji = selected_emoji + return True + else: + if selected_emoji == self.pad_info.previous_monster_emoji: + if self.m.prev_monster is None: + return False + self.m = self.m.prev_monster + elif selected_emoji == self.pad_info.next_monster_emoji: + if self.m.next_monster is None: + return False + self.m = self.m.next_monster + else: + self.selected_emoji = selected_emoji + return True + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m) + return True + + +class ScrollEmojiUpdater(EmojiUpdater): + def __init__(self, emoji_to_embed, m: "DgMonster" = None, + ms: "List[int]" = None, selected_emoji=None, pad_info=None, bot=None): + self.emoji_dict = emoji_to_embed + self.m = m + self.ms = ms + self.pad_info = pad_info + self.selected_emoji = selected_emoji + self.bot = bot + + def on_update(self, ctx, selected_emoji): + DGCOG = self.bot.get_cog('Dadguide') + index = self.ms.index(self.m.monster_id) + + if selected_emoji == self.pad_info.first_monster_emoji: + self.m = DGCOG.get_monster_by_id(self.ms[0]) + elif selected_emoji == self.pad_info.previous_monster_emoji: + self.m = DGCOG.get_monster_by_id(self.ms[index - 1]) + elif selected_emoji == self.pad_info.next_monster_emoji: + if index == len(self.ms) - 1: + self.m = DGCOG.get_monster_by_id(self.ms[0]) + else: + self.m = DGCOG.get_monster_by_id(self.ms[index + 1]) + elif selected_emoji == self.pad_info.last_monster_emoji: + self.m = DGCOG.get_monster_by_id(self.ms[-1]) + else: + self.selected_emoji = selected_emoji + return True + + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=True) + return True + + +class PadInfo(commands.Cog): + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + self.settings = PadInfoSettings("padinfo") + + self.index_all = None + self.index_na = None + self.index_jp = None + + self.menu = Menu(bot) + + # These emojis are the keys into the idmenu submenus + self.id_emoji = '\N{INFORMATION SOURCE}' + self.evo_emoji = char_to_emoji('e') + self.mats_emoji = char_to_emoji('m') + self.ls_emoji = '\N{INFORMATION SOURCE}' + self.left_emoji = char_to_emoji('l') + self.right_emoji = char_to_emoji('r') + self.pantheon_emoji = '\N{CLASSICAL BUILDING}' + self.skillups_emoji = '\N{MEAT ON BONE}' + self.pic_emoji = '\N{FRAME WITH PICTURE}' + self.other_info_emoji = '\N{SCROLL}' + self.first_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + self.previous_monster_emoji = '\N{BLACK LEFT-POINTING TRIANGLE}' + self.next_monster_emoji = '\N{BLACK RIGHT-POINTING TRIANGLE}' + self.last_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + self.remove_emoji = self.menu.emoji['no'] + + self.historic_lookups_file_path = _data_file('historic_lookups.json') + self.historic_lookups = safe_read_json(self.historic_lookups_file_path) + + self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') + self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) + + def cog_unload(self): + # Manually nulling out database because the GC for cogs seems to be pretty shitty + self.index_all = None + self.index_na = None + self.index_jp = None + self.historic_lookups = {} + self.historic_lookups_id2 = {} + + async def reload_nicknames(self): + await self.bot.wait_until_ready() + while self == self.bot.get_cog('PadInfo'): + try: + await self.refresh_index() + print('Done refreshing PadInfo') + except Exception as ex: + print("reload padinfo loop caught exception " + str(ex)) + traceback.print_exc() + + await asyncio.sleep(60 * 60 * 1) + + @commands.command() + @checks.is_owner() + async def refresh_index(self, ctx=None): + """Refresh the monster indexes.""" + dg_cog = self.bot.get_cog('Dadguide') + if not dg_cog: + print("Cog 'Dadguide' not loaded") + return + logger.info('Waiting until DG is ready') + await dg_cog.wait_until_ready() + + # Putting some sleeps in here to give the bot time to respond to Discord. + await asyncio.sleep(1) + + logger.info('Loading ALL index') + self.index_all = dg_cog.create_index() + await asyncio.sleep(1) + + logger.info('Loading NA index') + self.index_na = dg_cog.create_index(lambda m: m.on_na) + await asyncio.sleep(1) + + logger.info('Loading JP index') + self.index_jp = dg_cog.create_index(lambda m: m.on_jp) + + logger.info('Done refreshing indexes') + + def get_monster_by_id(self, monster_id: int): + dg_cog = self.bot.get_cog('Dadguide') + return dg_cog.get_monster_by_id(monster_id) + + @commands.command() + async def jpname(self, ctx, *, query: str): + """Print the Japanese name of a monster""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await ctx.send(monsterToHeader(m)) + await ctx.send(box(m.name_jp)) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(name="id", aliases="iD Id ID".split()) + async def _id(self, ctx, *, query: str): + """Monster info (main tab)""" + prefix = ctx.prefix + "id" + query = prefix.join(filter(None, query.split(prefix))) + await self._do_id(ctx, query) + + @commands.command() + async def idna(self, ctx, *, query: str): + """Monster info (limited to NA monsters ONLY)""" + await self._do_id(ctx, query, server_filter=ServerFilter.na) + + @commands.command() + async def idjp(self, ctx, *, query: str): + """Monster info (limited to JP monsters ONLY)""" + await self._do_id(ctx, query, server_filter=ServerFilter.jp) + + async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): + m, err, debug_info = self.findMonster(query, server_filter=server_filter) + if m is not None: + await self._do_idmenu(ctx, m, self.id_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def id2(self, ctx, *, query: str): + """Monster info (main tab)""" + await self._do_id2(ctx, query) + + @commands.command() + async def id2na(self, ctx, *, query: str): + """Monster info (limited to NA monsters ONLY)""" + await self._do_id2(ctx, query, server_filter=ServerFilter.na) + + @commands.command() + async def id2jp(self, ctx, *, query: str): + """Monster info (limited to JP monsters ONLY)""" + await self._do_id2(ctx, query, server_filter=ServerFilter.jp) + + async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): + m, err, debug_info = self.findMonster2(query, server_filter=server_filter) + if m is not None: + await self._do_idmenu(ctx, m, self.id_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(name="evos") + async def evos(self, ctx, *, query: str): + """Monster info (evolutions tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.evo_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(name="mats", aliases=['evomats', 'evomat']) + async def evomats(self, ctx, *, query: str): + """Monster info (evo materials tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.mats_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def pantheon(self, ctx, *, query: str): + """Monster info (pantheon tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) + if menu == EMBED_NOT_GENERATED: + await ctx.send(inline('Not a pantheon monster')) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def skillups(self, ctx, *, query: str): + """Monster info (evolutions tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + menu = await self._do_idmenu(ctx, m, self.skillups_emoji) + if menu == EMBED_NOT_GENERATED: + await ctx.send(inline('No skillups available')) + else: + await ctx.send(self.makeFailureMsg(err)) + + async def _do_idmenu(self, ctx, m, starting_menu_emoji): + emoji_to_embed = self.get_id_emoji_options(m=m) + return await self._do_menu( + ctx, + starting_menu_emoji, + IdEmojiUpdater(emoji_to_embed, pad_info=self, + m=m, selected_emoji=starting_menu_emoji, bot=self.bot) + ) + + async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=True) + return await self._do_menu( + ctx, + starting_menu_emoji, + ScrollEmojiUpdater(emoji_to_embed, pad_info=self, bot=self.bot, + m=m, ms=ms, selected_emoji=starting_menu_emoji) + ) + + def get_id_emoji_options(self, m=None, scroll=False): + id_embed = monsterToEmbed(m, self.get_emojis()) + evo_embed = monsterToEvoEmbed(m) + mats_embed = monsterToEvoMatsEmbed(m) + animated = m.has_animation + pic_embed = monsterToPicEmbed(m, animated=animated) + other_info_embed = monsterToOtherInfoEmbed(m) + + emoji_to_embed = OrderedDict() + emoji_to_embed[self.id_emoji] = id_embed + emoji_to_embed[self.evo_emoji] = evo_embed + emoji_to_embed[self.mats_emoji] = mats_embed + emoji_to_embed[self.pic_emoji] = pic_embed + pantheon_embed = monsterToPantheonEmbed(m) + if pantheon_embed: + emoji_to_embed[self.pantheon_emoji] = pantheon_embed + + skillups_embed = monsterToSkillupsEmbed(m) + if skillups_embed: + emoji_to_embed[self.skillups_emoji] = skillups_embed + + emoji_to_embed[self.other_info_emoji] = other_info_embed + + # it's impossible for the previous/next ones to be accessed because + # IdEmojiUpdater won't allow it, however they have to be defined + # so that the buttons print in the first place + + if scroll: emoji_to_embed[self.first_monster_emoji] = None + emoji_to_embed[self.previous_monster_emoji] = None + emoji_to_embed[self.next_monster_emoji] = None + if scroll: emoji_to_embed[self.last_monster_emoji] = None + + # remove emoji needs to be last + emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message + return emoji_to_embed + + + async def _do_evolistmenu(self, ctx, sm): + monsters = sm.alt_evos + monsters.sort(key=lambda m: m.monster_id) + + emoji_to_embed = OrderedDict() + for idx, m in enumerate(monsters): + emoji = char_to_emoji(str(idx)) + emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis()) + if m.monster_id == sm.monster_id: + starting_menu_emoji = emoji + + return await self._do_menu(ctx, starting_menu_emoji, EmojiUpdater(emoji_to_embed), timeout=60) + + async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): + if starting_menu_emoji not in emoji_to_embed.emoji_dict: + # Selected menu wasn't generated for this monster + return EMBED_NOT_GENERATED + + emoji_to_embed.emoji_dict[self.remove_emoji] = self.menu.reaction_delete_message + + try: + result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, + starting_menu_emoji, timeout=timeout) + if result_msg and result_embed: + # Message is finished but not deleted, clear the footer + result_embed.set_footer(text=discord.Embed.Empty) + await result_msg.edit(embed=result_embed) + except Exception as ex: + print('Menu failure', ex) + + @commands.command(aliases=['img']) + async def pic(self, ctx, *, query: str): + """Monster info (full image tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.pic_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def links(self, ctx, *, query: str): + """Monster links""" + m, err, debug_info = self.findMonster(query) + if m is not None: + embed = monsterToBaseEmbed(m) + embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + embed.set_footer(text='') + await ctx.send(embed=embed) + + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command(aliases=['stats']) + async def otherinfo(self, ctx, *, query: str): + """Monster info (misc info tab)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_idmenu(ctx, m, self.other_info_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def lookup(self, ctx, *, query: str): + """Short info results for a monster query""" + m, err, debug_info = self.findMonster(query) + if m is not None: + embed = monsterToHeaderEmbed(m) + await ctx.send(embed=embed) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def evolist(self, ctx, *, query): + """Monster info (for all monsters in the evo tree)""" + m, err, debug_info = self.findMonster(query) + if m is not None: + await self._do_evolistmenu(ctx, m) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def collabscroll(self, ctx, *, query: str): + """Scroll through the monsters in a collab""" + DGCOG = self.bot.get_cog("Dadguide") + m, err, debug_info = self.findMonster(query) + ms = DGCOG.database.get_monsters_by_series(m.series.series_id) + + ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) + ms = [m._alt_evo_id_list for m in ms + if m._base_monster_id == m.monster_id + and m.sell_mp >= 3000] + ms = [m for ml in ms for m in ml] + + if m._base_monster_id != m.monster_id: + m = self.get_monster_by_id(m._base_monster_id) + if m.monster_id not in ms: + m = m if m.monster_id in ms else self.get_monster_by_id(ms[0]) + + if m is not None: + await self._do_scrollmenu(ctx, m, ms, self.id_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + + + @commands.command(aliases=['leaders', 'leaderskills', 'ls']) + async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): + """Display the multiplier and leaderskills for two monsters + + If either your left or right query contains spaces, wrap in quotes. + e.g.: [p]leaderskill "r sonia" "b sonia" + """ + if bad: + await ctx.send(inline('Too many inputs. Try wrapping your queries in quotes.')) + return + + # Handle a very specific failure case, user typing something like "uuvo ragdra" + if ' ' not in left_query and right_query is not None and ' ' not in right_query and bad is None: + combined_query = left_query + ' ' + right_query + nm, err, debug_info = self._findMonster(combined_query) + if nm and left_query in nm.prefixes: + left_query = combined_query + right_query = None + + left_m, left_err, _ = self.findMonster(left_query) + if right_query: + right_m, right_err, _ = self.findMonster(right_query) + else: + right_m, right_err, = left_m, left_err + + err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, wrap it in quotes.' + if left_err: + await ctx.send(inline(err_msg.format('Left', left_query))) + return + if right_err: + await ctx.send(inline(err_msg.format('Right', right_query))) + return + + emoji_to_embed = OrderedDict() + emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) + emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis()) + emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis()) + + await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) + + @commands.command(aliases=['helppic', 'helpimg']) + async def helpid(self, ctx): + """Whispers you info on how to craft monster queries for [p]id""" + await ctx.author.send(box(HELP_MSG.format(ctx))) + + @commands.command() + async def padsay(self, ctx, server, *, query: str = None): + """Speak the voice line of a monster into your current chat""" + voice = ctx.author.voice + if not voice: + await ctx.send(inline('You must be in a voice channel to use this command')) + return + channel = voice.channel + + speech_cog = self.bot.get_cog('Speech') + if not speech_cog: + await ctx.send(inline('Speech seems to be offline')) + return + + if server.lower() not in ['na', 'jp']: + query = server + ' ' + (query or '') + server = 'na' + query = query.strip().lower() + + m, err, debug_info = self.findMonster(query) + if m is not None: + voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na + base_dir = '/home/tactical0retreat/dadguide/data/media/voices' + voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) + header = '{} ({})'.format(monsterToHeader(m), server) + if not os.path.exists(voice_file): + await ctx.send(inline('Could not find voice for ' + header)) + return + await ctx.send('Speaking for ' + header) + await speech_cog.play_path(channel, voice_file) + else: + await ctx.send(self.makeFailureMsg(err)) + + @commands.command() + async def idmode(self, ctx, id_type): + """Switch between number mode and evo mode""" + if id_type in ['evo']: + if self.settings.setEvoID(ctx.author.id): + await ctx.send(inline("Done")) + else: + await ctx.send(inline("You're already using evo mode")) + elif id_type in ['number', 'default']: + if self.settings.rmEvoID(ctx.author.id): + await ctx.send(inline("Done")) + else: + await ctx.send(inline("You're already using number mode")) + else: + await ctx.send(inline("id_type must be 'number' or 'evo'")) + + @commands.group() + @checks.is_owner() + async def padinfo(self, ctx): + """PAD info management""" + + @padinfo.command() + @checks.is_owner() + async def setemojiservers(self, ctx, *, emoji_servers=''): + """Set the emoji servers by ID (csv)""" + self.settings.emojiServers().clear() + if emoji_servers: + self.settings.setEmojiServers(emoji_servers.split(',')) + await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) + + @checks.is_owner() + @padinfo.command() + async def iddiff(self, ctx): + """Runs the diff checker for id and id2""" + await ctx.send("Running diff checker...") + hist_aggreg = list(self.historic_lookups) + list(self.historic_lookups_id2) + s = 0 + f = [] + for query in hist_aggreg: + m1, err1, debug_info1 = self.findMonster(query) + m2, err2, debug_info2 = self.findMonster2(query) + if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): + s += 1 + continue + + f.append((query, + [m1.monster_id if m1 else None, m2.monster_id if m2 else None], + [err1, err2], + [debug_info1, debug_info2] + )) + if m1 and m2: + ctx.send("Major Discrepency: {} -> {}/{}".format(query, m1.name_na, m2.name_na)) + await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) + print(f) + + def get_emojis(self): + server_ids = [int(sid) for sid in self.settings.emojiServers()] + return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] + + def makeFailureMsg(self, err): + msg = ('Lookup failed: {}.\n' + 'Try one of , , [argbld]/[rgbld] . ' + 'Unexpected results? Use ^helpid for more info.').format(err) + return box(msg) + + def findMonster(self, query, server_filter=ServerFilter.any): + query = rmdiacritics(query) + nm, err, debug_info = self._findMonster(query, server_filter) + + monster_no = nm.monster_id if nm else -1 + self.historic_lookups[query] = monster_no + json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) + + m = self.get_monster_by_id(nm.monster_id) if nm else None + + return m, err, debug_info + + def _findMonster(self, query, server_filter=ServerFilter.any): + if server_filter == ServerFilter.any: + monster_index = self.index_all + elif server_filter == ServerFilter.na: + monster_index = self.index_na + elif server_filter == ServerFilter.jp: + monster_index = self.index_jp + else: + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) + return monster_index.find_monster(query) + + def findMonster2(self, query, server_filter=ServerFilter.any): + query = rmdiacritics(query) + nm, err, debug_info = self._findMonster2(query, server_filter) + + monster_no = nm.monster_id if nm else -1 + self.historic_lookups_id2[query] = monster_no + json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) + + m = self.get_monster_by_id(nm.monster_id) if nm else None + + return m, err, debug_info + + def _findMonster2(self, query, server_filter=ServerFilter.any): + if server_filter == ServerFilter.any: + monster_index = self.index_all + elif server_filter == ServerFilter.na: + monster_index = self.index_na + elif server_filter == ServerFilter.jp: + monster_index = self.index_jp + else: + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) + return monster_index.find_monster2(query) + + +class PadInfoSettings(CogSettings): + def make_default_settings(self): + config = { + 'animation_dir': '', + 'evo_id_users': [], + } + return config + + def emojiServers(self): + key = 'emoji_servers' + if key not in self.bot_settings: + self.bot_settings[key] = [] + return self.bot_settings[key] + + def setEmojiServers(self, emoji_servers): + es = self.emojiServers() + es.clear() + es.extend(emoji_servers) + self.save_settings() + + def setEvoID(self, user_id): + if user_id not in self.bot_settings['evo_id_users']: + self.bot_settings['evo_id_users'].append(user_id) + self.save_settings() + return True + return False + + def rmEvoID(self, user_id): + if user_id in self.bot_settings['evo_id_users']: + self.bot_settings['evo_id_users'].remove(user_id) + self.save_settings() + return True + return False + + def checkEvoID(self, user_id): + return user_id in self.bot_settings['evo_id_users'] + + +def monsterToHeader(m: "DgMonster", link=False): + msg = 'No. {} {}'.format(m.monster_no_na, m.name_na) + return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg + + +def monsterToJpSuffix(m: "DgMonster"): + suffix = "" + if m.roma_subname: + suffix += ' [{}]'.format(m.roma_subname) + if not m.on_na: + suffix += ' (JP only)' + return suffix + + +def monsterToLongHeader(m: "DgMonster", link=False): + msg = monsterToHeader(m) + monsterToJpSuffix(m) + return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg + + +def monsterToEvoText(m: "DgMonster"): + output = monsterToLongHeader(m) + for ae in sorted(m.alt_evos, key=lambda x: int(x.monster_id)): + output += "\n\t- {}".format(monsterToLongHeader(ae)) + return output + + +def monsterToThumbnailUrl(m: "DgMonster"): + return get_portrait_url(m) + + +def monsterToBaseEmbed(m: "DgMonster"): + header = monsterToLongHeader(m) + embed = discord.Embed() + embed.set_thumbnail(url=monsterToThumbnailUrl(m)) + embed.title = header + embed.url = get_pdx_url(m) + embed.set_footer(text='Requester may click the reactions below to switch tabs') + return embed + + +def printEvoListFields(list_of_monsters, embed, name): + if not len(list_of_monsters): + return + field_name = name.format(len(list_of_monsters)) + field_data = '' + for ae in sorted(list_of_monsters, key=lambda x: int(x.monster_id)): + field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) + + embed.add_field(name=field_name, value=field_data) + + +def monsterToEvoEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + + if not len(m.alt_evos) and not m.evo_gem: + embed.description = 'No alternate evos or evo gem' + return embed + + printEvoListFields(m.alt_evos, embed, '{} alternate evo(s)') + if not m.evo_gem: + return embed + printEvoListFields([m.evo_gem], embed, '{} evo gem(s)') + + return embed + + +def printMonsterEvoOfList(monster_list, embed, field_name): + if not len(monster_list): + return + field_data = '' + if len(monster_list) > 5: + field_data = '{} monsters'.format(len(monster_list)) + else: + item_count = min(len(monster_list), 5) + for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: + field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) + embed.add_field(name=field_name, value=field_data) + + +def monsterToEvoMatsEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + + mats_for_evo = m.mats_for_evo + + field_name = 'Evo materials' + field_data = '' + if len(mats_for_evo) > 0: + for ae in m.mats_for_evo: + field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) + else: + field_data = 'None' + embed.add_field(name=field_name, value=field_data) + + printMonsterEvoOfList(m.material_of, embed, 'Material for') + if not m.evo_gem: + return embed + printMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") + return embed + + +def monsterToPantheonEmbed(m: "DgMonster"): + full_pantheon = m.series.monsters + pantheon_list = list(filter(lambda x: x.evo_from is None, full_pantheon)) + if len(pantheon_list) == 0 or len(pantheon_list) > 6: + return None + + embed = monsterToBaseEmbed(m) + + field_name = 'Pantheon: ' + m.series.name + field_data = '' + for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): + field_data += '\n' + monsterToHeader(monster, link=True) + embed.add_field(name=field_name, value=field_data) + + return embed + + +def monsterToSkillupsEmbed(m: "DgMonster"): + skillups_list = m.active_skill.skillups if m.active_skill else [] + if len(skillups_list) == 0: + return None + + embed = monsterToBaseEmbed(m) + + field_name = 'Skillups' + field_data = '' + + # Prevent huge skillup lists + if len(skillups_list) > 8: + field_data = '({} skillups omitted)'.format(len(skillups_list) - 8) + skillups_list = skillups_list[0:8] + + for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): + field_data += '\n' + monsterToHeader(monster, link=True) + + if len(field_data.strip()): + embed.add_field(name=field_name, value=field_data) + + return embed + + +def monsterToPicUrl(m: "DgMonster"): + return get_pic_url(m) + + +def monsterToPicEmbed(m: "DgMonster", animated=False): + embed = monsterToBaseEmbed(m) + url = monsterToPicUrl(m) + embed.set_image(url=url) + # Clear the thumbnail, don't need it on pic + embed.set_thumbnail(url='') + extra_links = [] + if animated: + extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), monsterToGifUrl(m))) + if m.orb_skin_id is not None: + extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), monsterToOrbSkinCBUrl(m))) + if len(extra_links) > 0: + embed.add_field(name='Extra Links', value='\n'.join(extra_links)) + + return embed + + +def monsterToVideoUrl(m: "DgMonster", link_text='(MP4)'): + return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) + + +def monsterToGifUrl(m: "DgMonster", link_text='(GIF)'): + return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) + + +def monsterToOrbSkinUrl(m: "DgMonster", link_text='Regular'): + return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) + + +def monsterToOrbSkinCBUrl(m: "DgMonster", link_text='Color Blind'): + return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) + + +def monsterToGifEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + url = monsterToGifUrl(m) + embed.set_image(url=url) + # Clear the thumbnail, don't need it on pic + embed.set_thumbnail(url='') + return embed + + +def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): + lls = left_m.leader_skill + rls = right_m.leader_skill + + if lls: + lhp, latk, lrcv, lresist = lls.data + else: + lhp, latk, lrcv, lresist = 1, 1, 1, 0 + + if rls: + rhp, ratk, rrcv, rresist = rls.data + else: + rhp, ratk, rrcv, rresist = 1, 1, 1, 0 + + multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) + + embed = discord.Embed() + embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) + description = '' + description += '\n**{}**\n{}'.format( + monsterToHeader(left_m, link=True), + left_m.leader_skill.desc if left_m.leader_skill else 'None') + description += '\n**{}**\n{}'.format( + monsterToHeader(right_m, link=True), + right_m.leader_skill.desc if right_m.leader_skill else 'None') + embed.description = description + + return embed + + +def monsterToHeaderEmbed(m: "DgMonster"): + header = monsterToLongHeader(m, link=True) + embed = discord.Embed() + embed.description = header + return embed + + +def monsterToTypeString(m: "DgMonster"): + return '/'.join([t.name for t in m.types]) + + +def monsterToAcquireString(m: "DgMonster"): + acquire_text = None + if m.farmable and not m.mp_evo: + # Some MP shop monsters 'drop' in PADR + acquire_text = 'Farmable' + elif m.farmable_evo and not m.mp_evo: + acquire_text = 'Farmable Evo' + elif m.in_pem: + acquire_text = 'In PEM' + elif m.pem_evo: + acquire_text = 'PEM Evo' + elif m.in_rem: + acquire_text = 'In REM' + elif m.rem_evo: + acquire_text = 'REM Evo' + elif m.in_mpshop: + acquire_text = 'MP Shop' + elif m.mp_evo: + acquire_text = 'MP Shop Evo' + return acquire_text + + +def match_emoji(emoji_list, name): + for e in emoji_list: + if e.name == name: + return e + return name + + +def monsterToEmbed(m: "DgMonster", emoji_list): + embed = monsterToBaseEmbed(m) + + info_row_1 = monsterToTypeString(m) + acquire_text = monsterToAcquireString(m) + tet_text = m.true_evo_type.value + + os = "" if m.orb_skin_id is None else " (Orb Skin)" + + info_row_2 = '**Rarity** {}{}\n**Cost** {}'.format(m.rarity, os, m.cost) + if acquire_text: + info_row_2 += '\n**{}**'.format(acquire_text) + if m.is_inheritable: + info_row_2 += '\n**Inheritable**' + else: + info_row_2 += '\n**Not inheritable**' + if tet_text: + info_row_2 += '\n**{}**'.format(tet_text) + + embed.add_field(name=info_row_1, value=info_row_2) + + hp, atk, rcv, weighted = m.stats() + if m.limit_mult > 0: + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) + stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, m.limit_mult) + stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( + hp, lb_hp, atk, lb_atk, rcv, lb_rcv) + else: + stats_row_1 = 'Weighted {}'.format(weighted) + stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) + embed.add_field(name=stats_row_1, value=stats_row_2) + + awakenings_row = '' + for idx, a in enumerate(m.awakenings): + as_id = a.awoken_skill_id + as_name = a.name + mapped_awakening = AWAKENING_MAP.get(as_id, as_name) + mapped_awakening = match_emoji(emoji_list, mapped_awakening) + + # Wrap superawakenings to the next line + if len(m.awakenings) - idx == m.superawakening_count: + awakenings_row += '\n{}'.format(mapped_awakening) + else: + awakenings_row += ' {}'.format(mapped_awakening) + + awakenings_row = awakenings_row.strip() + + if not len(awakenings_row): + awakenings_row = 'No Awakenings' + + killers_row = '**Available Killers:** {}'.format(' '.join(m.killers)) + + embed.description = '{}\n{}'.format(awakenings_row, killers_row) + + active_header = 'Active Skill' + active_body = 'None' + active_skill = m.active_skill + if active_skill: + active_header = 'Active Skill ({} -> {})'.format(active_skill.turn_max, + active_skill.turn_min) + active_body = active_skill.desc + embed.add_field(name=active_header, value=active_body, inline=False) + + leader_skill = m.leader_skill + ls_row = m.leader_skill.desc if leader_skill else 'None' + ls_header = 'Leader Skill' + if leader_skill: + hp, atk, rcv, resist = m.leader_skill.data + multiplier_text = createMultiplierText(hp, atk, rcv, resist) + ls_header += " [ {} ]".format(multiplier_text) + embed.add_field(name=ls_header, value=ls_row, inline=False) + + return embed + + +def monsterToOtherInfoEmbed(m: "DgMonster"): + embed = monsterToBaseEmbed(m) + # Clear the thumbnail, takes up too much space + embed.set_thumbnail(url='') + + body_text = '\n' + stat_cols = ['', 'HP', 'ATK', 'RCV'] + for plus in (0, 297): + body_text += '**Stats at +{}:**'.format(plus) + tbl = prettytable.PrettyTable(stat_cols) + tbl.hrules = prettytable.NONE + tbl.vrules = prettytable.NONE + tbl.align = "l" + levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) + for lv in levels: + for inh in (False, True): + hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) + row_name = 'Lv{}'.format(lv) + if inh: + row_name = '(Inh)' + tbl.add_row([row_name.format(plus), hp, atk, rcv]) + body_text += box(tbl.get_string()) + + body_text += "\n**JP Name**: {}".format(m.name_jp) + body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + + if m.history_us: + body_text += '\n**History:** {}'.format(m.history_us) + + body_text += '\n**Series:** {}'.format(m.series.name) + body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) + if m.buy_mp is not None: + body_text += " **Buy MP:** {:,}".format(m.buy_mp) + + if m.exp < 1000000: + xp_text = '{:,}'.format(m.exp) + else: + xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' + body_text += '\n**XP to Max:** {}'.format(xp_text) + body_text += ' **Max Level:**: {}'.format(m.level) + body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) + + embed.description = body_text + + return embed + + +AWAKENING_MAP = { + 1: 'boost_hp', + 2: 'boost_atk', + 3: 'boost_rcv', + 4: 'reduce_fire', + 5: 'reduce_water', + 6: 'reduce_wood', + 7: 'reduce_light', + 8: 'reduce_dark', + 9: 'misc_autoheal', + 10: 'res_bind', + 11: 'res_blind', + 12: 'res_jammer', + 13: 'res_poison', + 14: 'oe_fire', + 15: 'oe_water', + 16: 'oe_wood', + 17: 'oe_light', + 18: 'oe_dark', + 19: 'misc_te', + 20: 'misc_bindclear', + 21: 'misc_sb', + 22: 'row_fire', + 23: 'row_water', + 24: 'row_wood', + 25: 'row_light', + 26: 'row_dark', + 27: 'misc_tpa', + 28: 'res_skillbind', + 29: 'oe_heart', + 30: 'misc_multiboost', + 31: 'killer_dragon', + 32: 'killer_god', + 33: 'killer_devil', + 34: 'killer_machine', + 35: 'killer_balance', + 36: 'killer_attacker', + 37: 'killer_physical', + 38: 'killer_healer', + 39: 'killer_evomat', + 40: 'killer_awoken', + 41: 'killer_enhancemat', + 42: 'killer_vendor', + 43: 'misc_comboboost', + 44: 'misc_guardbreak', + 45: 'misc_extraattack', + 46: 'teamboost_hp', + 47: 'teamboost_rcv', + 48: 'misc_voidshield', + 49: 'misc_assist', + 50: 'misc_super_extraattack', + 51: 'misc_skillcharge', + 52: 'res_bind_super', + 53: 'misc_te_super', + 54: 'res_cloud', + 55: 'res_seal', + 56: 'misc_sb_super', + 57: 'attack_boost_high', + 58: 'attack_boost_low', + 59: 'l_shield', + 60: 'l_attack', + 61: 'misc_super_comboboost', + 62: 'orb_combo', + 63: 'misc_voice', + 64: 'misc_dungeonbonus', + 65: 'reduce_hp', + 66: 'reduce_atk', + 67: 'reduce_rcv', + 68: 'res_blind_super', + 69: 'res_jammer_super', + 70: 'res_poison_super', + 71: 'misc_jammerboost', + 72: 'misc_poisonboost', +} + + +def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=None, resist2=None): + if all([x is None for x in (hp2, atk2, rcv2, resist2)]): + hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 + + def fmtNum(val): + return ('{:.2f}').format(val).strip('0').rstrip('.') + + text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) + if resist1 > 0 or resist2 > 0: + text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) + return text From 1e5faed16ab507295aa517ec4e999df452cef1c4 Mon Sep 17 00:00:00 2001 From: RheingoldRiver <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 21 Aug 2020 03:03:48 +0000 Subject: [PATCH 0017/1220] use new pipeline cdn --- dadguide/dadguide.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 6984ff91..a41fda46 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -51,7 +51,7 @@ def _data_file(file_name: str) -> str: BASENAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('basenames')) PANTHNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('panthnames')) -DB_DUMP_URL = 'https://f002.backblazeb2.com/file/dadguide-data/db/dadguide.sqlite' +DB_DUMP_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/db/dadguide.sqlite' DB_DUMP_FILE = _data_file('dadguide.sqlite') DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') From 93d7bcadfbb6125add24557e51effb5791e4eb38 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Thu, 20 Aug 2020 23:02:42 -0500 Subject: [PATCH 0018/1220] send users to official docs for install instr, change source to here --- README.md | 155 +++++++----------------------------------------------- 1 file changed, 18 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 9d6e07ae..c6fbf44a 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,9 @@ -# miru-v3cogs +# padbot-cogs -Red v3 Cogs developed for Miru Bot. +Red v3 Cogs developed originally for Miru Bot, now for Tsubaki Bot. Code should be pep8 formatted with a 100 character line limit. -# Cog status - -### Completed and tested - -``` - -``` - -### First pass completed - -``` -automod2 -azurelane -baduser -calculator -channelmod -chronomagia -dadguide -damagecalc -donations -fancysay -memes -modnotes -padboard -padbuilds -padevents -padglobal -padguidedb -padinfo -padmonitor -padrem -padsearch -padvision -profile -rpadutils -schoolidol -seniority -sqlactivitylog -stickers -streamcopy -timecog -translate -trutils -voicerole -``` - -``` -# annoying -speech -``` - - - - - - # Setting up your own Miru instance for contributing code ## Installing Tools @@ -71,7 +15,7 @@ speech ## Check out the repo -Fork this repo (you are probably already on the page, but if not [go here](https://github.com/nachoapps/miru-v3cogs). +Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/padbot-cogs). You will need a GitHub account to do this. Click the button that says "Fork" in the upper-right-hand corner of the page. I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) for development. Change into the @@ -85,91 +29,33 @@ git clone https://github.com/nachoapps/miru-v3cogs.git wget https://raw.githubusercontent.com/nachoapps/miru-v3cogs/master/requirements.txt ``` -## Set up a virtualenv -If using Windows, just follow the Redbot setup guide's instructions to install dependencies (which includes Python 3.8). +## Set up your bot +The installation instructions in Red's official documentation are pretty good. You will need to be prepared to do the following: +* Create a venv +* Install the Red library +* Create a Discord API key, make a bot, and associate it to your installation -One way to install python 3.8: +Installation instruction links: +* [Windows install instructions](https://docs.discord.red/en/stable/install_windows.html) +* [Linux/Mac install instructions](https://docs.discord.red/en/stable/install_linux_mac.html) -```shell script -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt update -sudo apt install python3.8 -``` - -Then set up your 3.8 venv: +## Installing additional dependencies +* For Romanji/Kana conversion we use a library called Romkan. There are some problems with the version in PyPI, so it's commented from `requirements.txt`. Instead use pip *inside your venv* to install it separately: ```shell script -# Get inside your cloned repo. -cd miru-v3cogs - -# Set up the venv. -virtualenv --python=/usr/bin/python3.8 venv - -# Atlernatively, install python3.8-venv (possibly python3.8-distutils) and do: -python3.8 -m venv venv - -# Activate your venv -Linux/OS X: -source venv/bin/activate - -Windows: -venv\Scripts\activate.bat - -# Install python deps, including Red. -pip install -r requirements.txt +pip install git+git://github.com/tejstead/python-romkan ``` -## Set up Red Bot - -``` -# You will use the output from pwd in the next steps, so copy it -Linux/OS X: -pwd - -Windows: -echo %cd% - -# Initial setup of bot -redbot-setup - -# Use whatever you like below, this is my example -bot name> miru_bot_test - -# Replace current_dir with output from pwd -data location> (current_dir)/bot_data -replace / with \ if on Windows - -# Pick json for now -storage backend> 1 - -# Now start the bot, replace the bot name below -redbot miru_bot_test - -# Follow the directions to get your bot token -# If copying from a v2 bot, the token is in data/red/settings.json -bot token> MjQxdzM53TE3MAM2NzA3NTg2.Xc4VVA.RIcv2Ndkxb - -# Pick whatever you want but consider not overlapping with miru which uses ^ -prefix> ! -``` +The rest of the guide takes place from inside Discord. +## Configuring development version Once the bot is launched, set it to use your repo directory as a cog path. Type this in Discord where the bot is: ``` !addpath (pwd output) ``` - - -* For Romanji/Kana conversion you will use Romkan. This is a bit tricky to install so we'll go over it first. - * If you want to skip this, you can locally comment out all calls to it, but this might be annoying when syncing your code if you're editing files that import it, so you should probably just do it. But if figuring this out is a barrier to entry to start actually coding, feel free to skip at least at the start. - * git clone [this repo](https://github.com/tejstead/python-romkan) into any folder you want. - * Open a command line in this same directory and run `py setup.py install` - * Locate the folder named romkan-0.2.1-py3.8.egg - * Copy the folder inside that folder, named `romkan`, and copy it to your virtual environment's lib folder (on Windows this is named `venv\lib\site-packages`) - - -## Setting up the bot +## Loading cogs * Some cogs will have cross dependencies on each other. Check the command prompt that Miru is running from if you encounter any errors. * Common dependencies include: @@ -179,13 +65,8 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type * Note that when you edit cogs with dependencies, you might need to do multiple reloads. For example, if updating dadguide, you will need to reload padinfo as well. * After you have done all of this, restart the bot again. Hopefully by now `^id ` should work! -## Other - -#TODO: update this lol - - ### Emoji -* If you want emojis in `^id` commands, and you are setting this up for DEV PURPOSES ONLY, you can talk to tactical_retreat about getting your bot invited to the emoji servers. If you want to make your own separate Miru instance though, you're on your own for that. +* If you want emojis in `^id` commands, and you are setting this up for DEV PURPOSES ONLY, you can talk to River about getting your bot invited to the emoji servers. If you want to make your own separate Miru instance though, you're on your own for that. * Give t_r your bot's invite link & ask him for the server IDs * Then use `^padinfo setemojiservers` with the IDs he gives you. The main Miru server is one of them, you can get that ID yourself. From d34dc7aa23144982795ac0508cb7dd7e8d1d902a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 21 Aug 2020 18:43:56 -0700 Subject: [PATCH 0019/1220] Globalban and Error-silencing --- README.md | 11 +++-- automod2/automod2.py | 2 +- globalban/__init__.py | 5 +++ globalban/globalban.py | 94 ++++++++++++++++++++++++++++++++++++++++++ padevents/padevents.py | 4 +- trutils/trutils.py | 25 ++++------- 6 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 globalban/__init__.py create mode 100644 globalban/globalban.py diff --git a/README.md b/README.md index c6fbf44a..194bd29b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Red v3 Cogs developed originally for Miru Bot, now for Tsubaki Bot. Code should be pep8 formatted with a 100 character line limit. -# Setting up your own Miru instance for contributing code +# Setting up your own Tsubaki instance for contributing code ## Installing Tools @@ -15,7 +15,7 @@ Code should be pep8 formatted with a 100 character line limit. ## Check out the repo -Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/padbot-cogs). +Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/padbot-cogs). You will need a GitHub account to do this. Click the button that says "Fork" in the upper-right-hand corner of the page. I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) for development. Change into the @@ -23,10 +23,10 @@ I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) ```shell script # Clone your repo, not this one. -git clone https://github.com/nachoapps/miru-v3cogs.git +git clone https://github.com/TsubakiBotPad/padbot-cogs.git # If you're just setting up a production bot, no need to do this. But you should create a directory and do: -wget https://raw.githubusercontent.com/nachoapps/miru-v3cogs/master/requirements.txt +wget https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/requirements.txt ``` ## Set up your bot @@ -100,7 +100,7 @@ contain utility libraries. | memes | CustomCommands except role-limited | | rpadutils | Utility library shared by many other libraries | | sqlactivitylog | Archives messages in sqlite, allows for lookup | -| timecog | Convert/print time in different timezones | +| timecog | Convert/print time in different timezones | | trutils | Misc utilities intended for my usage only | | twitter2 | Mirrors a twitter feed to a channel | @@ -112,4 +112,3 @@ Cogs not intended for normal use, or superceded. | Cog | Purpose | | --- | --- | | donations | Tracks users who have donated for hosting fees | - diff --git a/automod2/automod2.py b/automod2/automod2.py index 649248f1..80cb4854 100644 --- a/automod2/automod2.py +++ b/automod2/automod2.py @@ -21,7 +21,7 @@ LOGS_PER_CHANNEL_USER = 5 -AUTOMOD_HELP = """ +AUTOMOD_HELP = r""" Automod works by creating named global patterns, and then applying them in specific channels as either whitelist or blacklist rules. This allows you to customize what text can be typed in a channel. Text from moderators is diff --git a/globalban/__init__.py b/globalban/__init__.py new file mode 100644 index 00000000..b1df7a5f --- /dev/null +++ b/globalban/__init__.py @@ -0,0 +1,5 @@ +from .globalban import GlobalBan + + +def setup(bot): + bot.add_cog(GlobalBan(bot)) diff --git a/globalban/globalban.py b/globalban/globalban.py new file mode 100644 index 00000000..37d23f1d --- /dev/null +++ b/globalban/globalban.py @@ -0,0 +1,94 @@ +import asyncio +import discord +import datetime + +from redbot.core import commands, Config, checks, modlog +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import inline + + +class GlobalBan(commands.Cog): + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config = Config.get_conf(self, identifier=1437847847) + self.config.register_global(banned={}, opted=[]) + self.bot = bot + + @commands.group() + async def globalban(self, ctx): + """Global ban related commands.""" + + @globalban.command() + @checks.mod_or_permissions(administrator=True) + async def optin(self, ctx): + """Opt your server in to the Global Ban system.""" + async with self.config.opted() as opted: + opted.append(ctx.guild.id) + await self.update_gbs() + await ctx.send(inline("Done")) + + @globalban.command() + @checks.mod_or_permissions(administrator=True) + async def optout(self, ctx): + """Opt your server out of the Global Ban system.""" + async with self.config.opted() as opted: + while ctx.guild.id in opted: + opted.remove(ctx.guild.id) + await self.remove_gbs_guild(ctx.guild.id) + await ctx.send(inline("Done")) + + @globalban.command() + @checks.is_owner() + async def ban(self, ctx, user, *, reason=""): + """Globally Ban a user across all opted-in servers.""" + async with self.config.banned() as banned: + banned[user] = reason + await self.update_gbs() + await ctx.send(inline("Done")) + + @globalban.command() + @checks.is_owner() + async def unban(self, ctx, user): + """Globally Unban a user across all opted-in servers.""" + async with self.config.banned() as banned: + if user in banned: + del banned[user] + await self.remove_gbs_user(user) + await ctx.send(inline("Done")) + + + async def update_gbs(self): + for gid in await self.config.opted(): + guild = self.bot.get_guild(int(gid)) + for uid, reason in (await self.config.banned()).items(): + if uid in [b.user.id for b in await guild.bans()]: + continue + m = guild.get_member(int(uid)) + if m is None: + try: + await guild.ban(discord.Object(id=uid), reason="GlobalBan", delete_message_days=0) + except discord.errors.NotFound: + pass + else: + await guild.ban(m, reason="GlobalBan", delete_message_days=0) + await modlog.create_case(bot=self.bot, + guild=guild, + created_at=datetime.datetime.now(), + action_type="globalban", + user=m, + reason='GlobalBan') + + async def remove_gbs_guild(self, gid): + guild = self.bot.get_guild(int(gid)) + for ban in await guild.bans(): + user = b.user + if user.id not in await self.config.banned(): + continue + await guild.unban(user) + + async def remove_gbs_user(self, uid): + for gid in await self.config.opted(): + guild = self.bot.get_guild(int(gid)) + users = [b.user for b in await guild.bans() if b.user.id == int(uid)] + if users: + await guild.unban(users[0]) diff --git a/padevents/padevents.py b/padevents/padevents.py index 29908289..b64dc504 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -108,7 +108,9 @@ async def check_started(self): for daily_registration in list(self.settings.listDailyReg()): try: if server == daily_registration['server']: - await self.pageOutput(ctx, msg, channel_id=daily_registration['channel_id']) + await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), + msg, channel_id=daily_registration['channel_id']) + print("daily_reg server") except Exception as ex: traceback.print_exc() # self.settings.removeDailyReg( diff --git a/trutils/trutils.py b/trutils/trutils.py index 36b7fc10..188a8b49 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -23,7 +23,7 @@ so I will install a variety of random entertainment plugins. To invite public Miru to your server, use the following link: -https://discordapp.com/oauth2/authorize?client_id=296443771229569026&scope=bot +https://discord.com/oauth2/authorize?client_id=744768152201854987&scope=bot&permissions=536870920 The following commands might come in handy: `{0.prefix}modhelp` - information on how to set up Miru's moderation commands @@ -151,6 +151,8 @@ def __init__(self, bot: Red, *args, **kwargs): self.bot = bot self.settings = TrUtilsSettings("trutils") + self.sessions = set() + @commands.command() async def revertname(self, ctx): """Unsets your nickname""" @@ -195,9 +197,11 @@ async def traceback2(self, ctx: commands.Context, public: bool = False): else: await ctx.send("No exception has occurred yet") - @commands.command() + @commands.command(aliases=["gettsubaki", "getsubaki"]) async def getmiru(self, ctx): - """Tells you how to get Miru into your server""" + """Tells you how to get Tsubaki into your server""" + await ctx.send(inline("Unfortunately, you can't (yet).")) + return for page in pagify(GETMIRU_HELP.format(ctx), delims=['\n'], shorten_by=8): await ctx.author.send(box(page)) @@ -230,20 +234,9 @@ async def credits(self, ctx): "is available for other servers on request." "".format(red_repo, rpad_invite)) - baby_miru_url = "http://www.pixiv.net/member_illust.php?illust_id=57613867&mode=medium" - baby_miru_author = "BOW @ Pixiv" - cute_miru_url = "https://www.dropbox.com/s/0wlfx3g4mk8c8bg/Screenshot%202016-12-03%2018.39.37.png?dl=0" - cute_miru_author = "Pancaaake18 on discord" - bot_miru_url = "https://puu.sh/urTm8/c3bdf993bd.png" - bot_miru_author = "graps on discord" avatar = ( "Bot avatars supplied by:\n" - "\t[Baby Miru]({}): {}\n" - "\t[Cute Miru]({}): {}\n" - "\t[Bot Miru]({}): {}" - "".format(baby_miru_url, baby_miru_author, - cute_miru_url, cute_miru_author, - bot_miru_url, bot_miru_author)) + "\t[Tsubaki]({}): {}").format("www.example.com", "Gungho") using = ( "You can use `{0.prefix}help` to get a full list of commands.\n" @@ -252,7 +245,7 @@ async def credits(self, ctx): ) embed = discord.Embed() - embed.add_field(name="Instance owned by", value='tactical_retreat') + embed.add_field(name="Instance owned by", value='The Tsubaki Team') embed.add_field(name="About the bot", value=about, inline=False) embed.add_field(name="Using the bot", value=using.format(ctx), inline=False) embed.add_field(name="Avatar credits", value=avatar, inline=False) From bde2ee64e39abfe5b6e9560576193aa9d7646779 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 24 Aug 2020 14:27:21 -0700 Subject: [PATCH 0020/1220] Lower Verbosity --- dadguide/dadguide.py | 2 +- modnotes/modnotes.py | 6 +++--- padinfo/padinfo.py | 6 +++--- requirements.txt | 2 +- scheduler/scheduler.py | 2 +- speech/speech.py | 5 ++++- timecog/timecog.py | 2 -- trutils/trutils.py | 30 +++--------------------------- 8 files changed, 16 insertions(+), 39 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index a41fda46..25b06309 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -30,7 +30,7 @@ import rpadutils -logger = logging.getLogger('red.miruv3.dadguide') +logger = logging.getLogger('red.tsubaki.dadguide') def _data_file(file_name: str) -> str: diff --git a/modnotes/modnotes.py b/modnotes/modnotes.py index a08c1b75..d6d875c4 100644 --- a/modnotes/modnotes.py +++ b/modnotes/modnotes.py @@ -25,10 +25,10 @@ async def usernotes(self, ctx): This module allows you to create notes to share between moderators. """ - @usernotes.command(name="print") + @usernotes.command() @checks.mod_or_permissions(manage_guild=True) - async def _print(self, ctx, user: discord.User): - """Print the notes for a user.""" + async def get(self, ctx, user: discord.User): + """Sends the notes for a user.""" notes = self.settings.getNotesForUser(ctx.guild.id, user.id) if not notes: await ctx.send(box('No notes for {}'.format(user.name))) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0a566c5e..20059065 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -17,7 +17,7 @@ import rpadutils from rpadutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics -logger = logging.getLogger('red.miruv3.padinfo') +logger = logging.getLogger('red.tsubaki.padinfo') HELP_MSG = """ {0.prefix}helpid : shows this message @@ -206,9 +206,9 @@ async def reload_nicknames(self): while self == self.bot.get_cog('PadInfo'): try: await self.refresh_index() - print('Done refreshing PadInfo') + logger.info('Done refreshing PadInfo') except Exception as ex: - print("reload padinfo loop caught exception " + str(ex)) + logger.exception("reload padinfo loop caught exception " + str(ex)) traceback.print_exc() await asyncio.sleep(60 * 60 * 1) diff --git a/requirements.txt b/requirements.txt index 41caffe8..c4de0d58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ wheel Red-DiscordBot[postgres] backoff prettytable -# romkan - commented out because this has to be manually installed +romkan google-api google-api-python-client google-cloud diff --git a/scheduler/scheduler.py b/scheduler/scheduler.py index aadf9552..5f598c86 100644 --- a/scheduler/scheduler.py +++ b/scheduler/scheduler.py @@ -48,7 +48,7 @@ def __init__(self, bot): self.settings = SchedulerSettings("scheduler") self.events = self.settings.getevents() - self.queue = asyncio.PriorityQueue(loop=bot.loop) + self.queue = asyncio.PriorityQueue() self.queue_lock = asyncio.Lock() self.to_kill = {} self._load_events() diff --git a/speech/speech.py b/speech/speech.py index 387e0a66..0b20b449 100644 --- a/speech/speech.py +++ b/speech/speech.py @@ -1,4 +1,5 @@ import asyncio +import logging import discord from google.cloud import texttospeech @@ -10,11 +11,13 @@ from rpadutils import CogSettings, corowrap +logger = logging.getLogger('red.tsubaki.speech') + try: if not discord.opus.is_loaded(): discord.opus.load_opus('libopus.so.0') except: # Missing opus - print('Failed to load opus') + logger.warning('Failed to load opus') opus = None else: opus = True diff --git a/timecog/timecog.py b/timecog/timecog.py index b4007d19..ed9f7adb 100644 --- a/timecog/timecog.py +++ b/timecog/timecog.py @@ -180,7 +180,6 @@ async def every(self, ctx, *, text): return tinstart = "now" tinstrs, input = match.groups() - print(tinstrs, tinstart, input) start = (datetime.utcnow() + tin2tdelta(tinstart)).timestamp() async with self.config.channel(ctx.channel).schedules() as scs: scs.append((start, tin2tdelta(tinstrs).seconds, input)) @@ -247,7 +246,6 @@ async def reminderloop(self): async with self.config.channel(self.bot.get_channel(c)).schedules() as sco: scind = sco.index(sc) sco[scind][0] += sco[scind][1] - print(sco, scind, datetime.utcnow()) await self.bot.get_channel(c).send(sc[2]) @commands.command() diff --git a/trutils/trutils.py b/trutils/trutils.py index 188a8b49..a4b209d7 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -17,22 +17,6 @@ from rpadutils import CogSettings -GETMIRU_HELP = """ -The new public Miru is open for invite to any server: personal, private, secret-handshake-entry-only, etc -Unlike the private Miru used by larger community servers, public Miru has lower stability requirements, -so I will install a variety of random entertainment plugins. - -To invite public Miru to your server, use the following link: -https://discord.com/oauth2/authorize?client_id=744768152201854987&scope=bot&permissions=536870920 - -The following commands might come in handy: -`{0.prefix}modhelp` - information on how to set up Miru's moderation commands -`{0.prefix}userhelp` - a user-focused guide to Miru's commands -`{0.prefix}help` - the full list of Miru commands - -If you want to be notified of updates to Miru, suggest features, or ask for help, join the Miru Support server: -https://discord.gg/zB4QHgn -""" USER_HELP = """ Bot user help @@ -72,7 +56,7 @@ Use the series of commands starting with {0.prefix}profile to configure your own profile. Use one of the following commands to retrieve data. -{0.prefix}idme print your profile to the current channel +{0.prefix}idme send your profile to the current channel {0.prefix}idfor get profile data for a specific user {0.prefix}idto have Miru DM your profile to a user {0.prefix}profile search search the list of configured (visible) profiles @@ -197,14 +181,6 @@ async def traceback2(self, ctx: commands.Context, public: bool = False): else: await ctx.send("No exception has occurred yet") - @commands.command(aliases=["gettsubaki", "getsubaki"]) - async def getmiru(self, ctx): - """Tells you how to get Tsubaki into your server""" - await ctx.send(inline("Unfortunately, you can't (yet).")) - return - for page in pagify(GETMIRU_HELP.format(ctx), delims=['\n'], shorten_by=8): - await ctx.author.send(box(page)) - @commands.command() async def userhelp(self, ctx): """Shows a summary of the useful user features""" @@ -228,7 +204,7 @@ async def credits(self, ctx): about = ( "This is an instance of [the Red Discord bot]({}), " "use the 'info' command for more info. " - "The various PAD related cogs were created by tactical_retreat. " + "The various PAD related cogs were created by Aradia Megido and tactical_retreat. " "Massive overhaul of the bot to Red v3 by Aradia Megido. " "This bot was created for the [PAD Community Server Discord]({}) but " "is available for other servers on request." @@ -236,7 +212,7 @@ async def credits(self, ctx): avatar = ( "Bot avatars supplied by:\n" - "\t[Tsubaki]({}): {}").format("www.example.com", "Gungho") + "\t[Tsubaki]({}): {}").format("https://www.example.com", "Gungho") using = ( "You can use `{0.prefix}help` to get a full list of commands.\n" From 3708012e6baed16e77515e752bda54ca960eaa00 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 24 Aug 2020 15:08:24 -0700 Subject: [PATCH 0021/1220] opus fix --- speech/speech.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/speech/speech.py b/speech/speech.py index 0b20b449..7b3c1b08 100644 --- a/speech/speech.py +++ b/speech/speech.py @@ -1,5 +1,6 @@ import asyncio import logging +import ctypes.util import discord from google.cloud import texttospeech @@ -15,7 +16,7 @@ try: if not discord.opus.is_loaded(): - discord.opus.load_opus('libopus.so.0') + discord.opus.load_opus(ctypes.util.find_library('opus')) except: # Missing opus logger.warning('Failed to load opus') opus = None From 9295db6805ecf5fbc89c87cb283490982dcbaa51 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 24 Aug 2020 16:56:04 -0700 Subject: [PATCH 0022/1220] padinfo add --- padinfo/padinfo.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 20059065..d926ae20 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -567,7 +567,7 @@ async def padsay(self, ctx, server, *, query: str = None): m, err, debug_info = self.findMonster(query) if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na - base_dir = '/home/tactical0retreat/dadguide/data/media/voices' + base_dir = self.settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) header = '{} ({})'.format(monsterToHeader(m), server) if not os.path.exists(voice_file): @@ -608,6 +608,13 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): self.settings.setEmojiServers(emoji_servers.split(',')) await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) + @padinfo.command() + @checks.is_owner() + async def setvoicepath(self, ctx, *, path=''): + """Set path to the voice direcory""" + self.settings.setVoiceDir(path) + await ctx.send(inline('Done')) + @checks.is_owner() @padinfo.command() async def iddiff(self, ctx): @@ -695,6 +702,7 @@ def make_default_settings(self): config = { 'animation_dir': '', 'evo_id_users': [], + 'voice_dir_path': '', } return config @@ -727,6 +735,13 @@ def rmEvoID(self, user_id): def checkEvoID(self, user_id): return user_id in self.bot_settings['evo_id_users'] + def setVoiceDir(self, path): + self.bot_settings['voice_dir_path'] = path + self.save_settings() + + def voiceDir(self): + return self.bot_settings['voice_dir_path'] + def monsterToHeader(m: "DgMonster", link=False): msg = 'No. {} {}'.format(m.monster_no_na, m.name_na) From 8e3ca86e9b9c2a071caa896b85be4cf92934bd97 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 24 Aug 2020 17:10:57 -0700 Subject: [PATCH 0023/1220] locks --- padguidedb/padguidedb.py | 2 +- padinfo/padinfo.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 04b2ea1f..9fc0905d 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -186,7 +186,7 @@ async def cleardungeon(self, ctx, dungeon_id: int): # @padguidedb.command() @is_padguidedb_admin() async def dungeondata(self, ctx, dungeon_id: int): - with ctx.typing(), self.get_connection() as cursor: + with self.get_connection() as cursor: sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) results = list(cursor.fetchall()) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d926ae20..e0c155ae 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -167,6 +167,7 @@ def __init__(self, bot: Red, *args, **kwargs): self.index_all = None self.index_na = None self.index_jp = None + self.index_lock = asyncio.Lock() self.menu = Menu(bot) @@ -224,14 +225,15 @@ async def refresh_index(self, ctx=None): logger.info('Waiting until DG is ready') await dg_cog.wait_until_ready() - logger.info('Loading ALL index') - self.index_all = await dg_cog.create_index() + async with self.index_lock: + logger.info('Loading ALL index') + self.index_all = await dg_cog.create_index() - logger.info('Loading NA index') - self.index_na = await dg_cog.create_index(lambda m: m.on_na) + logger.info('Loading NA index') + self.index_na = await dg_cog.create_index(lambda m: m.on_na) - logger.info('Loading JP index') - self.index_jp = await dg_cog.create_index(lambda m: m.on_jp) + logger.info('Loading JP index') + self.index_jp = await dg_cog.create_index(lambda m: m.on_jp) logger.info('Done refreshing indexes') if ctx is not None: @@ -567,6 +569,9 @@ async def padsay(self, ctx, server, *, query: str = None): m, err, debug_info = self.findMonster(query) if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na + if voice_id is None: + await ctx.send(inline("No voice file found for "+m.name)) + return base_dir = self.settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) header = '{} ({})'.format(monsterToHeader(m), server) From 32ce6aeb58a138a30b7e829a9e5c164c31c735cd Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Sat, 29 Aug 2020 00:45:31 -0500 Subject: [PATCH 0024/1220] tejstead fork of romkan for windows installations --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c4de0d58..39ad4b5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ wheel Red-DiscordBot[postgres] backoff prettytable -romkan +git+https://github.com/tejstead/python-romkan google-api google-api-python-client google-cloud From 680a114ecf982afa6ec7b390efa31949fa879236 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Sat, 29 Aug 2020 01:06:02 -0500 Subject: [PATCH 0025/1220] updated some more requirements.txt stuff, say talk to river --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 194bd29b..4d2ee06c 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,8 @@ I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) `PycharmProjects` directory and clone your fork of the repo: ```shell script -# Clone your repo, not this one. +# Clone your repo, not this one!!!!!!!! You will want to load your own changes!!!! git clone https://github.com/TsubakiBotPad/padbot-cogs.git - -# If you're just setting up a production bot, no need to do this. But you should create a directory and do: -wget https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/requirements.txt ``` ## Set up your bot @@ -39,25 +36,30 @@ Installation instruction links: * [Windows install instructions](https://docs.discord.red/en/stable/install_windows.html) * [Linux/Mac install instructions](https://docs.discord.red/en/stable/install_linux_mac.html) +If you are running on a cloud server for the first time and aren't sure what Linux distro to pick, please, please, please, please, please pick Ubuntu. Their docs aren't well-tested on other distros. -## Installing additional dependencies -* For Romanji/Kana conversion we use a library called Romkan. There are some problems with the version in PyPI, so it's commented from `requirements.txt`. Instead use pip *inside your venv* to install it separately: +## Installing dependencies +First download `requirements.txt` so that you can run it from your Red venv. In Linux this is done by running the following from the directory holding the folder `name-of-bot` that you created during the Red setup process: ```shell script -pip install git+git://github.com/tejstead/python-romkan +wget https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/requirements.txt +``` +Then run: +```shell script +source name-of-bot/bin/activate +pip install -r requirements.txt +deactivate ``` - The rest of the guide takes place from inside Discord. ## Configuring development version Once the bot is launched, set it to use your repo directory as a cog path. Type this in Discord where the bot is: ``` -!addpath (pwd output) +!addpath $pathToYourRepoThatYouClonedBefore ``` - ## Loading cogs -* Some cogs will have cross dependencies on each other. Check the command prompt that Miru is running from if you encounter any errors. +* Some cogs will have cross dependencies on each other. Check the command prompt that Tsubaki is running from or type `^traceback 1` if you encounter any errors. * Common dependencies include: * rpadutils * dadguide @@ -66,9 +68,9 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type * After you have done all of this, restart the bot again. Hopefully by now `^id ` should work! ### Emoji -* If you want emojis in `^id` commands, and you are setting this up for DEV PURPOSES ONLY, you can talk to River about getting your bot invited to the emoji servers. If you want to make your own separate Miru instance though, you're on your own for that. -* Give t_r your bot's invite link & ask him for the server IDs -* Then use `^padinfo setemojiservers` with the IDs he gives you. The main Miru server is one of them, you can get that ID yourself. +* You probably don't actually need emoji to be working in order to do development for Tsubaki. +* There are three emoji servers: the main Tsubaki support server, `miru_extra_emoji`, and `miru_monsters_1`. River has access to these, talk to her if you think you need access to them. HOSTING YOUR OWN TSUBAKI/MIRU CLONE IS NOT REASON TO GET ACCESS TO THEM. In-depth development of the `^id` command may be reason to get access to them. +* You will use use `^padinfo setemojiservers` with the IDs she gives you. # Puzzle and Dragons From e7693382da899df53bb06511407d33af0a777f31 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 30 Aug 2020 22:22:44 -0700 Subject: [PATCH 0026/1220] padinfo fix --- baduser/baduser.py | 18 ++++++------------ dadguide/dadguide.py | 8 +++----- padguidedb/padguidedb.py | 38 ++++++++++++++++++++++++++++++++++---- padinfo/padinfo.py | 5 ++++- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/baduser/baduser.py b/baduser/baduser.py index ff338c51..8474d5a6 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -49,54 +49,48 @@ async def baduser(self, ctx): @baduser.command(name="addnegativerole") @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addNegativeRole(self, ctx, *, role): + async def addNegativeRole(self, ctx, *, role: discord.Role): """Designate a role as a 'punishment' role.""" - role = get_role(ctx.guild.roles, role) self.settings.addPunishmentRole(ctx.guild.id, role.id) await ctx.send(inline('Added punishment role "' + role.name + '"')) @baduser.command(name="rmnegativerole") @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmNegativeRole(self, ctx, *, role): + async def rmNegativeRole(self, ctx, *, role: discord.Role): """Cancels a role from 'punishment' status.""" - role = get_role(ctx.guild.roles, role) self.settings.rmPunishmentRole(ctx.guild.id, role.id) await ctx.send(inline('Removed punishment role "' + role.name + '"')) @baduser.command(name="addpositiverole") @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addPositiveRole(self, ctx, *, role): + async def addPositiveRole(self, ctx, *, role: discord.Role): """Designate a role as a 'benefit' role.""" - role = get_role(ctx.guild.roles, role) self.settings.addPositiveRole(ctx.guild.id, role.id) await ctx.send(inline('Added positive role "' + role.name + '"')) @baduser.command(name="rmpositiverole") @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmPositiveRole(self, ctx, *, role): + async def rmPositiveRole(self, ctx, *, role: discord.Role): """Cancels a role from 'benefit' status.""" - role = get_role(ctx.guild.roles, role) self.settings.rmPositiveRole(ctx.guild.id, role.id) await ctx.send(inline('Removed positive role "' + role.name + '"')) @baduser.command(name="addneutralrole") @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addNeutralRole(self, ctx, *, role): + async def addNeutralRole(self, ctx, *, role: discord.Role): """Designate a role as a notable but not ping-worthy role.""" - role = get_role(ctx.guild.roles, role) self.settings.addNeutralRole(ctx.guild.id, role.id) await ctx.send(inline('Added neutral role "' + role.name + '"')) @baduser.command(name="rmneutralrole") @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmNeutralRole(self, ctx, *, role): + async def rmNeutralRole(self, ctx, *, role: discord.Role): """Cancels a role from notable but not ping-worthy status.""" - role = get_role(ctx.guild.roles, role) self.settings.rmNeutralRole(ctx.guild.id, role.id) await ctx.send(inline('Removed neutral role "' + role.name + '"')) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 25b06309..938b3141 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -323,12 +323,10 @@ def load_database(existing_db): class DadguideDatabase(object): - def __init__(self, data_file=None): + def __init__(self, data_file): self._con = None - - if data_file is not None: - self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) - self._con.row_factory = lite.Row + self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) + self._con.row_factory = lite.Row def has_database(self): return self._con is not None diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 9fc0905d..d3782666 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -97,10 +97,24 @@ async def searchdungeon(self, ctx, *, search_text): @padguidedb.command() @checks.is_owner() async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): - """Set the dungeon script.""" + """Set the dungeon script file.""" self.settings.setDungeonScriptFile(dungeon_script_file) await ctx.send(inline('Done')) + @padguidedb.command() + @checks.is_owner() + async def setfulletlfile(self, ctx, *, full_etl_file): + """Set the full ETL file.""" + self.settings.setFullETLFile(full_etl_file) + await ctx.send(inline('Done')) + + @padguidedb.command() + @checks.is_owner() + async def setimageupdatefile(self, ctx, *, image_update_file): + """Set the image update file.""" + self.settings.setImageUpdateFile(image_update_file) + await ctx.send(inline('Done')) + @padguidedb.command() @checks.is_owner() async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): @@ -218,7 +232,7 @@ async def fulletl(self, ctx): await ctx.send(inline('Running full ETL pipeline: this could take a while')) process = await asyncio.create_subprocess_exec( 'bash', - '/home/tactical0retreat/dadguide/dadguide-jobs/run_loader.sh', + self.settings.fullETLFile(), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -243,7 +257,7 @@ async def extractimages(self, ctx): await ctx.send(inline('Running image extract pipeline: this could take a while')) process = await asyncio.create_subprocess_exec( 'bash', - '/home/tactical0retreat/dadguide/dadguide-jobs/media/update_image_files.sh', + self.settings.imageUpdateFile(), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -263,7 +277,9 @@ def make_default_settings(self): 'admins': [], 'config_file': '', 'dungeon_script_file': '', - 'users': {} + 'full_etl_file': '', + 'update_image_file': '', + 'users': {}, } return config @@ -300,6 +316,20 @@ def setDungeonScriptFile(self, dungeon_script_file): self.bot_settings['dungeon_script_file'] = dungeon_script_file self.save_settings() + def fullETLFile(self): + return self.bot_settings.get('dungeon_script_file', '') + + def setFullETLFile(self, full_etl_file): + self.bot_settings['full_etl_file'] = full_etl_file + self.save_settings() + + def updateImageFile(self): + return self.bot_settings.get('dungeon_script_file', '') + + def setUpdateImageFile(self, update_image_file): + self.bot_settings['update_image_file'] = update_image_file + self.save_settings() + def setUserInfo(self, server, user_uuid, user_intid): self.bot_settings['users'][server.upper()] = [user_uuid, user_intid] self.save_settings() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e0c155ae..09331617 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -205,14 +205,17 @@ def cog_unload(self): async def reload_nicknames(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadInfo'): + wait_time = 60 * 60 * 1 try: await self.refresh_index() logger.info('Done refreshing PadInfo') except Exception as ex: + wait_time = 60 + print(self.bot.get_cog('Dadguide').database._con) logger.exception("reload padinfo loop caught exception " + str(ex)) traceback.print_exc() - await asyncio.sleep(60 * 60 * 1) + await asyncio.sleep(wait_time) @commands.command() @checks.is_owner() From 9f89e3f312666e80586e996782fd2994f0b59a47 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 31 Aug 2020 13:16:08 -0700 Subject: [PATCH 0027/1220] Data Transfer --- datatransfer/__init__.py | 5 ++ datatransfer/datatransfer.py | 103 +++++++++++++++++++++++++++++++++++ memes/memes.py | 14 +++-- 3 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 datatransfer/__init__.py create mode 100644 datatransfer/datatransfer.py diff --git a/datatransfer/__init__.py b/datatransfer/__init__.py new file mode 100644 index 00000000..b2dc12ed --- /dev/null +++ b/datatransfer/__init__.py @@ -0,0 +1,5 @@ +from .datatransfer import * + + +def setup(bot): + bot.add_cog(DataTransfer(bot)) diff --git a/datatransfer/datatransfer.py b/datatransfer/datatransfer.py new file mode 100644 index 00000000..c0ca3c7d --- /dev/null +++ b/datatransfer/datatransfer.py @@ -0,0 +1,103 @@ +import json +import os +import re +import base64 +import io + +import discord +from redbot.core import checks, data_manager, commands +from redbot.core.utils.chat_formatting import inline, box, pagify + +class DataTransfer(commands.Cog): + """Transfer cog data.""" + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + """ + self._import = bot.get_cog("Alias").alias.command(name="import")(self._import) + self.export = bot.get_cog("Alias").alias.command()(self.export) + """ + + @commands.group(name="export") + @commands.guild_only() + @checks.mod_or_permissions(administrator=True) + async def _export(self, ctx): + """Data Export Utils""" + + @commands.group(name="import") + @commands.guild_only() + @checks.mod_or_permissions(administrator=True) + async def _import(self, ctx): + """Data Import Utils""" + + @_import.command(name="alias") + async def import_alias(self, ctx): + if not ctx.message.attachments: + await ctx.send(inline("Please attatch the .enc file retrieved from export function.")) + return + try: + data = json.loads(base64.b64decode(await ctx.message.attachments[0].read()).decode()) + + for k,v in data.items(): + await self.bot.get_cog("Alias").config.guild(ctx.guild).set_raw(k, value=v) + + await self.bot.get_cog("Core").reload(ctx, "alias") + await ctx.send(inline("Done.")) + except Exception as e: + print(e) + await ctx.send(inline("Invalid file.")) + + @_export.command(name="alias") + async def export_alias(self, ctx): + raw_data = (await self.bot.get_cog("Alias").config.all_guilds())[ctx.guild.id] + data = base64.b64encode(json.dumps(raw_data).encode()) + await ctx.send(file=discord.File(io.BytesIO(data), "alias_settings.enc")) + + @_import.command(name="customcom", aliases=["cc", "customcommands", "customcommand"]) + async def import_customcommand(self, ctx): + if not ctx.message.attachments: + await ctx.send(inline("Please attatch the .enc file retrieved from export function.")) + return + try: + data = json.loads(base64.b64decode(await ctx.message.attachments[0].read()).decode()) + + for k,v in data.items(): + await self.bot.get_cog("CustomCommands").config.guild(ctx.guild).set_raw(k, value=v) + + await self.bot.get_cog("Core").reload(ctx, "customcom") + await ctx.send(inline("Done.")) + except Exception as e: + print(e) + await ctx.send(inline("Invalid file.")) + + @_export.command(name="customcom", aliases=["cc", "customcommands", "customcommand"]) + async def export_customcommand(self, ctx): + raw_data = (await self.bot.get_cog("CustomCommands").config.all_guilds())[ctx.guild.id] + data = base64.b64encode(json.dumps(raw_data).encode()) + await ctx.send(file=discord.File(io.BytesIO(data), "cc_settings.enc")) + + @_import.command(name="memes", aliases=["meme"]) + async def import_meme(self, ctx): + if not ctx.message.attachments: + await ctx.send(inline("Please send the .enc file retrieved from export function.")) + return + try: + data = json.loads(base64.b64decode(await ctx.message.attachments[0].read()).decode()) + self.bot.get_cog("Memes").settings.bot_settings['configs'][ctx.guild.id] = data['configs'] + self.bot.get_cog("Memes").c_commands[ctx.guild.id] = data['commands'] + json.dump(self.bot.get_cog("Memes").c_commands, open(self.bot.get_cog("Memes").file_path, 'w+')) + await ctx.send(inline("Done.")) + except Exception as e: + print(e) + await ctx.send(inline("Invalid file.")) + + @_export.command(name="memes", aliases=["meme"]) + async def export(self, ctx): + raw_data = { + 'configs': self.bot.get_cog("Memes").settings.bot_settings['configs'].get(ctx.guild.id, {}), + 'commands': self.bot.get_cog("Memes").c_commands[ctx.guild.id], + } + data = base64.b64encode(json.dumps(raw_data).encode()) + await ctx.send(file=discord.File(io.BytesIO(data), "meme_settings.enc")) diff --git a/memes/memes.py b/memes/memes.py index 8bcf8d68..7b386333 100644 --- a/memes/memes.py +++ b/memes/memes.py @@ -1,12 +1,15 @@ import json import os import re +import base64 +import io import discord -from redbot.core import checks, data_manager -from redbot.core import commands +from redbot.core import checks, data_manager, commands +from redbot.core.utils.chat_formatting import inline, box, pagify -from rpadutils import CogSettings, get_role_from_id, get_role, safe_read_json + +from rpadutils import CogSettings, get_role_from_id, safe_read_json def _data_file(file_name: str) -> str: @@ -100,15 +103,14 @@ async def delmeme(self, ctx, command: str): @commands.command() @commands.guild_only() @checks.mod_or_permissions(administrator=True) - async def setmemerole(self, ctx, rolename: str): + async def setmemerole(self, ctx, role: discord.Role): """Sets the meme role Example: [p]setmemerole Regular""" - role = get_role(ctx.guild.roles, rolename) self.settings.setPrivileged(ctx.guild.id, role.id) - await ctx.send("done") + await ctx.send(inline("Done")) @commands.command() @commands.guild_only() From f72137781e6475191cd3f58fab5e8cc2e27003a8 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 31 Aug 2020 22:53:07 -0700 Subject: [PATCH 0028/1220] Data xfer --- datatransfer/datatransfer.py | 95 +++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/datatransfer/datatransfer.py b/datatransfer/datatransfer.py index c0ca3c7d..64c2c52c 100644 --- a/datatransfer/datatransfer.py +++ b/datatransfer/datatransfer.py @@ -3,11 +3,15 @@ import re import base64 import io +import aiohttp import discord from redbot.core import checks, data_manager, commands from redbot.core.utils.chat_formatting import inline, box, pagify +R_MESSAGE_LINK = r"https://discordapp.com/channels/(\d+)/(\d+)/(\d+)" +R_ATTATCH_LINK = r"https://cdn.discordapp.com/attachments/\d+/\d+/.+" + class DataTransfer(commands.Cog): """Transfer cog data.""" @@ -24,21 +28,37 @@ def __init__(self, bot, *args, **kwargs): @commands.guild_only() @checks.mod_or_permissions(administrator=True) async def _export(self, ctx): - """Data Export Utils""" + """Get a .enc file to load data on another bot via the import command.""" @commands.group(name="import") @commands.guild_only() @checks.mod_or_permissions(administrator=True) async def _import(self, ctx): - """Data Import Utils""" + """Load data from another bot via an attatched .enc file.""" @_import.command(name="alias") - async def import_alias(self, ctx): - if not ctx.message.attachments: - await ctx.send(inline("Please attatch the .enc file retrieved from export function.")) + async def import_alias(self, ctx, bot_mention: discord.User, link_or_attatchment=None): + if not bot_mention == ctx.me: + return + + if re.match(R_MESSAGE_LINK, link_or_attatchment): + m = await commands.MessageConverter().convert(ctx, link_or_attatchment) + raw_data = await m.attachments[0].read() + elif re.match(R_ATTATCH_LINK, link_or_attatchment): + async with aiohttp.ClientSession() as session: + async with session.get(link_or_attatchment) as response: + raw_data = await response.read() + elif ctx.message.attachments: + raw_data = await ctx.message.attachments[0].read() + elif link_or_attatchment is None: + await ctx.send(inline("Please supply the .enc file retrieved from export function.")) return + else: + await ctx.send(inline("Invalid link to file. Please use a message or attachment link.")) + return + try: - data = json.loads(base64.b64decode(await ctx.message.attachments[0].read()).decode()) + data = json.loads(base64.b64decode(raw_data).decode()) for k,v in data.items(): await self.bot.get_cog("Alias").config.guild(ctx.guild).set_raw(k, value=v) @@ -50,18 +70,37 @@ async def import_alias(self, ctx): await ctx.send(inline("Invalid file.")) @_export.command(name="alias") - async def export_alias(self, ctx): + async def export_alias(self, ctx, bot_mention: discord.Member): + if not bot_mention == ctx.me: + return + raw_data = (await self.bot.get_cog("Alias").config.all_guilds())[ctx.guild.id] data = base64.b64encode(json.dumps(raw_data).encode()) await ctx.send(file=discord.File(io.BytesIO(data), "alias_settings.enc")) @_import.command(name="customcom", aliases=["cc", "customcommands", "customcommand"]) - async def import_customcommand(self, ctx): - if not ctx.message.attachments: - await ctx.send(inline("Please attatch the .enc file retrieved from export function.")) + async def import_customcommand(self, ctx, bot_mention: discord.User, link_or_attatchment=None): + if not bot_mention == ctx.me: + return + + if re.match(R_MESSAGE_LINK, link_or_attatchment): + m = await commands.MessageConverter().convert(ctx, link_or_attatchment) + raw_data = await m.attachments[0].read() + elif re.match(R_ATTATCH_LINK, link_or_attatchment): + async with aiohttp.ClientSession() as session: + async with session.get(link_or_attatchment) as response: + raw_data = await response.read() + elif ctx.message.attachments: + raw_data = await ctx.message.attachments[0].read() + elif link_or_attatchment is None: + await ctx.send(inline("Please supply the .enc file retrieved from export function.")) + return + else: + await ctx.send(inline("Invalid link to file. Please use a message or attachment link.")) return + try: - data = json.loads(base64.b64decode(await ctx.message.attachments[0].read()).decode()) + data = json.loads(base64.b64decode(raw_data).decode()) for k,v in data.items(): await self.bot.get_cog("CustomCommands").config.guild(ctx.guild).set_raw(k, value=v) @@ -73,18 +112,37 @@ async def import_customcommand(self, ctx): await ctx.send(inline("Invalid file.")) @_export.command(name="customcom", aliases=["cc", "customcommands", "customcommand"]) - async def export_customcommand(self, ctx): + async def export_customcommand(self, ctx, bot_mention: discord.Member): + if not bot_mention == ctx.me: + return + raw_data = (await self.bot.get_cog("CustomCommands").config.all_guilds())[ctx.guild.id] data = base64.b64encode(json.dumps(raw_data).encode()) await ctx.send(file=discord.File(io.BytesIO(data), "cc_settings.enc")) @_import.command(name="memes", aliases=["meme"]) - async def import_meme(self, ctx): - if not ctx.message.attachments: - await ctx.send(inline("Please send the .enc file retrieved from export function.")) + async def import_meme(self, ctx, bot_mention: discord.User, link_or_attatchment=None): + if not bot_mention == ctx.me: + return + + if re.match(R_MESSAGE_LINK, link_or_attatchment): + m = await commands.MessageConverter().convert(ctx, link_or_attatchment) + raw_data = await m.attachments[0].read() + elif re.match(R_ATTATCH_LINK, link_or_attatchment): + async with aiohttp.ClientSession() as session: + async with session.get(link_or_attatchment) as response: + raw_data = await response.read() + elif ctx.message.attachments: + raw_data = await ctx.message.attachments[0].read() + elif link_or_attatchment is None: + await ctx.send(inline("Please supply the .enc file retrieved from export function.")) return + else: + await ctx.send(inline("Invalid link to file. Please use a message or attachment link.")) + return + try: - data = json.loads(base64.b64decode(await ctx.message.attachments[0].read()).decode()) + data = json.loads(base64.b64decode(raw_data).decode()) self.bot.get_cog("Memes").settings.bot_settings['configs'][ctx.guild.id] = data['configs'] self.bot.get_cog("Memes").c_commands[ctx.guild.id] = data['commands'] json.dump(self.bot.get_cog("Memes").c_commands, open(self.bot.get_cog("Memes").file_path, 'w+')) @@ -94,7 +152,10 @@ async def import_meme(self, ctx): await ctx.send(inline("Invalid file.")) @_export.command(name="memes", aliases=["meme"]) - async def export(self, ctx): + async def export(self, ctx, bot_mention: discord.Member): + if not bot_mention == ctx.me: + return + raw_data = { 'configs': self.bot.get_cog("Memes").settings.bot_settings['configs'].get(ctx.guild.id, {}), 'commands': self.bot.get_cog("Memes").c_commands[ctx.guild.id], From bb0ed21d91075ef9427c7467ab65c9c55e6457c7 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 31 Aug 2020 22:54:10 -0700 Subject: [PATCH 0029/1220] Add prefices --- datatransfer/datatransfer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datatransfer/datatransfer.py b/datatransfer/datatransfer.py index 64c2c52c..f2d55f78 100644 --- a/datatransfer/datatransfer.py +++ b/datatransfer/datatransfer.py @@ -28,13 +28,13 @@ def __init__(self, bot, *args, **kwargs): @commands.guild_only() @checks.mod_or_permissions(administrator=True) async def _export(self, ctx): - """Get a .enc file to load data on another bot via the import command.""" + """Get a .enc file to load data on another bot via the [p]import command.""" @commands.group(name="import") @commands.guild_only() @checks.mod_or_permissions(administrator=True) async def _import(self, ctx): - """Load data from another bot via an attatched .enc file.""" + """Load data from another bot via an attatched .enc file. (Obtain this with [p]export)""" @_import.command(name="alias") async def import_alias(self, ctx, bot_mention: discord.User, link_or_attatchment=None): From a6e8b300c710dcd1e26c351ed6d29fa9ec79c38b Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Sep 2020 12:58:20 -0700 Subject: [PATCH 0030/1220] Trutils Updates --- trutils/trutils.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/trutils/trutils.py b/trutils/trutils.py index a4b209d7..c5a9e0e3 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -23,8 +23,8 @@ This command gives you an overview of the most commonly used user-focused commands, with an emphasis on the ones unique to this bot. -Join the Miru Support Server for info, update, and bot support: -https://discord.gg/zB4QHgn +Join the Tsubaki Support Server for info, update, and bot support: +https://discord.gg/tVPmeG8 Use {0.prefix}help to get a full list of help commands. Execute any command with no arguments to get more details on how they work. @@ -52,13 +52,13 @@ {0.prefix}pic display the image for a monster Profile: -Miru will store your personal PAD details, and provide them on request. +Tsubaki will store your personal PAD details, and provide them on request. Use the series of commands starting with {0.prefix}profile to configure your own profile. Use one of the following commands to retrieve data. {0.prefix}idme send your profile to the current channel {0.prefix}idfor get profile data for a specific user -{0.prefix}idto have Miru DM your profile to a user +{0.prefix}idto have Tsubaki DM your profile to a user {0.prefix}profile search search the list of configured (visible) profiles Time conversion: @@ -73,7 +73,7 @@ Bot Moderator Help ~~~~~~~~~~~~~~~~~~~~~~ -Miru is a set of plugins inside the Red Discord bot, running on discord.py. There +Tsubaki is a set of plugins inside the Red Discord bot, running on discord.py. There are some custom ones, but a lot of them are generic to all Red Discord bots, so things you've used elsewhere will probably also work here. @@ -86,7 +86,7 @@ Check out the {0.prefix}help command from inside your server. You'll see a wider list of commands than normal users do. -If you've just added Miru to your server, start with the {0.prefix}modset command. You +If you've just added Tsubaki to your server, start with the {0.prefix}modset command. You might want to configure an Admin and a Mod role (they can be the same thing). ~~~~~~~~~~~~~~~~~~~~~~ @@ -113,15 +113,15 @@ applied to a user, the last 10 things they said (and where they said it) are recorded, and a strike is added to their record. -You can configure a channel where Miru will log when these moderation events occur, and ping +You can configure a channel where Tsubaki will log when these moderation events occur, and ping @here asking for an explanation. She will also track when a user with a strike leaves the server, and when they rejoin the server (as this is generally done to evade negative roles). Custom commands: -Miru supports three types of custom commands, you can find the list of associated commands via {0.prefix}help. +Tsubaki supports three types of custom commands, you can find the list of associated commands via {0.prefix}help. * CustomCommands: Added by server mods, executable by anyone * Memes: Added by server mods, executable only by people with a specific Role (configured by mods) -* Pad: Added by Miru PAD admins and executable by users in any server +* Pad: Added by Tsubaki PAD admins and executable by users in any server Limiting command execution: The '{0.prefix}p' command can be used to prevent users from executing specific commands on the server, @@ -203,16 +203,16 @@ async def credits(self, ctx): about = ( "This is an instance of [the Red Discord bot]({}), " - "use the 'info' command for more info. " + "use the '{}info' command for more info. " "The various PAD related cogs were created by Aradia Megido and tactical_retreat. " "Massive overhaul of the bot to Red v3 by Aradia Megido. " "This bot was created for the [PAD Community Server Discord]({}) but " "is available for other servers on request." - "".format(red_repo, rpad_invite)) + "".format(red_repo, ctx.prefix, rpad_invite)) avatar = ( "Bot avatars supplied by:\n" - "\t[Tsubaki]({}): {}").format("https://www.example.com", "Gungho") + "\t[Tsubaki]({}): {}").format("https://twitter.com/_violebot", "Violebot") using = ( "You can use `{0.prefix}help` to get a full list of commands.\n" @@ -382,7 +382,7 @@ async def feedback(self, ctx, *, message: str): """ feedback_channel = self.bot.get_channel(int(self.settings.get_feedback_channel())) await self._send_feedback(ctx, message, feedback_channel, - " Join the Miru Server to see any responses ({0.prefix}miruserver).".format(ctx)) + " Join the Tsubaki Server to see any responses ({0.prefix}miruserver).".format(ctx)) @commands.command() @commands.guild_only() From a4ee497f5a5308d8a5bde715d7ec2a6286f57361 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Sep 2020 13:07:06 -0700 Subject: [PATCH 0031/1220] donations fix --- dadguide/dadguide.py | 1 - donations/donations.py | 223 +++++++++++------------------------------ 2 files changed, 56 insertions(+), 168 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 938b3141..1c99f76d 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -324,7 +324,6 @@ def load_database(existing_db): class DadguideDatabase(object): def __init__(self, data_file): - self._con = None self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) self._con.row_factory = lite.Row diff --git a/donations/donations.py b/donations/donations.py index 273347bb..69dd5efe 100644 --- a/donations/donations.py +++ b/donations/donations.py @@ -12,11 +12,10 @@ DONATE_MSG = """ To donate to cover bot hosting fees you can use one of: - Patreon : https://www.patreon.com/miru_bot - Venmo : https://venmo.com/TacticalRetreat + Patreon : https://www.patreon.com/tsubaki_bot -Read the Patreon or join the Miru Support Server for more details: - https://discord.gg/zB4QHgn +Read the Patreon or join the Tsubaki Support Server for more details: + https://discord.gg/tVPmeG8 You permanently get some special perks for donating even $1. @@ -45,6 +44,22 @@ def roll(chance: int): return random.randrange(100) < chance +def is_patron(ctx): + tsubaki_guild = ctx.bot.get_guild(746131494875168770) + patron_role = tsubaki_guild.get_role(749849451769757726) + author = tsubaki_guild.get_member(ctx.author.id) + if author is None or patron_role not in author.roles: + return False + return True + +def is_donor(ctx): + tsubaki_guild = ctx.bot.get_guild(746131494875168770) + donor_role = tsubaki_guild.get_role(749849518467580015) + patron_role = tsubaki_guild.get_role(749849451769757726) + author = tsubaki_guild.get_member(ctx.author.id) + if author is None or (donor_role not in author.roles and patron_role not in author.roles): + return False + return True class Donations(commands.Cog): """Manages donations and perks.""" @@ -69,33 +84,34 @@ def __init__(self, bot: Red, *args, **kwargs): self.sexy_list = love_json.get('sexy', DEFAULT_LOVE['sexy']) self.perverted_list = love_json.get('perverted', DEFAULT_LOVE['perverted']) + self.tsubaki_guild = self.bot.get_guild(746131494875168770) + self.donor_role = self.tsubaki_guild.get_role(749849518467580015) + self.patron_role = self.tsubaki_guild.get_role(749849451769757726) + @commands.command() async def donate(self, ctx): """Prints information about donations.""" - donors = self.settings.donors() donor_names = set() - for user in self.bot.get_all_members(): - if user.id in donors: + for user in self.tsubaki_guild.members: + if self.donor_role in user.roles or self.patron_role in user.roles: donor_names.add(user.name) - msg = DONATE_MSG.format(count=len(donors), donors=', '.join(sorted(donor_names))) + msg = DONATE_MSG.format(count=len(donor_names), donors=', '.join(sorted(donor_names))) await ctx.send(box(msg)) @commands.command() + @commands.check(is_donor) async def mycommand(self, ctx, command: str, *, text: str): - """Sets your custom command (donor only).""" - user_id = ctx.author.id + """Sets your custom command.""" text = clean_global_mentions(text) - if user_id not in self.settings.donors(): - await ctx.send(inline('Only donors can set a personal command')) - return - self.settings.addCustomCommand(user_id, command, text) + self.settings.addCustomCommand(ctx.author.id, command, text) await ctx.send(inline('I set up your command: ' + command)) @commands.command() + @commands.check(is_donor) async def myembed(self, ctx, command: str, title: str, url: str, footer: str): - """Sets your custom embed command (donor only). + """Sets your custom embed command. This lets you create a fancier image message. For example you can set up a simple inline image without a link using: @@ -107,107 +123,43 @@ async def myembed(self, ctx, command: str, title: str, url: str, footer: str): Want a footer? Fill in the last argument: [p]myembed lewd "L-lewd!" "" "source: some managa i read" """ - user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Only donors can set a personal command')) - return - self.settings.addCustomEmbed(user_id, command, title, url, footer) + self.settings.addCustomEmbed(ctx.author.id, command, title, url, footer) await ctx.send(inline('I set up your embed: ' + command)) @commands.command() + @commands.check(is_donor) async def spankme(self, ctx): - """You are trash (donor only).""" - user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Donor-only command')) - return - + """You are trash.""" await ctx.send(ctx.author.mention + ' ' + random.choice(self.insults_list)) @commands.command() + @commands.check(is_donor) async def insultme(self, ctx): - """You are consistently trash (donor only).""" + """You are consistently trash.""" user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Donor-only command')) - return self.settings.addInsultsEnabled(user_id) await ctx.send(ctx.author.mention + ' ' 'Oh, I will.\n' + random.choice(self.insults_list)) - @commands.command(hidden=True) - async def insultripper(self, ctx): - """Fuck ripper.""" - ripper_id = '123529484476350467' - ripper = ctx.guild.get_member(ripper_id) - insult = random.choice(self.insults_list) - if ripper: - await ctx.send(ripper.mention + ' ' + insult) - else: - await ctx.send('Ripper is not in this server but I let him know anyway') - ripper = discord.utils.get(self.bot.get_all_members(), id=ripper_id) - await ripper.send('{} asked me to send you this:\n{}'.format(ctx.author.name, insult)) - - @commands.command() - async def vcinsultripper(self, ctx): - """Fuck ripper (verbally)""" - user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Donor-only command')) - return - - ripper_id = '123529484476350467' - ripper = ctx.guild.get_member(ripper_id) - - if ripper is None: - await ctx.send(inline('Ripper must be in this server to use this command')) - - voice = ripper.voice - if not voice: - await ctx.send(inline('Ripper must be in a voice channel on this server to use this command')) - return - - channel = voice.channel - - insult = random.choice(self.insults_list) - speech_cog = self.bot.get_cog('Speech') - if not speech_cog: - await ctx.send(inline('Speech seems to be offline')) - return - - await ctx.send(ripper.mention + ' ' + insult) - await speech_cog.speak(channel, 'Hey Ripper, ' + insult) - @commands.command() + @commands.check(is_donor) async def plsno(self, ctx): - """I am merciful (donor only).""" - user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Donor-only command')) - return + """I am merciful.""" - self.settings.rmInsultsEnabled(user_id) + self.settings.rmInsultsEnabled(ctx.author.id) await ctx.send('I will let you off easy this time.') @commands.command() + @commands.check(is_donor) async def kissme(self, ctx): - """You are so cute! (donor only).""" - user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Donor-only command')) - return - + """You are so cute!.""" await ctx.send(ctx.author.mention + ' ' + random.choice(self.cute_list)) @commands.command() + @commands.check(is_donor) async def lewdme(self, ctx): - """So nsfw. (donor only).""" - user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Donor-only command')) - return - + """So nsfw..""" if 'nsfw' in ctx.channel.name.lower(): await ctx.send(ctx.author.mention + ' ' + random.choice(self.sexy_list)) else: @@ -215,13 +167,9 @@ async def lewdme(self, ctx): await ctx.author.send(random.choice(self.sexy_list)) @commands.command() + @commands.check(is_donor) async def pervme(self, ctx): - """Hentai!!! (donor only).""" - user_id = ctx.author.id - if user_id not in self.settings.donors(): - await ctx.send(inline('Donor-only command')) - return - + """Hentai!!!.""" if 'nsfw' in ctx.channel.name.lower(): await ctx.send(ctx.author.mention + ' ' + random.choice(self.perverted_list)) else: @@ -234,7 +182,6 @@ async def donations(self, ctx): """Manage donation options.""" @donations.command() - @checks.admin_or_permissions(manage_guild=True) async def togglePerks(self, ctx): """Enable or disable donor-specific perks for the server.""" server_id = ctx.guild.id @@ -245,40 +192,13 @@ async def togglePerks(self, ctx): self.settings.addDisabledServer(server_id) await ctx.send(inline('Donor perks disabled on this server')) - @donations.command() - @checks.is_owner() - async def addDonor(self, ctx, user: discord.User): - """Adds a a user as a donor.""" - self.settings.addDonor(user.id) - await ctx.send(inline('Done')) - - @donations.command() - @checks.is_owner() - async def rmDonor(self, ctx, user: discord.User): - """Removes a user as a donor.""" - self.settings.rmDonor(user.id) - await ctx.send(inline('Done')) - - @donations.command() - @checks.is_owner() - async def addPatron(self, ctx, user: discord.User): - """Adds a a user as a patron.""" - self.settings.addPatron(user.id) - await ctx.send(inline('Done')) - - @donations.command() - @checks.is_owner() - async def rmPatron(self, ctx, user: discord.User): - """Removes a user as a patron.""" - self.settings.rmPatron(user.id) - await ctx.send(inline('Done')) - @donations.command() @checks.is_owner() async def info(self, ctx): """Print donation related info.""" - patrons = self.settings.patrons() - donors = self.settings.donors() + patrons = [user for user in self.tsubaki_guild.members if self.patron_role in user.roles] + donors = [user for user in self.tsubaki_guild.members if self.donor_role in user.roles + and self.patron_role not in user.roles] cmds = self.settings.customCommands() embeds = self.settings.customEmbeds() disabled_servers = self.settings.disabledServers() @@ -288,12 +208,12 @@ async def info(self, ctx): msg = 'Donations Info' msg += '\n\nPatrons:' - for user_id in patrons: - msg += '\n\t{} ({})'.format(id_to_name.get(user_id, 'unknown'), user_id) + for user in patrons: + msg += '\n\t{} ({})'.format(user.name, user.id) msg += '\n\nDonors:' - for user_id in donors: - msg += '\n\t{} ({})'.format(id_to_name.get(user_id, 'unknown'), user_id) + for user in donors: + msg += '\n\t{} ({})'.format(user.name, user.id) msg += '\n\nDisabled servers:' for server_id in disabled_servers: @@ -313,7 +233,8 @@ async def checkCC(self, message): prefix = (await self.bot.get_prefix(message))[0] user_id = message.author.id - if user_id not in self.settings.donors(): + if user_id not in [user.id for user in self.tsubaki_guild if self.donor_role in user.roles] and \ + user_id not in [user.id for user in self.tsubaki_guild if self.patron_role in user.roles]: return if message.guild and message.guild.id in self.settings.disabledServers(): @@ -359,10 +280,10 @@ async def check_insult(self, message): msg = message.author.mention # Pretty frequently respond to direct messages - mentions_bot = re.search(r'(miru|myr) bot', content, re.IGNORECASE) and roll(40) + mentions_bot = re.search(r'(miru|myr|tsubaki) bot', content, re.IGNORECASE) and roll(40) # Semi-frequently respond to miru in msg mentions_miru_and_roll = re.search( - r'\b(miru|myr)\b', content, re.IGNORECASE) and roll(20) + r'\b(miru|myr|tsubaki)\b', content, re.IGNORECASE) and roll(20) if mentions_bot or mentions_miru_and_roll: msg += ' ' + random.choice(self.insults_miru_reference) @@ -390,8 +311,6 @@ async def check_insult(self, message): class DonationsSettings(CogSettings): def make_default_settings(self): config = { - 'patrons': [], - 'donors': [], 'custom_commands': {}, 'custom_embeds': {}, 'disabled_servers': [], @@ -399,36 +318,6 @@ def make_default_settings(self): } return config - def patrons(self): - return self.bot_settings['patrons'] - - def addPatron(self, user_id): - patrons = self.patrons() - if user_id not in patrons: - patrons.append(user_id) - self.save_settings() - - def rmPatron(self, user_id): - patrons = self.patrons() - if user_id in patrons: - patrons.remove(user_id) - self.save_settings() - - def donors(self): - return self.bot_settings['donors'] - - def addDonor(self, user_id): - donors = self.donors() - if user_id not in donors: - donors.append(user_id) - self.save_settings() - - def rmDonor(self, user_id): - donors = self.donors() - if user_id in donors: - donors.remove(user_id) - self.save_settings() - def customCommands(self): return self.bot_settings['custom_commands'] From 82ee201d40ca88e837813f65f0bdc49317a656cc Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Sep 2020 13:21:17 -0700 Subject: [PATCH 0032/1220] made it so it can shout at you --- donations/donations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/donations/donations.py b/donations/donations.py index 69dd5efe..385f4ca6 100644 --- a/donations/donations.py +++ b/donations/donations.py @@ -233,8 +233,8 @@ async def checkCC(self, message): prefix = (await self.bot.get_prefix(message))[0] user_id = message.author.id - if user_id not in [user.id for user in self.tsubaki_guild if self.donor_role in user.roles] and \ - user_id not in [user.id for user in self.tsubaki_guild if self.patron_role in user.roles]: + if user_id not in [user.id for user in self.tsubaki_guild.members if self.donor_role in user.roles] and \ + user_id not in [user.id for user in self.tsubaki_guild.members if self.patron_role in user.roles]: return if message.guild and message.guild.id in self.settings.disabledServers(): From 4ab53254d4eed6b286f56cf05a8f4c97a628fcd7 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Sep 2020 16:01:05 -0700 Subject: [PATCH 0033/1220] wait until ready --- donations/donations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/donations/donations.py b/donations/donations.py index 385f4ca6..88e3f17a 100644 --- a/donations/donations.py +++ b/donations/donations.py @@ -84,6 +84,7 @@ def __init__(self, bot: Red, *args, **kwargs): self.sexy_list = love_json.get('sexy', DEFAULT_LOVE['sexy']) self.perverted_list = love_json.get('perverted', DEFAULT_LOVE['perverted']) + await bot.wait_until_ready() self.tsubaki_guild = self.bot.get_guild(746131494875168770) self.donor_role = self.tsubaki_guild.get_role(749849518467580015) self.patron_role = self.tsubaki_guild.get_role(749849451769757726) From 442368825d75100578a2834781b0e57373cdedb8 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Sep 2020 16:07:00 -0700 Subject: [PATCH 0034/1220] Fixed --- donations/__init__.py | 6 ++++-- donations/donations.py | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/donations/__init__.py b/donations/__init__.py index ee75fcb4..1c0c83ee 100644 --- a/donations/__init__.py +++ b/donations/__init__.py @@ -1,5 +1,7 @@ from .donations import * - + def setup(bot): - bot.add_cog(Donations(bot)) + n = Donations(bot) + bot.add_cog(n) + bot.loop.create_task(n.set_server_attributes()) diff --git a/donations/donations.py b/donations/donations.py index 88e3f17a..17f00b8f 100644 --- a/donations/donations.py +++ b/donations/donations.py @@ -84,7 +84,12 @@ def __init__(self, bot: Red, *args, **kwargs): self.sexy_list = love_json.get('sexy', DEFAULT_LOVE['sexy']) self.perverted_list = love_json.get('perverted', DEFAULT_LOVE['perverted']) - await bot.wait_until_ready() + self.tsubaki_guild = None + self.donor_role = None + self.patron_role = None + + async def set_server_attributes(self): + await self.bot.wait_until_ready() self.tsubaki_guild = self.bot.get_guild(746131494875168770) self.donor_role = self.tsubaki_guild.get_role(749849518467580015) self.patron_role = self.tsubaki_guild.get_role(749849451769757726) From dbf4d70b3215674a0e886324a8b5e87ea36560b8 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Sep 2020 17:03:01 -0700 Subject: [PATCH 0035/1220] BadUser --- baduser/baduser.py | 83 +++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/baduser/baduser.py b/baduser/baduser.py index 8474d5a6..30094cbe 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -16,6 +16,8 @@ LOGS_PER_USER = 10 +def opted_in(ctx): + return ctx.guild.id in ctx.bot.get_cog("BadUser").settings.buEnabled() class BadUser(commands.Cog): def __init__(self, bot: Red, *args, **kwargs): @@ -48,6 +50,7 @@ async def baduser(self, ctx): @baduser.command(name="addnegativerole") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def addNegativeRole(self, ctx, *, role: discord.Role): """Designate a role as a 'punishment' role.""" @@ -56,6 +59,7 @@ async def addNegativeRole(self, ctx, *, role: discord.Role): @baduser.command(name="rmnegativerole") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def rmNegativeRole(self, ctx, *, role: discord.Role): """Cancels a role from 'punishment' status.""" @@ -64,6 +68,7 @@ async def rmNegativeRole(self, ctx, *, role: discord.Role): @baduser.command(name="addpositiverole") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def addPositiveRole(self, ctx, *, role: discord.Role): """Designate a role as a 'benefit' role.""" @@ -72,6 +77,7 @@ async def addPositiveRole(self, ctx, *, role: discord.Role): @baduser.command(name="rmpositiverole") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def rmPositiveRole(self, ctx, *, role: discord.Role): """Cancels a role from 'benefit' status.""" @@ -80,6 +86,7 @@ async def rmPositiveRole(self, ctx, *, role: discord.Role): @baduser.command(name="addneutralrole") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def addNeutralRole(self, ctx, *, role: discord.Role): """Designate a role as a notable but not ping-worthy role.""" @@ -88,6 +95,7 @@ async def addNeutralRole(self, ctx, *, role: discord.Role): @baduser.command(name="rmneutralrole") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def rmNeutralRole(self, ctx, *, role: discord.Role): """Cancels a role from notable but not ping-worthy status.""" @@ -96,6 +104,7 @@ async def rmNeutralRole(self, ctx, *, role: discord.Role): @baduser.command(name="setchannel") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def setChannel(self, ctx, channel: discord.TextChannel): """Set the channel for moderation announcements.""" @@ -104,6 +113,7 @@ async def setChannel(self, ctx, channel: discord.TextChannel): @baduser.command(name="clearchannel") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def clearChannel(self, ctx): """Clear the channel for moderation announcements.""" @@ -112,6 +122,7 @@ async def clearChannel(self, ctx): @baduser.command() @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def togglestrikeprivacy(self, ctx): """Change strike existance policy.""" @@ -123,6 +134,7 @@ async def togglestrikeprivacy(self, ctx): @baduser.command() @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def config(self, ctx): """Print the baduser configuration.""" @@ -159,6 +171,7 @@ async def config(self, ctx): @baduser.command(name="strikes") @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def strikes(self, ctx, user: discord.User): """Print the strike count for a user.""" @@ -167,6 +180,7 @@ async def strikes(self, ctx, user: discord.User): @baduser.command() @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def addstrike(self, ctx, user: discord.User, *, strike_text: str): """Manually add a strike to a user.""" @@ -180,6 +194,7 @@ async def addstrike(self, ctx, user: discord.User, *, strike_text: str): @baduser.command() @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def clearstrikes(self, ctx, user: discord.User): """Clear all strikes for a user.""" @@ -188,6 +203,7 @@ async def clearstrikes(self, ctx, user: discord.User): @baduser.command() @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def printstrikes(self, ctx, user: discord.User): """Print all strikes for a user.""" @@ -202,6 +218,7 @@ async def printstrikes(self, ctx, user: discord.User): @baduser.command() @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def deletestrike(self, ctx, user: discord.User, strike_num: int): """Delete a specific strike for a user.""" @@ -218,6 +235,7 @@ async def deletestrike(self, ctx, user: discord.User, strike_num: int): @baduser.command() @commands.guild_only() + @commands.check(opted_in) @checks.mod_or_permissions(manage_guild=True) async def report(self, ctx): """Displays a report of information on bad users for the server.""" @@ -306,44 +324,25 @@ async def rmban(self, ctx, user_id: int): await ctx.send(inline('Done')) @baduser.command() - @commands.guild_only() @checks.is_owner() - async def globalcheckbanlist(self, ctx): - bans = await self._load_banned_users() - msg = 'Checking for banned users in {} guilds'.format(len(self.bot.guilds)) - for cur_server in self.bot.guilds: - msg += await self._check_ban_list(cur_server) - - for page in pagify(msg): - await ctx.send(box(page)) + async def opt_in(self, ctx): + self.settings.addBuEnabled(ctx.guild.id) + await ctx.send(inline('Done')) @baduser.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def checkbanlist(self, ctx): - msg = 'Checking for banned users' - msg += await self._check_ban_list(ctx.guild) - await ctx.send(box(msg)) - - async def _check_ban_list(self, server: discord.Guild): - # external_ban_list is [user_id] - # local_ban_list is {user.id : reason} - external_bans = await self._load_banned_users() - local_bans = self.settings.bannedUsers() - msg = '\n\tChecking {}'.format(server.name) - for member in server.members: - if member.id in external_bans: - msg += '\n\t\tExternal banned user: {} ({})'.format(member.name, member.id) - if member.id in local_bans: - msg += '\n\t\tLocal banned user: {} ({}) for: {}'.format( - member.name, member.id, local_bans[member.id]) - return msg + @checks.is_owner() + async def opt_out(self, ctx): + self.settings.rmBuEnabled(ctx.guild.id) + await ctx.send(inline('Done')) @commands.Cog.listener('on_message') - async def mod_message(self, message): + async def log_message(self, message): if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): return + if message.guild.id not in self.settings.buEnabled(): + return + author = message.author content = message.clean_content channel = message.channel @@ -354,10 +353,14 @@ async def mod_message(self, message): @commands.Cog.listener('on_member_ban') async def mod_ban(self, guild, user): + if guild.id not in self.settings.buEnabled(): + return await self.recordBadUser(user, 'BANNED') @commands.Cog.listener('on_member_remove') async def mod_user_left(self, member): + if member.guild.id not in self.settings.buEnabled(): + return strikes = self.settings.countUserStrikes(member.guild.id, member.id) if strikes: msg = 'FYI: A user with {} strikes just left the server: {}'.format( @@ -369,6 +372,8 @@ async def mod_user_left(self, member): @commands.Cog.listener('on_member_join') async def mod_user_join(self, member): + if member.guild.id not in self.settings.buEnabled(): + return update_channel = self.settings.getChannel(member.guild.id) if update_channel is None: return @@ -388,6 +393,9 @@ async def mod_user_join(self, member): @commands.Cog.listener('on_member_update') async def check_punishment(self, before, after): + if before.guild.id not in self.settings.buEnabled(): + return + if before.roles == after.roles: return @@ -464,7 +472,8 @@ class BadUserSettings(CogSettings): def make_default_settings(self): config = { 'servers': {}, - 'banned_users': {} + 'banned_users': {}, + 'opted_in': [], } return config @@ -600,3 +609,15 @@ def addBannedUser(self, user_id: int, reason: str): def rmBannedUser(self, user_id: int): self.bannedUsers().pop(user_id, None) self.save_settings() + + def buEnabled(self): + return [int(gid) for gid in self.bot_settings['opted_in']] + + def addBuEnabled(self, gid: int): + self.bot_settings['opted_in'].append(gid) + self.save_settings() + + def rmBuEnabled(self, gid: int): + if str(gid) in self.bot_settings['opted_in']: + self.bot_settings['opted_in'].remove(str(gid)) + self.save_settings() From ba00a4563279090486a4c047c6772cbb90f5c96f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Sep 2020 17:17:15 -0700 Subject: [PATCH 0036/1220] Update for Red 3 --- baduser/baduser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/baduser/baduser.py b/baduser/baduser.py index 30094cbe..da0ab005 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -252,13 +252,13 @@ async def report(self, ctx): continue try: - ban_list = await self.bot.get_bans(server) + ban_list = await server.bans() except: ban_list = list() error_messages.append("Server '{}' refused access to ban list".format(server.name)) - for user in ban_list: - user_id_to_ban_server[user.id].append(server.id) + for banentry in ban_list: + user_id_to_ban_server[banentry.user.id].append(server.id) baduser_list = self.settings.getBadUsers(server.id) for user_id in baduser_list: From 3825d55689298570bb44fe4d784b9e4783b5f81b Mon Sep 17 00:00:00 2001 From: TsubakiBot Date: Fri, 4 Sep 2020 06:19:37 +0000 Subject: [PATCH 0037/1220] update media path --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 09331617..41e3a1b8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -43,7 +43,7 @@ INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' -MEDIA_PATH = 'https://f002.backblazeb2.com/file/dadguide-data/media/' +MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png' RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' From 66799700fec286aa471a9f4916829de3870c16e7 Mon Sep 17 00:00:00 2001 From: TsubakiBot Date: Fri, 4 Sep 2020 06:23:38 +0000 Subject: [PATCH 0038/1220] send confirmation message on start refresh_index --- padinfo/padinfo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 41e3a1b8..e60e30bc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -221,6 +221,7 @@ async def reload_nicknames(self): @checks.is_owner() async def refresh_index(self, ctx=None): """Refresh the monster indexes.""" + await ctx.send(inline("Okay, starting")) dg_cog = self.bot.get_cog('Dadguide') if not dg_cog: print("Cog 'Dadguide' not loaded") From 1c6874137036eaba57d33ee63df6c5827fbd7658 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 4 Sep 2020 01:53:36 -0700 Subject: [PATCH 0039/1220] PadguideDB Fix --- padglobal/padglobal.py | 2 +- padguidedb/padguidedb.py | 12 ++++++------ padinfo/padinfo.py | 9 ++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 7615b9be..b192d13f 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -61,7 +61,7 @@ def mod_help(self, ctx, help_type): def is_padglobal_admin_check(ctx): - return checks.is_owner() or PADGLOBAL_COG.settings.check_admin(ctx.author.id) + return checks.is_owner() or ctx.bot.get_cog("PadGlobal").settings.check_admin(ctx.author.id) def is_padglobal_admin(): diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index d3782666..9fa0da0c 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -39,9 +39,9 @@ def __init__(self, bot: Red, *args, **kwargs): global PADGUIDEDB_COG PADGUIDEDB_COG = self - def get_connection(self): + def get_cursor(self): with open(self.settings.configFile(), 'r') as db_config: - return self.connect(json.load(db_config)) + return self.connect(json.load(db_config)).cursor() def connect(self, db_config): return pymysql.connect(host=db_config['host'], @@ -83,7 +83,7 @@ async def setconfigfile(self, ctx, *, config_file): async def searchdungeon(self, ctx, *, search_text): """Search""" search_text = '%{}%'.format(search_text) - with self.get_connection() as cursor: + with self.get_cursor() as cursor: sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' ' where lower(name_na) like %s or lower(name_jp) like %s' ' order by dungeon_id desc limit 20') @@ -175,7 +175,7 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): @padguidedb.command() @is_padguidedb_admin() async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): - with self.get_connection() as cursor: + with self.get_cursor() as cursor: sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" " FROM wave_data" " WHERE dungeon_id = {} AND floor_id = {}" @@ -192,7 +192,7 @@ async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): @padguidedb.command() @checks.is_owner() async def cleardungeon(self, ctx, dungeon_id: int): - with self.get_connection() as cursor: + with self.get_cursor() as cursor: sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) await ctx.send(inline("Done")) @@ -200,7 +200,7 @@ async def cleardungeon(self, ctx, dungeon_id: int): # @padguidedb.command() @is_padguidedb_admin() async def dungeondata(self, ctx, dungeon_id: int): - with self.get_connection() as cursor: + with self.get_cursor() as cursor: sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) results = list(cursor.fetchall()) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e60e30bc..c78af59e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -210,16 +210,13 @@ async def reload_nicknames(self): await self.refresh_index() logger.info('Done refreshing PadInfo') except Exception as ex: - wait_time = 60 - print(self.bot.get_cog('Dadguide').database._con) + wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) traceback.print_exc() await asyncio.sleep(wait_time) - @commands.command() - @checks.is_owner() - async def refresh_index(self, ctx=None): + async def refresh_index(self): """Refresh the monster indexes.""" await ctx.send(inline("Okay, starting")) dg_cog = self.bot.get_cog('Dadguide') @@ -240,8 +237,6 @@ async def refresh_index(self, ctx=None): self.index_jp = await dg_cog.create_index(lambda m: m.on_jp) logger.info('Done refreshing indexes') - if ctx is not None: - await ctx.send(inline("Done.")) def get_monster_by_id(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') From 17d8cc6579a7ba484e9f23115689bf20e95e61ab Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 4 Sep 2020 02:33:48 -0700 Subject: [PATCH 0040/1220] original concept --- padbuildimg/padbuildimg.py | 12 ++++----- padglobal/padglobal.py | 22 +++++++--------- padinfo/padinfo.py | 52 ++++++++++++++++++-------------------- padsearch/padsearch.py | 2 +- 4 files changed, 41 insertions(+), 47 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 5129c5bc..b5a65c05 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -401,7 +401,7 @@ def __init__(self, params, padinfo_cog, build_name='pad_build'): } self.build_img = None - def process_build(self, input_str): + async def process_build(self, input_str): team_strings = [row for row in csv.reader(re.split('[;\n]', input_str), delimiter='/') if len(row) > 0] if len(team_strings) > 3: team_strings = team_strings[0:3] @@ -409,13 +409,13 @@ def process_build(self, input_str): team_sublist = [] for slot in team: try: - team_sublist.extend(self.process_card(slot)) + team_sublist.extend(await self.process_card(slot)) except Exception as ex: self.build['TEAM'] = [] raise ex self.build['TEAM'].append(team_sublist) - def process_card(self, card_str, is_assist=False): + async def process_card(self, card_str, is_assist=False): if not is_assist: result_card = { '+ATK': 99, @@ -468,7 +468,7 @@ def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = self.padinfo_cog.findMonster(tok.value) + card, err, debug_info = await self.padinfo_cog.findMonster(tok.value) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) if not card.is_inheritable: @@ -522,7 +522,7 @@ def process_card(self, card_str, is_assist=False): else: parsed_cards = [result_card] if isinstance(assist_str, str): - assist_card, assist_att = self.process_card(assist_str, is_assist=True) + assist_card, assist_att = await self.process_card(assist_str, is_assist=True) if card_att is not None and assist_att is not None: assist_card['ON_COLOR'] = card_att == assist_att parsed_cards.append(assist_card) @@ -732,7 +732,7 @@ async def padbuildimg(self, ctx, *, build_str: str): try: pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) # print('PARSE: {}'.format(time.perf_counter() - start)) - pbg.process_build(build_str) + await pbg.process_build(build_str) # start = time.perf_counter() pbg.generate_build_image() # print('DRAW: {}'.format(time.perf_counter() - start)) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 7615b9be..372bcd5a 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -68,11 +68,11 @@ def is_padglobal_admin(): return commands.check(is_padglobal_admin_check) -def lookup_named_monster(query: str): +async def lookup_named_monster(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') if padinfo_cog is None: raise Exception("Cog not Loaded") - nm, err, debug_info = padinfo_cog._findMonster(str(query)) + nm, err, debug_info = await padinfo_cog._findMonster(str(query)) return nm, err, debug_info @@ -202,7 +202,7 @@ async def write_send(nn_map, file_name): async def debugid(self, ctx, *, query): padinfo_cog = self.bot.get_cog('PadInfo') # m is a named monster - m, err, debug_info = lookup_named_monster(query) + m, err, debug_info = await lookup_named_monster(query) if m is None: await ctx.send(box('No match: ' + err)) @@ -724,7 +724,7 @@ async def which(self, ctx, *, term: str = None): async def _resolve_which(self, ctx, term): term = term.lower().replace('?', '') - nm, _, _ = lookup_named_monster(term) + nm, _, _ = await lookup_named_monster(term) if nm is None: await ctx.send(inline('No monster matched that query')) return None, None, None, None @@ -868,7 +868,7 @@ async def getwhich(self, ctx): async def debuglookup(self, ctx, *, term: str): """Shows why a query matches to a monster""" term = term.lower().replace('?', '') - nm, err, deb = lookup_named_monster(term) + nm, err, deb = await lookup_named_monster(term) base = nm.group_computed_basename.title() if nm else nm name = nm.name_na if nm else nm monster_id = nm.monster_id if nm else nm @@ -1068,19 +1068,19 @@ async def guide(self, ctx, *, term: str = None): await self.send_guide(ctx) return - term, text, err = self.get_guide_text(term) + term, text, err = await self.get_guide_text(term) if text is None: await ctx.send(inline(err)) return await ctx.send(text) - def get_guide_text(self, term: str): + async def get_guide_text(self, term: str): term = term.lower() if term in self.settings.dungeonGuide(): return term, self.settings.dungeonGuide()[term], None - nm, _, _ = lookup_named_monster(term) + nm, _, _ = await lookup_named_monster(term) if nm is None: return None, None, 'No dungeon or monster matched that query' @@ -1120,7 +1120,7 @@ async def guideto(self, ctx, to_user: discord.Member, *, term: str): [p]guideto @{0.author.name} osc10 """ - term, text, err = self.get_guide_text(term) + term, text, err = await self.get_guide_text(term) if text is None: await ctx.send(inline(err)) return @@ -1183,10 +1183,6 @@ async def rmleaderguide(self, ctx, monster_id: int): self.settings.rmLeaderGuide(name) await ctx.send("done") - def term_to_monster_name(self, term): - nm, _, _ = lookup_named_monster(term) - return nm.group_computed_basename.title() - def check_simple_tree(monster): attr1 = monster.attr1 diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e60e30bc..c234ed76 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -208,7 +208,6 @@ async def reload_nicknames(self): wait_time = 60 * 60 * 1 try: await self.refresh_index() - logger.info('Done refreshing PadInfo') except Exception as ex: wait_time = 60 print(self.bot.get_cog('Dadguide').database._con) @@ -221,7 +220,6 @@ async def reload_nicknames(self): @checks.is_owner() async def refresh_index(self, ctx=None): """Refresh the monster indexes.""" - await ctx.send(inline("Okay, starting")) dg_cog = self.bot.get_cog('Dadguide') if not dg_cog: print("Cog 'Dadguide' not loaded") @@ -250,7 +248,7 @@ def get_monster_by_id(self, monster_id: int): @commands.command() async def jpname(self, ctx, *, query: str): """Print the Japanese name of a monster""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: await ctx.send(monsterToHeader(m)) await ctx.send(box(m.name_jp)) @@ -275,7 +273,7 @@ async def idjp(self, ctx, *, query: str): await self._do_id(ctx, query, server_filter=ServerFilter.jp) async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = self.findMonster(query, server_filter=server_filter) + m, err, debug_info = await self.findMonster(query, server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -297,7 +295,7 @@ async def id2jp(self, ctx, *, query: str): await self._do_id2(ctx, query, server_filter=ServerFilter.jp) async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = self.findMonster2(query, server_filter=server_filter) + m, err, debug_info = await self.findMonster2(query, server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -306,7 +304,7 @@ async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): @commands.command(name="evos") async def evos(self, ctx, *, query: str): """Monster info (evolutions tab)""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: await self._do_idmenu(ctx, m, self.evo_emoji) else: @@ -315,7 +313,7 @@ async def evos(self, ctx, *, query: str): @commands.command(name="mats", aliases=['evomats', 'evomat']) async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: await self._do_idmenu(ctx, m, self.mats_emoji) else: @@ -324,7 +322,7 @@ async def evomats(self, ctx, *, query: str): @commands.command() async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) if menu == EMBED_NOT_GENERATED: @@ -335,7 +333,7 @@ async def pantheon(self, ctx, *, query: str): @commands.command() async def skillups(self, ctx, *, query: str): """Monster info (evolutions tab)""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: menu = await self._do_idmenu(ctx, m, self.skillups_emoji) if menu == EMBED_NOT_GENERATED: @@ -431,7 +429,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): @commands.command(aliases=['img']) async def pic(self, ctx, *, query: str): """Monster info (full image tab)""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: await self._do_idmenu(ctx, m, self.pic_emoji) else: @@ -440,7 +438,7 @@ async def pic(self, ctx, *, query: str): @commands.command() async def links(self, ctx, *, query: str): """Monster links""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: embed = monsterToBaseEmbed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( @@ -457,7 +455,7 @@ async def links(self, ctx, *, query: str): @commands.command(aliases=['stats']) async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: await self._do_idmenu(ctx, m, self.other_info_emoji) else: @@ -466,7 +464,7 @@ async def otherinfo(self, ctx, *, query: str): @commands.command() async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: embed = monsterToHeaderEmbed(m) await ctx.send(embed=embed) @@ -476,7 +474,7 @@ async def lookup(self, ctx, *, query: str): @commands.command() async def evolist(self, ctx, *, query): """Monster info (for all monsters in the evo tree)""" - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: await self._do_evolistmenu(ctx, m) else: @@ -486,7 +484,7 @@ async def evolist(self, ctx, *, query): async def collabscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" DGCOG = self.bot.get_cog("Dadguide") - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) ms = DGCOG.database.get_monsters_by_series(m.series.series_id) ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) @@ -520,14 +518,14 @@ async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, ba # Handle a very specific failure case, user typing something like "uuvo ragdra" if ' ' not in left_query and right_query is not None and ' ' not in right_query and bad is None: combined_query = left_query + ' ' + right_query - nm, err, debug_info = self._findMonster(combined_query) + nm, err, debug_info = await self._findMonster(combined_query) if nm and left_query in nm.prefixes: left_query = combined_query right_query = None - left_m, left_err, _ = self.findMonster(left_query) + left_m, left_err, _ = await self.findMonster(left_query) if right_query: - right_m, right_err, _ = self.findMonster(right_query) + right_m, right_err, _ = await self.findMonster(right_query) else: right_m, right_err, = left_m, left_err @@ -570,7 +568,7 @@ async def padsay(self, ctx, server, *, query: str = None): server = 'na' query = query.strip().lower() - m, err, debug_info = self.findMonster(query) + m, err, debug_info = await self.findMonster(query) if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: @@ -633,8 +631,8 @@ async def iddiff(self, ctx): s = 0 f = [] for query in hist_aggreg: - m1, err1, debug_info1 = self.findMonster(query) - m2, err2, debug_info2 = self.findMonster2(query) + m1, err1, debug_info1 = await self.findMonster(query) + m2, err2, debug_info2 = await self.findMonster2(query) if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): s += 1 continue @@ -659,9 +657,9 @@ def makeFailureMsg(self, err): 'Unexpected results? Use ^helpid for more info.').format(err) return box(msg) - def findMonster(self, query, server_filter=ServerFilter.any): + async def findMonster(self, query, server_filter=ServerFilter.any): query = rmdiacritics(query) - nm, err, debug_info = self._findMonster(query, server_filter) + nm, err, debug_info = await self._findMonster(query, server_filter) monster_no = nm.monster_id if nm else -1 self.historic_lookups[query] = monster_no @@ -671,7 +669,7 @@ def findMonster(self, query, server_filter=ServerFilter.any): return m, err, debug_info - def _findMonster(self, query, server_filter=ServerFilter.any): + async def _findMonster(self, query, server_filter=ServerFilter.any): if server_filter == ServerFilter.any: monster_index = self.index_all elif server_filter == ServerFilter.na: @@ -682,9 +680,9 @@ def _findMonster(self, query, server_filter=ServerFilter.any): raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster(query) - def findMonster2(self, query, server_filter=ServerFilter.any): + async def findMonster2(self, query, server_filter=ServerFilter.any): query = rmdiacritics(query) - nm, err, debug_info = self._findMonster2(query, server_filter) + nm, err, debug_info = await self._findMonster2(query, server_filter) monster_no = nm.monster_id if nm else -1 self.historic_lookups_id2[query] = monster_no @@ -694,7 +692,7 @@ def findMonster2(self, query, server_filter=ServerFilter.any): return m, err, debug_info - def _findMonster2(self, query, server_filter=ServerFilter.any): + async def _findMonster2(self, query, server_filter=ServerFilter.any): if server_filter == ServerFilter.any: monster_index = self.index_all elif server_filter == ServerFilter.na: diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index ae8fd31e..d8baf29a 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -797,7 +797,7 @@ def _make_search_config(self, input): @checks.is_owner() async def debugsearch(self, ctx, *, query): padinfo_cog = self.bot.get_cog('PadInfo') - m, err, debug_info = padinfo_cog.findMonster(query) + m, err, debug_info = await padinfo_cog.findMonster(query) if m is None: await ctx.send(box('No match: ' + err)) From 15a8252809f6ad93a805822d1e4490d0be174b8a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 4 Sep 2020 02:34:35 -0700 Subject: [PATCH 0041/1220] Removed start confirmation that didn't work --- padinfo/padinfo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c78af59e..52632df2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -218,7 +218,6 @@ async def reload_nicknames(self): async def refresh_index(self): """Refresh the monster indexes.""" - await ctx.send(inline("Okay, starting")) dg_cog = self.bot.get_cog('Dadguide') if not dg_cog: print("Cog 'Dadguide' not loaded") From 6dfbc300c1edeb847d44d25d67543c0f9d20ef08 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 4 Sep 2020 02:38:35 -0700 Subject: [PATCH 0042/1220] add sleep while forcing refresh --- padinfo/padinfo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c234ed76..92375a6c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -670,6 +670,9 @@ async def findMonster(self, query, server_filter=ServerFilter.any): return m, err, debug_info async def _findMonster(self, query, server_filter=ServerFilter.any): + while self.index_lock.locked(): + await asyncio.sleep(1) + if server_filter == ServerFilter.any: monster_index = self.index_all elif server_filter == ServerFilter.na: @@ -693,6 +696,9 @@ async def findMonster2(self, query, server_filter=ServerFilter.any): return m, err, debug_info async def _findMonster2(self, query, server_filter=ServerFilter.any): + while self.index_lock.locked(): + await asyncio.sleep(1) + if server_filter == ServerFilter.any: monster_index = self.index_all elif server_filter == ServerFilter.na: From 9156a6f7dab0f755681484e22b5738f291c71c72 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 4 Sep 2020 08:28:54 -0700 Subject: [PATCH 0043/1220] dungeondrops rewrite --- dadguide/dadguide.py | 2 ++ padguidedb/padguidedb.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 1c99f76d..27b06544 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -88,6 +88,7 @@ async def wait_until_ready(self): async def create_index(self, accept_filter=None): """Exported function that allows a client cog to create a monster index""" + print("DEBUG:",self.database._con) return await MonsterIndex(self.database, self.nickname_overrides, self.basename_overrides, @@ -100,6 +101,7 @@ def get_monster_by_id(self, monster_id: int): def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty + logger.info('Unloading Dadguide') if self.database: self.database.close() self.database = None diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 9fa0da0c..6460ec13 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -174,7 +174,7 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): @padguidedb.command() @is_padguidedb_admin() - async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): + async def olddungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): with self.get_cursor() as cursor: sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" " FROM wave_data" @@ -189,6 +189,38 @@ async def dungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): for page in pagify(msg): await ctx.send(box(page)) + @padguidedb.command() + @is_padguidedb_admin() + async def dungeondrops(self, ctx, dungeon_id: int): + with self.get_cursor() as cursor: + sql = ("SELECT `floor_id`, COUNT(DISTINCT `entry_id`)" + " FROM `dadguide`.`wave_data`" + " WHERE `dungeon_id` = {}" + " GROUP BY `floor_id`" + " ORDER BY `wave_data`.`floor_id` DESC;").format(dungeon_id) + cursor.execute(sql) + results1 = list(cursor.fetchall()) + + sql = ("SELECT `floor_id`, `drop_monster_id`, COUNT(id)" + " FROM `dadguide`.`wave_data`" + " WHERE `dungeon_id` = {} AND `drop_monster_id` != 9900 AND `drop_monster_id` != 0" + " GROUP BY `floor_id`, `drop_monster_id`" + " ORDER BY `wave_data`.`floor_id` DESC;").format(dungeon_id) + cursor.execute(sql) + results2 = list(cursor.fetchall()) + + msg = 'floor_id,count\n' + for row in results1: + print(row) + msg += ','.join(map(str,row.values()))+"\n" + msg += '\n\nfloor_id,monster_id,count\n' + for row in results2: + print(row) + msg += ','.join(map(str,row.values()))+"\n" + + for page in pagify(msg): + await ctx.send(box(page)) + @padguidedb.command() @checks.is_owner() async def cleardungeon(self, ctx, dungeon_id: int): From aaad562bee3444732c18f3ea802d4cd01cf4d2af Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 4 Sep 2020 08:32:53 -0700 Subject: [PATCH 0044/1220] tsubakiserver --- trutils/trutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trutils/trutils.py b/trutils/trutils.py index c5a9e0e3..7a223621 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -382,7 +382,7 @@ async def feedback(self, ctx, *, message: str): """ feedback_channel = self.bot.get_channel(int(self.settings.get_feedback_channel())) await self._send_feedback(ctx, message, feedback_channel, - " Join the Tsubaki Server to see any responses ({0.prefix}miruserver).".format(ctx)) + " Join the Tsubaki Server to see any responses ({0.prefix}tsubakiserver).".format(ctx)) @commands.command() @commands.guild_only() From 2543c086fdfc41def7f5be6b9f3e51dd418e2210 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 4 Sep 2020 15:41:58 -0700 Subject: [PATCH 0045/1220] onlinecount --- padbuildimg/padbuildimg.py | 2 +- padinfo/padinfo.py | 3 ++- trutils/trutils.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index b5a65c05..e46eceb7 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -129,7 +129,7 @@ AWK_CIRCLE = 'circle' AWK_STAR = 'star' DELAY_BUFFER = 'delay_buffer' -REMOTE_ASSET_URL = 'https://github.com/Mushymato/pdchu-cog/raw/master/assets/' +REMOTE_ASSET_URL = 'https://github.com/chasehult/pdchu-cog/raw/master/assets/' REMOTE_AWK_URL = 'https://f002.backblazeb2.com/file/dadguide-data/media/awakenings/{0:03d}.png' # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 553b63a7..f806705f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -420,6 +420,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): await result_msg.edit(embed=result_embed) except Exception as ex: print('Menu failure', ex) + traceback.print_exc() @commands.command(aliases=['img']) async def pic(self, ctx, *, query: str): @@ -693,7 +694,7 @@ async def findMonster2(self, query, server_filter=ServerFilter.any): async def _findMonster2(self, query, server_filter=ServerFilter.any): while self.index_lock.locked(): await asyncio.sleep(1) - + if server_filter == ServerFilter.any: monster_index = self.index_all elif server_filter == ServerFilter.na: diff --git a/trutils/trutils.py b/trutils/trutils.py index 7a223621..809c8538 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -465,6 +465,24 @@ async def pipupdate(self, ctx, module="Red-DiscordBot", updatepip=True): async def makeaa(self, ctx, *, task): await self.bot.get_user(144250811315257344).send(task) + @commands.command() + @checks.is_owner() + async def onlinecount(self, ctx): + gonline = 0 + gmobile = 0 + conline = 0 + cmobile = 0 + for member in ctx.guild.members: + gonline += member.status.name == "online" + gmobile += member.is_on_mobile() + for member in ctx.channel.members: + conline += member.status.name == "online" + cmobile += member.is_on_mobile() + await ctx.send(box("There are {} members online ({} online on mobile).\n" + "There are {} members online in this channel ({} online on mobile).") + .format(gonline, gmobile, conline, cmobile)) + + class TrUtilsSettings(CogSettings): def make_default_settings(self): From 1b8899947f807401b8aa75790a6f613dfcab9125 Mon Sep 17 00:00:00 2001 From: lessuknow Date: Sat, 5 Sep 2020 00:27:42 -0400 Subject: [PATCH 0046/1220] Displays the monster's base rarity next to the monster's current rarity when calling id --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 553b63a7..9899a9a1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1017,7 +1017,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): os = "" if m.orb_skin_id is None else " (Orb Skin)" - info_row_2 = '**Rarity** {}{}\n**Cost** {}'.format(m.rarity, os, m.cost) + info_row_2 = '**Rarity** {} (**Tree** {}){}\n**Cost** {}'.format(m.rarity, m.base_monster.rarity, os, m.cost) if acquire_text: info_row_2 += '\n**{}**'.format(acquire_text) if m.is_inheritable: From 972f04fda4d2d3e48c7e27e08e6d877384406a15 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 5 Sep 2020 04:06:14 -0700 Subject: [PATCH 0047/1220] trigger (i guess) why did you make me port this it's so long --- padbuildimg/padbuildimg.py | 2 +- trigger/__init__.py | 8 + trigger/trigger.py | 606 +++++++++++++++++++++++++++++++++++++ trutils/trutils.py | 5 +- 4 files changed, 616 insertions(+), 5 deletions(-) create mode 100644 trigger/__init__.py create mode 100644 trigger/trigger.py diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index e46eceb7..bc1349ae 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -145,7 +145,7 @@ class PadBuildImgSettings(CogSettings): def make_default_build_img_params(self): build_img_params = DictWithAttributeAccess({ 'ASSETS_DIR': './assets/', - 'PORTRAIT_DIR': 'https://f002.backblazeb2.com/file/dadguide-data/media/icons/{monster_id:05d}.png', + 'PORTRAIT_DIR': 'https://d1kpnpud0qoyxf.cloudfront.net/media/icons/{monster_id:05d}.png', # 'OUTPUT_DIR': './data/padbuildimg/output/', 'PORTRAIT_WIDTH': 100, 'PADDING': 10, diff --git a/trigger/__init__.py b/trigger/__init__.py new file mode 100644 index 00000000..8bb25e26 --- /dev/null +++ b/trigger/__init__.py @@ -0,0 +1,8 @@ +from .trigger import Trigger + + +def setup(bot): + n = Trigger(bot) + bot.loop.create_task(n.save_stats()) + bot.loop.create_task(n.load_triggers()) + bot.add_cog(n) diff --git a/trigger/trigger.py b/trigger/trigger.py new file mode 100644 index 00000000..17bee858 --- /dev/null +++ b/trigger/trigger.py @@ -0,0 +1,606 @@ +import discord +import datetime +import os +import asyncio +import re + +from redbot.core import checks, commands, Config +from redbot.core.utils.chat_formatting import box, pagify, escape +from random import choice + + +class TriggerError(Exception): + pass + + +class Unauthorized(TriggerError): + pass + + +class NotFound(TriggerError): + pass + + +class AlreadyExists(TriggerError): + pass + + +class Trigger(commands.Cog): + """Custom triggers""" + + def __init__(self, bot): + self.bot = bot + self.config = Config.get_conf(self, identifier=7173306) + self.config.register_global(triggers=[]) + + self.triggers = [] + + @commands.group() + @commands.guild_only() + async def trigger(self, ctx): + """Trigger creation commands""" + + @trigger.command() + @checks.admin_or_permissions(administrator=True) + async def create(self, ctx, trigger_name : str, *, triggered_by : str): + """Creates a trigger""" + try: + self.create_trigger(trigger_name, triggered_by, ctx) + except AlreadyExists: + await ctx.send("A trigger with that name already exists.") + else: + await self.save_triggers() + await ctx.send("Trigger created. Entering interactive " + "add mode...".format(ctx.prefix)) + trigger = self.get_trigger_by_name(trigger_name) + await self.interactive_add_mode(trigger, ctx) + await self.save_triggers() + + @trigger.command() + @checks.admin_or_permissions(administrator=True) + async def delete(self, ctx, trigger_name : str): + """Deletes a trigger""" + try: + self.delete_trigger(trigger_name, ctx) + await self.save_triggers() + except Unauthorized: + await ctx.send("You're not authorized to delete that trigger.") + except NotFound: + await ctx.send("That trigger doesn't exist.") + else: + await ctx.send("Trigger deleted.") + + @trigger.command() + @checks.admin_or_permissions(administrator=True) + async def add(self, ctx, trigger_name : str, *, response : str=None): + """Adds a response to a trigger + + Leaving the response argument empty will enable interactive mode + + Owner only: + Adding a response as 'file: filename.jpg' will send that file as + response if present in data/trigger/files""" + trigger = self.get_trigger_by_name(trigger_name) + + if trigger is None: + await ctx.send("That trigger doesn't exist.") + return + if not trigger.can_edit(ctx.author): + await ctx.send("You're not allowed to edit that trigger.") + return + + if response is not None: + trigger.responses.append(response) + await ctx.send("Response added.") + else: # Interactive mode + await self.interactive_add_mode(trigger, ctx) + await self.save_triggers() + + @trigger.command() + @checks.admin_or_permissions(administrator=True) + async def remove(self, ctx, trigger_name : str): + """Lets you choose a response to remove""" + trigger = self.get_trigger_by_name(trigger_name) + + if trigger is None: + await ctx.send("That trigger doesn't exist.") + return + if trigger.responses == []: + await ctx.send("That trigger has no responses to delete.") + return + if not trigger.can_edit(ctx.author): + await ctx.send("You're not allowed to do that.") + return + + msg = None + current_list = None + past_messages = [] + quit_msg = "\nType 'exit' to quit removal mode." + + while self.get_n_trigger_responses(trigger) is not None: + r_list = self.get_n_trigger_responses(trigger, truncate=100) + if current_list is None: + current_list = await ctx.send(r_list + quit_msg) + else: + if r_list != current_list.content: + await current_list.edit(content=r_list + quit_msg) + msg = await self.bot.wait_for("message", timeout=15.0, check=lambda m:m.author==ctx.author) + if msg is None: + await ctx.send("Nothing else to remove I guess.") + break + elif msg.content.lower().strip() == "exit": + past_messages.append(msg) + await ctx.send("Removal mode quit.") + break + try: + i = int(msg.content) + del trigger.responses[i] + except: + pass + past_messages.append(msg) + + if not trigger.responses: + await ctx.send("No more responses to delete.") + + past_messages.append(current_list) + await self.attempt_cleanup(past_messages) + + async def attempt_cleanup(self, messages): + try: + for message in messages: + await message.delete() + except: + pass + + @trigger.command() + async def info(self, ctx, trigger_name : str): + """Shows a trigger's info""" + trigger = self.get_trigger_by_name(trigger_name) + if trigger: + msg = "Name: {}\n".format(trigger.name) + owner_name = discord.utils.get(self.bot.get_all_members(), id=trigger.owner) + owner_name = owner_name if owner_name is not None else "not found" + msg += "Owner: {} ({})\n".format(owner_name, trigger.owner) + trigger_type = "all responses" if trigger.type == "all" else "random response" + msg += "Type: {}\n".format(trigger_type) + influence = "server" if trigger.server is not None else "global" + msg += "Influence: {}\n".format(influence) + cs = "yes" if trigger.case_sensitive else "no" + msg += "Case Sensitive: {}\n".format(cs) + regex = "yes" if trigger.regex else "no" + msg += "Regex: {}\n".format(regex) + msg += "Cooldown: {} seconds\n".format(trigger.cooldown) + msg += "Triggered By: \"{}\"\n".format(trigger.triggered_by.replace("`", "\\`")) + msg += "Payload: {} responses\n".format(len(trigger.responses)) + msg += "Triggered: {} times\n".format(trigger.triggered) + await ctx.send(box(msg, lang="xl")) + else: + await ctx.send("There is no trigger with that name.") + + @trigger.command() + async def show(self, ctx, trigger_name : str): + """Shows all responses of a trigger""" + trigger = self.get_trigger_by_name(trigger_name) + if trigger: + payload = self.elaborate_payload(trigger.responses, truncate=9999) + if payload: + payload = "\n\n".join(payload) + if len(payload) > 2000: + for page in pagify(payload, delims=[" "]): + await ctx.author.send(page) + else: + await ctx.send(payload) + else: + await ctx.send("That trigger has no responses.") + else: + await ctx.send("That trigger doesn't exist.") + + @trigger.command(name="list") + async def _list(self, ctx, trigger_type="local"): + """Lists local / global triggers + + Defaults to local""" + server = ctx.guild + results = [] + if trigger_type == "local": + for trigger in self.triggers: + if trigger.server == server.id: + results.append(trigger) + elif trigger_type == "global": + for trigger in self.triggers: + if trigger.server is None: + results.append(trigger) + else: + await ctx.send("Invalid type.") + return + if results: + results = ", ".join([r.name for r in results]) + for page in pagify(results, delims=[" ", "\n"]): + await ctx.send("```\n{}\n```".format(page.lstrip(" "), results)) + else: + await ctx.send("I couldn't find any trigger of that type.") + + @trigger.command() + async def search(self, ctx, *, search_terms : str): + """Returns triggers matching the search terms""" + result = self.search_triggers(search_terms.lower()) + if result: + result = ", ".join(sorted([t.name for t in result])) + await ctx.send("Triggers found:\n\n{}".format(result)) + else: + await ctx.send("No triggers matching your search.") + + @commands.group() + @commands.guild_only() + @checks.admin_or_permissions(administrator=True) + async def triggerset(self, ctx): + """Edits the settings of a trigger""" + + @triggerset.command() + async def cooldown(self, ctx, trigger_name : str, seconds : int): + """Sets the trigger's cooldown""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + if seconds < 1: + seconds = 1 + trigger.cooldown = seconds + await self.save_triggers() + await ctx.send("Cooldown set to {} seconds.".format(seconds)) + + @triggerset.command() + async def phrase(self, ctx, trigger_name : str, *, triggered_by : str): + """Sets the word/phrase by which the trigger is activated by""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + if not triggered_by: + await ctx.send("Invalid setting.") + return + trigger.triggered_by = triggered_by + await self.save_triggers() + await ctx.send("The trigger will be activated by `{}`.".format(triggered_by)) + + @triggerset.command() + async def response(self, ctx, trigger_name : str, _type : str): + """Sets the response type for the trigger. + + Available types: all, random + + All will show all responses in order + Random will pick one at random""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + _type = _type.lower() + if _type not in ("all", "random"): + await ctx.send("Invalid type.") + return + trigger.type = _type + await self.save_triggers() + await ctx.send("Response type set to {}.".format(_type)) + + @triggerset.command() + @checks.is_owner() + async def influence(self, ctx, trigger_name : str, _type : str): + """Sets the influence of the trigger. + + Available types: server, global""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + _type = _type.lower() + if _type not in ("server", "global"): + await ctx.send("Invalid type.") + return + trigger.server = ctx.guild.id if _type == "server" else None + await self.save_triggers() + await ctx.send("Influence set to {}.".format(_type)) + + @triggerset.command() + async def channels(self, ctx, trigger_name : str, *channels : discord.TextChannel): + """Sets the channel(s) in which the trigger will be active + + Not entering any channel will revert the trigger to server-wide""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + if channels: + channels = [c.id for c in channels] + trigger.channels[ctx.guild.id] = list(channels) + await self.save_triggers() + if trigger.server is not None: + await ctx.send("The trigger will be enabled only on " + "those channels.") + else: + await ctx.send("In this server the trigger will be " + "enabled only on those channels") + else: + trigger.channels[ctx.guild.id] = [] + await self.save_triggers() + await ctx.send("The trigger will be active in all channels.") + + @triggerset.command() + async def casesensitive(self, ctx, trigger_name : str, + true_or_false : bool): + """Toggles the trigger's case sensitivity. + + Can be true or false""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + trigger.case_sensitive = true_or_false + await self.save_triggers() + await ctx.send("Case sensitivity set to {}.".format(true_or_false)) + + @triggerset.command() + async def regex(self, ctx, trigger_name : str, true_or_false : bool): + """Toggles the trigger's case capabilities. + + Can be true or false""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + trigger.regex = true_or_false + await self.save_triggers() + await ctx.send("Regex set to {}.".format(true_or_false)) + + @triggerset.command() + async def active(self, ctx, trigger_name : str, true_or_false : bool): + """Toggles the trigger on/off. + + Can be true or false""" + trigger = self.get_trigger_by_name(trigger_name) + if not await self.settings_check(ctx, trigger, ctx.author): + return + trigger.active = true_or_false + await self.save_triggers() + await ctx.send("Trigger active: {}.".format(true_or_false)) + + async def settings_check(ctx, self, trigger, author): + if not trigger: + await ctx.send("That trigger doesn't exist.") + return False + if not trigger.can_edit(author): + await ctx.send("You're not authorized to edit that triggers' " + "settings.") + return False + return True + + def get_trigger_by_name(self, name): + for trigger in self.triggers: + if trigger.name.lower() == name.lower(): + return trigger + return None + + def search_triggers(self, search_terms): + results = [] + for trigger in self.triggers: + if search_terms in trigger.name.lower(): + results.append(trigger) + continue + for payload in trigger.responses: + if search_terms in payload.lower(): + results.append(trigger) + break + else: + if search_terms in trigger.triggered_by.lower(): + results.append(trigger) + return results + + def create_trigger(self, name, triggered_by, ctx): + trigger = self.get_trigger_by_name(name) + if not trigger: + trigger = TriggerObj(bot=self.bot, + name=name, + triggered_by=triggered_by, + owner=ctx.author.id, + server=ctx.guild.id + ) + self.triggers.append(trigger) + else: + raise AlreadyExists() + + def delete_trigger(self, name, ctx): + trigger = self.get_trigger_by_name(name) + if trigger: + if not trigger.can_edit(ctx.author): + raise Unauthorized() + self.triggers.remove(trigger) + else: + raise NotFound() + + def elaborate_payload(self, payload, truncate=50, escape=True): + shortened = [] + for p in payload: + if escape: + p = escape(p) + if len(p) < truncate: + shortened.append(p) + else: + shortened.append(p[:truncate] + "...") + return shortened + + async def interactive_add_mode(self, trigger, ctx): + msg = "" + await ctx.send("Everything you type will be added as response " + "to the trigger. Type 'exit' to quit.") + while msg is not None: + msg = await self.bot.wait_for("message", timeout=60.0, check=lambda m:m.author==ctx.author) + if msg is None: + await ctx.send("No more responses then. " + "Your changes have been saved.") + break + if msg.content.lower().strip() == "exit": + await ctx.send("Your changes have been saved.") + break + trigger.responses.append(msg.content) + + def get_n_trigger_responses(self, trigger, *, truncate=2000): + msg = "" + responses = trigger.responses + i = 0 + for r in responses: + if len(r) > truncate: + r = r[:truncate] + "..." + r = r.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_") + msg += "{}. {}\n".format(i, r) + i += 1 + if msg != "": + return box(msg, lang="py") + else: + return None + + async def is_command(self, msg): + prefixes = await self.bot.get_prefix(msg) + if not isinstance(prefixes, list): + prefixes = [prefixes] + for p in prefixes: + if msg.content.startswith(p): + return True + return False + + def elaborate_response(self, trigger, r): + is_owner = trigger.owner in self.bot.owner_ids + if not is_owner: + return "text", r + if not r.startswith("file:"): + return "text", r + else: + path = r.replace("file:", "").strip() + path = os.path.join("data", "trigger", "files", path) + if os.path.isfile(path): + return "file", path + else: + return "text", r + + @commands.Cog.listener('on_message') + async def on_message(self, message): + channel = message.channel + author = message.author + + if message.guild is None: + return + + if author == self.bot.user: + return + + if await self.is_command(message): + return + + for trigger in self.triggers: + if not trigger.check(message): + continue + payload = trigger.payload() + for p in payload: + resp_type, resp = self.elaborate_response(trigger, p) + if resp_type == "text": + await channel.send(resp) + elif resp_type == "file": + await channel.send(file=resp) + + async def save_stats(self): + """Saves triggers every 10 minutes to preserve stats""" + await self.bot.wait_until_ready() + try: + await asyncio.sleep(60) + while True: + await self.save_triggers() + await asyncio.sleep(60 * 10) + except asyncio.CancelledError: + pass + + async def load_triggers(self): + triggers = await self.config.triggers() + for trigger in triggers: + trigger["bot"] = self.bot + self.triggers.append(TriggerObj(**trigger)) + + async def save_triggers(self): + triggers = [t.export() for t in self.triggers] + await self.config.triggers.set(triggers) + + +class TriggerObj: + def __init__(self, **kwargs): + self.bot = kwargs.get("bot") + self.name = kwargs.get("name") + self.owner = kwargs.get("owner") + self.triggered_by = kwargs.get("triggered_by") + self.responses = kwargs.get("responses", []) + self.server = kwargs.get("server") # if it's None, the trigger will be implicitly global + self.channels = kwargs.get("channels", {}) + self.type = kwargs.get("type", "all") # Type of payload. Types: all, random + self.case_sensitive = kwargs.get("case_sensitive", False) + self.regex = kwargs.get("regex", False) + self.cooldown = kwargs.get("cooldown", 1) # Seconds + self.triggered = kwargs.get("triggered", 0) # Counter + self.last_triggered = datetime.datetime(1970, 2, 6) # Initialized + self.active = kwargs.get("active", True) + + def export(self): + data = self.__dict__.copy() + del data["bot"] + del data["last_triggered"] + return data + + def check(self, msg): + if not self.active: + return False + + channels = self.channels.get(msg.guild.id, []) + if channels: + if msg.channel.id not in channels: + return False + + content = msg.content + triggered_by = self.triggered_by + + if (self.server == msg.guild.id or self.server is None) is False: + return False + + if not self.case_sensitive: + triggered_by = triggered_by.lower() + content = content.lower() + + if not self.regex: + if triggered_by not in content: + return False + else: + found = re.search(triggered_by, content) + if not found: + return False + + timestamp = datetime.datetime.now() + passed = (timestamp - self.last_triggered).seconds + if passed > self.cooldown: + self.last_triggered = timestamp + return True + else: + return False + + def payload(self): + if self.responses: + self.triggered += 1 + if self.type == "all": + return self.responses + elif self.type == "random": + if self.responses: + return [choice(self.responses)] + else: + return [] + else: + raise RuntimeError("Invalid trigger type.") + + def can_edit(self, user): + server = user.guild + # Using the is_owner_check would be better but I don't always have + # context here, nor I feel like mocking it + is_owner = user.id in self.bot.owner_ids + is_admin = user.guild.permissions_for(user).administrator + is_trigger_owner = user.id == self.owner + trigger_is_global = self.server is None + if trigger_is_global: + return is_trigger_owner or is_owner + else: + return is_trigger_owner or is_owner or is_admin diff --git a/trutils/trutils.py b/trutils/trutils.py index 809c8538..f9ce7c65 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -468,10 +468,7 @@ async def makeaa(self, ctx, *, task): @commands.command() @checks.is_owner() async def onlinecount(self, ctx): - gonline = 0 - gmobile = 0 - conline = 0 - cmobile = 0 + gonline = gmobile = conline = cmobile = 0 for member in ctx.guild.members: gonline += member.status.name == "online" gmobile += member.is_on_mobile() From 0a1a47e46f8f9bdd084f86be318b6fd2d0755067 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 05:48:12 -0700 Subject: [PATCH 0048/1220] Trigger Fix --- trigger/trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trigger/trigger.py b/trigger/trigger.py index 17bee858..ae2a2144 100644 --- a/trigger/trigger.py +++ b/trigger/trigger.py @@ -597,7 +597,7 @@ def can_edit(self, user): # Using the is_owner_check would be better but I don't always have # context here, nor I feel like mocking it is_owner = user.id in self.bot.owner_ids - is_admin = user.guild.permissions_for(user).administrator + is_admin = user.guild_permissions.administrator is_trigger_owner = user.id == self.owner trigger_is_global = self.server is None if trigger_is_global: From 57afda09a7401dec0b8f06eb63243c1783f06af7 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 06:06:47 -0700 Subject: [PATCH 0049/1220] Fix pdg rmadmin and also adds servercount I know I wasn't supposed to write servercount but I use ^eval print(len(self.bot.guilds)) so much that anyone who did write it would probably copy off of me anyway --- padglobal/padglobal.py | 13 +++++++++++-- trutils/trutils.py | 11 ++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 308e44a6..b8099c07 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -885,9 +885,18 @@ async def addadmin(self, ctx, user: discord.Member): @padglobal.command() @checks.is_owner() - async def rmadmin(self, ctx, user: discord.Member): + async def rmadmin(self, ctx, user): """Removes a user from the pad global admin""" - self.settings.rmAdmin(user.id) + try: + u = await commands.MemberConverter().convert(ctx, user) + self.settings.rmAdmin(u.id) + except commands.BadArgument as e: + try: + u = int(user) + self.settings.rmAdmin(u) + except ValueError: + await ctx.send(inline("Invalid user id.")) + return await ctx.send(inline("Done")) @padglobal.command() diff --git a/trutils/trutils.py b/trutils/trutils.py index 809c8538..89c67f74 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -466,12 +466,9 @@ async def makeaa(self, ctx, *, task): await self.bot.get_user(144250811315257344).send(task) @commands.command() - @checks.is_owner() + @checks.mod_or_permissions(manage_guild=True) async def onlinecount(self, ctx): - gonline = 0 - gmobile = 0 - conline = 0 - cmobile = 0 + gonline = gmobile = conline = cmobile = 0 for member in ctx.guild.members: gonline += member.status.name == "online" gmobile += member.is_on_mobile() @@ -482,6 +479,10 @@ async def onlinecount(self, ctx): "There are {} members online in this channel ({} online on mobile).") .format(gonline, gmobile, conline, cmobile)) + @commands.command() + async def servercount(self, ctx): + await ctx.send("{} is in {} servers.".format(self.bot.user.name, len(self.bot.guilds))) + class TrUtilsSettings(CogSettings): From c1a48b2b8a4105ce53a29068a33d055ae652bbf5 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 06:12:07 -0700 Subject: [PATCH 0050/1220] other rmadmin fixes --- padguidedb/padguidedb.py | 13 +++++++++++-- stickers/stickers.py | 13 +++++++++++-- streamcopy/streamcopy.py | 13 +++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 6460ec13..1fc897a1 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -66,9 +66,18 @@ async def addadmin(self, ctx, user: discord.Member): @padguidedb.command() @checks.is_owner() - async def rmadmin(self, ctx, user: discord.Member): + async def rmadmin(self, ctx, user): """Removes a user from the padguide db admin""" - self.settings.rmAdmin(user.id) + try: + u = await commands.MemberConverter().convert(ctx, user) + self.settings.rmAdmin(u.id) + except commands.BadArgument as e: + try: + u = int(user) + self.settings.rmAdmin(u) + except ValueError: + await ctx.send(inline("Invalid user id.")) + return await ctx.send("done") @padguidedb.command() diff --git a/stickers/stickers.py b/stickers/stickers.py index c6bf7683..bc9afa4c 100644 --- a/stickers/stickers.py +++ b/stickers/stickers.py @@ -111,9 +111,18 @@ async def addadmin(self, ctx, user: discord.Member): @sticker.command() @checks.is_owner() - async def rmadmin(self, ctx, user: discord.Member): + async def rmadmin(self, ctx, user): """Removes a user from the stickers admin""" - self.settings.rm_admin(user.id) + try: + u = await commands.MemberConverter().convert(ctx, user) + self.settings.rmAdmin(u.id) + except commands.BadArgument as e: + try: + u = int(user) + self.settings.rmAdmin(u) + except ValueError: + await ctx.send(inline("Invalid user id.")) + return await ctx.send("done") @commands.Cog.listener("on_message") diff --git a/streamcopy/streamcopy.py b/streamcopy/streamcopy.py index d50e9c58..d0e09b23 100644 --- a/streamcopy/streamcopy.py +++ b/streamcopy/streamcopy.py @@ -63,8 +63,17 @@ async def addUser(self, ctx, user: discord.User): @streamcopy.command(name="rmuser") @checks.is_owner() - async def rmUser(self, ctx, user: discord.User): - self.settings.rm_user(user.id) + async def rmUser(self, ctx, user): + try: + u = await commands.MemberConverter().convert(ctx, user) + self.settings.rm_user(u.id) + except commands.BadArgument as e: + try: + u = int(user) + self.settings.rm_user(u) + except ValueError: + await ctx.send(inline("Invalid user id.")) + return await ctx.send(inline('Done')) @streamcopy.command(name="list") From 66b1467eede5faca5b2b7ead88d7fb3ce8bd680d Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 06:16:23 -0700 Subject: [PATCH 0051/1220] freload fix --- trutils/trutils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/trutils/trutils.py b/trutils/trutils.py index 89c67f74..fb0940f8 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -432,7 +432,13 @@ async def freload(self, ctx, cmd, *, args=""): if cmd is None: await ctx.send("Invalid Command: {}".format(full_cmd)) return + _send = ctx.send + async def fakesend(*args, **kwargs): pass + + ctx.send = fakesend await self.bot.get_cog("Core").reload(ctx, cmd.cog.__module__.split('.')[0]) + + ctx.send = _send ctx.message.content = full_cmd await self.bot.process_commands(ctx.message) From 365c9a977fad6d313f98984cc107923609190089 Mon Sep 17 00:00:00 2001 From: RheingoldRiver <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 6 Sep 2020 12:12:27 -0500 Subject: [PATCH 0052/1220] Tree -> Base --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 870680af..cbb0d70c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1018,7 +1018,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): os = "" if m.orb_skin_id is None else " (Orb Skin)" - info_row_2 = '**Rarity** {} (**Tree** {}){}\n**Cost** {}'.format(m.rarity, m.base_monster.rarity, os, m.cost) + info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format(m.rarity, m.base_monster.rarity, os, m.cost) if acquire_text: info_row_2 += '\n**{}**'.format(acquire_text) if m.is_inheritable: From 339dc4d362a87792fbd2c29875707d7e1e3c2379 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 10:29:44 -0700 Subject: [PATCH 0053/1220] fix colors --- trigger/trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trigger/trigger.py b/trigger/trigger.py index ae2a2144..40791c51 100644 --- a/trigger/trigger.py +++ b/trigger/trigger.py @@ -447,7 +447,7 @@ def get_n_trigger_responses(self, trigger, *, truncate=2000): msg += "{}. {}\n".format(i, r) i += 1 if msg != "": - return box(msg, lang="py") + return box(msg) else: return None From 13361d9ded11d44b7cf8f461954f90281c5a15ff Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 19:51:32 -0700 Subject: [PATCH 0054/1220] Make AutoMod2 GDPR compliant --- automod2/automod2.py | 76 +++++++++++++++++++++++++++++++++++++++++- automod2/info.json | 16 +++++++++ info.json | 4 ++- rpadutils/rpadutils.py | 12 ++++++- 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 automod2/info.json diff --git a/automod2/automod2.py b/automod2/automod2.py index 80cb4854..7e8beb1e 100644 --- a/automod2/automod2.py +++ b/automod2/automod2.py @@ -9,6 +9,7 @@ from collections import defaultdict from collections import deque from datetime import datetime +from io import BytesIO import discord import prettytable @@ -81,13 +82,48 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.settings = AutoMod2Settings("automod2") + self.settings = AutoMod2Settings("automod2", bot) self.settings.cleanup() self.channel_user_logs = defaultdict(lambda: deque(maxlen=LOGS_PER_CHANNEL_USER)) self.server_user_last = defaultdict(dict) self.server_phrase_last = defaultdict(dict) + async def red_get_data_for_user(self, *, user_id): + udata = self.settings.getUserData(user_id) + + data = "Stored data for user with ID {}:\n".format(user_id) + if udata['bannings']: + data += " - You have setup watchdogs for {} user(s).\n".format(udata['bannings']) + if udata['phrases']: + data += " - You have created {} phrase(s).\n".format(udata['phrases']) + if udata['baduser']: + data += (" - You have been watchlisted in {} servers for the following reasons: " + "(This data is sensitive cannot be cleared automatically due to abuse. " + "Please contact a bot owner to get this data cleared.)\n" + "").format(len(udata['baduser'])) + for reason in udata['baduser']: + data += " - {}\n".format(reason) + + if not any(udata.values()): + data = "No data is stored for user with ID {}.\n".format(user_id) + + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's data. + + The personal data stored in this cog is for essential moderation use, + so data deletion requests can only be made by the bot owner and Discord + itself. If this is an issue, please contact a bot owner. + """ + if requester not in ("discord_deleted_user", "owner"): + self.settings.clearUserData(user_id) + else: + self.settings.clearUserDataFull(user_id) + + + @commands.command() @checks.mod_or_permissions(manage_guild=True) async def automodhelp(self, ctx): @@ -815,3 +851,41 @@ def setWatchdogPhrase(self, server_id, name, request_user_id, cooldown_secs, phr else: watchdog_phrases.pop(name, None) self.save_settings() + + # GDPR Compliance Functions + def getUserData(self, user_id): + o = { + 'bannings': 0, + 'phrases': 0, + 'baduser': [], + } + + for gid in self.bot_settings['configs']: + for uid in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): + if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users', uid, 'request_user_id'], -2141): + o['bannings'] += 1 + for phr in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases'], []): + if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases', phr, 'request_user_id'], -2141): + o['phrases'] += 1 + if user_id in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): + o['baduser'].append(self.bot_settings['configs'][gid]['watchdog']['users'][user_id]['reason']) + + return o + + def clearUserData(self, user_id): + # Anonymize requesting banners and phrasemakers + for gid in self.bot_settings['configs']: + for uid in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): + if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users', uid, 'request_user_id'], -2141): + self.bot_settings['configs'][gid]['watchdog']['users'][uid]['request_user_id'] = -1 + for phr in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases'], []): + if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases', phr, 'request_user_id'], -2141): + self.bot_settings['configs'][gid]['watchdog']['phrases'][phr]['request_user_id'] = -1 + + def clearUserDataFull(self, user_id): + self.clearUserData(user_id) + + # Remove bannees + for gid in self.bot_settings['configs']: + if user_id in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): + del self.bot_settings['configs'][gid]['watchdog']['users'][user_id] diff --git a/automod2/info.json b/automod2/info.json new file mode 100644 index 00000000..07bf3813 --- /dev/null +++ b/automod2/info.json @@ -0,0 +1,16 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Uses regex pattern matching to filter message content", + "end_user_data_statement": "This cog stores id of users manually marked as problematic.", + "install_msg": "", + "short": "Filter messages via regex", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,7,2], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["prettytable"], + "tags": ["moderation", "filter", "filtering"], + "type": "COG" +} diff --git a/info.json b/info.json index 05ed0357..4a4eaa9f 100644 --- a/info.json +++ b/info.json @@ -1,3 +1,5 @@ { - "author" : ["The Miru Bot Team"] + "author": ["The Miru Bot Team"], + "description": "A collection of cogs, primarily related to the mobile game Puzzle and Dragons, along with miscellaneous moderation and other utilities.", + "short": "The main Tsubotki repo." } diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index fa984867..fe3b4269 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -515,10 +515,12 @@ def intify(input): class CogSettings(object): SETTINGS_FILE_NAME = "legacy_settings.json" - def __init__(self, cog_name): + def __init__(self, cog_name, bot=None): self.folder = str(data_manager.cog_data_path(raw_name=cog_name)) self.file_path = os.path.join(self.folder, CogSettings.SETTINGS_FILE_NAME) + self.bot = bot + self.check_folder() self.default_settings = self.make_default_settings() @@ -736,6 +738,14 @@ async def repeating_timer(seconds, condition=lambda:True, start_immediately=True await asyncio.sleep(seconds) yield +def deepget(mapping, keys, default): + o = mapping + for key in keys: + try: + o = o[key] + except KeyError: + return default + return o class aobject(object): """Inheriting this class allows you to define an async __init__.""" From ecae8614543d7ef6944c0d8193d1e5bc303ed657 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 20:15:12 -0700 Subject: [PATCH 0055/1220] Debug print to squash the menu bug --- rpadutils/rpadutils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index fa984867..2bc9a2e6 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -226,7 +226,12 @@ class Forbidden(): def default_check(payload): - return not payload.member.bot + try: + return not payload.member.bot + except Exception as e: + print("DEBUG: "+e) + print(payload) + raise e class EmojiUpdater(object): From 62da9e9e0955ae4b964e56a0bc0f257caefac0ca Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 22:41:38 -0700 Subject: [PATCH 0056/1220] More GDPR compliance stuff --- .github/FUNDING.yml | 12 ++++++++ automod2/automod2.py | 14 ++++----- automod2/info.json | 5 ++-- azurelane/azurelane.py | 14 ++++++++- azurelane/info.json | 15 ++++++++++ baduser/baduser.py | 58 ++++++++++++++++++++++++++++++++++++++ baduser/info.json | 15 ++++++++++ calculator/calculator.py | 18 ++++++++++-- calculator/info.json | 15 ++++++++++ channelmod/channelmod.py | 17 +++++++---- channelmod/info.json | 15 ++++++++++ chronomagia/chronomagia.py | 12 ++++++++ chronomagia/info.json | 15 ++++++++++ 13 files changed, 206 insertions(+), 19 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 azurelane/info.json create mode 100644 baduser/info.json create mode 100644 calculator/info.json create mode 100644 channelmod/info.json create mode 100644 chronomagia/info.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..fe894291 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: tsubaki_bot +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: [] diff --git a/automod2/automod2.py b/automod2/automod2.py index 7e8beb1e..10b3cb63 100644 --- a/automod2/automod2.py +++ b/automod2/automod2.py @@ -90,6 +90,7 @@ def __init__(self, bot, *args, **kwargs): self.server_phrase_last = defaultdict(dict) async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" udata = self.settings.getUserData(user_id) data = "Stored data for user with ID {}:\n".format(user_id) @@ -98,12 +99,10 @@ async def red_get_data_for_user(self, *, user_id): if udata['phrases']: data += " - You have created {} phrase(s).\n".format(udata['phrases']) if udata['baduser']: - data += (" - You have been watchlisted in {} servers for the following reasons: " - "(This data is sensitive cannot be cleared automatically due to abuse. " + data += (" - You have been watchlisted in {} servers. " + "(This data is sensitive and cannot be cleared automatically due to abuse. " "Please contact a bot owner to get this data cleared.)\n" "").format(len(udata['baduser'])) - for reason in udata['baduser']: - data += " - {}\n".format(reason) if not any(udata.values()): data = "No data is stored for user with ID {}.\n".format(user_id) @@ -111,11 +110,11 @@ async def red_get_data_for_user(self, *, user_id): return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's data. + """Delete a user's personal data. The personal data stored in this cog is for essential moderation use, - so data deletion requests can only be made by the bot owner and Discord - itself. If this is an issue, please contact a bot owner. + so some data deletion requests can only be made by the bot owner and + Discord itself. If this is an issue, please contact a bot owner. """ if requester not in ("discord_deleted_user", "owner"): self.settings.clearUserData(user_id) @@ -123,7 +122,6 @@ async def red_delete_data_for_user(self, *, requester, user_id): self.settings.clearUserDataFull(user_id) - @commands.command() @checks.mod_or_permissions(manage_guild=True) async def automodhelp(self, ctx): diff --git a/automod2/info.json b/automod2/info.json index 07bf3813..12d49bed 100644 --- a/automod2/info.json +++ b/automod2/info.json @@ -6,11 +6,10 @@ "short": "Filter messages via regex", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", - "min_python_version" : [3,7,2], + "min_python_version" : [3,8,1], "hidden": false, "disabled": false, "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, "requirements": ["prettytable"], - "tags": ["moderation", "filter", "filtering"], - "type": "COG" + "tags": ["moderation", "filter", "filtering"] } diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 2d98465a..6d988fde 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -1,6 +1,6 @@ import difflib import json -from _collections import OrderedDict +from collections import OrderedDict import aiohttp import discord @@ -23,6 +23,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.card_data = [] self.menu = Menu(bot) + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + async def reload_al(self): await self.bot.wait_until_ready() diff --git a/azurelane/info.json b/azurelane/info.json new file mode 100644 index 00000000..3d740a86 --- /dev/null +++ b/azurelane/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Displays data about Azur Lane.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Azur Lane data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["aiohttp"], + "tags": ["azurlane", "azurelane"] +} diff --git a/baduser/baduser.py b/baduser/baduser.py index da0ab005..8229adfc 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -27,6 +27,41 @@ def __init__(self, bot: Red, *args, **kwargs): self.settings = BadUserSettings("baduser") self.logs = defaultdict(lambda: deque(maxlen=LOGS_PER_USER)) + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + udata = self.settings.getUserData(user_id) +o = { + "gban": "", + "baduser": 0, +} + data = "Stored data for user with ID {}:\n".format(user_id) + if udata['gban']: + data += (" - You are on the global banlist. " + "(This data is sensitive and cannot be cleared automatically due to abuse. " + "Please contact a bot owner to get this data cleared.)\n") + if udata['baduser']: + data += (" - You have been punished/banned in {} servers: " + "(This data is sensitive and cannot be cleared automatically due to abuse. " + "Please contact a bot owner to get this data cleared.)\n" + "").format(len(udata['baduser'])) + + if not any(udata.values()): + data = "No data is stored for user with ID {}.\n".format(user_id) + + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + The personal data stored in this cog is for essential moderation use, + so some data deletion requests can only be made by the bot owner and + Discord itself. If this is an issue, please contact a bot owner. + """ + if requester not in ("discord_deleted_user", "owner"): + self.settings.clearUserData(user_id) + else: + self.settings.clearUserDataFull(user_id) + @commands.group() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) @@ -621,3 +656,26 @@ def rmBuEnabled(self, gid: int): if str(gid) in self.bot_settings['opted_in']: self.bot_settings['opted_in'].remove(str(gid)) self.save_settings() + + def getUserData(self, uid): + o = { + "gban": "", + "baduser": 0, + } + if str(uid) in self.bot_settings['banned_users']: + o['gban'] = self.bot_settings['banned_users'][str(uid)] + for gid in self.bot_settings['servers']: + if str(uid) in self.bot_settings['servers'][gid]["badusers"]: + o['baduser'] += 1 + return o + + def clearUserData(self, uid): + # Do nothing + return + + def clearUserDataFull(self, uid): + if str(uid) in self.bot_settings['banned_users']: + del self.bot_settings['banned_users'][str(uid)] + for gid in self.bot_settings['servers']: + if str(uid) in self.bot_settings['servers'][gid]["badusers"]: + del self.bot_settings['servers'][gid]["badusers"][str(uid)] diff --git a/baduser/info.json b/baduser/info.json new file mode 100644 index 00000000..5e602bd4 --- /dev/null +++ b/baduser/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Allows for more specific punishments than regular discord including global strikes and positive/negative roles.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "General purpose punishment cog.", + "short": "A fancy python powered calculator", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["administration", "moderation"] +} diff --git a/calculator/calculator.py b/calculator/calculator.py index 899d80b3..e5abf7f3 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -14,8 +14,6 @@ from redbot.core.utils.chat_formatting import inline, box from functools import reduce -from rpadutils.rpadutils import CtxIO - ACCEPTED_TOKENS = r'[\[\]\-()*+/0-9=.,% |&<>~_^]|>=|<=|==|!=|factorial|randrange|isfinite|copysign|radians|isclose|degrees|randint|lgamma|choice|random|round|log1p|log10|ldexp|isnan|isinf|hypot|gamma|frexp|floor|expm1|atanh|atan2|asinh|acosh|False|range|tanh|sqrt|sinh|modf|log2|fmod|fabs|erfc|cosh|ceil|atan|asin|acos|else|True|fsum|tan|sin|pow|nan|log|inf|gcd|sum|exp|erf|cos|for|not|and|ans|pi|in|is|or|if|e|x' ALTERED_TOKENS = {'^': '**', '_':'ans'} @@ -38,6 +36,22 @@ def __init__(self, bot, *args, **kwargs): | GUILDS: Config | | ans: channel_id -> Any """ + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + udata = await self.config.user(ctx.author).ans() + + data = "You have previous answers stored in {} channels.\n".format(len(udata)) + + if not udata: + data = "No data is stored for user with ID {}.\n".format(user_id) + + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + await self.config.user_from_id(user_id).clear() + @commands.group() async def helpcalc(self, ctx): '''Whispers info on how to use the calculator.''' diff --git a/calculator/info.json b/calculator/info.json new file mode 100644 index 00000000..c22bf216 --- /dev/null +++ b/calculator/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Calculate expressions via python exec, but safely so users can do it too!", + "end_user_data_statement": "The most recent answer in each channel is stored.", + "install_msg": "", + "short": "A fancy python powered calculator", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [], + "tags": ["calculator"] +} diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 2c34cb0f..c128de05 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -29,6 +29,18 @@ def __init__(self, bot, *args, **kwargs): self.settings = ChannelModSettings("channelmod") self.channel_last_spoke = {} + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group() async def channelmod(self, ctx): """Manage channel moderation settings""" @@ -103,11 +115,6 @@ async def countreactions(self, ctx, message: discord.Message): await ctx.send(o) - - - - - @commands.Cog.listener('on_message') async def mirror_msg(self, message): if message.author.id == self.bot.user.id: diff --git a/channelmod/info.json b/channelmod/info.json new file mode 100644 index 00000000..e875485a --- /dev/null +++ b/channelmod/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Allows you to mirror channels across servers.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "Mirror channels across servers.", + "short": "A fancy python powered calculator", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["aiohttp"], + "tags": ["administration", "mirror"] +} diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index 456634f9..f2b48377 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -51,6 +51,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.id_emoji = '\N{INFORMATION SOURCE}' self.pic_emoji = '\N{FRAME WITH PICTURE}' + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + async def reload_cm_task(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('ChronoMagia'): diff --git a/chronomagia/info.json b/chronomagia/info.json new file mode 100644 index 00000000..1a99fb82 --- /dev/null +++ b/chronomagia/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Displays data about Chrono Ma:Gia.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Chrono Ma:Gia data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["aiohttp"], + "tags": ["chronomagia"], +} From 302da0e644e16bf4c017f88d55e3c5693db3ef9f Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 6 Sep 2020 22:44:00 -0700 Subject: [PATCH 0057/1220] shoot --- baduser/baduser.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/baduser/baduser.py b/baduser/baduser.py index 8229adfc..d8418328 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -30,10 +30,7 @@ def __init__(self, bot: Red, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" udata = self.settings.getUserData(user_id) -o = { - "gban": "", - "baduser": 0, -} + data = "Stored data for user with ID {}:\n".format(user_id) if udata['gban']: data += (" - You are on the global banlist. " From 9f2716ad569d287d945d3c1f532a79dcbc916c89 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 09:04:27 -0700 Subject: [PATCH 0058/1220] GDPR --- dadguide/dadguide.py | 14 +++++++++++++- dadguide/info.json | 15 +++++++++++++++ damagecalc/damagecalc.py | 12 ++++++++++++ damagecalc/info.json | 15 +++++++++++++++ datatransfer/datatransfer.py | 12 ++++++++++++ datatransfer/info.json | 15 +++++++++++++++ 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 dadguide/info.json create mode 100644 damagecalc/info.json create mode 100644 datatransfer/info.json diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 27b06544..b9b9dfed 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -16,7 +16,7 @@ import shutil import sqlite3 as lite import traceback -from _collections import defaultdict, deque, OrderedDict +from collections import defaultdict, deque, OrderedDict from datetime import datetime from enum import Enum @@ -86,6 +86,18 @@ async def wait_until_ready(self): """ return await self._is_ready.wait() + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + async def create_index(self, accept_filter=None): """Exported function that allows a client cog to create a monster index""" print("DEBUG:",self.database._con) diff --git a/dadguide/info.json b/dadguide/info.json new file mode 100644 index 00000000..e442c40d --- /dev/null +++ b/dadguide/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "No commands, but loads all the monster data stored in Tsubaki.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Loads monster data (LOAD THIS SECOND)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["pytz", "romkan"], + "tags": ["pad"] +} diff --git a/damagecalc/damagecalc.py b/damagecalc/damagecalc.py index 7a714e75..a6b794ed 100644 --- a/damagecalc/damagecalc.py +++ b/damagecalc/damagecalc.py @@ -191,6 +191,18 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group() async def helpdamage(self, ctx): """Help info for the damage command diff --git a/damagecalc/info.json b/damagecalc/info.json new file mode 100644 index 00000000..e6456626 --- /dev/null +++ b/damagecalc/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Calculate Puzzle & Dragons data", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Calculates Puzzle & Dragons data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["ply"], + "tags": ["pad"] +} diff --git a/datatransfer/datatransfer.py b/datatransfer/datatransfer.py index f2d55f78..6567439a 100644 --- a/datatransfer/datatransfer.py +++ b/datatransfer/datatransfer.py @@ -24,6 +24,18 @@ def __init__(self, bot, *args, **kwargs): self.export = bot.get_cog("Alias").alias.command()(self.export) """ + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group(name="export") @commands.guild_only() @checks.mod_or_permissions(administrator=True) diff --git a/datatransfer/info.json b/datatransfer/info.json new file mode 100644 index 00000000..66d55928 --- /dev/null +++ b/datatransfer/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Export and import data for memes, customcom, and alias", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Exports/Imports data for some cogs", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["aiohttp"], + "tags": ["export", "import"] +} From 0eaaf4d4128cc0382eb240c5745ba2fc7fef451b Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 09:22:43 -0700 Subject: [PATCH 0059/1220] padboard fix --- padboard/padboard.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/padboard/padboard.py b/padboard/padboard.py index 61d1041e..73df504d 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -6,8 +6,7 @@ import cv2 import discord import numpy as np -from redbot.core import checks -from redbot.core import commands +from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline from padvision import padvision @@ -23,6 +22,10 @@ class PadBoard(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + + self.config = Config.get_conf(self, identifier=94080420) + self.config.register_global(tflite_path="") + self.logs = defaultdict(lambda: deque(maxlen=1)) @commands.Cog.listener("on_message") @@ -36,6 +39,11 @@ async def log_message(self, message): async def padboard(self, ctx): """PAD board utilities.""" + @padboard.command() + async def set_tflite_path(self, ctx, *, path): + await self.config.tflite_path.set(path) + + def find_image(self, user_id): urls = list(self.logs[user_id]) if urls: @@ -62,7 +70,11 @@ async def dawnglare(self, ctx, user: discord.Member = None): if not image_data: return - img_board_nc = self.nc_classify(image_data) + img_board_nc = await self.nc_classify(image_data) + + if not img_board_nc: + await ctx.send(inline("TFLite path not set.")) + return board_text_nc = ''.join([''.join(r) for r in img_board_nc]) # Convert O (used by padvision code) to X (used by Puzzled for bombs) @@ -96,10 +108,12 @@ async def get_recent_image(self, ctx, user: discord.Member = None, message: disc return image_data - def nc_classify(self, image_data): + async def nc_classify(self, image_data): # TODO: Test this (for TR to do) nparr = np.fromstring(image_data, np.uint8) img_np = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - model_path = '/home/tactical0retreat/git/pad-models/ICN3582626462823189160/model.tflite' + model_path = await self.config.tflite_path() + if not model_path: + return None img_extractor = padvision.NeuralClassifierBoardExtractor(model_path, img_np, image_data) return img_extractor.get_board() From 271c6b3907c78306a5f07dc1da344993beb0f0ae Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 09:23:03 -0700 Subject: [PATCH 0060/1220] funding yml --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..fe894291 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: tsubaki_bot +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: [] From d780c4814149806edd9f0a3400be1532aeae4a47 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 10:32:37 -0700 Subject: [PATCH 0061/1220] More cogs! --- datatransfer/datatransfer.py | 5 ----- datatransfer/info.json | 2 +- fancysay/fancysay.py | 12 ++++++++++++ fancysay/info.json | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 fancysay/info.json diff --git a/datatransfer/datatransfer.py b/datatransfer/datatransfer.py index 6567439a..6d34bee1 100644 --- a/datatransfer/datatransfer.py +++ b/datatransfer/datatransfer.py @@ -19,11 +19,6 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - """ - self._import = bot.get_cog("Alias").alias.command(name="import")(self._import) - self.export = bot.get_cog("Alias").alias.command()(self.export) - """ - async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) diff --git a/datatransfer/info.json b/datatransfer/info.json index 66d55928..119055a8 100644 --- a/datatransfer/info.json +++ b/datatransfer/info.json @@ -1,5 +1,5 @@ { - "author": ["The Tsubaki Bot Team"], + "author": ["Aradia Megido#2552"], "description": "Export and import data for memes, customcom, and alias", "end_user_data_statement": "No personal data is stored.", "install_msg": "", diff --git a/fancysay/fancysay.py b/fancysay/fancysay.py index 4ff5f01b..5858d5b2 100644 --- a/fancysay/fancysay.py +++ b/fancysay/fancysay.py @@ -15,6 +15,18 @@ def __init__(self, bot: Red, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group() @commands.guild_only() @checks.mod_or_permissions(manage_messages=True) diff --git a/fancysay/info.json b/fancysay/info.json new file mode 100644 index 00000000..020063f3 --- /dev/null +++ b/fancysay/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Allows the user to make the bot say things in a variety of ways.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Say things with \u2728style\u2728~", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["announcement", "fancy"] +} From d2afdd5c9a68d2e5ab1413e967e1875327fa6cc1 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 10:38:11 -0700 Subject: [PATCH 0062/1220] Make padboard stop giving warnings --- padboard/padboard.py | 3 ++- padvision/padvision.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/padboard/padboard.py b/padboard/padboard.py index 73df504d..29b2a916 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -42,6 +42,7 @@ async def padboard(self, ctx): @padboard.command() async def set_tflite_path(self, ctx, *, path): await self.config.tflite_path.set(path) + await ctx.send(inline("Done")) def find_image(self, user_id): @@ -110,7 +111,7 @@ async def get_recent_image(self, ctx, user: discord.Member = None, message: disc async def nc_classify(self, image_data): # TODO: Test this (for TR to do) - nparr = np.fromstring(image_data, np.uint8) + nparr = np.frombuffer(image_data, np.uint8) img_np = cv2.imdecode(nparr, cv2.IMREAD_COLOR) model_path = await self.config.tflite_path() if not model_path: diff --git a/padvision/padvision.py b/padvision/padvision.py index 545f8a84..b0c514fc 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -126,7 +126,7 @@ def _process(self): oe = OrbExtractor(self.np_img) # Load TFLite model and allocate tensors. - interpreter = tf.contrib.lite.Interpreter(model_path=self.model_path) + interpreter = tf.lite.Interpreter(model_path=self.model_path) interpreter.allocate_tensors() # Get input and output tensors. From f74aa902a7c1aa5a8056e3233d31b1bb73945368 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 16:00:10 -0700 Subject: [PATCH 0063/1220] more gdpr stuff --- automod2/__init__.py | 1 + automod2/automod2.py | 2 ++ automod2/info.json | 2 +- azurelane/__init__.py | 1 + baduser/__init__.py | 1 + baduser/baduser.py | 1 + calculator/__init__.py | 3 ++- channelmod/__init__.py | 1 + chronomagia/__init__.py | 3 ++- dadguide/__init__.py | 1 + damagecalc/__init__.py | 3 ++- datatransfer/__init__.py | 1 + donations/__init__.py | 1 + donations/donations.py | 53 ++++++++++++++++++++++++++++++++++++++++ donations/info.json | 15 ++++++++++++ fancysay/__init__.py | 3 ++- globalban/__init__.py | 1 + globalban/globalban.py | 12 +++++++++ globalban/info.json | 15 ++++++++++++ memes/__init__.py | 3 ++- memes/info.json | 15 ++++++++++++ memes/memes.py | 12 +++++++++ modnotes/modnotes.py | 39 ++++++++++++++++++++++++++++- 23 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 donations/info.json create mode 100644 globalban/info.json create mode 100644 memes/info.json diff --git a/automod2/__init__.py b/automod2/__init__.py index a297064c..5c42dfce 100644 --- a/automod2/__init__.py +++ b/automod2/__init__.py @@ -1,5 +1,6 @@ from .automod2 import * +__red_end_user_data_statement__ = "This cog stores id of users manually marked as problematic." def setup(bot): bot.add_cog(AutoMod2(bot)) diff --git a/automod2/automod2.py b/automod2/automod2.py index 10b3cb63..0dd26c93 100644 --- a/automod2/automod2.py +++ b/automod2/automod2.py @@ -879,6 +879,7 @@ def clearUserData(self, user_id): for phr in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases'], []): if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases', phr, 'request_user_id'], -2141): self.bot_settings['configs'][gid]['watchdog']['phrases'][phr]['request_user_id'] = -1 + self.save_settings() def clearUserDataFull(self, user_id): self.clearUserData(user_id) @@ -887,3 +888,4 @@ def clearUserDataFull(self, user_id): for gid in self.bot_settings['configs']: if user_id in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): del self.bot_settings['configs'][gid]['watchdog']['users'][user_id] + self.save_settings() diff --git a/automod2/info.json b/automod2/info.json index 12d49bed..f3da836e 100644 --- a/automod2/info.json +++ b/automod2/info.json @@ -1,6 +1,6 @@ { "author": ["The Tsubaki Bot Team"], - "description": "Uses regex pattern matching to filter message content", + "description": "Uses regex pattern matching to filter message content and set limits on users", "end_user_data_statement": "This cog stores id of users manually marked as problematic.", "install_msg": "", "short": "Filter messages via regex", diff --git a/azurelane/__init__.py b/azurelane/__init__.py index ceba31fd..00b7bfe4 100644 --- a/azurelane/__init__.py +++ b/azurelane/__init__.py @@ -1,5 +1,6 @@ from .azurelane import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = AzureLane(bot) diff --git a/baduser/__init__.py b/baduser/__init__.py index 75e19fac..a604bdea 100644 --- a/baduser/__init__.py +++ b/baduser/__init__.py @@ -1,5 +1,6 @@ from .baduser import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(BadUser(bot)) diff --git a/baduser/baduser.py b/baduser/baduser.py index d8418328..b39ae6d7 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -676,3 +676,4 @@ def clearUserDataFull(self, uid): for gid in self.bot_settings['servers']: if str(uid) in self.bot_settings['servers'][gid]["badusers"]: del self.bot_settings['servers'][gid]["badusers"][str(uid)] + self.save_settings() diff --git a/calculator/__init__.py b/calculator/__init__.py index 78f2ca51..1854399a 100644 --- a/calculator/__init__.py +++ b/calculator/__init__.py @@ -1,5 +1,6 @@ from .calculator import * - + +__red_end_user_data_statement__ = "The most recent answer in each channel is stored." def setup(bot): bot.add_cog(Calculator(bot)) diff --git a/channelmod/__init__.py b/channelmod/__init__.py index a2598279..7df09ee4 100644 --- a/channelmod/__init__.py +++ b/channelmod/__init__.py @@ -1,5 +1,6 @@ from .channelmod import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = ChannelMod(bot) diff --git a/chronomagia/__init__.py b/chronomagia/__init__.py index 4ad398ce..b96f5f30 100644 --- a/chronomagia/__init__.py +++ b/chronomagia/__init__.py @@ -1,5 +1,6 @@ from .chronomagia import * - + +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = ChronoMagia(bot) diff --git a/dadguide/__init__.py b/dadguide/__init__.py index bc15651f..5bff88a7 100644 --- a/dadguide/__init__.py +++ b/dadguide/__init__.py @@ -1,5 +1,6 @@ from .dadguide import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = Dadguide(bot) diff --git a/damagecalc/__init__.py b/damagecalc/__init__.py index 2907cb11..26416e02 100644 --- a/damagecalc/__init__.py +++ b/damagecalc/__init__.py @@ -1,5 +1,6 @@ from .damagecalc import * - + +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = DamageCalc(bot) diff --git a/datatransfer/__init__.py b/datatransfer/__init__.py index b2dc12ed..bc9ebb1a 100644 --- a/datatransfer/__init__.py +++ b/datatransfer/__init__.py @@ -1,5 +1,6 @@ from .datatransfer import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(DataTransfer(bot)) diff --git a/donations/__init__.py b/donations/__init__.py index 1c0c83ee..90050530 100644 --- a/donations/__init__.py +++ b/donations/__init__.py @@ -1,5 +1,6 @@ from .donations import * +__red_end_user_data_statement__ = "This cog stores your custom commands." def setup(bot): n = Donations(bot) diff --git a/donations/donations.py b/donations/donations.py index 17f00b8f..d0023273 100644 --- a/donations/donations.py +++ b/donations/donations.py @@ -88,6 +88,30 @@ def __init__(self, bot: Red, *args, **kwargs): self.donor_role = None self.patron_role = None + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + udata = self.settings.getUserData(user_id) + + data = "Stored data for user with ID {}:\n".format(user_id) + if udata['command']: + data += " - You have setup the command '{}'.\n".format(udata['command']) + if udata['embed']: + data += " - You have setup the embed '{}'.\n".format(udata['embed']) + if udata['insult']: + data += " - You have asked the bot to insult you occasionally.\n" + + if not any(udata.values()): + data = "No data is stored for user with ID {}.\n".format(user_id) + + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + if requester not in ("discord_deleted_user", "owner"): + self.settings.clearUserData(user_id) + else: + self.settings.clearUserDataFull(user_id) + async def set_server_attributes(self): await self.bot.wait_until_ready() self.tsubaki_guild = self.bot.get_guild(746131494875168770) @@ -389,3 +413,32 @@ def rmInsultsEnabled(self, user_id): if user_id in insults_enabled: insults_enabled.remove(user_id) self.save_settings() + + # GDPR Compliance Functions + def getUserData(self, user_id): + o = { + 'command': "", + 'embed': "", + 'insult': False, + } + + if user_id in self.bot_settings['custom_commands']: + o['command'] = self.bot_settings['custom_commands'][user_id]["command"] + if user_id in self.bot_settings['custom_embeds']: + o['embed'] = self.bot_settings['custom_embeds'][user_id]["command"] + if user_id in self.bot_settings['insults_enabled']: + o['insult'] = True + + return o + + def clearUserData(self, user_id): + if user_id in self.bot_settings['custom_commands']: + del self.bot_settings['custom_commands'][user_id] + if user_id in self.bot_settings['custom_embeds']: + del self.bot_settings['custom_embeds'][user_id] + if user_id in self.bot_settings['insults_enabled']: + self.bot_settings['insults_enabled'].remove(user_id) + self.save_settings() + + def clearUserDataFull(self, user_id): + self.clearUserData(user_id) diff --git a/donations/info.json b/donations/info.json new file mode 100644 index 00000000..d67449da --- /dev/null +++ b/donations/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Tracks donors and gives them perks", + "end_user_data_statement": "This cog stores your custom commands.", + "install_msg": "", + "short": "Patreon/Donor perks", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["donations", "patreon"] +} diff --git a/fancysay/__init__.py b/fancysay/__init__.py index 247cfe96..8c372871 100644 --- a/fancysay/__init__.py +++ b/fancysay/__init__.py @@ -1,5 +1,6 @@ from .fancysay import * - + +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(FancySay(bot)) diff --git a/globalban/__init__.py b/globalban/__init__.py index b1df7a5f..7bb8347d 100644 --- a/globalban/__init__.py +++ b/globalban/__init__.py @@ -1,5 +1,6 @@ from .globalban import GlobalBan +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(GlobalBan(bot)) diff --git a/globalban/globalban.py b/globalban/globalban.py index 37d23f1d..bec67798 100644 --- a/globalban/globalban.py +++ b/globalban/globalban.py @@ -14,6 +14,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.config.register_global(banned={}, opted=[]) self.bot = bot + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group() async def globalban(self, ctx): """Global ban related commands.""" diff --git a/globalban/info.json b/globalban/info.json new file mode 100644 index 00000000..5d73b968 --- /dev/null +++ b/globalban/info.json @@ -0,0 +1,15 @@ +{ + "author": ["Aradia Megido#2552"], + "description": "Allows bot owners to make global bans across servers.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Ban users across multiple servers", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [], + "tags": ["globalban", "moderation"] +} diff --git a/memes/__init__.py b/memes/__init__.py index 9b36324a..ef8a0c2f 100644 --- a/memes/__init__.py +++ b/memes/__init__.py @@ -1,5 +1,6 @@ from .memes import * - + +__red_end_user_data_statement__ = "All stored data is anonymized." def setup(bot): bot.add_cog(Memes(bot)) diff --git a/memes/info.json b/memes/info.json new file mode 100644 index 00000000..d3598dcb --- /dev/null +++ b/memes/info.json @@ -0,0 +1,15 @@ +{ + "author": ["Aradia Megido#2552"], + "description": "Allows users to setup custom commands only accessable by a certain role.", + "end_user_data_statement": "All stored data is anonymized.", + "install_msg": "", + "short": "Priviledged custom commands", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["customcommands", "memes"] +} diff --git a/memes/memes.py b/memes/memes.py index 7b386333..b0c00fca 100644 --- a/memes/memes.py +++ b/memes/memes.py @@ -27,6 +27,18 @@ def __init__(self, bot, *args, **kwargs): self.c_commands = {int(k): v for k, v in self.c_commands.items()} # Fix legacy issues self.settings = MemesSettings("memes") + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.command() @commands.guild_only() @checks.mod_or_permissions(administrator=True) diff --git a/modnotes/modnotes.py b/modnotes/modnotes.py index d6d875c4..a4a1a4cb 100644 --- a/modnotes/modnotes.py +++ b/modnotes/modnotes.py @@ -1,6 +1,8 @@ """ Utilities for managing moderator notes about users. """ +import re + import discord from redbot.core import checks from redbot.core import commands @@ -43,7 +45,7 @@ async def get(self, ctx, user: discord.User): async def add(self, ctx, user: discord.User, *, note_text: str): """Add a note to a user.""" timestamp = str(ctx.message.created_at)[:-7] - msg = 'Added by {} ({}): {}'.format(ctx.author.name, timestamp, note_text) + msg = 'Added by {} ({}): {}'.format(ctx.author.id, timestamp, note_text) server_id = ctx.guild.id notes = self.settings.addNoteForUser(server_id, user.id, msg) await ctx.send(inline('Done. User {} now has {} notes'.format(user.name, len(notes)))) @@ -120,3 +122,38 @@ def addNoteForUser(self, server_id, user_id, note: str): notes.append(note) self.setNotesForUser(server_id, user_id, notes) return notes + + # GDPR Compliance Functions + def getUserData(self, user_id): + o = { + 'notes': 0 + } + + for gid in self.bot_settings['servers']: + for uid in rpadutils.deepget(self.bot_settings, ['servers', gid, 'user_notes'], []): + for c in range(len(self.bot_settings['servers'][gid]['user_notes'][uid])): + self.bot_settings['servers'][gid]['user_notes'][uid][c] = re.sub( + r'^Added by {} \('.format(uid), + 'Added by ANONYMOUS (', + self.bot_settings['servers'][gid]['user_notes'][uid][c]) + return o + + def clearUserData(self, user_id): + # Anonymize requesting banners and phrasemakers + for gid in self.bot_settings['configs']: + for uid in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): + if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users', uid, 'request_user_id'], -2141): + self.bot_settings['configs'][gid]['watchdog']['users'][uid]['request_user_id'] = -1 + for phr in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases'], []): + if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases', phr, 'request_user_id'], -2141): + self.bot_settings['configs'][gid]['watchdog']['phrases'][phr]['request_user_id'] = -1 + self.save_settings() + + def clearUserDataFull(self, user_id): + self.clearUserData(user_id) + + # Remove bannees + for gid in self.bot_settings['configs']: + if user_id in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): + del self.bot_settings['configs'][gid]['watchdog']['users'][user_id] + self.save_settings() From f6c07709b3aef30254b0cd21c2d23f0e0ede9d3e Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 16:33:55 -0700 Subject: [PATCH 0064/1220] This might fix the issue --- seniority/seniority.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seniority/seniority.py b/seniority/seniority.py index bc82024f..c0ace601 100644 --- a/seniority/seniority.py +++ b/seniority/seniority.py @@ -134,7 +134,7 @@ async def init(self): return if os.name != 'nt' and sys.platform != 'win32': - dsn = 'Driver=SQLite3;Database=' + self.db_path + dsn = 'Driver=ODBC Driver 17 for SQL Server;Database=' + self.db_path else: dsn = 'Driver=SQLite3 ODBC Driver;Database=' + self.db_path self.pool = await aioodbc.create_pool(dsn=dsn, autocommit=True) From 1f15e7355d698f5a4642c56c2cea40e31aaa4dc2 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 16:54:16 -0700 Subject: [PATCH 0065/1220] some code to let me fiddle with it --- seniority/seniority.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/seniority/seniority.py b/seniority/seniority.py index c0ace601..7ab91222 100644 --- a/seniority/seniority.py +++ b/seniority/seniority.py @@ -133,10 +133,11 @@ async def init(self): print('Seniority: bailing on unlock') return - if os.name != 'nt' and sys.platform != 'win32': - dsn = 'Driver=ODBC Driver 17 for SQL Server;Database=' + self.db_path - else: - dsn = 'Driver=SQLite3 ODBC Driver;Database=' + self.db_path + dsn = self.settings.get_db_string().format(self.db_path) + + if not dsn: + print("DB_string not configured. Seniority will not initialize.") + self.pool = await aioodbc.create_pool(dsn=dsn, autocommit=True) async with self.pool.acquire() as conn: async with conn.cursor() as cur: @@ -432,6 +433,13 @@ async def usercurrent(self, ctx, user: discord.User): args = [now_date(), server.id, user.id] await self.queryAndPrint(ctx, server, GET_DATE_POINTS_QUERY, args) + @seniority.command() + @checks.is_owner() + async def set_dbstr(self, ctx, dbstr): + """Print the current day's points for a user.""" + self.settings.set_db_string(dbstr) + await ctx.send(inline("Done")) + @seniority.command() @commands.guild_only() async def blacklist(self, ctx, user: discord.User, reason: str): @@ -791,7 +799,8 @@ def now_date(): class SenioritySettings(CogSettings): def make_default_settings(self): config = { - 'servers': {} + 'servers': {}, + 'dbstring': '' } return config @@ -966,6 +975,13 @@ def set_channel(self, server_id: str, channel_id: str, max_ppd: int): } self.save_settings() + def get_db_string(self): + return self.bot_settings['dbstring'] + + def set_db_string(self, dbstr): + self.bot_settings['dbstring'] = dbstr + self.save_settings() + def force_number(s): try: From 4f2ed68abe16626bb5e2941c0a984fcab56cee17 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 17:23:22 -0700 Subject: [PATCH 0066/1220] back to how it was --- seniority/seniority.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/seniority/seniority.py b/seniority/seniority.py index 7ab91222..bc82024f 100644 --- a/seniority/seniority.py +++ b/seniority/seniority.py @@ -133,11 +133,10 @@ async def init(self): print('Seniority: bailing on unlock') return - dsn = self.settings.get_db_string().format(self.db_path) - - if not dsn: - print("DB_string not configured. Seniority will not initialize.") - + if os.name != 'nt' and sys.platform != 'win32': + dsn = 'Driver=SQLite3;Database=' + self.db_path + else: + dsn = 'Driver=SQLite3 ODBC Driver;Database=' + self.db_path self.pool = await aioodbc.create_pool(dsn=dsn, autocommit=True) async with self.pool.acquire() as conn: async with conn.cursor() as cur: @@ -433,13 +432,6 @@ async def usercurrent(self, ctx, user: discord.User): args = [now_date(), server.id, user.id] await self.queryAndPrint(ctx, server, GET_DATE_POINTS_QUERY, args) - @seniority.command() - @checks.is_owner() - async def set_dbstr(self, ctx, dbstr): - """Print the current day's points for a user.""" - self.settings.set_db_string(dbstr) - await ctx.send(inline("Done")) - @seniority.command() @commands.guild_only() async def blacklist(self, ctx, user: discord.User, reason: str): @@ -799,8 +791,7 @@ def now_date(): class SenioritySettings(CogSettings): def make_default_settings(self): config = { - 'servers': {}, - 'dbstring': '' + 'servers': {} } return config @@ -975,13 +966,6 @@ def set_channel(self, server_id: str, channel_id: str, max_ppd: int): } self.save_settings() - def get_db_string(self): - return self.bot_settings['dbstring'] - - def set_db_string(self, dbstr): - self.bot_settings['dbstring'] = dbstr - self.save_settings() - def force_number(s): try: From 87091fab58cda2b0c8e07b23502299a5a5877d80 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 17:54:14 -0700 Subject: [PATCH 0067/1220] Miru can ping spam now! :) --- baduser/baduser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/baduser/baduser.py b/baduser/baduser.py index da0ab005..a4f1cb82 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -383,13 +383,13 @@ async def mod_user_join(self, member): if strikes: msg = 'Hey @here a user with {} strikes just joined the server: {}'.format( strikes, member.mention) - await channel_obj.send(msg) + await channel_obj.send(msg, allowed_mentions=discord.AllowedMentions(everyone=True)) local_ban = self.settings.bannedUsers().get(member.id, None) if local_ban: msg = 'Hey @here locally banned user {} (for: {}) just joined the server'.format( member.mention, local_ban) - await channel_obj.send(msg) + await channel_obj.send(msg, allowed_mentions=discord.AllowedMentions(everyone=True)) @commands.Cog.listener('on_member_update') async def check_punishment(self, before, after): From d023cda9db25888b0b67808e5d9aa5cc3cad85cc Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 22:09:27 -0700 Subject: [PATCH 0068/1220] Big GDPR dump --- automod2/automod2.py | 1 - dadguide/info.json | 2 +- modnotes/__init__.py | 1 + modnotes/info.json | 15 ++++++++++ modnotes/modnotes.py | 49 +++++++++------------------------ msgutils/__init__.py | 1 + msgutils/info.json | 15 ++++++++++ msgutils/msgutils.py | 12 ++++++++ padboard/__init__.py | 1 + padboard/info.json | 15 ++++++++++ padboard/padboard.py | 12 ++++++++ padbuildimg/__init__.py | 2 ++ padbuildimg/info.json | 18 +++++++++++- padbuildimg/padbuildimg.py | 12 ++++++++ padbuilds/__init__.py | 1 + padbuilds/info.json | 15 ++++++++++ padbuilds/padbuilds.py | 12 ++++++++ padevents/__init__.py | 3 +- padevents/info.json | 18 ++++++++++++ padevents/padevents.py | 12 ++++++++ padglobal/__init__.py | 1 + padglobal/info.json | 19 +++++++++++++ padglobal/padglobal.py | 12 ++++++++ padguidedb/__init__.py | 2 +- padguidedb/info.json | 17 ++++++++++++ padguidedb/padguidedb.py | 12 ++++++++ padinfo/__init__.py | 1 + padinfo/info.json | 19 +++++++++++++ padinfo/padinfo.py | 12 ++++++++ padmonitor/__init__.py | 1 + padmonitor/info.json | 18 ++++++++++++ padmonitor/padmonitor.py | 12 ++++++++ padsearch/__init__.py | 1 + padsearch/info.json | 15 ++++++++++ padsearch/padsearch.py | 12 ++++++++ padvision/__init__.py | 1 + padvision/info.json | 15 ++++++++++ padvision/padvision.py | 12 ++++++++ playground/__init__.py | 1 + playground/info.json | 15 ++++++++++ playground/playground.py | 24 ++++++++-------- profile/__init__.py | 1 + profile/info.json | 15 ++++++++++ profile/profile.py | 50 +++++++++++++++++++++++++++++++--- rpadutils/__init__.py | 1 + rpadutils/info.json | 15 ++++++++++ rpadutils/rpadutils.py | 56 +++++++------------------------------- 47 files changed, 474 insertions(+), 103 deletions(-) create mode 100644 modnotes/info.json create mode 100644 msgutils/info.json create mode 100644 padboard/info.json create mode 100644 padbuilds/info.json create mode 100644 padevents/info.json create mode 100644 padglobal/info.json create mode 100644 padguidedb/info.json create mode 100644 padinfo/info.json create mode 100644 padmonitor/info.json create mode 100644 padsearch/info.json create mode 100644 padvision/info.json create mode 100644 playground/info.json create mode 100644 profile/info.json create mode 100644 rpadutils/info.json diff --git a/automod2/automod2.py b/automod2/automod2.py index 0dd26c93..089eed1e 100644 --- a/automod2/automod2.py +++ b/automod2/automod2.py @@ -121,7 +121,6 @@ async def red_delete_data_for_user(self, *, requester, user_id): else: self.settings.clearUserDataFull(user_id) - @commands.command() @checks.mod_or_permissions(manage_guild=True) async def automodhelp(self, ctx): diff --git a/dadguide/info.json b/dadguide/info.json index e442c40d..4f3bfab6 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -11,5 +11,5 @@ "disabled": false, "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, "requirements": ["pytz", "romkan"], - "tags": ["pad"] + "tags": ["PAD"] } diff --git a/modnotes/__init__.py b/modnotes/__init__.py index 96ebb516..4711ba81 100644 --- a/modnotes/__init__.py +++ b/modnotes/__init__.py @@ -1,5 +1,6 @@ from .modnotes import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(ModNotes(bot)) diff --git a/modnotes/info.json b/modnotes/info.json new file mode 100644 index 00000000..e120fb07 --- /dev/null +++ b/modnotes/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Keeps global tabs on iffy users.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Global user notes", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["moderation"] +} diff --git a/modnotes/modnotes.py b/modnotes/modnotes.py index a4a1a4cb..57f1bb07 100644 --- a/modnotes/modnotes.py +++ b/modnotes/modnotes.py @@ -27,6 +27,18 @@ async def usernotes(self, ctx): This module allows you to create notes to share between moderators. """ + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @usernotes.command() @checks.mod_or_permissions(manage_guild=True) async def get(self, ctx, user: discord.User): @@ -45,7 +57,7 @@ async def get(self, ctx, user: discord.User): async def add(self, ctx, user: discord.User, *, note_text: str): """Add a note to a user.""" timestamp = str(ctx.message.created_at)[:-7] - msg = 'Added by {} ({}): {}'.format(ctx.author.id, timestamp, note_text) + msg = 'Added by {} ({}): {}'.format(ctx.author.name, timestamp, note_text) server_id = ctx.guild.id notes = self.settings.addNoteForUser(server_id, user.id, msg) await ctx.send(inline('Done. User {} now has {} notes'.format(user.name, len(notes)))) @@ -122,38 +134,3 @@ def addNoteForUser(self, server_id, user_id, note: str): notes.append(note) self.setNotesForUser(server_id, user_id, notes) return notes - - # GDPR Compliance Functions - def getUserData(self, user_id): - o = { - 'notes': 0 - } - - for gid in self.bot_settings['servers']: - for uid in rpadutils.deepget(self.bot_settings, ['servers', gid, 'user_notes'], []): - for c in range(len(self.bot_settings['servers'][gid]['user_notes'][uid])): - self.bot_settings['servers'][gid]['user_notes'][uid][c] = re.sub( - r'^Added by {} \('.format(uid), - 'Added by ANONYMOUS (', - self.bot_settings['servers'][gid]['user_notes'][uid][c]) - return o - - def clearUserData(self, user_id): - # Anonymize requesting banners and phrasemakers - for gid in self.bot_settings['configs']: - for uid in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): - if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users', uid, 'request_user_id'], -2141): - self.bot_settings['configs'][gid]['watchdog']['users'][uid]['request_user_id'] = -1 - for phr in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases'], []): - if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases', phr, 'request_user_id'], -2141): - self.bot_settings['configs'][gid]['watchdog']['phrases'][phr]['request_user_id'] = -1 - self.save_settings() - - def clearUserDataFull(self, user_id): - self.clearUserData(user_id) - - # Remove bannees - for gid in self.bot_settings['configs']: - if user_id in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): - del self.bot_settings['configs'][gid]['watchdog']['users'][user_id] - self.save_settings() diff --git a/msgutils/__init__.py b/msgutils/__init__.py index bdfe8cd4..bae0d4f1 100644 --- a/msgutils/__init__.py +++ b/msgutils/__init__.py @@ -1,5 +1,6 @@ from .msgutils import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(MsgUtils(bot)) diff --git a/msgutils/info.json b/msgutils/info.json new file mode 100644 index 00000000..8736d91c --- /dev/null +++ b/msgutils/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Allows users to see raw messages and manipulate bot messages in new ways.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Message manipulation", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [], + "tags": ["administration"] +} diff --git a/msgutils/msgutils.py b/msgutils/msgutils.py index 66650c99..25314f63 100644 --- a/msgutils/msgutils.py +++ b/msgutils/msgutils.py @@ -15,6 +15,18 @@ def __init__(self, bot: Red, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.command() @checks.mod_or_permissions(manage_guild=True) async def editmsg(self, ctx, channel: discord.TextChannel, msg_id: int, *, new_msg: str): diff --git a/padboard/__init__.py b/padboard/__init__.py index ec1690d2..97b63a4e 100644 --- a/padboard/__init__.py +++ b/padboard/__init__.py @@ -1,5 +1,6 @@ from .padboard import * +__red_end_user_data_statement__ = "No personal data is stored persistantly." def setup(bot): bot.add_cog(PadBoard(bot)) diff --git a/padboard/info.json b/padboard/info.json new file mode 100644 index 00000000..e6036c5d --- /dev/null +++ b/padboard/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Turn a screenshot into a P&D simulation via DawnGlare.", + "end_user_data_statement": "No personal data is stored persistantly.", + "install_msg": "", + "short": "Dawnglare utils", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [], + "tags": ["PAD"] +} diff --git a/padboard/padboard.py b/padboard/padboard.py index 61d1041e..ff6d66aa 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -25,6 +25,18 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.logs = defaultdict(lambda: deque(maxlen=1)) + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.Cog.listener("on_message") async def log_message(self, message): url = rpadutils.extract_image_url(message) diff --git a/padbuildimg/__init__.py b/padbuildimg/__init__.py index fb5cbbbc..5a0008d3 100644 --- a/padbuildimg/__init__.py +++ b/padbuildimg/__init__.py @@ -1,4 +1,6 @@ from .padbuildimg import * +__red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadBuildImage(bot)) diff --git a/padbuildimg/info.json b/padbuildimg/info.json index 2152df91..b3d00b27 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -1,3 +1,19 @@ { - "author" : ["Chu (chu2.718281828459045235360287471#0110)"] + "author": ["chu2.718281828459045235360287471#0110", "The Tsubaki Bot Team"], + "description": "Build a PAD team image via commands.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PDChu cog", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["ply", "Pillow", "aiohttp"], + "tags": ["PAD"] } diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index bc1349ae..c4ead205 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -158,6 +158,18 @@ def make_default_build_img_params(self): # os.mkdir(build_img_params.OUTPUT_DIR) return build_img_params + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + def buildImgParams(self): if 'build_img_params' not in self.bot_settings: self.bot_settings['build_img_params'] = self.make_default_build_img_params() diff --git a/padbuilds/__init__.py b/padbuilds/__init__.py index ea10d782..d3532a22 100644 --- a/padbuilds/__init__.py +++ b/padbuilds/__init__.py @@ -1,5 +1,6 @@ from .padbuilds import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(PadBuilds(bot)) diff --git a/padbuilds/info.json b/padbuilds/info.json new file mode 100644 index 00000000..0978625f --- /dev/null +++ b/padbuilds/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "View user-set builds for various dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Builds to help you through dungeons", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["PAD"] +} diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index 4580cf31..97dbd22f 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -18,6 +18,18 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.c_commands = PadBuildSettings("padbuilds") + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group(aliases=["build"]) @commands.guild_only() async def builds(self, ctx): diff --git a/padevents/__init__.py b/padevents/__init__.py index a73d6a75..e72e9a91 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -1,7 +1,6 @@ -import warnings - from .padevents import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = PadEvents(bot) diff --git a/padevents/info.json b/padevents/info.json new file mode 100644 index 00000000..67c1550a --- /dev/null +++ b/padevents/info.json @@ -0,0 +1,18 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Stay up to date on PAD dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PAD Event tracker", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["pytz", "prettytable"], + "tags": ["PAD"] +} diff --git a/padevents/padevents.py b/padevents/padevents.py index b64dc504..f2dbbcb6 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -33,6 +33,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.fake_uid = -999 + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.events = list() diff --git a/padglobal/__init__.py b/padglobal/__init__.py index 2b679c34..e722b093 100644 --- a/padglobal/__init__.py +++ b/padglobal/__init__.py @@ -1,5 +1,6 @@ from .padglobal import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(PadGlobal(bot)) diff --git a/padglobal/info.json b/padglobal/info.json new file mode 100644 index 00000000..5f75dec1 --- /dev/null +++ b/padglobal/info.json @@ -0,0 +1,19 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Tsubaki guides and tips written by P&D professionals", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["aiohttp", "prettytable"], + "tags": ["PAD"] +} diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index b8099c07..1a326aa8 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -114,6 +114,18 @@ def __init__(self, bot: Red, *args, **kwargs): self._export_data() + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + def _export_data(self): faq_and_boards = self.settings.faq() + self.settings.boards() diff --git a/padguidedb/__init__.py b/padguidedb/__init__.py index b11e1b1a..4a503383 100644 --- a/padguidedb/__init__.py +++ b/padguidedb/__init__.py @@ -1,7 +1,7 @@ from .padguidedb import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): - # TODO: Test this! I don't have the databases pdb = PadGuideDb(bot) bot.add_cog(pdb) diff --git a/padguidedb/info.json b/padguidedb/info.json new file mode 100644 index 00000000..12d66bd4 --- /dev/null +++ b/padguidedb/info.json @@ -0,0 +1,17 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Dungeon scraper and general database manager.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Dungeon data scraper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["pymysql"], + "tags": ["PAD"] +} diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 1fc897a1..c5d0fddb 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -39,6 +39,18 @@ def __init__(self, bot: Red, *args, **kwargs): global PADGUIDEDB_COG PADGUIDEDB_COG = self + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + def get_cursor(self): with open(self.settings.configFile(), 'r') as db_config: return self.connect(json.load(db_config)).cursor() diff --git a/padinfo/__init__.py b/padinfo/__init__.py index b35067a7..13827c7b 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -1,5 +1,6 @@ from .padinfo import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = PadInfo(bot) diff --git a/padinfo/info.json b/padinfo/info.json new file mode 100644 index 00000000..a94f0339 --- /dev/null +++ b/padinfo/info.json @@ -0,0 +1,19 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Serves monster data (LOAD THIS THIRD)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "speech": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["prettytable"], + "tags": ["PAD"] +} diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 870680af..d4dfb206 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -202,6 +202,18 @@ def cog_unload(self): self.historic_lookups = {} self.historic_lookups_id2 = {} + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + async def reload_nicknames(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadInfo'): diff --git a/padmonitor/__init__.py b/padmonitor/__init__.py index b2457b78..db55be78 100644 --- a/padmonitor/__init__.py +++ b/padmonitor/__init__.py @@ -1,5 +1,6 @@ from .padmonitor import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = PadMonitor(bot) diff --git a/padmonitor/info.json b/padmonitor/info.json new file mode 100644 index 00000000..927aeb17 --- /dev/null +++ b/padmonitor/info.json @@ -0,0 +1,18 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Checks for and announces new monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Announce new monsters", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + }, + "requirements": [], + "tags": ["PAD"] +} diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 5ef48188..04fa520d 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -12,6 +12,18 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.settings = PadMonitorSettings("padmonitor") + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + async def check_seen_loop(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadMonitor'): diff --git a/padsearch/__init__.py b/padsearch/__init__.py index af5597eb..bbe85cbb 100644 --- a/padsearch/__init__.py +++ b/padsearch/__init__.py @@ -1,5 +1,6 @@ from .padsearch import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(PadSearch(bot)) diff --git a/padsearch/info.json b/padsearch/info.json new file mode 100644 index 00000000..794cf225 --- /dev/null +++ b/padsearch/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Search for cards using advanced filters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Card search", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["ply"], + "tags": ["PAD"] +} diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index d8baf29a..dacb2d7c 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -723,6 +723,18 @@ def __init__(self, bot: Red, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.command() async def helpsearch(self, ctx): """Help info for the search command.""" diff --git a/padvision/__init__.py b/padvision/__init__.py index 0f721afb..eca6ba01 100644 --- a/padvision/__init__.py +++ b/padvision/__init__.py @@ -1,5 +1,6 @@ from .padvision import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(PadVision(bot)) diff --git a/padvision/info.json b/padvision/info.json new file mode 100644 index 00000000..cdc1fa5f --- /dev/null +++ b/padvision/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "A beefy helper cog for neural networks.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Neural net helper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["numpy", "tensorflow", "Pillow"], + "tags": ["PAD"] +} diff --git a/padvision/padvision.py b/padvision/padvision.py index 545f8a84..f333ce13 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -12,6 +12,18 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + EXTRACTABLE = 'rbgldhjpmo' diff --git a/playground/__init__.py b/playground/__init__.py index bb38f2fb..27cd12eb 100644 --- a/playground/__init__.py +++ b/playground/__init__.py @@ -1,5 +1,6 @@ from .playground import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(Playground(bot)) diff --git a/playground/info.json b/playground/info.json new file mode 100644 index 00000000..b1b9c022 --- /dev/null +++ b/playground/info.json @@ -0,0 +1,15 @@ +{ + "author": ["Aradia Megido#2552"], + "description": "This cog doesn't do anything.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "\u00af\\_(\u30c4)_/\u00af", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [], + "tags": [] +} diff --git a/playground/playground.py b/playground/playground.py index 37fff9f4..f7f3095d 100644 --- a/playground/playground.py +++ b/playground/playground.py @@ -1,17 +1,19 @@ -import json -import re -from fnmatch import fnmatch - -from ply import lex -from redbot.core import checks -from redbot.core import commands +from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, pagify -from rpadutils import rpadutils - class Playground(commands.Cog): - """Just lil' fun things.""" - def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return diff --git a/profile/__init__.py b/profile/__init__.py index 3132499e..244090d8 100644 --- a/profile/__init__.py +++ b/profile/__init__.py @@ -1,5 +1,6 @@ from .profile import * +__red_end_user_data_statement__ = "Profile data is stored persistantly." def setup(bot): bot.add_cog(Profile(bot)) diff --git a/profile/info.json b/profile/info.json new file mode 100644 index 00000000..001ccfc2 --- /dev/null +++ b/profile/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Holds all of your Puzzle & Dragons accounts in one place", + "end_user_data_statement": "Profile data is stored persistantly.", + "install_msg": "", + "short": "P&D Phonebook", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["PAD"] +} diff --git a/profile/profile.py b/profile/profile.py index aa6acbfe..1869cc3f 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -34,7 +34,7 @@ def formatId(id): def computeOldGroup(str_id): - old_id_digit = str_id[2] + old_id_digit = str(str_id)[2] return chr(ord('A') + (int(old_id_digit) % 5)) @@ -44,9 +44,28 @@ def __init__(self, bot: Red, *args, **kwargs): self.bot = bot self.settings = ProfileSettings("profile") - async def on_ready(self): - """ready""" - print("started profile") + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + udata = self.settings.getUserData(user_id) + + data = "Stored data for user with ID {}:\n".format(user_id) + if udata['servers']: + data += " - You have a profile on the following server(s): {}.\n".format(', '.join(udata['servers'])) + if udata['default_server']: + data += " - Your default server is {}.\n".format(udata['default_server']) + + if not any(udata.values()): + data = "No data is stored for user with ID {}.\n".format(user_id) + + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + + if requester not in ("discord_deleted_user", "owner"): + self.settings.clearUserData(user_id) + else: + self.settings.clearUserDataFull(user_id) @commands.command(name="idme") async def idMe(self, ctx, server=None): @@ -307,3 +326,26 @@ def clearProfile(self, user, server=None): else: self.getProfile(user, server).clear() self.save_settings() + + # GDPR Compliance Functions + def getUserData(self, user_id): + o = { + 'servers': [], + 'default_server': '', + } + + if user_id in self.bot_settings['default_servers']: + o['default_server'] = self.bot_settings['default_servers'][user_id] + if user_id in self.bot_settings['user_profiles']: + o['servers'] = list(self.bot_settings['user_profiles'][user_id]) + return o + + def clearUserData(self, user_id): + if user_id in self.bot_settings['default_servers']: + del self.bot_settings['default_servers'][user_id] + if user_id in self.bot_settings['user_profiles']: + del list(self.bot_settings['user_profiles'][user_id]) + self.save_settings() + + def clearUserDataFull(self, user_id): + self.clearUserData(user_id) diff --git a/rpadutils/__init__.py b/rpadutils/__init__.py index 6530747d..89dcf2b5 100644 --- a/rpadutils/__init__.py +++ b/rpadutils/__init__.py @@ -1,5 +1,6 @@ from .rpadutils import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(RpadUtils(bot)) diff --git a/rpadutils/info.json b/rpadutils/info.json new file mode 100644 index 00000000..6f32d4a6 --- /dev/null +++ b/rpadutils/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Helper functions for most other Tsubaki cogs.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Helper functions (LOAD THIS FIRST)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["pytz", "aiohttp", "backoff"], + "tags": ["PAD", "sharedlibrary"] +} diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index fe3b4269..e89cd58b 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -31,13 +31,17 @@ def __init__(self, bot: Red, *args, **kwargs): global RPADCOG RPADCOG = self - def user_allowed(self, message): - author = message.author + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} - if author.bot: - return False - return True + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + No personal data is stored in this cog. + """ + return # TZ used for PAD NA # NA_TZ_OBJ = pytz.timezone('America/Los_Angeles') @@ -67,35 +71,6 @@ def containsJp(txt): return JP_REGEX.search(txt) -class PermissionsError(CommandNotFound): - """ - Base exception for all others in this module - """ - - -class BadCommand(PermissionsError): - """ - Thrown when we can't decipher a command from string into a command object. - """ - pass - - -class RoleNotFound(PermissionsError): - """ - Thrown when we can't get a valid role from a list and given name - """ - pass - - -class SpaceNotation(BadCommand): - """ - Throw when, with some certainty, we can say that a command was space - notated, which would only occur when some idiot...fishy...tries to - surround a command in quotes. - """ - pass - - def get_role(roles, role_string): if role_string.lower() == "everyone": role_string = "@everyone" @@ -118,7 +93,7 @@ def get_role_from_id(bot, guild, roleid): try: roles = guild.roles except AttributeError: - raise RoleNotFound(guild, roleid) + raise ValueError("Role with id {} not found.".format(roleid)) role = discord.utils.get(roles, id=roleid) if role is None: @@ -221,10 +196,6 @@ async def boxPagifySay(say_fn, msg): await say_fn(box(page)) -class Forbidden(): - pass - - def default_check(payload): return not payload.member.bot @@ -639,13 +610,6 @@ async def run_in_loop(bot, task, *args): return await running_task -async def translate_jp_en(bot, jp_text): - translate_cog = bot.get_cog('Translate') - if not translate_cog: - return None - return await run_in_loop(bot, translate_cog.translate_jp_en, jp_text) - - def validate_json(fp): try: json.load(open(fp)) From 6175ac3a5d8177d8c9250625cb2a81a9d91dfd78 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 22:13:13 -0700 Subject: [PATCH 0069/1220] Quick Fix --- profile/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/profile/profile.py b/profile/profile.py index 1869cc3f..cb0eaede 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -61,7 +61,7 @@ async def red_get_data_for_user(self, *, user_id): async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data.""" - + if requester not in ("discord_deleted_user", "owner"): self.settings.clearUserData(user_id) else: @@ -344,7 +344,7 @@ def clearUserData(self, user_id): if user_id in self.bot_settings['default_servers']: del self.bot_settings['default_servers'][user_id] if user_id in self.bot_settings['user_profiles']: - del list(self.bot_settings['user_profiles'][user_id]) + del self.bot_settings['user_profiles'][user_id] self.save_settings() def clearUserDataFull(self, user_id): From a68c57a34f3bb400cedf158f69d231d6a2687e89 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 22:19:19 -0700 Subject: [PATCH 0070/1220] fix invalid jsons --- chronomagia/info.json | 2 +- padmonitor/info.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chronomagia/info.json b/chronomagia/info.json index 1a99fb82..5fa1b88a 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -11,5 +11,5 @@ "disabled": false, "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, "requirements": ["aiohttp"], - "tags": ["chronomagia"], + "tags": ["chronomagia"] } diff --git a/padmonitor/info.json b/padmonitor/info.json index 927aeb17..709561eb 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -11,7 +11,7 @@ "disabled": false, "require_cogs": { "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [], "tags": ["PAD"] From eeb136148d07d9475e710795ce6d6b2c7f4afd0a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 22:51:54 -0700 Subject: [PATCH 0071/1220] Update rpadutils.py --- rpadutils/rpadutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index 2bc9a2e6..8bab464f 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -229,7 +229,7 @@ def default_check(payload): try: return not payload.member.bot except Exception as e: - print("DEBUG: "+e) + print("DEBUG:",e) print(payload) raise e From 941083b02b2862b239ccf545d4320d17f3cd8846 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Sep 2020 23:08:26 -0700 Subject: [PATCH 0072/1220] lets just lie and hope this fixes things --- padvision/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padvision/info.json b/padvision/info.json index cdc1fa5f..6dc84461 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -10,6 +10,6 @@ "hidden": false, "disabled": false, "require_cogs": {}, - "requirements": ["numpy", "tensorflow", "Pillow"], + "requirements": ["Pillow"], "tags": ["PAD"] } From ef29a9055fe6d4ba5893725d71fb17e14d50940d Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 00:21:38 -0700 Subject: [PATCH 0073/1220] Silence Errors --- dadguide/dadguide.py | 2 +- rpadutils/rpadutils.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b9b9dfed..ddaa9e46 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -100,7 +100,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def create_index(self, accept_filter=None): """Exported function that allows a client cog to create a monster index""" - print("DEBUG:",self.database._con) + await self.wait_until_ready() return await MonsterIndex(self.database, self.nickname_overrides, self.basename_overrides, diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index ba5a1f1d..150abc95 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -197,12 +197,7 @@ async def boxPagifySay(say_fn, msg): def default_check(payload): - try: - return not payload.member.bot - except Exception as e: - print("DEBUG:",e) - print(payload) - raise e + return payload.user_id != RPADCOG.bot.user.id and not (payload.guild_id and payload.member.bot) class EmojiUpdater(object): From 9bc4850f170ef8457ad7b0c7c02a57e97094035b Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 13:38:39 -0700 Subject: [PATCH 0074/1220] scheduler --- scheduler/__init__.py | 6 - scheduler/scheduler.py | 313 ------------------------------------- timecog/timecog.py | 341 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 300 insertions(+), 360 deletions(-) delete mode 100644 scheduler/__init__.py delete mode 100644 scheduler/scheduler.py diff --git a/scheduler/__init__.py b/scheduler/__init__.py deleted file mode 100644 index 791c6d29..00000000 --- a/scheduler/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .scheduler import * - -def setup(bot): - n = Scheduler(bot) - bot.loop.create_task(n.queue_manager()) - bot.add_cog(n) diff --git a/scheduler/scheduler.py b/scheduler/scheduler.py deleted file mode 100644 index 5f598c86..00000000 --- a/scheduler/scheduler.py +++ /dev/null @@ -1,313 +0,0 @@ -import discord - -from redbot.core import commands, checks, Config -from redbot.core.utils.chat_formatting import * - -import logging -import os -import asyncio -import time -from random import randint -from math import ceil - -from rpadutils import CogSettings - -log = logging.getLogger("red.scheduler") -log.setLevel(logging.INFO) - - -class Event: - def __init__(self, data=None): - self.name = data.pop('name') - self.channel = data.pop('channel') - self.server = data.pop('server') - self.author = data.pop('author') - self.command = data.pop('command') - self.timedelta = data.pop('timedelta') - self.repeat = data.pop('repeat') - self.starttime = data.pop('starttime', None) - - def __lt__(self, other): - my_sig = "{}-{}-{}-{}".format(self.timedelta, self.name, - self.starttime, self.channel) - other_sig = "{}-{}-{}-{}".format(other.timedelta, other.name, - other.starttime, other.channel) - return hash(my_sig) < hash(other_sig) - - -class Scheduler(commands.Cog): - """Schedules commands to run every so often. - Times are formed as follows: 1s, 2m, 3h, 5d, 1w - """ - - def __init__(self, bot): - self.bot = bot - - self.config = Config.get_conf(self, identifier=537367732) - self.config.register_guild(events=[]) # We should use this, but aaaa - - self.settings = SchedulerSettings("scheduler") - self.events = self.settings.getevents() - self.queue = asyncio.PriorityQueue() - self.queue_lock = asyncio.Lock() - self.to_kill = {} - self._load_events() - - def save_events(self): - self.settings.setevents(self.events) - - def _load_events(self): - # for entry in the self.events make an Event - for server in self.events: - for name, event in self.events[server].items(): - ret = {} - ret['server'] = server - ret.update(event) - e = Event(ret) - self.bot.loop.create_task(self._put_event(e)) - - async def _put_event(self, event, fut=None, offset=None): - if fut is None: - now = int(time.time()) - if event.repeat: - diff = now - event.starttime - fut = ((ceil(diff / event.timedelta) * event.timedelta) + - event.starttime) - else: - fut = now + event.timedelta - if offset: - fut += offset - await self.queue.put((fut, event)) - log.debug('Added "{}" to the scheduler queue at {}'.format(event.name, - fut)) - - async def _add_event(self, name, command, - dest_server: discord.Guild, - dest_channel: discord.TextChannel, - author: discord.User, - timedelta, repeat=False): - dest_server = dest_server.id - dest_channel = dest_channel.id - author = author.id - - if dest_server not in self.events: - self.events[dest_server] = {} - - event_dict = {'name': name, - 'channel': dest_channel, - 'author': author, - 'command': command, - 'timedelta': timedelta, - 'repeat': repeat} - - log.debug('event dict:\n\t{}'.format(event_dict)) - - now = int(time.time()) - event_dict['starttime'] = now - self.events[dest_server][name] = event_dict.copy() - - event_dict['server'] = dest_server - e = Event(event_dict.copy()) - await self._put_event(e) - - self.save_events() - - async def _remove_event(self, name, server): - async with self.queue_lock: - events = [] - while self.queue.qsize() != 0: - time, event = await self.queue.get() - if not (name == event.name and server.id == event.server): - events.append((time, event)) - - for event in events: - await self.queue.put(event) - self.queue_lock.release() - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_messages=True) - async def scheduler(self, ctx): - pass - - @scheduler.command() - async def add(self, ctx, time_interval, *, command): - """Add a command to run in [time_interval] seconds. - Times are formed as follows: 1s, 2m, 3h, 5d, 1w - """ - channel = ctx.channel - server = ctx.guild - author = ctx.author - name = command.lower() - try: - s = self._parse_time(time_interval) - log.debug('run command in {}s'.format(s)) - except: - await self.bot.send_cmd_help(ctx) - return - if s < 30: - await ctx.send('yeah I can\'t do that, your time' - ' interval is waaaay too short and I\'ll' - ' likely get rate limited. Try going above' - ' 30 seconds.') - return - log.info('add {} "{}" to {} on {} in {}s'.format( - name, command, channel.name, server.name, s)) - await self._add_event(name, command, server, channel, author, s) - await ctx.send('I will run "{}" in {}s'.format(command, s)) - - @scheduler.command() - async def repeat(self, ctx, name, time_interval, *, command): - """Add a command to run every [time_interval] seconds. - Times are formed as follows: 1s, 2m, 3h, 5d, 1w - """ - channel = ctx.channel - server = ctx.guild - author = ctx.author - name = name.lower() - try: - s = self._parse_time(time_interval) - log.debug('run command in {}s'.format(s)) - except: - await self.bot.send_cmd_help(ctx) - return - if s < 30: - await ctx.send('yeah I can\'t do that, your time' - ' interval is waaaay too short and I\'ll' - ' likely get rate limited. Try going above' - ' 30 seconds.') - return - log.info('add {} "{}" to {} on {} every {}s'.format( - name, command, channel.name, server.name, s)) - await self._add_event(name, command, server, channel, author, s, True) - await ctx.send('"{}" will run "{}" every {}s'.format(name, command, s)) - - @scheduler.command() - async def remove(self, ctx, name): - """Removes scheduled command from running. - """ - server = ctx.guild - name = name.lower() - if server.id not in self.events: - await ctx.send('No events are scheduled for this server.') - return - if name not in self.events[server.id]: - await ctx.send('That event does not exist on this server.') - return - - del self.events[server.id][name] - await self._remove_event(name, server) - self.save_events() - await ctx.send('"{}" has successfully been removed but' - ' it may run once more.'.format(name)) - - @scheduler.command() - async def list(self, ctx): - """Lists all repeated commands - """ - server = ctx.guild - if server.id not in self.events: - await ctx.send('No events scheduled for this server.') - return - elif len(self.events[server.id]) == 0: - await ctx.send('No events scheduled for this server.') - return - mess = "Names:\n\t" - mess += "\n\t".join(sorted(self.events[server.id].keys())) - await ctx.send(box(mess)) - - def _parse_time(self, time): - translate = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400, 'w': 604800} - timespec = time[-1] - if timespec.lower() not in translate: - raise ValueError - timeint = int(time[:-1]) - return timeint * translate.get(timespec) - - def run_coro(self, event): - channel = self.bot.get_channel(event.channel) - try: - server = channel.guild - prefix = self.bot.settings.get_prefixes(server)[0] - except AttributeError: - log.debug("Channel no longer found, not running scheduled event.") - return - data = {} - data['timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.gmtime()) - data['id'] = randint(10**(17), (10**18) - 1) - data['content'] = prefix + event.command - data['channel'] = self.bot.get_channel(event.channel) - data['author'] = {'id': event.author} - data['nonce'] = randint(-2**32, (2**32) - 1) - data['channel_id'] = event.channel - data['reactions'] = [] - fake_message = discord.Message(**data) - # coro = self.bot.process_commands(fake_message) - log.info("Running '{}' in {}".format(event.name, event.server)) - # self.bot.loop.create_task(coro) - self.bot.dispatch('message', fake_message) - - async def queue_manager(self): - while self == self.bot.get_cog('Scheduler'): - async with self.queue_lock: - if self.queue.qsize() != 0: - next_time, next_event = await self.queue.get() - diff = max(next_time-int(time.time()), 0) - if diff < 30: - log.debug('scheduling call of "{}" in {}s'.format( - next_event.name, diff)) - fut = self.bot.loop.call_later(diff, self.run_coro, - next_event) - self.to_kill[next_time] = fut - if next_event.repeat: - await self._put_event(next_event, next_time, - next_event.timedelta) - else: - del self.events[next_event.server][next_event.name] - self.save_events() - else: - log.debug('Will run {} "{}" in {}s'.format( - next_event.name, next_event.command, diff)) - await self._put_event(next_event, next_time) - - to_delete = [] - for start_time, old_command in self.to_kill.items(): - if time.time() > start_time + 30: - old_command.cancel() - to_delete.append(start_time) - for item in to_delete: - del self.to_kill[item] - - await asyncio.sleep(5) - log.debug('manager dying') - while self.queue.qsize() != 0: - await self.queue.get() - while len(self.to_kill) != 0: - curr = self.to_kill.pop() - curr.cancel() - - -def check_folder(): - if not os.path.exists('data/scheduler'): - os.mkdir('data/scheduler') - - -def check_files(): - f = 'data/scheduler/events.json' - if not os.path.exists(f): - fileIO(f, 'save', {}) - - -class SchedulerSettings(CogSettings): - def make_default_settings(self): - config = { - 'events': {}, - } - return config - - def getevents(self): - return self.bot_settings['events'] - - def setevents(self, events): - self.bot_settings['events'] = events - self.save_settings() diff --git a/timecog/timecog.py b/timecog/timecog.py index ed9f7adb..0af4cdfe 100644 --- a/timecog/timecog.py +++ b/timecog/timecog.py @@ -5,10 +5,11 @@ from datetime import timedelta, datetime from dateutil.relativedelta import relativedelta +import discord import pytz from redbot.core import commands, Config, checks from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline +from redbot.core.utils.chat_formatting import inline, pagify, box import rpadutils @@ -28,7 +29,21 @@ r'^\s*((?:-?\d+ ?(?:m|h|d|w|y|s)\w* ?)+)\b\s*\|\s*((?:-?\d+ ?(?:m|h|d|w|y|s)\w* ?)+|now)\b (.*)$', #Unused ] +exact_tats = [ + r'^\s*(?P\d{4})[-/](?P\d+)[-/](?P\d+) (?P\d+):(?P\d\d) ?(?Ppm|am)?\s*$', + r'^\s*(?P\d{4})[-/](?P\d+)[-/](?P\d+)\s*$', + r'^\s*(?P\d+)[-/](?P\d+)\s*$', + r'^\s*(?P\d+):(?P\d\d) ?(?P\d?pm|am)?\s*$', + r'^\s*(?P\d+) ?(?P\d?pm|am)\s*$', +] + +exact_tins = [ + r'^\s*((?:-?\d+ ?(?:m|h|d|w|y|s)\w* ?)+)$', # One tinstr +] + DT_FORMAT = "%A, %b %-d, %Y at %-I:%M %p" +SHORT_DT_FORMAT = "%b %-d, %Y at %-I:%M %p" + class TimeCog(commands.Cog): @@ -38,47 +53,31 @@ def __init__(self, bot: Red, *args, **kwargs): super().__init__(*args, **kwargs) self.config = Config.get_conf(self, identifier=7173306) self.config.register_user(reminders=[], tz='') - self.config.register_channel(schedules=[]) + self.config.register_guild(schedules={}) """ CONFIG: Config | USERS: Config - | | REMINDERS: list + | | reminders: list | | | REMINDER: tuple | | | | TIME: int (timestamp) | | | | TEXT: str | | | | INTERVAL: Optional[int] - | | TZ: str (tzstr) + | | tz: str (tzstr) | CHANNELS: Config - | | SCHEDULES: list - | | | SCHEDULE: tuple - | | | | START: int (timestamp) - | | | | INTERVAL: int (seconds) - | | | | TEXT: str + | | schedules: dict + | | | NAME: dict + | | | | start: int (timestamp) (unused) + | | | | time: int (timestamp) + | | | | end: int (timestamp) + | | | | interval: int (seconds) + | | | | enabled: bool + | | | | channels: list (cids) + | | | | message: str """ self.bot = bot - #@commands.group(invoke_without_command=True) - async def schedule(self, ctx, *, text): - raise ValueError() - match = re.search(time_in_regeces[0], text, re.IGNORECASE) - if match: - tinstart = "now" - tinstrs, input = match.groups() - else: - match = re.search(time_in_regeces[1], text, re.IGNORECASE) - if not match: - await ctx.send("Invalid interval") - return - tinstart, tinstrs, input = match.groups() - start = (datetime.utcnow() + tin2tdelta(tinstart)).timestamp() - async with self.config.channel(ctx.channel).schedules() as scs: - scs.append((start, tin2tdelta(tinstrs).seconds, input)) - m = await ctx.send("Schedule created. I will send a message on my next cycle (10s or less)") - await asyncio.sleep(5.) - await m.delete() - @commands.group(aliases=['remindmeat', 'remindmein'], invoke_without_command=True) async def remindme(self, ctx, *, time): """Reminds you to do something at a specified time @@ -89,8 +88,6 @@ async def remindme(self, ctx, *, time): [p]remindme 2020-05-03 Do something! [p]remindme 04-13 Do something! """ - if time is None: - return user_tz_str = await self.config.user(ctx.author).tz() user_timezone = tzstr_to_tz(user_tz_str or 'UTC') @@ -201,8 +198,8 @@ async def get(self, ctx): o = "```\n" + '\n'.join(o) + "\n```" await ctx.send(o) - @remindme.command() - async def remove(self, ctx, no: int): + @remindme.command(name="remove") + async def remindme_remove(self, ctx, no: int): """Remove a specific pending reminder""" rlist = sorted(await self.config.user(ctx.author).reminders(), key=lambda x: x[0]) if len(rlist) < no: @@ -218,6 +215,204 @@ async def purge(self, ctx): await self.config.user(ctx.author).reminders.set([]) await ctx.send(inline("Done")) + @commands.group(invoke_without_command=True) + async def schedule(self, ctx, name, *, text): + """Sets up a schedule to fire at the specified interval + + [p]schedule name 1d Do something! + [p]schedule name 5 hours Do something! + """ + match = re.search(time_in_regeces[0], text, re.IGNORECASE) + if match: + tinstart = "now" + tinstrs, input = match.groups() + else: + match = re.search(time_in_regeces[1], text, re.IGNORECASE) + if not match: + if name.isdigit() or re.search(time_in_regeces[0], name+" .", re.IGNORECASE): + await ctx.send("Invalid interval. Remember to put 'name' as the first argument.") + else: + await ctx.send_help() + return + tinstart, tinstrs, input = match.groups() + start = (datetime.utcnow() + tin2tdelta(tinstart)).timestamp() + + async with self.config.guild(ctx.guild).schedules() as schedules: + if name in schedules: + await ctx.send("There is already a schedule with this name.") + return + schedules[name] = { + "start": start, + "time": start, + "end": 2e11, + "interval": tin2tdelta(tinstrs).seconds, + "enabled": True, + "channels": [ctx.channel.id], + "message": input, + } + m = await ctx.send("Schedule created.") + await asyncio.sleep(5.) + await m.delete() + + @schedule.command() + async def begin(self, ctx, name, *, time): + """Sets the start time for a schedule.""" + time = await self.exact_tartintodt(ctx, time) + if time is None: + return + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + schedules[name]['start'] = time.timestamp() + schedules[name]['time'] = time.timestamp() + await ctx.send(inline("Done.")) + + @schedule.command() + async def end(self, ctx, name, *, time): + """Sets the end time for a schedule.""" + time = await self.exact_tartintodt(ctx, time) + if time is None: + return + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + schedules[name]['end'] = time.timestamp() + await ctx.send(inline("Done.")) + + @schedule.command() + async def interval(self, ctx, name, *, time): + """Sets the interval for a schedule.""" + if not re.match(exact_tins[0], time): + await ctx.send("Invalid interval.") + return + interval = tin2tdelta(time) + if interval is None: + return + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + schedules[name]['interval'] = interval.seconds + await ctx.send(inline("Done.")) + + @schedule.command() + async def message(self, ctx, name, *, message): + """Sets the message for a schedule.""" + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + schedules[name]['message'] = message + await ctx.send(inline("Done.")) + + @schedule.group() + async def channel(self, ctx): + """Set or remove channels for a schedule.""" + + @channel.command() + async def add(self, ctx, name, channel: discord.TextChannel): + """Add a channel.""" + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + if channel.id in schedules[name]['channels']: + await ctx.send("This channel is already registered.") + return + schedules[name]['channels'].append(channel.id) + await ctx.send(inline("Done")) + + @channel.command(name="remove") + async def channel_remove(self, ctx, name, channel): + """Remove a channel.""" + c = bot.get_channel() + if c is None and channel.isdigit(): + channel_id = channel + elif c: + channel_id = channel.id + else: + await ctx.send("Invalid channel.") + return + + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + if channel_id not in schedules[name]['channels']: + await ctx.send("This channel is not registered.") + return + schedules[name]['channels'].remove(channel_id) + await ctx.send(inline("Done")) + + @channel.command(name="list") + async def channel_list(self, ctx, name): + """List the registered channels.""" + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + if not schedules[name]['channels']: + await ctx.send("This schedule has no channels.") + return + o = "" + for c in schedules[name]['channels']: + ch = self.bot.get_channel(c) + if ch: + o += "{} ({})\n".format(ch.name, c) + else: + o += "deleted-channel ({})\n".format(c) + for p in pagify(o): + await ctx.send(box(p)) + + @schedule.command() + async def enable(self, ctx, name): + """Enable a schedule.""" + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + schedules[name]['enabled'] = True + await ctx.send(inline("Done.")) + + @schedule.command() + async def disable(self, ctx, name): + """Disable a schedule.""" + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + schedules[name]['enabled'] = False + await ctx.send(inline("Done.")) + + @schedule.command(name="remove") + async def schedule_remove(self, ctx, name): + """Remove a schedule.""" + async with self.config.guild(ctx.guild).schedules() as schedules: + if name not in schedules: + await ctx.send("There is no schedule with this name.") + return + del schedules[name] + await ctx.send(inline("Deleted schedule {}.".format(name))) + + @schedule.command(name="list") + async def schedule_list(self, ctx): + """List the guild's schedules.""" + async with self.config.guild(ctx.guild).schedules() as schedules: + o = "" + for n,s in schedules.items(): + o += " - {}{}\n".format(n, " (disabled)" if not s['enabled'] else " (expired)" if s['end'] < datetime.utcnow().timestamp() else "") + o += " - Start Time: {}\n".format(datetime.fromtimestamp(s['start']).strftime(SHORT_DT_FORMAT)) + o += " - Next Time: {}\n".format(datetime.fromtimestamp(s['time']).strftime(SHORT_DT_FORMAT)) + o += " - End Time: {}\n".format(datetime.fromtimestamp(s['end']).strftime(SHORT_DT_FORMAT)) + o += " - Interval: {}\n".format(timedelta(seconds=s['interval'])) + o += " - Message: {}\n".format(s['message']) + if not o: + await ctx.send(inline("There are no schedules for this guild.")) + for page in pagify(o): + await ctx.send(box(page)) + @commands.command(aliases=['settz']) async def settimezone(self, ctx, tzstr): """Set your timezone.""" @@ -232,7 +427,7 @@ async def reminderloop(self): await self.bot.wait_until_ready() async for _ in rpadutils.repeating_timer(10, lambda: self == self.bot.get_cog('TimeCog')): urs = await self.config.all_users() - chs = await self.config.all_channels() + gds = await self.config.all_guilds() now = datetime.utcnow() for u in urs: for rm in urs[u]['reminders']: @@ -240,13 +435,15 @@ async def reminderloop(self): async with self.config.user(self.bot.get_user(u)).reminders() as rms: rms.remove(rm) await self.bot.get_user(u).send(rm[1]) - for c in chs: - for sc in chs[c]['schedules']: - if datetime.fromtimestamp(float(sc[0])) < now: - async with self.config.channel(self.bot.get_channel(c)).schedules() as sco: - scind = sco.index(sc) - sco[scind][0] += sco[scind][1] - await self.bot.get_channel(c).send(sc[2]) + for g in gds: + for n, sc in gds[g]['schedules'].items(): + if datetime.fromtimestamp(sc['end']) < now or not sc['enabled']: + continue + if datetime.fromtimestamp(float(sc['time'])) < now: + async with self.config.guild(self.bot.get_guild(g)).schedules() as scs: + scs[n]['time'] += scs[n]['interval'] + for ch in sc['channels']: + await self.bot.get_channel(ch).send(sc['message']) @commands.command() async def time(self, ctx, *, tz: str): @@ -287,6 +484,68 @@ async def timeto(self, ctx, tz: str, *, time: str): " until " + time.strip() + " in " + now.strftime('%Z')) await ctx.send(inline(msg)) + async def exact_tartintodt(self, ctx, time, allowtat=True): + user_tz_str = await self.config.user(ctx.author).tz() + user_timezone = tzstr_to_tz(user_tz_str or 'UTC') + + for ar in exact_tats: + if not allowtat: + continue + match = re.match(ar, time, re.IGNORECASE) + if not match: + continue + match = match.groupdict() + + if not user_tz_str: + await ctx.send( + "Please configure your personal timezone with `{0.clean_prefix}settimezone` first.".format(ctx)) + return + + now = datetime.now(tz=user_timezone) + defaults = { + 'year': now.year, + 'month': now.month, + 'day': now.day, + 'hour': now.hour, + 'minute': now.minute, + 'merid': 'NONE' + } + defaults.update({k: v for k, v in match.items() if v}) + for key in defaults: + if key not in ['merid']: + defaults[key] = int(defaults[key]) + if defaults['merid'] == 'pm' and defaults['hour'] <= 12: + defaults['hour'] += 12 + elif defaults['merid'] == 'NONE' and defaults['hour'] < now.hour: + defaults['hour'] += 12 + if defaults['hour'] >= 24: + defaults['day'] += int(defaults['hour'] // 24) + defaults['hour'] = defaults['hour'] % 24 + del defaults['merid'] + try: + rmtime = user_timezone.localize(datetime(**defaults)) + except ValueError as e: + await ctx.send(inline(str(e).capitalize())) + return + if rmtime < now: + rmtime += timedelta(days=1) + rmtime = rmtime.astimezone(pytz.utc).replace(tzinfo=None) + break + else: + ir = exact_tins[0] + match = re.search(ir, time, re.IGNORECASE) + if not match: # Only use the first one + raise commands.UserFeedbackCheckFailure("Invalid time string: " + time) + tinstrs, = match.groups() + rmtime = datetime.utcnow() + try: + rmtime += tin2tdelta(tinstrs) + except OverflowError: + raise commands.UserFeedbackCheckFailure( + inline("That's way too far in the future! Please keep it in your lifespan!")) + + return rmtime + def timestr_to_time(timestr): timestr = timestr.replace(" ", "") From fddd95d30c5cc90916f6cbcbbc67c349d357ce48 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 16:41:20 -0700 Subject: [PATCH 0075/1220] seniority --- schoolidol/__init__.py | 1 + schoolidol/info.json | 15 +++++++++++++++ schoolidol/schoolidol.py | 14 +++++++++++++- selfroleoverride/__init__.py | 1 + selfroleoverride/info.json | 15 +++++++++++++++ selfroleoverride/selfroleoverride.py | 12 +++++++++++- seniority/__init__.py | 2 +- seniority/info.json | 15 +++++++++++++++ seniority/seniority.py | 28 ++++++++++++++++++++++++++++ 9 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 schoolidol/info.json create mode 100644 selfroleoverride/info.json create mode 100644 seniority/info.json diff --git a/schoolidol/__init__.py b/schoolidol/__init__.py index 0e01b9b3..44e09da4 100644 --- a/schoolidol/__init__.py +++ b/schoolidol/__init__.py @@ -1,5 +1,6 @@ from .schoolidol import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = SchoolIdol(bot) diff --git a/schoolidol/info.json b/schoolidol/info.json new file mode 100644 index 00000000..7c980c12 --- /dev/null +++ b/schoolidol/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Displays data about School Idol.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows School Idol data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["aiohttp"], + "tags": ["schoolidol"] +} diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index 6a877e9a..9bf9784d 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -1,6 +1,6 @@ import difflib import json -from _collections import OrderedDict +from collections import OrderedDict import aiohttp import discord @@ -23,6 +23,18 @@ def __init__(self, bot, *args, **kwargs): self.regular_emoji = char_to_emoji('r') self.idol_emoji = char_to_emoji('i') + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + async def reload_sif(self): await self.bot.wait_until_ready() diff --git a/selfroleoverride/__init__.py b/selfroleoverride/__init__.py index ef89ae6f..dd86057e 100644 --- a/selfroleoverride/__init__.py +++ b/selfroleoverride/__init__.py @@ -1,5 +1,6 @@ from .selfroleoverride import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): pdb = SelfRoleOverride(bot) diff --git a/selfroleoverride/info.json b/selfroleoverride/info.json new file mode 100644 index 00000000..d0b6f030 --- /dev/null +++ b/selfroleoverride/info.json @@ -0,0 +1,15 @@ +{ + "author": ["Aradia Megido#2552"], + "description": "Makes [p]selfrole case insensitive.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Case-insensitive Selfrole", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [], + "tags": ["patch"] +} diff --git a/selfroleoverride/selfroleoverride.py b/selfroleoverride/selfroleoverride.py index f9f30145..eb89cd78 100644 --- a/selfroleoverride/selfroleoverride.py +++ b/selfroleoverride/selfroleoverride.py @@ -3,7 +3,6 @@ import discord.utils import asyncio -from ply import lex from redbot.core import checks, Config from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify @@ -61,6 +60,17 @@ def cog_unload(self): pass self.bot.add_command(cmd) + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return @commands.guild_only() @commands.group() diff --git a/seniority/__init__.py b/seniority/__init__.py index 3cdeedf5..f69dc5f8 100644 --- a/seniority/__init__.py +++ b/seniority/__init__.py @@ -1,8 +1,8 @@ from .seniority import * +__red_end_user_data_statement__ = "Activity data is stored." def setup(bot): - # TODO: Test this! n = Seniority(bot) bot.add_cog(n) bot.loop.create_task(n.init()) diff --git a/seniority/info.json b/seniority/info.json new file mode 100644 index 00000000..3c25a3ed --- /dev/null +++ b/seniority/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Assigns a role to active members.", + "end_user_data_statement": "Activity data is stored.", + "install_msg": "", + "short": "Activity Tracker", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["aioodbc", "prettytable", "pytz"], + "tags": ["moderation", "administration"] +} diff --git a/seniority/seniority.py b/seniority/seniority.py index bc82024f..fc62ae80 100644 --- a/seniority/seniority.py +++ b/seniority/seniority.py @@ -104,6 +104,15 @@ AND server_id = ? ''' +DELETE_USER_DATA = ''' +DELETE FROM seniority +WHERE user_id = ? +''' + +GET_USER_DATA = ''' +SELECT * FROM seniority +WHERE user_id = ? +''' class Seniority(commands.Cog): """Automatically promote people based on activity.""" @@ -117,11 +126,30 @@ def __init__(self, bot: Red, *args, **kwargs): self.pool = None self.insert_timing = deque(maxlen=1000) + @commands.command() + @checks.is_owner() + async def red_get_data_for_user(self, ctx, *, user_id: int): + """Get a user's personal data.""" + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute(GET_USER_DATA, user_id) + rows = await cur.fetchall() + columns = [x[0] for x in cur.description] + print(columns) + print(rows) + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute(REPLACE_POINTS_QUERY, user_id) + def cog_unload(self): print('Seniority: unloading') self.lock = True if self.pool: self.pool.close() + self.bot.loop.create_task(self.pool.wait_closed()) self.pool = None else: print('unexpected error: pool was None') From bcec5262316c02bc42f3d7497033960bb8520e7c Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 16:46:50 -0700 Subject: [PATCH 0076/1220] real seniority --- seniority/seniority.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/seniority/seniority.py b/seniority/seniority.py index fc62ae80..91ef4ae9 100644 --- a/seniority/seniority.py +++ b/seniority/seniority.py @@ -126,17 +126,15 @@ def __init__(self, bot: Red, *args, **kwargs): self.pool = None self.insert_timing = deque(maxlen=1000) - @commands.command() - @checks.is_owner() - async def red_get_data_for_user(self, ctx, *, user_id: int): + async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" async with self.pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(GET_USER_DATA, user_id) rows = await cur.fetchall() - columns = [x[0] for x in cur.description] - print(columns) - print(rows) + guilds = len({r[1] for r in rows}) + data = "You have activity data stored in {} guilds.\n".format(guilds) + return {"user_data.txt": BytesIO("data".encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data.""" From 7f6da5b6697d3fea32bf071e45aaa223ce1e84b9 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 18:11:27 -0700 Subject: [PATCH 0077/1220] last GDPR --- calculator/calculator.py | 2 +- padvision/info.json | 2 +- speech/__init__.py | 2 +- speech/info.json | 20 ++++++++++++ speech/speech.py | 12 +++++++ sqlactivitylog/__init__.py | 1 + sqlactivitylog/info.json | 15 +++++++++ sqlactivitylog/sqlactivitylog.py | 56 ++++++++++++++++++++++++++++++-- stickers/__init__.py | 1 + stickers/info.json | 15 +++++++++ stickers/stickers.py | 12 +++++++ streamcopy/__init__.py | 1 + streamcopy/info.json | 15 +++++++++ streamcopy/streamcopy.py | 12 +++++++ timecog/__init__.py | 1 + timecog/info.json | 15 +++++++++ timecog/timecog.py | 17 ++++++++++ translate/__init__.py | 1 + translate/info.json | 20 ++++++++++++ translate/translate.py | 12 +++++++ trigger/__init__.py | 1 + trigger/info.json | 15 +++++++++ trigger/trigger.py | 15 +++++++++ trutils/__init__.py | 1 + trutils/info.json | 15 +++++++++ trutils/trutils.py | 24 ++++++++------ voicerole/__init__.py | 1 + voicerole/info.json | 15 +++++++++ voicerole/voicerole.py | 12 +++++++ 29 files changed, 316 insertions(+), 15 deletions(-) create mode 100644 speech/info.json create mode 100644 sqlactivitylog/info.json create mode 100644 stickers/info.json create mode 100644 streamcopy/info.json create mode 100644 timecog/info.json create mode 100644 translate/info.json create mode 100644 trigger/info.json create mode 100644 trutils/info.json create mode 100644 voicerole/info.json diff --git a/calculator/calculator.py b/calculator/calculator.py index e5abf7f3..d3ce0d2b 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -39,7 +39,7 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" - udata = await self.config.user(ctx.author).ans() + udata = await self.config.user_from_id(user_id).ans() data = "You have previous answers stored in {} channels.\n".format(len(udata)) diff --git a/padvision/info.json b/padvision/info.json index 6dc84461..f2228d47 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -11,5 +11,5 @@ "disabled": false, "require_cogs": {}, "requirements": ["Pillow"], - "tags": ["PAD"] + "tags": ["PAD", "sharedlibrary"] } diff --git a/speech/__init__.py b/speech/__init__.py index c051d94e..425372ba 100644 --- a/speech/__init__.py +++ b/speech/__init__.py @@ -1,6 +1,6 @@ from .speech import Speech +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): - # TODO: Test! I don't have an API! bot.add_cog(Speech(bot)) diff --git a/speech/info.json b/speech/info.json new file mode 100644 index 00000000..49410046 --- /dev/null +++ b/speech/info.json @@ -0,0 +1,20 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Audio helper functions for other cogs + a few commands of its own.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Audio helper commands", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [ + "google-api", + "google-api-python-client", + "google-cloud", + "google-cloud-texttospeech" + ], + "tags": ["audio"] +} diff --git a/speech/speech.py b/speech/speech.py index 7b3c1b08..aa2c8c68 100644 --- a/speech/speech.py +++ b/speech/speech.py @@ -38,6 +38,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.try_setup_api() self.busy = False + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + def try_setup_api(self): api_key_file = self.settings.get_key_file() if api_key_file: diff --git a/sqlactivitylog/__init__.py b/sqlactivitylog/__init__.py index 2abd6ca8..75dde5eb 100644 --- a/sqlactivitylog/__init__.py +++ b/sqlactivitylog/__init__.py @@ -1,5 +1,6 @@ from .sqlactivitylog import * +__red_end_user_data_statement__ = "All message edits/deletions less than 3 weeks old are saved." def setup(bot): n = SqlActivityLogger(bot) diff --git a/sqlactivitylog/info.json b/sqlactivitylog/info.json new file mode 100644 index 00000000..6fcd63fd --- /dev/null +++ b/sqlactivitylog/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Stores message edits and deletions.", + "end_user_data_statement": "All message edits/deletions less than 3 weeks old are saved.", + "install_msg": "", + "short": "Message deletion log", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["prettytable", "pytz"], + "tags": ["moderation"] +} diff --git a/sqlactivitylog/sqlactivitylog.py b/sqlactivitylog/sqlactivitylog.py index 8f99f078..ec679476 100644 --- a/sqlactivitylog/sqlactivitylog.py +++ b/sqlactivitylog/sqlactivitylog.py @@ -136,6 +136,21 @@ def _data_file(file_name: str) -> str: WHERE timestamp < :before ''' +GET_USER_DATA_QUERY = ''' +SELECT * FROM ( + SELECT timestamp, channel_id, msg_type, clean_content + FROM messages INDEXED BY idx_messages_server_id_user_id_timestamp + WHERE user_id = :user_id + ORDER BY timestamp DESC +) +ORDER BY timestamp ASC +''' + +DELETE_USER_DATA_QUERY = ''' +DELETE +FROM messages +WHERE user_id = :user_id +''' class SqlActivityLogger(commands.Cog): """Log activity seen by bot""" @@ -159,6 +174,38 @@ def cog_unload(self): self.lock = True self.con.close() + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + values = { + 'user_id': user.id, + } + column_data = [ + ('timestamp', 'Time (PT)'), + ('channel_id', 'Channel'), + ('msg_type', 'Type'), + ('clean_content', 'Message'), + ] + + data = ("NOTE: Data stored by this cog is essential for moderator use " + "so data deletion requests can only be made by the bot owner and " + "Discord itself. If you need your data deleted, please contact a " + "bot owner.\n\n\n") + data += await self.queryAndSave(None, None, USER_QUERY, values, column_data) + + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + The personal data stored in this cog is for essential moderation use, + so some data deletion requests can only be made by the bot owner and + Discord itself. If this is an issue, please contact a bot owner. + """ + if requester not in ("discord_deleted_user", "owner"): + return + cursor = self.con.execute(DELETE_USER_DATA_QUERY, {'user_id': user_id}) + self.con.commit() + @commands.command() @checks.is_owner() async def rawquery(self, ctx, *, query: str): @@ -296,6 +343,11 @@ async def query(self, ctx, query, count=10): await self.queryAndPrint(ctx, server, CONTENT_QUERY, values, column_data) async def queryAndPrint(self, ctx, server, query, values, column_data, max_rows=MAX_LOGS * 2, verbose=True): + result_text = queryAndSave(self, ctx, server, query, values, column_data, max_rows, verbose) + for p in pagify(result_text): + await ctx.send(box(p)) + + async def queryAndSave(self, ctx, server, query, values, column_data, max_rows=MAX_LOGS * 2, verbose=True): before_time = timeit.default_timer() cursor = self.con.execute(query, values) rows = cursor.fetchall() @@ -351,11 +403,11 @@ async def queryAndPrint(self, ctx, server, query, values, column_data, max_rows= tbl.add_row(table_row) + result_text = "" if verbose: result_text = "{} results fetched in {}s\n{}".format( len(rows), round(execution_time, 2), tbl.get_string()) - for p in pagify(result_text): - await ctx.send(box(p)) + return result_text @commands.Cog.listener("on_message_edit") async def on_message_edit(self, before, after): diff --git a/stickers/__init__.py b/stickers/__init__.py index bc2f0704..82c0c12d 100644 --- a/stickers/__init__.py +++ b/stickers/__init__.py @@ -1,5 +1,6 @@ from .stickers import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(Stickers(bot)) diff --git a/stickers/info.json b/stickers/info.json new file mode 100644 index 00000000..206c94dd --- /dev/null +++ b/stickers/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Custom commands to pull up images.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Sticker commands", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["customcommands"] +} diff --git a/stickers/stickers.py b/stickers/stickers.py index bc9afa4c..19cdcebc 100644 --- a/stickers/stickers.py +++ b/stickers/stickers.py @@ -31,6 +31,18 @@ def __init__(self, bot: Red, *args, **kwargs): global STICKER_COG STICKER_COG = self + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group() @is_sticker_admin() async def sticker(self, context): diff --git a/streamcopy/__init__.py b/streamcopy/__init__.py index 2c48bb04..1b5c94cb 100644 --- a/streamcopy/__init__.py +++ b/streamcopy/__init__.py @@ -1,5 +1,6 @@ from .streamcopy import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n = StreamCopy(bot) diff --git a/streamcopy/info.json b/streamcopy/info.json new file mode 100644 index 00000000..ad20c27f --- /dev/null +++ b/streamcopy/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Show which members are streaming and opt members in and out of being public for the guild", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Streaming Utils", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["streaming"] +} diff --git a/streamcopy/streamcopy.py b/streamcopy/streamcopy.py index d0e09b23..c9da2d49 100644 --- a/streamcopy/streamcopy.py +++ b/streamcopy/streamcopy.py @@ -18,6 +18,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.settings = StreamCopySettings("streamcopy") self.current_user_id = None + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + async def refresh_stream(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('StreamCopy'): diff --git a/timecog/__init__.py b/timecog/__init__.py index 76fc7e5c..88e8321c 100644 --- a/timecog/__init__.py +++ b/timecog/__init__.py @@ -1,5 +1,6 @@ from .timecog import * +__red_end_user_data_statement__ = "Reminders are stored." def setup(bot): n = TimeCog(bot) diff --git a/timecog/info.json b/timecog/info.json new file mode 100644 index 00000000..6a83b09a --- /dev/null +++ b/timecog/info.json @@ -0,0 +1,15 @@ +{ + "author": ["Araida Megido#2552"], + "description": "A cog to track time-related things such as reminders and schedules.", + "end_user_data_statement": "Reminders are stored.", + "install_msg": "", + "short": "remindme and scheduler", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["pytz", "python-dateutil"], + "tags": ["remindme", "timer", "scheduler"] +} diff --git a/timecog/timecog.py b/timecog/timecog.py index 0af4cdfe..2f3573c8 100644 --- a/timecog/timecog.py +++ b/timecog/timecog.py @@ -78,6 +78,21 @@ def __init__(self, bot: Red, *args, **kwargs): self.bot = bot + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + udata = await self.config.user_from_id(user_id).reminders() + + data = "You have {} reminder(s) set.\n".format(len(udata)) + + if not udata: + data = "No data is stored for user with ID {}.\n".format(user_id) + + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + await self.config.user_from_id(user_id).clear() + @commands.group(aliases=['remindmeat', 'remindmein'], invoke_without_command=True) async def remindme(self, ctx, *, time): """Reminds you to do something at a specified time @@ -215,7 +230,9 @@ async def purge(self, ctx): await self.config.user(ctx.author).reminders.set([]) await ctx.send(inline("Done")) + @commands.group(invoke_without_command=True) + @checks.mod_or_permissions(administrator=True) async def schedule(self, ctx, name, *, text): """Sets up a schedule to fire at the specified interval diff --git a/translate/__init__.py b/translate/__init__.py index c1ce592f..56afed9e 100644 --- a/translate/__init__.py +++ b/translate/__init__.py @@ -1,5 +1,6 @@ from .translate import Translate +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): n=Translate(bot) diff --git a/translate/info.json b/translate/info.json new file mode 100644 index 00000000..d0627224 --- /dev/null +++ b/translate/info.json @@ -0,0 +1,20 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Translates text between langauges and writing systems.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Translation Cog", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [ + "romkan", + "google-api", + "google-api-python-client", + "google-cloud" + ], + "tags": ["translation"] +} diff --git a/translate/translate.py b/translate/translate.py index 9ee41549..4c39c2bd 100644 --- a/translate/translate.py +++ b/translate/translate.py @@ -18,6 +18,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.service = None + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.group() @checks.is_owner() async def translate(self, context): diff --git a/trigger/__init__.py b/trigger/__init__.py index 8bb25e26..643916a6 100644 --- a/trigger/__init__.py +++ b/trigger/__init__.py @@ -1,5 +1,6 @@ from .trigger import Trigger +__red_end_user_data_statement__ = "Triggers are stored persistantly." def setup(bot): n = Trigger(bot) diff --git a/trigger/info.json b/trigger/info.json new file mode 100644 index 00000000..e0561290 --- /dev/null +++ b/trigger/info.json @@ -0,0 +1,15 @@ +{ + "author": ["Twentysix26", "Aradia Megido#2552"], + "description": "Setup commands that trigger certain behaviors.", + "end_user_data_statement": "Triggers are stored persistantly.", + "install_msg": "", + "short": "You're an eyesore!", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [], + "tags": ["general", "customcommands"] +} diff --git a/trigger/trigger.py b/trigger/trigger.py index 40791c51..1e432b0a 100644 --- a/trigger/trigger.py +++ b/trigger/trigger.py @@ -35,6 +35,21 @@ def __init__(self, bot): self.triggers = [] + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + c = 0 + for trigger in self.triggers: + if trigger.owner == user_id: + c += 1 + data = "You own {} triggers.\n".format(c) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + for trigger in self.triggers: + if trigger.owner == user_id: + trigger.owner = None + @commands.group() @commands.guild_only() async def trigger(self, ctx): diff --git a/trutils/__init__.py b/trutils/__init__.py index 29d0b69e..5dbf0f51 100644 --- a/trutils/__init__.py +++ b/trutils/__init__.py @@ -1,5 +1,6 @@ from .trutils import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(TrUtils(bot)) diff --git a/trutils/info.json b/trutils/info.json new file mode 100644 index 00000000..213fcbd9 --- /dev/null +++ b/trutils/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Commands for bot owners.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Owner Utils (and credits)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["utility", "moderation", "administration", "developer"] +} diff --git a/trutils/trutils.py b/trutils/trutils.py index fb0940f8..b2d148f1 100644 --- a/trutils/trutils.py +++ b/trutils/trutils.py @@ -4,15 +4,7 @@ import sys import discord -from redbot.core.bot import Red - -try: - from google.cloud import vision -except: - print('google cloud vision not found, some features unavailable') - -from redbot.core import checks, modlog -from redbot.core import commands +from redbot.core import checks, modlog, commands from redbot.core.utils.chat_formatting import inline, box, pagify from rpadutils import CogSettings @@ -130,13 +122,25 @@ class TrUtils(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.settings = TrUtilsSettings("trutils") self.sessions = set() + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.command() async def revertname(self, ctx): """Unsets your nickname""" diff --git a/voicerole/__init__.py b/voicerole/__init__.py index 31a64b83..5ef141b3 100644 --- a/voicerole/__init__.py +++ b/voicerole/__init__.py @@ -1,5 +1,6 @@ from .voicerole import * +__red_end_user_data_statement__ = "No personal data is stored." def setup(bot): bot.add_cog(VoiceRole(bot)) diff --git a/voicerole/info.json b/voicerole/info.json new file mode 100644 index 00000000..244f211e --- /dev/null +++ b/voicerole/info.json @@ -0,0 +1,15 @@ +{ + "author": ["The Tsubaki Bot Team"], + "description": "Only allow people in a voice channel to join a specific text channel.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Muted text for voice channels", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": [], + "tags": ["utility"] +} diff --git a/voicerole/voicerole.py b/voicerole/voicerole.py index bfe5d836..7d0f5c9d 100644 --- a/voicerole/voicerole.py +++ b/voicerole/voicerole.py @@ -17,6 +17,18 @@ def __init__(self, bot: Red, *args, **kwargs): self.bot = bot self.settings = VoiceRoleSettings("voicerole") + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO("data".encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + @commands.Cog.listener("on_voice_state_update") async def _on_voice_state_update(self, member, before, after): guild = member.guild From b153dc9b37c16165b509abaf611e73ddc4fb5432 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 18:17:57 -0700 Subject: [PATCH 0078/1220] add requirement --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 39ad4b5f..84ca97ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ google-api-python-client google-cloud google-cloud-texttospeech pymysql +python-dateutil ply aioodbc # might require: sudo apt-get install unixodbc-dev python3.8-dev opencv-python From 8a9c852673cb56416eaacf9136162208e0731307 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 19:47:17 -0700 Subject: [PATCH 0079/1220] finish info.jsons --- padsearch/info.json | 5 ++++- selfroleoverride/info.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/padsearch/info.json b/padsearch/info.json index 794cf225..288a4a8c 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -9,7 +9,10 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "require_cogs": { + "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, "requirements": ["ply"], "tags": ["PAD"] } diff --git a/selfroleoverride/info.json b/selfroleoverride/info.json index d0b6f030..27f6a3b8 100644 --- a/selfroleoverride/info.json +++ b/selfroleoverride/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {}, + "require_cogs": {"admin": "https://github.com/Cog-Creators/Red-DiscordBot"}, "requirements": [], "tags": ["patch"] } From 947468bfddf2afbe65334564607337c4d85ec086 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 19:54:51 -0700 Subject: [PATCH 0080/1220] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..46077dbe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 TsubakiBotPad + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 8163f9f4a619e9236f29f3fc693b0cc209f27f6a Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 8 Sep 2020 21:15:27 -0700 Subject: [PATCH 0081/1220] padglobal fix --- padglobal/padglobal.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 1a326aa8..6fd6637a 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -567,7 +567,7 @@ async def glossary(self, ctx, *, term: str = None): term, definition = self.lookup_glossary(term) if definition: definition_output = '**{}** : {}'.format(term, definition) - await ctx.send(definition_output) + await ctx.send(self.emojify(definition_output)) else: await ctx.send(inline('No definition found')) return @@ -641,7 +641,7 @@ async def boss(self, ctx, *, term: str = None): if definition: if term_new != term.lower(): await ctx.send('No entry for {} found, corrected to {}'.format(term, term_new)) - await ctx.send(definition) + await ctx.send(self.emojify(definition)) else: await ctx.send(inline('No mechanics found')) return @@ -732,7 +732,7 @@ async def which(self, ctx, *, term: str = None): await ctx.send('`Which {}`\n{}'.format(name, definition)) return await ctx.send(inline('Which {} - Last Updated {}'.format(name, timestamp))) - await ctx.send(definition) + await ctx.send(self.emojify(definition)) async def _resolve_which(self, ctx, term): term = term.lower().replace('?', '') @@ -917,7 +917,7 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): """Set the emoji servers by ID (csv)""" self.settings.emojiServers().clear() if emoji_servers: - self.settings.setEmojiServers(emoji_servers.split(',')) + self.settings.setEmojiServers(re.findall(r'\d+',emoji_servers)) await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) def _get_emojis(self): @@ -1094,7 +1094,7 @@ async def guide(self, ctx, *, term: str = None): await ctx.send(inline(err)) return - await ctx.send(text) + await ctx.send(self.emojify(text)) async def get_guide_text(self, term: str): term = term.lower() @@ -1204,6 +1204,15 @@ async def rmleaderguide(self, ctx, monster_id: int): self.settings.rmLeaderGuide(name) await ctx.send("done") + def emojify(self, message): + emojis = list() + for guild in self.settings.emojiServers: + if self.bot.get_guild(int(guild)): + emojis.extend(self.bot.get_guild(int(guild)).emojis) + for guild in self.bot.guilds: + emojis.extend(guild.emojis) + message = rpadutils.replace_emoji_names_with_code(emojis, message) + return rpadutils.fix_emojis_for_server(emojis, message) def check_simple_tree(monster): attr1 = monster.attr1 From afcd762e52ef7ffc2abe4ba8aaba0d2923a64506 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 9 Sep 2020 01:02:51 -0700 Subject: [PATCH 0082/1220] de-miruing and id --- dadguide/dadguide.py | 15 +++++++++++++++ fancysay/fancysay.py | 6 +++--- info.json | 2 +- padboard/padboard.py | 7 +------ padbuildimg/padbuildimg.py | 2 +- padglobal/padglobal.py | 4 ++-- rpadutils/rpadutils.py | 4 ++-- 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index ddaa9e46..4908ada4 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -1404,6 +1404,15 @@ def find_monster(self, query): # TODO: this should be a length-limited priority queue matches = set() + + print(query) + # prefix search for ids, take max id + for nickname, m in self.all_entries.items(): + if query.endswith("base "+str(m.monster_id)): + matches.add([mo for mo in self.all_entries.values() if mo.monster_id == m.base_monster_no][0]) + if len(matches): + return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) + # prefix search for nicknames, space-preceeded, take max id for nickname, m in self.all_entries.items(): if nickname.startswith(query + ' '): @@ -1512,6 +1521,12 @@ def find_monster2(self, query): matches = PotentialMatches() + # prefix search for ids, take max id + for nickname, m in self.all_entries.items(): + if query.endswith("base "+str(m.monster_id)): + matches.add([mo for mo in self.all_entries.values() if mo.monster_id == m.base_monster_no][0]) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + # first try to get matches from nicknames for nickname, m in self.all_entries.items(): if new_query in nickname: diff --git a/fancysay/fancysay.py b/fancysay/fancysay.py index 5858d5b2..f15bf1a5 100644 --- a/fancysay/fancysay.py +++ b/fancysay/fancysay.py @@ -123,20 +123,20 @@ async def title_description_image_footer(self, ctx, title, description, image, f @commands.command(aliases = ["parrot", "repeat"]) @checks.mod_or_permissions(manage_messages=True) async def say(self, ctx, *, message): - """Make Miru parrot a phrase.""" + """Make the bot parrot a phrase.""" message = self.emojify(message) await ctx.send(message) @commands.command(aliases = ["testparrot", "testrepeat"]) @checks.mod_or_permissions(manage_messages=True) async def testsay(self, ctx, *, message): - """Make Miru parrot a phrase without smart emoji replacements.""" + """Make the bot parrot a phrase without smart emoji replacements.""" await ctx.send(message) @commands.command() @checks.mod_or_permissions(manage_messages=True) async def mask(self, ctx, *, message): - """Sends a message as Miru.""" + """Sends a message as the bot.""" message = self.emojify(message) await ctx.message.delete() await ctx.send(message) diff --git a/info.json b/info.json index 4a4eaa9f..4965c11a 100644 --- a/info.json +++ b/info.json @@ -1,5 +1,5 @@ { - "author": ["The Miru Bot Team"], + "author": ["The Tsubaki Bot Team"], "description": "A collection of cogs, primarily related to the mobile game Puzzle and Dragons, along with miscellaneous moderation and other utilities.", "short": "The main Tsubotki repo." } diff --git a/padboard/padboard.py b/padboard/padboard.py index 55544aee..f35e40af 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -15,8 +15,6 @@ DATA_DIR = os.path.join('data', 'padboard') DAWNGLARE_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" -MIRUGLARE_BOARD_TEMPLATE = "https://storage.googleapis.com/mirubot/websites/padsim/index.html?patt={}" - class PadBoard(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -92,10 +90,7 @@ async def dawnglare(self, ctx, user: discord.Member = None): board_text_nc = ''.join([''.join(r) for r in img_board_nc]) # Convert O (used by padvision code) to X (used by Puzzled for bombs) board_text_nc = board_text_nc.replace('o', 'x') - img_url = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc) - img_url2 = MIRUGLARE_BOARD_TEMPLATE.format(board_text_nc) - - msg = '{}\n{}'.format(img_url, img_url2) + msg = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc)) await ctx.send(msg) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index c4ead205..5c7b6967 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -130,7 +130,7 @@ AWK_STAR = 'star' DELAY_BUFFER = 'delay_buffer' REMOTE_ASSET_URL = 'https://github.com/chasehult/pdchu-cog/raw/master/assets/' -REMOTE_AWK_URL = 'https://f002.backblazeb2.com/file/dadguide-data/media/awakenings/{0:03d}.png' +REMOTE_AWK_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/media/awakenings/{0:03d}.png' # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' class DictWithAttributeAccess(dict): diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 6fd6637a..d86919b8 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -41,7 +41,7 @@ def _data_file(file_name: str) -> str: BLACKLISTED_CHARACTERS = '^[]*`~_' -PORTRAIT_TEMPLATE = 'https://storage.googleapis.com/mirubot/padimages/{}/portrait/{}.png' +PORTRAIT_TEMPLATE = 'https://d1kpnpud0qoyxf.cloudfront.net/media/portraits/{0:05d}.png' DISABLED_MSG = 'PAD Global info disabled on this server' @@ -955,7 +955,7 @@ async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): server_ids = self.settings.emojiServers() all_emojis = self._get_emojis() - source_url = PORTRAIT_TEMPLATE.format(server, monster_id) + source_url = PORTRAIT_TEMPLATE.format(monster_id) emoji_name = 'pad_' + ('na_' if server == 'na' else '') + str(monster_id) for e in all_emojis: diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index 150abc95..a6a67f3c 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -322,7 +322,7 @@ def check(payload): try: await message.clear_reactions() except Exception as e: - # This is expected when miru doesn't have manage messages + # This is expected when bot doesn't have manage messages pass return message, new_message_content @@ -341,7 +341,7 @@ def check(payload): try: await message.remove_reaction(react_emoji, p.member) except: - # This is expected when miru doesn't have manage messages + # This is expected when bot doesn't have manage messages pass # update the emoji mapping however we need to, or just pass through and do nothing From 39535bd06a696d00ed5b0a32a87e9a93f8adf06b Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 9 Sep 2020 01:06:20 -0700 Subject: [PATCH 0083/1220] padboard fix --- padboard/padboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padboard/padboard.py b/padboard/padboard.py index f35e40af..9ea5e285 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -90,7 +90,7 @@ async def dawnglare(self, ctx, user: discord.Member = None): board_text_nc = ''.join([''.join(r) for r in img_board_nc]) # Convert O (used by padvision code) to X (used by Puzzled for bombs) board_text_nc = board_text_nc.replace('o', 'x') - msg = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc)) + msg = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc) await ctx.send(msg) From 2aaae75feeae09d04a92701a70693bd58f72af1c Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 9 Sep 2020 01:13:03 -0700 Subject: [PATCH 0084/1220] fir typing --- padglobal/padglobal.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index d86919b8..7e239989 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -289,12 +289,13 @@ def list_or_none(l): @commands.command() @is_padglobal_admin() async def forceindexreload(self, ctx): - await ctx.send('starting reload') - dadguide_cog = self.bot.get_cog('Dadguide') - await dadguide_cog.reload_config_files() - padinfo_cog = self.bot.get_cog('PadInfo') - await padinfo_cog.refresh_index() - await ctx.send('finished reload') + async with ctx.typing(): + await ctx.send('starting reload') + dadguide_cog = self.bot.get_cog('Dadguide') + await dadguide_cog.reload_config_files() + padinfo_cog = self.bot.get_cog('PadInfo') + await padinfo_cog.refresh_index() + await ctx.send('finished reload') @commands.group(aliases = ['pdg']) @is_padglobal_admin() From 743e819dc482676352193c2badf5722a62d184db Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 9 Sep 2020 10:34:27 -0700 Subject: [PATCH 0085/1220] PadGlobal fix --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 7e239989..c28011ea 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1207,7 +1207,7 @@ async def rmleaderguide(self, ctx, monster_id: int): def emojify(self, message): emojis = list() - for guild in self.settings.emojiServers: + for guild in self.settings.emojiServers(): if self.bot.get_guild(int(guild)): emojis.extend(self.bot.get_guild(int(guild)).emojis) for guild in self.bot.guilds: From f7a7998689a8a1161cd466481cce367c4df71199 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 9 Sep 2020 10:50:32 -0700 Subject: [PATCH 0086/1220] padglobalfix 2 --- padglobal/padglobal.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index c28011ea..21e232fd 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1207,10 +1207,13 @@ async def rmleaderguide(self, ctx, monster_id: int): def emojify(self, message): emojis = list() - for guild in self.settings.emojiServers(): + emoteservers = self.settings.emojiServers() + for guild in emoteservers: if self.bot.get_guild(int(guild)): emojis.extend(self.bot.get_guild(int(guild)).emojis) for guild in self.bot.guilds: + if guild.id in emoteservers: + continue emojis.extend(guild.emojis) message = rpadutils.replace_emoji_names_with_code(emojis, message) return rpadutils.fix_emojis_for_server(emojis, message) From 3bf23c09fdf00125d7b26979072dffd01fca939c Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 9 Sep 2020 11:12:14 -0700 Subject: [PATCH 0087/1220] remove query print --- dadguide/dadguide.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 4908ada4..c6f397e6 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -1405,7 +1405,6 @@ def find_monster(self, query): # TODO: this should be a length-limited priority queue matches = set() - print(query) # prefix search for ids, take max id for nickname, m in self.all_entries.items(): if query.endswith("base "+str(m.monster_id)): From 73f0c82729a3ca6912c6a472440bc9acaf8e05cb Mon Sep 17 00:00:00 2001 From: RheingoldRiver <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 9 Sep 2020 15:37:22 -0500 Subject: [PATCH 0088/1220] add everyone permissions for here msgs --- baduser/baduser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/baduser/baduser.py b/baduser/baduser.py index 10a83b82..ac1a2724 100644 --- a/baduser/baduser.py +++ b/baduser/baduser.py @@ -472,7 +472,8 @@ async def recordBadUser(self, member, role_name): channel_obj = member.guild.get_channel(update_channel) await channel_obj.send(inline('Detected bad user')) await channel_obj.send(box(msg)) - await channel_obj.send('Hey @here please leave a note explaining why this user is punished') + followup_msg = 'Hey @here please leave a note explaining why this user is punished' + await channel_obj.send(followup_msg, allowed_mentions=discord.AllowedMentions(everyone=True)) await channel_obj.send('This user now has {} strikes'.format(strikes)) try: @@ -495,7 +496,8 @@ async def recordRoleChange(self, member, role_name, is_added, send_ping=True): try: await channel_obj.send(inline(msg)) if send_ping: - await channel_obj.send('Hey @here please leave a note explaining why this role was modified') + followup_msg = 'Hey @here please leave a note explaining why this role was modified' + await channel_obj.send(followup_msg, allowed_mentions=discord.AllowedMentions(everyone=True)) except: print('Failed to notify in', update_channel, msg) From 775040a6214ae3e36d02dcff3ba8fc2de8b35894 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 9 Sep 2020 19:54:17 -0700 Subject: [PATCH 0089/1220] pdg base update --- dadguide/dadguide.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index c6f397e6..a11b725f 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -22,6 +22,7 @@ import pytz import romkan +import discord from redbot.core import checks, data_manager from redbot.core import commands from redbot.core.bot import Red @@ -1407,10 +1408,10 @@ def find_monster(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): - if query.endswith("base "+str(m.monster_id)): - matches.add([mo for mo in self.all_entries.values() if mo.monster_id == m.base_monster_no][0]) + if query.endswith("base {}".format(m.monster_id)): + matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) if len(matches): - return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) + return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() # prefix search for nicknames, space-preceeded, take max id for nickname, m in self.all_entries.items(): @@ -1522,8 +1523,8 @@ def find_monster2(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): - if query.endswith("base "+str(m.monster_id)): - matches.add([mo for mo in self.all_entries.values() if mo.monster_id == m.base_monster_no][0]) + if query.endswith("base {}".format(m.monster_id)): + matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) matches.remove_potential_matches_without_all_prefixes(query_prefixes) # first try to get matches from nicknames From dd62ec3c52aa7b5858bdd5f0331a235d7be74d86 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 11:42:27 -0700 Subject: [PATCH 0090/1220] emoji fix --- rpadutils/rpadutils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index a6a67f3c..3a4ce68f 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -426,10 +426,12 @@ def replace_emoji_names_with_code(emoji_list, msg_text): return msg_text # For each unique looking emoji thing + finished_names = [] for m in set(matches): emoji_name = m.strip(':') for e in emoji_list: - if e.name == emoji_name: + if e.name == emoji_name and e.name not in finished_names: + finished_names.append(e.name) msg_text = msg_text.replace(m, str(e)) return msg_text From a95639f5ce4b75f8b030c3a7e7608027d333ecf8 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 12:29:27 -0700 Subject: [PATCH 0091/1220] Emoji Fix channelmod --- channelmod/channelmod.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index c128de05..3d07d57f 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -11,7 +11,7 @@ from redbot.core import commands from redbot.core.utils.chat_formatting import inline -from rpadutils import CogSettings, box +from rpadutils import rpadutils, CogSettings, box # Three hour cooldown ATTRIBUTION_TIME_SECONDS = 60 * 60 * 3 @@ -291,8 +291,17 @@ async def mformat(self, text, from_channel, dest_channel): #EVERYONE text = re.sub(r"@everyone\b", "@\u200beveryone", text) text = re.sub(r"@here\b", "@\u200bhere", text) + #EMOJI + text = self.emojify(text) return text + def emojify(self, message): + emojis = list() + for guild in self.bot.guilds: + emojis.extend(guild.emojis) + message = rpadutils.replace_emoji_names_with_code(emojis, message) + return rpadutils.fix_emojis_for_server(emojis, message) + class ChannelModSettings(CogSettings): def make_default_settings(self): config = { From db6b59c909eab01bbdc31568b9d1baf73482da5e Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 13:22:06 -0700 Subject: [PATCH 0092/1220] undo fix --- channelmod/channelmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 3d07d57f..8c27b4f4 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -292,7 +292,7 @@ async def mformat(self, text, from_channel, dest_channel): text = re.sub(r"@everyone\b", "@\u200beveryone", text) text = re.sub(r"@here\b", "@\u200bhere", text) #EMOJI - text = self.emojify(text) + # text = self.emojify(text) return text def emojify(self, message): From e1d731e7021594b452b71c0b86b390f7389f9630 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 15:34:01 -0700 Subject: [PATCH 0093/1220] Rpadutils verbosity --- rpadutils/rpadutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py index 3a4ce68f..4b49b3cf 100644 --- a/rpadutils/rpadutils.py +++ b/rpadutils/rpadutils.py @@ -498,6 +498,7 @@ def __init__(self, cog_name, bot=None): self.default_settings = self.make_default_settings() if not os.path.isfile(self.file_path): + print("CogSettings config for {} not found. Creating default...".format(self.file_path)) self.bot_settings = self.default_settings self.save_settings() else: From aaa7a270ee8d924375e3c4f32b89e577ee37c8c0 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 19:31:20 -0700 Subject: [PATCH 0094/1220] padbuildimg fix --- padbuildimg/padbuildimg.py | 126 ++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 52 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 5c7b6967..dda9c29e 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -38,6 +38,7 @@ evk(evo mat), rek(redeemable), awk(awoken mat), enk(enhance) Stats (+ for 2 slot): hp, atk, rcv, all(all stat), hp+, atk+, rcv+ Resists (+ for 2 slot): rres, bres, gres, lres, dres, rres+, bres+, gres+, lres+, dres+ + AA4: ls(leader swap), jsf(jammer skyfall), psf(poison skyfall), attr(attribute absorb res), vdp(void damage penatration) Others: sdr, ah(autoheal) Repeat: *# defines number of times to repeat this particular card @@ -81,39 +82,52 @@ MAX_LATENTS = 8 LATENTS_MAP = { - 1: 'bak', - 2: 'phk', - 3: 'hek', - 4: 'drk', - 5: 'gok', - 6: 'aak', - 7: 'dek', - 8: 'mak', - 9: 'evk', - 10: 'rek', - 11: 'awk', - 12: 'enk', - 13: 'all', - 14: 'hp+', - 15: 'atk+', - 16: 'rcv+', - 17: 'rres+', - 18: 'bres+', - 19: 'gres+', - 20: 'lres+', - 21: 'dres+', - 22: 'hp', - 23: 'atk', - 24: 'rcv', - 25: 'rres', - 26: 'bres', - 27: 'gres', - 28: 'lres', - 29: 'dres', - 30: 'ah', - 31: 'sdr', - 32: 'te', - 33: 'te+' + # NOT APPLICABLE + 1: 'emp', + + # ONE SLOT + 101: 'hp', + 102: 'atk', + 103: 'rcv', + 104: 'rres', + 105: 'bres', + 106: 'gres', + 107: 'lres', + 108: 'dres', + 109: 'ah', + 110: 'sdr', + 111: 'te', + + # TWO SLOTS + 201: 'bak', + 202: 'phk', + 203: 'hek', + 204: 'drk', + 205: 'gok', + 206: 'aak', + 207: 'dek', + 208: 'mak', + 209: 'evk', + 210: 'rek', + 211: 'awk', + 212: 'enk', + 213: 'all', + 214: 'hp+', + 215: 'atk+', + 216: 'rcv+', + 217: 'rres+', + 218: 'bres+', + 219: 'gres+', + 220: 'lres+', + 221: 'dres+', + 222: 'te+', + + # SIX SLOTS + 601: 'psf', + 602: 'jsf', + 603: 'ls', + 604: 'vdp', + 605: 'attr', } REVERSE_LATENTS_MAP = {v: k for k, v in LATENTS_MAP.items()} TYPE_TO_KILLERS_MAP = { @@ -129,7 +143,7 @@ AWK_CIRCLE = 'circle' AWK_STAR = 'star' DELAY_BUFFER = 'delay_buffer' -REMOTE_ASSET_URL = 'https://github.com/chasehult/pdchu-cog/raw/master/assets/' +REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/chasehult/pdchu-cog/master/padbuildimg/assets/' REMOTE_AWK_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/media/awakenings/{0:03d}.png' # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' @@ -368,7 +382,7 @@ def validate_latents(latents, card_types): if 'Balance' in card_types: return latents for idx, l in enumerate(latents): - if 0 < l < 9: + if 200 < l < 209: if not any([l in TYPE_TO_KILLERS_MAP[t] for t in card_types if t is not None]): latents[idx] = None latents = [l for l in latents if l is not None] @@ -441,7 +455,8 @@ async def process_card(self, card_str, is_assist=False): 'LATENT': None, 'LV': 99, 'SLV': 0, - 'ON_COLOR': True + 'ON_COLOR': True, + 'EXTRA_SLOTS': False } else: result_card = { @@ -457,7 +472,8 @@ async def process_card(self, card_str, is_assist=False): 'LV': 1, 'SLV': 0, 'MAX_SLV': 0, - 'ON_COLOR': False + 'ON_COLOR': False, + 'EXTRA_SLOTS': False } if len(card_str) == 0: if is_assist: @@ -554,17 +570,22 @@ def combine_latents(self, latents): x_offset = 0 y_offset = 0 row_count = 0 - one_slot, two_slot = [], [] + one_slot, two_slot, six_slot = [], [], [] for l in latents: - if l < 22: - two_slot.append(l) - else: + if 100 <= l < 200: one_slot.append(l) + elif 200 <= l < 300: + two_slot.append(l) + elif 600 <= l < 700: + six_slot.append(l) + six_slot.append(1) sorted_latents = [] if len(one_slot) > len(two_slot): + sorted_latents.extend(six_slot) sorted_latents.extend(one_slot) sorted_latents.extend(two_slot) else: + sorted_latents.extend(six_slot) sorted_latents.extend(two_slot) sorted_latents.extend(one_slot) last_height = 0 @@ -740,17 +761,18 @@ async def padbuildimg(self, ctx, *, build_str: str): # print('BUILD_STR: {}'.format(build_str)) # start = time.perf_counter() - params = self.settings.buildImgParams() - try: - pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) - # print('PARSE: {}'.format(time.perf_counter() - start)) - await pbg.process_build(build_str) - # start = time.perf_counter() - pbg.generate_build_image() - # print('DRAW: {}'.format(time.perf_counter() - start)) - except commands.UserFeedbackCheckFailure as ex: - await ctx.send(box(str(ex) + '\nSee ^helpbuildimg for syntax')) - return -1 + async with ctx.typing(): + params = self.settings.buildImgParams() + try: + pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) + # print('PARSE: {}'.format(time.perf_counter() - start)) + await pbg.process_build(build_str) + # start = time.perf_counter() + pbg.generate_build_image() + # print('DRAW: {}'.format(time.perf_counter() - start)) + except commands.UserFeedbackCheckFailure as ex: + await ctx.send(box(str(ex) + '\nSee ^helpbuildimg for syntax')) + return -1 # start = time.perf_counter() if pbg.build_img is not None: From 86a5a3db7efb95f9986dcc6da5e53046bc0c28ec Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 23:15:28 -0700 Subject: [PATCH 0095/1220] remove non-padcogs --- automod2/__init__.py | 6 - automod2/automod2.py | 890 ----------------------- automod2/info.json | 15 - baduser/__init__.py | 6 - baduser/baduser.py | 679 ----------------- baduser/info.json | 15 - calculator/__init__.py | 6 - calculator/calculator.py | 125 ---- calculator/info.json | 15 - channelmod/__init__.py | 7 - channelmod/channelmod.py | 396 ---------- channelmod/info.json | 15 - datatransfer/__init__.py | 6 - datatransfer/datatransfer.py | 171 ----- datatransfer/info.json | 15 - donations/__init__.py | 8 - donations/donations.py | 444 ------------ donations/info.json | 15 - fancysay/__init__.py | 6 - fancysay/fancysay.py | 156 ---- fancysay/info.json | 15 - globalban/__init__.py | 6 - globalban/globalban.py | 106 --- globalban/info.json | 15 - memes/__init__.py | 6 - memes/info.json | 15 - memes/memes.py | 241 ------- modnotes/__init__.py | 6 - modnotes/info.json | 15 - modnotes/modnotes.py | 136 ---- msgutils/__init__.py | 6 - msgutils/info.json | 15 - msgutils/msgutils.py | 97 --- playground/__init__.py | 6 - playground/info.json | 15 - playground/playground.py | 19 - rpadutils/__init__.py | 6 - rpadutils/info.json | 15 - rpadutils/rpadutils.py | 725 ------------------- selfroleoverride/__init__.py | 7 - selfroleoverride/info.json | 15 - selfroleoverride/selfroleoverride.py | 124 ---- seniority/__init__.py | 8 - seniority/info.json | 15 - seniority/seniority.py | 1000 -------------------------- speech/__init__.py | 6 - speech/info.json | 20 - speech/speech.py | 158 ---- sqlactivitylog/__init__.py | 7 - sqlactivitylog/info.json | 15 - sqlactivitylog/sqlactivitylog.py | 469 ------------ stickers/__init__.py | 6 - stickers/info.json | 15 - stickers/stickers.py | 198 ----- streamcopy/__init__.py | 9 - streamcopy/info.json | 15 - streamcopy/streamcopy.py | 215 ------ timecog/__init__.py | 8 - timecog/info.json | 15 - timecog/timecog.py | 686 ------------------ translate/__init__.py | 8 - translate/info.json | 20 - translate/translate.py | 87 --- trigger/__init__.py | 9 - trigger/info.json | 15 - trigger/trigger.py | 621 ---------------- trutils/__init__.py | 6 - trutils/info.json | 15 - trutils/trutils.py | 526 -------------- voicerole/__init__.py | 6 - voicerole/info.json | 15 - voicerole/voicerole.py | 122 ---- 72 files changed, 8922 deletions(-) delete mode 100644 automod2/__init__.py delete mode 100644 automod2/automod2.py delete mode 100644 automod2/info.json delete mode 100644 baduser/__init__.py delete mode 100644 baduser/baduser.py delete mode 100644 baduser/info.json delete mode 100644 calculator/__init__.py delete mode 100644 calculator/calculator.py delete mode 100644 calculator/info.json delete mode 100644 channelmod/__init__.py delete mode 100644 channelmod/channelmod.py delete mode 100644 channelmod/info.json delete mode 100644 datatransfer/__init__.py delete mode 100644 datatransfer/datatransfer.py delete mode 100644 datatransfer/info.json delete mode 100644 donations/__init__.py delete mode 100644 donations/donations.py delete mode 100644 donations/info.json delete mode 100644 fancysay/__init__.py delete mode 100644 fancysay/fancysay.py delete mode 100644 fancysay/info.json delete mode 100644 globalban/__init__.py delete mode 100644 globalban/globalban.py delete mode 100644 globalban/info.json delete mode 100644 memes/__init__.py delete mode 100644 memes/info.json delete mode 100644 memes/memes.py delete mode 100644 modnotes/__init__.py delete mode 100644 modnotes/info.json delete mode 100644 modnotes/modnotes.py delete mode 100644 msgutils/__init__.py delete mode 100644 msgutils/info.json delete mode 100644 msgutils/msgutils.py delete mode 100644 playground/__init__.py delete mode 100644 playground/info.json delete mode 100644 playground/playground.py delete mode 100644 rpadutils/__init__.py delete mode 100644 rpadutils/info.json delete mode 100644 rpadutils/rpadutils.py delete mode 100644 selfroleoverride/__init__.py delete mode 100644 selfroleoverride/info.json delete mode 100644 selfroleoverride/selfroleoverride.py delete mode 100644 seniority/__init__.py delete mode 100644 seniority/info.json delete mode 100644 seniority/seniority.py delete mode 100644 speech/__init__.py delete mode 100644 speech/info.json delete mode 100644 speech/speech.py delete mode 100644 sqlactivitylog/__init__.py delete mode 100644 sqlactivitylog/info.json delete mode 100644 sqlactivitylog/sqlactivitylog.py delete mode 100644 stickers/__init__.py delete mode 100644 stickers/info.json delete mode 100644 stickers/stickers.py delete mode 100644 streamcopy/__init__.py delete mode 100644 streamcopy/info.json delete mode 100644 streamcopy/streamcopy.py delete mode 100644 timecog/__init__.py delete mode 100644 timecog/info.json delete mode 100644 timecog/timecog.py delete mode 100644 translate/__init__.py delete mode 100644 translate/info.json delete mode 100644 translate/translate.py delete mode 100644 trigger/__init__.py delete mode 100644 trigger/info.json delete mode 100644 trigger/trigger.py delete mode 100644 trutils/__init__.py delete mode 100644 trutils/info.json delete mode 100644 trutils/trutils.py delete mode 100644 voicerole/__init__.py delete mode 100644 voicerole/info.json delete mode 100644 voicerole/voicerole.py diff --git a/automod2/__init__.py b/automod2/__init__.py deleted file mode 100644 index 5c42dfce..00000000 --- a/automod2/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .automod2 import * - -__red_end_user_data_statement__ = "This cog stores id of users manually marked as problematic." - -def setup(bot): - bot.add_cog(AutoMod2(bot)) diff --git a/automod2/automod2.py b/automod2/automod2.py deleted file mode 100644 index 089eed1e..00000000 --- a/automod2/automod2.py +++ /dev/null @@ -1,890 +0,0 @@ -""" -Lets you create patterns to match against messages and apply them as whitelists -or blacklists to a channel. - -If a violation occurs, the message will be deleted and the user notified. -""" -import asyncio -import re -from collections import defaultdict -from collections import deque -from datetime import datetime -from io import BytesIO - -import discord -import prettytable -from redbot.core import checks -from redbot.core import commands -from redbot.core.utils.chat_formatting import inline, box, pagify - -import rpadutils -from rpadutils import CogSettings, boxPagifySay - -LOGS_PER_CHANNEL_USER = 5 - -AUTOMOD_HELP = r""" -Automod works by creating named global patterns, and then applying them in -specific channels as either whitelist or blacklist rules. This allows you -to customize what text can be typed in a channel. Text from moderators is -always ignored by this cog. - -Check out [p]automod2 patterns to see the current server-specific list of patterns. - -Each pattern has an 'include' component and an 'exclude' component. If text -matches the include, then the rule matches. If it subsequently matches the -exclude, then it does not match. - -Here's an example pattern: -Rule Name Include regex Exclude regex ------------------------------------------------------------------------------ -messages must start with a room code ^\d{4}\s?\d{4}.* .*test.* - -This pattern will match values like: - 12345678 foo fiz - 1234 5678 bar baz - -However, if the pattern contains 'test', it won't match: - 12345678 foo fiz test bar baz - -To add the pattern, you'd use the following command: -[p]automod2 addpattern "messages must start with a room code" "^\d{4}\s?\d{4}.*" ".*test.*" - -Remember that to bundle multiple words together you need to surround the -argument with quotes, as above. - -Once you've added a pattern, you need to enable it in a channel using one -of [p]addwhitelist or [p]addblacklist, e.g.: - {0.prefix}automod2 addwhitelist "messages must start with a room code" - -If a channel has any whitelists, then text typed in the channel must match -AT LEAST one whitelist, or it will be deleted. If ANY blacklist is matched -the text will be deleted. - -You can see the configuration for the server using [p]automod2 list - -You can also prevent users from spamming images using [p]automod2 imagelimit -""" -EMOJIS = { - 'tips': [ - '\N{THUMBS UP SIGN}', - '\N{THUMBS DOWN SIGN}', - '\N{EYES}', - ] -} - - -def linked_img_count(message): - return len(message.embeds) + len(message.attachments) - - -class AutoMod2(commands.Cog): - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - self.settings = AutoMod2Settings("automod2", bot) - self.settings.cleanup() - self.channel_user_logs = defaultdict(lambda: deque(maxlen=LOGS_PER_CHANNEL_USER)) - - self.server_user_last = defaultdict(dict) - self.server_phrase_last = defaultdict(dict) - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - udata = self.settings.getUserData(user_id) - - data = "Stored data for user with ID {}:\n".format(user_id) - if udata['bannings']: - data += " - You have setup watchdogs for {} user(s).\n".format(udata['bannings']) - if udata['phrases']: - data += " - You have created {} phrase(s).\n".format(udata['phrases']) - if udata['baduser']: - data += (" - You have been watchlisted in {} servers. " - "(This data is sensitive and cannot be cleared automatically due to abuse. " - "Please contact a bot owner to get this data cleared.)\n" - "").format(len(udata['baduser'])) - - if not any(udata.values()): - data = "No data is stored for user with ID {}.\n".format(user_id) - - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - The personal data stored in this cog is for essential moderation use, - so some data deletion requests can only be made by the bot owner and - Discord itself. If this is an issue, please contact a bot owner. - """ - if requester not in ("discord_deleted_user", "owner"): - self.settings.clearUserData(user_id) - else: - self.settings.clearUserDataFull(user_id) - - @commands.command() - @checks.mod_or_permissions(manage_guild=True) - async def automodhelp(self, ctx): - """Sends you info on how to use automod.""" - for page in pagify(AUTOMOD_HELP.format(ctx.prefix), delims=['\n'], shorten_by=8): - await ctx.author.send(box(page)) - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def automod2(self, ctx): - """AutoMod2 tools. - - This cog works by creating named global patterns, and then applying them in - specific channels as either whitelist or blacklist rules. - - For more information, use [p]automodhelp - """ - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def addpattern(self, ctx, name, include_pattern, exclude_pattern='', error=None): - """Add a pattern for use in this server.""" - if error is not None: - await ctx.send(inline('Too many inputs detected, check your quotes')) - return - try: - re.compile(include_pattern) - re.compile(exclude_pattern) - except Exception as ex: - await ctx.send(inline(str(ex))) - return - self.settings.addPattern(ctx.guild.id, name, include_pattern, exclude_pattern) - await ctx.send(inline('Added pattern')) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmpattern(self, ctx, *, name): - """Remove a pattern from this server. Pattern must not be in use.""" - exit_code = self.settings.rmPattern(ctx.guild.id, name) - if exit_code == 1: - await ctx.send(inline('Removed pattern')) - elif exit_code == 0: - await ctx.send(inline("Rule '{}' is undefined.".format(name))) - else: - await ctx.send(inline("Rule '{}' is in use.".format(name))) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def addwhitelist(self, ctx, *, name): - """Add the named pattern as a whitelist for this channel.""" - name = name.strip('"') - if self.settings.addWhitelist(ctx.guild.id, ctx.channel.id, name): - await ctx.send(inline('Added whitelist config for: ' + name)) - else: - await ctx.send(inline("Rule '{}' is undefined.".format(name))) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmwhitelist(self, ctx, *, name): - """Remove the named pattern as a whitelist for this channel.""" - if self.settings.rmWhitelist(ctx.guild.id, ctx.channel.id, name): - await ctx.send(inline('Removed whitelist config for: ' + name)) - else: - await ctx.send(inline("Rule '{}' is undefined.".format(name))) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def addblacklist(self, ctx, *, name): - """Add the named pattern as a blacklist for this channel.""" - name = name.strip('"') - if self.settings.addBlacklist(ctx.guild.id, ctx.channel.id, name): - await ctx.send(inline('Added blacklist config for: ' + name)) - else: - await ctx.send(inline("Rule '{}' is undefined.".format(name))) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmblacklist(self, ctx, *, name): - """Remove the named pattern as a blacklist for this channel.""" - if self.settings.rmBlacklist(ctx.guild.id, ctx.channel.id, name): - await ctx.send(inline('Removed blacklist config for: ' + name)) - else: - await ctx.send(inline("Rule '{}' is undefined.".format(name))) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def list(self, ctx): - """List the whitelist/blacklist configuration for the current channel.""" - output = 'AutoMod configs\n' - channels = self.settings.getChannels(ctx.guild.id) - for channel_id, config in channels.items(): - channel = self.bot.get_channel(int(channel_id)) - if channel is None: - continue - - whitelists = config['whitelist'] - blacklists = config['blacklist'] - image_limit = config.get('image_limit', 0) - auto_emojis_type = config.get('auto_emojis_type', None) - if len(whitelists + blacklists) + image_limit == 0 and not auto_emojis_type: - continue - - output += '\n#{}'.format(channel.name) - output += '\n\tWhitelists' - for name in whitelists: - output += '\t\t{}\n'.format(name) - output += '\n\tBlacklists' - for name in blacklists: - output += '\t\t{}\n'.format(name) - output += '\n\tImage Limit: {}'.format(image_limit) - output += '\n\tAuto Emojis Type: {}'.format(auto_emojis_type) - await boxPagifySay(ctx.send, output) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def patterns(self, ctx): - """List the registered patterns.""" - patterns = self.settings.getPatterns(ctx.guild.id) - output = 'AutoMod patterns for this server\n\n' - output += self.patternsToTableText(patterns.values()) - await boxPagifySay(ctx.send, output) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def imagelimit(self, ctx, limit: int): - """Prevents users from spamming images in a channel. - - If a user attempts to link/attach more than images in the active channel - within the the lookback window (currently 5), all those messages are deleted. - - Set to 0 to clear. - - Set to -1 to enable image only. - """ - self.settings.setImageLimit(ctx.guild.id, ctx.channel.id, limit) - if limit == 0: - await ctx.send(inline('Limit cleared')) - elif limit == -1: - await ctx.send(inline('I will only allow images in this channel')) - else: - await ctx.send(inline('I will delete excess images in this channel')) - - @commands.Cog.listener('on_message') - async def mod_message_images(self, message): - if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): - return - image_limit = self.settings.getImageLimit(message.guild.id, message.channel.id) - if image_limit == 0: - return - elif image_limit > 0: - if message.channel.permissions_for(message.author).manage_messages: - return - - key = (message.channel.id, message.author.id) - self.channel_user_logs[key].append(message) - - user_logs = self.channel_user_logs[key] - count = 0 - for m in user_logs: - count += linked_img_count(m) - if count <= image_limit: - return - - for m in list(user_logs): - if linked_img_count(m) > 0: - try: - await m.delete() - except: - pass - try: - user_logs.remove(m) - except: - pass - - msg = m.author.mention + inline(' Upload multiple images to an imgur gallery #endimagespam') - alert_msg = await message.channel.send(msg) - await asyncio.sleep(10) - await alert_msg.delete() - else: - if message.channel.permissions_for(message.author).manage_messages: - return - if len(message.embeds) or len(message.attachments): - return - msg_template = box('Your message in {} was deleted for not containing an image') - msg = msg_template.format(message.channel.name) - await self.deleteAndReport(message, msg) - - @commands.Cog.listener('on_message_edit') - async def mod_message_edit(self, before, after): - await self.mod_message(after) - - @commands.Cog.listener('on_message') - async def mod_message(self, message): - if isinstance(message.channel, discord.abc.PrivateChannel): - return - - if message.author.bot or not isinstance(message.author, discord.Member): - return - - if message.channel.permissions_for(message.author).manage_messages: - return - - whitelists, blacklists = self.settings.getRulesForChannel(message.guild.id, message.channel.id) - - msg_template = box('Your message in {} was deleted for violating the following policy: {}\n' - 'Message content: {}') - - msg_content = message.clean_content - for value in blacklists: - name = value['name'] - include_pattern = value['include_pattern'] - exclude_pattern = value['exclude_pattern'] - - if not matchesIncludeExclude(include_pattern, exclude_pattern, msg_content): - continue - - msg = msg_template.format(message.channel.name, name, msg_content) - await self.deleteAndReport(message, msg) - - if len(whitelists): - failed_whitelists = list() - for value in whitelists: - name = value['name'] - include_pattern = value['include_pattern'] - exclude_pattern = value['exclude_pattern'] - - if matchesIncludeExclude(include_pattern, exclude_pattern, msg_content): - return - failed_whitelists.append(name) - - msg = msg_template.format(message.channel.name, - ','.join(failed_whitelists), msg_content) - await self.deleteAndReport(message, msg) - - @automod2.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def autoemojis(self, ctx, key: str = None): - """Will automatically add a set of emojis to all messages sent in this channel. - - Currently supported: tips - - Include the text [noemojis] to suppress emoji addition - """ - if not key: - self.settings.setAutoEmojis(ctx.guild.id, ctx.channel.id, None) - await ctx.send(inline('Auto emojis cleared')) - return - key = key.lower() - if key not in EMOJIS: - await ctx.send(inline('Unknown key')) - return - self.settings.setAutoEmojis(ctx.guild.id, ctx.channel.id, key) - await ctx.send(inline('Auto Emojis for this channel configured!')) - - @commands.Cog.listener('on_message') - async def add_auto_emojis(self, message): - if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): - return - key = self.settings.getAutoEmojis(message.guild.id, message.channel.id) - if not key: - return - if '[noemojis]' in message.content: - return - emoji_list = EMOJIS[key] - for emoji in emoji_list: - await message.add_reaction(emoji) - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def watchdog(self, ctx): - """User monitoring tools.""" - - @watchdog.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def printconfig(self, ctx): - """Print the watchdog configuration.""" - server_id = ctx.guild.id - msg = 'Watchdog config:' - watchdog_channel_id = self.settings.getWatchdogChannel(server_id) - if watchdog_channel_id: - watchdog_channel = self.bot.get_channel(int(watchdog_channel_id)) - if watchdog_channel: - msg += '\nChannel: ' + watchdog_channel.name - else: - msg += '\nChannel configured but not found' - else: - msg += '\nChannel not set' - - msg += '\n\nUsers' - for user_id, user_settings in self.settings.getWatchdogUsers(server_id).items(): - user_cooldown = user_settings['cooldown'] - request_user_id = user_settings['request_user_id'] - reason = user_settings['reason'] or 'no reason' - - request_user = ctx.guild.get_member(int(request_user_id)) - request_user_txt = request_user.name if request_user else '???' - member = ctx.guild.get_member(int(user_id)) - if user_cooldown and member: - msg += '\n{} ({})\n\tcooldown {}\n\tby {} because [{}]'.format( - member.name, member.id, user_cooldown, request_user_txt, reason) - - msg += '\n\nPhrases' - for name, phrase_settings in self.settings.getWatchdogPhrases(server_id).items(): - phrase_cooldown = phrase_settings['cooldown'] - request_user_id = phrase_settings['request_user_id'] - phrase = phrase_settings['phrase'] - - request_user = ctx.guild.get_member(int(request_user_id)) - request_user_txt = request_user.name if request_user else '???' - if phrase_cooldown: - msg += '\n{} -> {}\n\tcooldown {}\n\tby {}'.format( - name, phrase, phrase_cooldown, request_user_txt) - - for page in pagify(msg): - await ctx.send(box(page)) - - @watchdog.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def user(self, ctx, user: discord.User, cooldown: int = None, *, reason: str = ''): - """Keep an eye on a user. - - Whenever the user speaks in this server, a note will be printed to the watchdog - channel, subject to the specified cooldown in seconds. Set to 0 to clear. - """ - server_id = ctx.guild.id - if cooldown is None: - user_settings = self.settings.getWatchdogUsers(server_id).get(user.id, {}) - existing_cd = user_settings.get('cooldown', 0) - if existing_cd == 0: - await ctx.send(inline('No watchdog for that user')) - else: - await ctx.send(inline('Watchdog set with cooldown of {} seconds'.format(existing_cd))) - else: - self.settings.setWatchdogUser( - server_id, user.id, ctx.author.id, cooldown, reason) - if cooldown == 0: - await ctx.send(inline('Watchdog cleared for {}'.format(user.name))) - else: - await ctx.send(inline('Watchdog set on {} with cooldown of {} seconds'.format(user.name, cooldown))) - - @watchdog.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def phrase(self, ctx, name: str, cooldown: int, *, phrase: str = None): - """Keep an eye out for a phrase (regex). - - Whenever the regex is matched, a note will be printed to the watchdog - channel, subject to the specified cooldown in seconds. - - Name is descriptive. Set a cooldown of 0 to clear. - """ - server_id = ctx.guild.id - - if cooldown == 0: - self.settings.setWatchdogPhrase( - server_id, name, None, None, None) - await ctx.send(inline('Watchdog phrase cleared for {}'.format(name))) - return - - try: - re.compile(phrase) - except Exception as ex: - raise commands.UserFeedbackCheckFailure(str(ex)) - - if cooldown < 300: - await ctx.send(inline('Overriding cooldown to minimum')) - cooldown = 300 - self.settings.setWatchdogPhrase(server_id, name, ctx.author.id, cooldown, phrase) - self.server_phrase_last[server_id][name] = None - await ctx.send(inline('Watchdog named {} set on {} with cooldown of {} seconds'.format(name, phrase, cooldown))) - - @watchdog.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def channel(self, ctx, channel: discord.TextChannel): - """Set the announcement channel.""" - server_id = ctx.guild.id - self.settings.setWatchdogChannel(server_id, channel.id) - await ctx.send(inline('Watchdog channel set')) - - @commands.Cog.listener('on_message') - async def mod_message_watchdog(self, message): - if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): - return - if self.settings.getWatchdogChannel(message.guild.id) is None: - return - - await self.mod_message_watchdog_user(message) - await self.mod_message_watchdog_phrase(message) - - async def mod_message_watchdog_user(self, message): - user_id = message.author.id - server_id = message.guild.id - watchdog_channel_id = self.settings.getWatchdogChannel(server_id) - user_settings = self.settings.getWatchdogUsers(server_id).get(user_id) - - if user_settings is None: - return - - cooldown = user_settings['cooldown'] - if cooldown <= 0: - return - - request_user_id = user_settings['request_user_id'] - reason = user_settings['reason'] or 'no reason' - - request_user = message.guild.get_member(request_user_id) - request_user_txt = request_user.mention if request_user else '???' - - now = datetime.utcnow() - last_spoke_at = self.server_user_last[server_id].get(user_id) - self.server_user_last[server_id][user_id] = now - time_since = (now - last_spoke_at).total_seconds() if last_spoke_at else 9999 - - report = time_since > cooldown - if not report: - return - - output_msg = '**Watchdog:** {} spoke in {} ({} monitored because [{}])\n{}'.format( - message.author.mention, message.channel.mention, - request_user_txt, reason, box(message.clean_content)) - await self._watchdog_print(watchdog_channel_id, output_msg) - - async def mod_message_watchdog_phrase(self, message): - server_id = message.guild.id - watchdog_channel_id = self.settings.getWatchdogChannel(server_id) - - for phrase_settings in self.settings.getWatchdogPhrases(server_id).values(): - name = phrase_settings['name'] - cooldown = phrase_settings['cooldown'] - request_user_id = phrase_settings['request_user_id'] - phrase = phrase_settings['phrase'] - - if cooldown <= 0: - continue - - now = datetime.utcnow() - last_spoke_at = self.server_phrase_last[server_id].get(name) - time_since = (now - last_spoke_at).total_seconds() if last_spoke_at else 9999 - - report = time_since > cooldown - if not report: - continue - - p = re.compile(phrase, re.IGNORECASE | re.MULTILINE | re.DOTALL) - if p.match(message.clean_content): - self.server_phrase_last[server_id][name] = now - output_msg = '**Watchdog:** {} spoke in {} `(rule [{}] matched phrase [{}])`\n{}'.format( - message.author.mention, message.channel.mention, - name, phrase, box(message.clean_content)) - await self._watchdog_print(watchdog_channel_id, output_msg) - return - - async def _watchdog_print(self, watchdog_channel_id, output_msg): - try: - watchdog_channel = self.bot.get_channel(watchdog_channel_id) - await watchdog_channel.send(output_msg) - except Exception as ex: - print('failed to watchdog', str(ex)) - - async def deleteAndReport(self, delete_msg, outgoing_msg): - try: - await delete_msg.delete() - await delete_msg.author.send(outgoing_msg) - except Exception as e: - print('Failure while deleting message from {}, tried to send : {}'.format( - delete_msg.author.name, outgoing_msg)) - print(str(e)) - - def patternsToTableText(self, patterns): - tbl = prettytable.PrettyTable(["Rule Name", "Include regex", "Exclude regex"]) - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.NONE - tbl.align = "l" - - for value in patterns: - tbl.add_row([value['name'], value['include_pattern'], value['exclude_pattern']]) - - return rpadutils.strip_right_multiline(tbl.get_string()) - - -def matchesPattern(pattern, txt): - if not len(pattern): - return False - - try: - if pattern[0] == pattern[-1] == ':': - check_method = globals().get(pattern[1:-1]) - if check_method: - return check_method(txt) - except: - return False - - p = re.compile(pattern, re.IGNORECASE | re.MULTILINE | re.DOTALL) - return p.match(txt) - - -def starts_with_code(txt): - # ignore spaces before or in code - txt = txt.replace(' ', '') - # ignore tilde, some users use them to cross out rooms - txt = txt.replace('~', '') - if len(txt) < 8: - return False - return pad_checkdigit(txt[0:8]) - - -def pad_checkdigit(n): - n = str(n) - checkdigit = int(n[7]) - sum = 7 - for idx in range(0, 7): - sum += int(n[idx]) - calcdigit = sum % 10 - return checkdigit == calcdigit - - -def matchesIncludeExclude(include_pattern, exclude_pattern, txt): - if matchesPattern(include_pattern, txt): - return not matchesPattern(exclude_pattern, txt) - return False - - -class AutoMod2Settings(CogSettings): - def make_default_settings(self): - config = { - 'configs': {} - } - return config - - def cleanup(self): - for server in self.serverConfigs().values(): - channels = server['channels'] - for channel_id in list(channels.keys()): - channel = channels[channel_id] - if any([channel.get(x) for x in ['whitelist', 'blacklist', 'image_limit', 'auto_emojis_type']]): - continue - channels.pop(channel_id) - - def serverConfigs(self): - return self.bot_settings['configs'] - - def getServer(self, server_id): - configs = self.serverConfigs() - if server_id not in configs: - configs[server_id] = { - 'patterns': {}, - 'channels': {}, - } - return configs[server_id] - - def getChannels(self, server_id): - server = self.getServer(server_id) - if 'channels' not in server: - server['channels'] = {} - return server['channels'] - - def getChannel(self, server_id, channel_id): - channels = self.getChannels(server_id) - - if channel_id not in channels: - channels[channel_id] = { - 'whitelist': [], - 'blacklist': [], - 'image_limit': 0, - 'auto_emojis_type': None - } - - return channels[channel_id] - - def getRulesForChannel(self, server_id, channel_id): - patterns = self.getPatterns(server_id) - channel = self.getChannel(server_id, channel_id) - - whitelist = [patterns[name] for name in channel['whitelist']] - blacklist = [patterns[name] for name in channel['blacklist']] - return whitelist, blacklist - - def getPatterns(self, server_id): - server = self.getServer(server_id) - if 'patterns' not in server: - server['patterns'] = {} - return server['patterns'] - - def addPattern(self, server_id, name, include_pattern, exclude_pattern): - patterns = self.getPatterns(server_id) - patterns[name] = { - 'name': name, - 'include_pattern': include_pattern, - 'exclude_pattern': exclude_pattern, - } - self.save_settings() - - def checkPatternUsed(self, server_id, name): - server = self.getServer(server_id) - for channel_id, channel_config in server['channels'].items(): - if name in channel_config['whitelist']: - return True - if name in channel_config['blacklist']: - return True - return False - - def rmPattern(self, server_id, name): - patterns = self.getPatterns(server_id) - if self.checkPatternUsed(server_id, name): - return -1 - elif name not in patterns: - return 0 - patterns.pop(name) - self.save_settings() - return 1 - - def addRule(self, server_id, channel_id, name, list_type): - patterns = self.getPatterns(server_id) - if name not in patterns: - return False - self.getChannel(server_id, channel_id)[list_type].append(name) - self.save_settings() - return True - - def rmRule(self, server_id, channel_id, name, list_type): - chan_list = self.getChannel(server_id, channel_id)[list_type] - if name in chan_list: - chan_list.remove(name) - self.save_settings() - return True - else: - return False - - def addWhitelist(self, server_id, channel_id, name): - return self.addRule(server_id, channel_id, name, 'whitelist') - - def rmWhitelist(self, server_id, channel_id, name): - return self.rmRule(server_id, channel_id, name, 'whitelist') - - def addBlacklist(self, server_id, channel_id, name): - return self.addRule(server_id, channel_id, name, 'blacklist') - - def rmBlacklist(self, server_id, channel_id, name): - return self.rmRule(server_id, channel_id, name, 'blacklist') - - def getImageLimit(self, server_id, channel_id): - channel = self.getChannel(server_id, channel_id) - return channel.get('image_limit', 0) - - def setImageLimit(self, server_id, channel_id, image_limit): - channel = self.getChannel(server_id, channel_id) - channel['image_limit'] = image_limit - self.save_settings() - - def getAutoEmojis(self, server_id, channel_id): - channel = self.getChannel(server_id, channel_id) - return channel.get('auto_emojis_type', None) - - def setAutoEmojis(self, server_id, channel_id, key): - channel = self.getChannel(server_id, channel_id) - channel['auto_emojis_type'] = key - self.save_settings() - - def getWatchdog(self, server_id): - server = self.getServer(server_id) - key = 'watchdog' - if key not in server: - server[key] = {} - return server[key] - - def getWatchdogChannel(self, server_id): - watchdog = self.getWatchdog(server_id) - return watchdog.get('announce_channel') - - def setWatchdogChannel(self, server_id, channel_id): - watchdog = self.getWatchdog(server_id) - watchdog['announce_channel'] = channel_id - self.save_settings() - - def getWatchdogUsers(self, server_id): - watchdog = self.getWatchdog(server_id) - key = 'users' - if key not in watchdog: - watchdog[key] = {} - return watchdog[key] - - def setWatchdogUser(self, server_id, user_id, request_user_id, cooldown_secs, reason): - watchdog_users = self.getWatchdogUsers(server_id) - if cooldown_secs: - watchdog_users[user_id] = { - 'request_user_id': request_user_id, - 'cooldown': cooldown_secs, - 'reason': reason, - } - else: - watchdog_users.pop(user_id, None) - self.save_settings() - - def getWatchdogPhrases(self, server_id): - watchdog = self.getWatchdog(server_id) - key = 'phrases' - if key not in watchdog: - watchdog[key] = {} - return watchdog[key] - - def setWatchdogPhrase(self, server_id, name, request_user_id, cooldown_secs, phrase): - watchdog_phrases = self.getWatchdogPhrases(server_id) - if cooldown_secs: - watchdog_phrases[name] = { - 'name': name, - 'request_user_id': request_user_id, - 'cooldown': cooldown_secs, - 'phrase': phrase, - } - else: - watchdog_phrases.pop(name, None) - self.save_settings() - - # GDPR Compliance Functions - def getUserData(self, user_id): - o = { - 'bannings': 0, - 'phrases': 0, - 'baduser': [], - } - - for gid in self.bot_settings['configs']: - for uid in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): - if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users', uid, 'request_user_id'], -2141): - o['bannings'] += 1 - for phr in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases'], []): - if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases', phr, 'request_user_id'], -2141): - o['phrases'] += 1 - if user_id in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): - o['baduser'].append(self.bot_settings['configs'][gid]['watchdog']['users'][user_id]['reason']) - - return o - - def clearUserData(self, user_id): - # Anonymize requesting banners and phrasemakers - for gid in self.bot_settings['configs']: - for uid in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): - if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users', uid, 'request_user_id'], -2141): - self.bot_settings['configs'][gid]['watchdog']['users'][uid]['request_user_id'] = -1 - for phr in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases'], []): - if user_id == rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'phrases', phr, 'request_user_id'], -2141): - self.bot_settings['configs'][gid]['watchdog']['phrases'][phr]['request_user_id'] = -1 - self.save_settings() - - def clearUserDataFull(self, user_id): - self.clearUserData(user_id) - - # Remove bannees - for gid in self.bot_settings['configs']: - if user_id in rpadutils.deepget(self.bot_settings, ['configs', gid, 'watchdog', 'users'], []): - del self.bot_settings['configs'][gid]['watchdog']['users'][user_id] - self.save_settings() diff --git a/automod2/info.json b/automod2/info.json deleted file mode 100644 index f3da836e..00000000 --- a/automod2/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Uses regex pattern matching to filter message content and set limits on users", - "end_user_data_statement": "This cog stores id of users manually marked as problematic.", - "install_msg": "", - "short": "Filter messages via regex", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["prettytable"], - "tags": ["moderation", "filter", "filtering"] -} diff --git a/baduser/__init__.py b/baduser/__init__.py deleted file mode 100644 index a604bdea..00000000 --- a/baduser/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .baduser import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(BadUser(bot)) diff --git a/baduser/baduser.py b/baduser/baduser.py deleted file mode 100644 index 10a83b82..00000000 --- a/baduser/baduser.py +++ /dev/null @@ -1,679 +0,0 @@ -""" -Utilities for managing misbehaving users and facilitating administrator -communication about role changes. -""" - -from collections import defaultdict -from collections import deque - -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import box, inline, pagify - -from rpadutils import CogSettings, get_role, get_role_from_id - -LOGS_PER_USER = 10 - -def opted_in(ctx): - return ctx.guild.id in ctx.bot.get_cog("BadUser").settings.buEnabled() - -class BadUser(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - self.settings = BadUserSettings("baduser") - self.logs = defaultdict(lambda: deque(maxlen=LOGS_PER_USER)) - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - udata = self.settings.getUserData(user_id) - - data = "Stored data for user with ID {}:\n".format(user_id) - if udata['gban']: - data += (" - You are on the global banlist. " - "(This data is sensitive and cannot be cleared automatically due to abuse. " - "Please contact a bot owner to get this data cleared.)\n") - if udata['baduser']: - data += (" - You have been punished/banned in {} servers: " - "(This data is sensitive and cannot be cleared automatically due to abuse. " - "Please contact a bot owner to get this data cleared.)\n" - "").format(len(udata['baduser'])) - - if not any(udata.values()): - data = "No data is stored for user with ID {}.\n".format(user_id) - - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - The personal data stored in this cog is for essential moderation use, - so some data deletion requests can only be made by the bot owner and - Discord itself. If this is an issue, please contact a bot owner. - """ - if requester not in ("discord_deleted_user", "owner"): - self.settings.clearUserData(user_id) - else: - self.settings.clearUserDataFull(user_id) - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def baduser(self, ctx): - """BadUser tools. - - The scope of this module has expanded a bit. It now covers both 'positive' and 'negative' - roles. The goal is to assist coordination across moderators. - - When a user receives a negative role, a strike is automatically recorded for them. This - captures their recent message history. - - You can specify a moderation channel for announcements. An announcement occurs on the - following events: - * User gains or loses a negative/positive role (includes a ping to @here) - * User with a strike leaves the server - * User with a strike joins the server (includes a ping to @here) - - Besides the automatic tracking, you can manually add strikes, print them, and clear them. - """ - - @baduser.command(name="addnegativerole") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def addNegativeRole(self, ctx, *, role: discord.Role): - """Designate a role as a 'punishment' role.""" - self.settings.addPunishmentRole(ctx.guild.id, role.id) - await ctx.send(inline('Added punishment role "' + role.name + '"')) - - @baduser.command(name="rmnegativerole") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def rmNegativeRole(self, ctx, *, role: discord.Role): - """Cancels a role from 'punishment' status.""" - self.settings.rmPunishmentRole(ctx.guild.id, role.id) - await ctx.send(inline('Removed punishment role "' + role.name + '"')) - - @baduser.command(name="addpositiverole") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def addPositiveRole(self, ctx, *, role: discord.Role): - """Designate a role as a 'benefit' role.""" - self.settings.addPositiveRole(ctx.guild.id, role.id) - await ctx.send(inline('Added positive role "' + role.name + '"')) - - @baduser.command(name="rmpositiverole") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def rmPositiveRole(self, ctx, *, role: discord.Role): - """Cancels a role from 'benefit' status.""" - self.settings.rmPositiveRole(ctx.guild.id, role.id) - await ctx.send(inline('Removed positive role "' + role.name + '"')) - - @baduser.command(name="addneutralrole") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def addNeutralRole(self, ctx, *, role: discord.Role): - """Designate a role as a notable but not ping-worthy role.""" - self.settings.addNeutralRole(ctx.guild.id, role.id) - await ctx.send(inline('Added neutral role "' + role.name + '"')) - - @baduser.command(name="rmneutralrole") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def rmNeutralRole(self, ctx, *, role: discord.Role): - """Cancels a role from notable but not ping-worthy status.""" - self.settings.rmNeutralRole(ctx.guild.id, role.id) - await ctx.send(inline('Removed neutral role "' + role.name + '"')) - - @baduser.command(name="setchannel") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def setChannel(self, ctx, channel: discord.TextChannel): - """Set the channel for moderation announcements.""" - self.settings.updateChannel(ctx.guild.id, channel.id) - await ctx.send(inline('Set the announcement channel')) - - @baduser.command(name="clearchannel") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def clearChannel(self, ctx): - """Clear the channel for moderation announcements.""" - self.settings.updateChannel(ctx.guild.id, None) - await ctx.send(inline('Cleared the announcement channel')) - - @baduser.command() - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def togglestrikeprivacy(self, ctx): - """Change strike existance policy.""" - server = ctx.guild - self.settings.setStrikesPrivate(server.id, not self.settings.getStrikesPrivate(server.id)) - output = '\nStrike existance is now ' + \ - 'private' if self.settings.getStrikesPrivate(server.id) else 'public' - await ctx.send(inline(output)) - - @baduser.command() - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def config(self, ctx): - """Print the baduser configuration.""" - server = ctx.guild - output = 'Punishment roles:\n' - for role_id in self.settings.getPunishmentRoles(server.id): - try: - role = get_role_from_id(self.bot, server, role_id) - output += '\t' + role.name + '\n' - except Exception as e: - pass # Role was deleted - - output += '\nPositive roles:\n' - for role_id in self.settings.getPositiveRoles(server.id): - try: - role = get_role_from_id(self.bot, server, role_id) - output += '\t' + role.name + '\n' - except Exception as e: - pass # Role was deleted - - output += '\nNeutral roles:\n' - for role_id in self.settings.getNeutralRoles(server.id): - try: - role = get_role_from_id(self.bot, server, role_id) - output += '\t' + role.name + '\n' - except Exception as e: - pass # Role was deleted - - output += '\nStrike contents are private' - output += '\nStrike existence is ' + \ - ('private' if self.settings.getStrikesPrivate(server.id) else 'public') - - await ctx.send(box(output)) - - @baduser.command(name="strikes") - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def strikes(self, ctx, user: discord.User): - """Print the strike count for a user.""" - strikes = self.settings.countUserStrikes(ctx.guild.id, user.id) - await ctx.send(box('User {} has {} strikes'.format(user.name, strikes))) - - @baduser.command() - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def addstrike(self, ctx, user: discord.User, *, strike_text: str): - """Manually add a strike to a user.""" - timestamp = str(ctx.message.created_at)[:-7] - msg = 'Manually added by {} ({}): {}'.format( - ctx.author.name, timestamp, strike_text) - server_id = ctx.guild.id - self.settings.updateBadUser(server_id, user.id, msg) - strikes = self.settings.countUserStrikes(server_id, user.id) - await ctx.send(box('Done. User {} now has {} strikes'.format(user.name, strikes))) - - @baduser.command() - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def clearstrikes(self, ctx, user: discord.User): - """Clear all strikes for a user.""" - self.settings.clearUserStrikes(ctx.guild.id, user.id) - await ctx.send(box('Cleared strikes for {}'.format(user.name))) - - @baduser.command() - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def printstrikes(self, ctx, user: discord.User): - """Print all strikes for a user.""" - strikes = self.settings.getUserStrikes(ctx.guild.id, user.id) - if not strikes: - await ctx.send(box('No strikes for {}'.format(user.name))) - return - - for idx, strike in enumerate(strikes): - await ctx.send(inline('Strike {} of {}:'.format(idx + 1, len(strikes)))) - await ctx.send(box(strike)) - - @baduser.command() - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def deletestrike(self, ctx, user: discord.User, strike_num: int): - """Delete a specific strike for a user.""" - strikes = self.settings.getUserStrikes(ctx.guild.id, user.id) - if not strikes or len(strikes) < strike_num: - await ctx.send(box('Strike not found for {}'.format(user.name))) - return - - strike = strikes[strike_num - 1] - strikes.remove(strike) - self.settings.setUserStrikes(ctx.guild.id, user.id, strikes) - await ctx.send(inline('Removed strike {}. User has {} remaining.'.format(strike_num, len(strikes)))) - await ctx.send(box(strike)) - - @baduser.command() - @commands.guild_only() - @commands.check(opted_in) - @checks.mod_or_permissions(manage_guild=True) - async def report(self, ctx): - """Displays a report of information on bad users for the server.""" - cur_server = ctx.guild - user_id_to_ban_server = defaultdict(list) - user_id_to_baduser_server = defaultdict(list) - error_messages = list() - for server in self.bot.guilds: - if server.id == cur_server.id: - continue - - if self.settings.getStrikesPrivate(server.id): - error_messages.append("Server '{}' set its strikes private".format(server.name)) - continue - - try: - ban_list = await server.bans() - except: - ban_list = list() - error_messages.append("Server '{}' refused access to ban list".format(server.name)) - - for banentry in ban_list: - user_id_to_ban_server[banentry.user.id].append(server.id) - - baduser_list = self.settings.getBadUsers(server.id) - for user_id in baduser_list: - user_id_to_baduser_server[user_id].append(server.id) - - bad_users = self.settings.getBadUsers(cur_server.id) - - baduser_entries = list() - otheruser_entries = list() - - for member in cur_server.members: - local_strikes = self.settings.getUserStrikes(cur_server.id, member.id) - other_baduser_servers = user_id_to_baduser_server[member.id] - other_banned_servers = user_id_to_ban_server[member.id] - - if not len(local_strikes) and not len(other_baduser_servers) and not len(other_banned_servers): - continue - - tmp_msg = "{} ({})".format(member.name, member.id) - if other_baduser_servers: - tmp_msg += "\n\tbad user in {} other servers".format(len(other_baduser_servers)) - if other_banned_servers: - tmp_msg += "\n\tbanned from {} other servers".format(len(other_banned_servers)) - - if len(local_strikes): - tmp_msg += "\n\t{} strikes in this server".format(len(local_strikes)) - for strike in local_strikes: - tmp_msg += "\n\t\t{}".format(strike.splitlines()[0]) - baduser_entries.append(tmp_msg) - else: - otheruser_entries.append(tmp_msg) - - other_server_count = len(self.bot.guilds) - 1 - other_ban_count = len([x for x, l in user_id_to_ban_server.items() if len(l)]) - other_baduser_count = len([x for x, l in user_id_to_baduser_server.items() if len(l)]) - msg = "Across {} other servers, {} users are banned and {} have baduser entries".format( - other_server_count, other_ban_count, other_baduser_count) - - msg += "\n\n{} baduser entries for this server".format(len(baduser_entries)) - msg += "\n" + "\n".join(baduser_entries) - msg += "\n\n{} entries for users with no record in this server".format( - len(otheruser_entries)) - msg += "\n" + "\n".join(otheruser_entries) - - if error_messages: - msg += "\n\nSome errors occurred:" - msg += "\n" + "\n".join(error_messages) - - for page in pagify(msg): - await ctx.send(box(page)) - - @baduser.command() - @checks.is_owner() - async def addban(self, ctx, user_id: int, *, reason: str): - self.settings.addBannedUser(user_id, reason) - await ctx.send(inline('Done')) - - @baduser.command() - @checks.is_owner() - async def rmban(self, ctx, user_id: int): - user_id = str(user_id) - self.settings.rmBannedUser(user_id) - await ctx.send(inline('Done')) - - @baduser.command() - @checks.is_owner() - async def opt_in(self, ctx): - self.settings.addBuEnabled(ctx.guild.id) - await ctx.send(inline('Done')) - - @baduser.command() - @checks.is_owner() - async def opt_out(self, ctx): - self.settings.rmBuEnabled(ctx.guild.id) - await ctx.send(inline('Done')) - - @commands.Cog.listener('on_message') - async def log_message(self, message): - if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): - return - - if message.guild.id not in self.settings.buEnabled(): - return - - author = message.author - content = message.clean_content - channel = message.channel - timestamp = str(message.created_at)[:-7] - log_msg = '[{}] {} ({}): {}/{}'.format(timestamp, author.name, - author.id, channel.name, content) - self.logs[author.id].append(log_msg) - - @commands.Cog.listener('on_member_ban') - async def mod_ban(self, guild, user): - if guild.id not in self.settings.buEnabled(): - return - await self.recordBadUser(user, 'BANNED') - - @commands.Cog.listener('on_member_remove') - async def mod_user_left(self, member): - if member.guild.id not in self.settings.buEnabled(): - return - strikes = self.settings.countUserStrikes(member.guild.id, member.id) - if strikes: - msg = 'FYI: A user with {} strikes just left the server: {}'.format( - strikes, member.name) - update_channel = self.settings.getChannel(member.guild.id) - if update_channel is not None: - channel_obj = member.guild.get_channel(update_channel) - await channel_obj.send(msg) - - @commands.Cog.listener('on_member_join') - async def mod_user_join(self, member): - if member.guild.id not in self.settings.buEnabled(): - return - update_channel = self.settings.getChannel(member.guild.id) - if update_channel is None: - return - - channel_obj = member.guild.get_channel(update_channel) - strikes = self.settings.countUserStrikes(member.guild.id, member.id) - if strikes: - msg = 'Hey @here a user with {} strikes just joined the server: {}'.format( - strikes, member.mention) - await channel_obj.send(msg, allowed_mentions=discord.AllowedMentions(everyone=True)) - - local_ban = self.settings.bannedUsers().get(member.id, None) - if local_ban: - msg = 'Hey @here locally banned user {} (for: {}) just joined the server'.format( - member.mention, local_ban) - await channel_obj.send(msg, allowed_mentions=discord.AllowedMentions(everyone=True)) - - @commands.Cog.listener('on_member_update') - async def check_punishment(self, before, after): - if before.guild.id not in self.settings.buEnabled(): - return - - if before.roles == after.roles: - return - - new_roles = set(after.roles).difference(before.roles) - removed_roles = set(before.roles).difference(after.roles) - - bad_role_ids = self.settings.getPunishmentRoles(after.guild.id) - positive_role_ids = self.settings.getPositiveRoles(after.guild.id) - neutral_role_ids = self.settings.getNeutralRoles(after.guild.id) - - for role in new_roles: - if role.id in bad_role_ids: - await self.recordBadUser(after, role.name) - return - - if role.id in positive_role_ids: - await self.recordRoleChange(after, role.name, True) - return - - if role.id in neutral_role_ids: - await self.recordRoleChange(after, role.name, True, send_ping=False) - return - - for role in removed_roles: - if role.id in positive_role_ids: - await self.recordRoleChange(after, role.name, False) - return - if role.id in neutral_role_ids: - await self.recordRoleChange(after, role.name, False, send_ping=False) - return - - async def recordBadUser(self, member, role_name): - latest_messages = self.logs.get(member.id, "") - msg = 'Name={} Nick={} ID={} Joined={} Role={}\n'.format( - member.name, member.nick, member.id, member.joined_at, role_name) - msg += '\n'.join(latest_messages) - self.settings.updateBadUser(member.guild.id, member.id, msg) - strikes = self.settings.countUserStrikes(member.guild.id, member.id) - - update_channel = self.settings.getChannel(member.guild.id) - if update_channel is not None: - channel_obj = member.guild.get_channel(update_channel) - await channel_obj.send(inline('Detected bad user')) - await channel_obj.send(box(msg)) - await channel_obj.send('Hey @here please leave a note explaining why this user is punished') - await channel_obj.send('This user now has {} strikes'.format(strikes)) - - try: - dm_msg = ('You were assigned the punishment role "{}" in the server "{}".\n' - 'The Mods will contact you shortly regarding this.\n' - 'Attempting to clear this role yourself will result in punishment.').format(role_name, - member.guild.name) - await member.send(box(dm_msg)) - await channel_obj.send('User successfully notified') - except Exception as e: - await channel_obj.send('Failed to notify the user! I might be blocked\n' + box(str(e))) - - async def recordRoleChange(self, member, role_name, is_added, send_ping=True): - msg = 'Detected role {} : Name={} Nick={} ID={} Joined={} Role={}'.format( - "Added" if is_added else "Removed", member.name, member.nick, member.id, member.joined_at, role_name) - - update_channel = self.settings.getChannel(member.guild.id) - if update_channel is not None: - channel_obj = member.guild.get_channel(update_channel) - try: - await channel_obj.send(inline(msg)) - if send_ping: - await channel_obj.send('Hey @here please leave a note explaining why this role was modified') - except: - print('Failed to notify in', update_channel, msg) - - -class BadUserSettings(CogSettings): - def make_default_settings(self): - config = { - 'servers': {}, - 'banned_users': {}, - 'opted_in': [], - } - return config - - def serverConfigs(self): - return self.bot_settings['servers'] - - def getServer(self, server_id): - configs = self.serverConfigs() - if server_id not in configs: - configs[server_id] = {} - return configs[server_id] - - def getBadUsers(self, server_id): - server = self.getServer(server_id) - if 'badusers' not in server: - server['badusers'] = {} - return server['badusers'] - - def getPunishmentRoles(self, server_id): - server = self.getServer(server_id) - if 'role_ids' not in server: - server['role_ids'] = [] - return server['role_ids'] - - def addPunishmentRole(self, server_id, role_id): - role_ids = self.getPunishmentRoles(server_id) - if role_id not in role_ids: - role_ids.append(role_id) - self.save_settings() - - def rmPunishmentRole(self, server_id, role_id): - role_ids = self.getPunishmentRoles(server_id) - if role_id in role_ids: - role_ids.remove(role_id) - self.save_settings() - - def getPositiveRoles(self, server_id): - server = self.getServer(server_id) - if 'positive_role_ids' not in server: - server['positive_role_ids'] = [] - return server['positive_role_ids'] - - def addPositiveRole(self, server_id, role_id): - role_ids = self.getPositiveRoles(server_id) - if role_id not in role_ids: - role_ids.append(role_id) - self.save_settings() - - def rmPositiveRole(self, server_id, role_id): - role_ids = self.getPositiveRoles(server_id) - if role_id in role_ids: - role_ids.remove(role_id) - self.save_settings() - - def addNeutralRole(self, server_id, role_id): - role_ids = self.getNeutralRoles(server_id) - if role_id not in role_ids: - role_ids.append(role_id) - self.save_settings() - - def getNeutralRoles(self, server_id): - server = self.getServer(server_id) - if 'neutral_role_ids' not in server: - server['neutral_role_ids'] = [] - return server['neutral_role_ids'] - - def rmNeutralRole(self, server_id, role_id): - role_ids = self.getNeutralRoles(server_id) - if role_id in role_ids: - role_ids.remove(role_id) - self.save_settings() - - def updateBadUser(self, server_id, user_id, msg): - badusers = self.getBadUsers(server_id) - if user_id not in badusers: - badusers[user_id] = [] - - badusers[user_id].append(msg) - self.save_settings() - - def countUserStrikes(self, server_id, user_id): - badusers = self.getBadUsers(server_id) - if user_id not in badusers: - return 0 - else: - return len(badusers[user_id]) - - def setUserStrikes(self, server_id, user_id, strikes): - badusers = self.getBadUsers(server_id) - badusers[user_id] = strikes - self.save_settings() - - def clearUserStrikes(self, server_id, user_id): - badusers = self.getBadUsers(server_id) - badusers.pop(user_id, None) - self.save_settings() - - def getUserStrikes(self, server_id, user_id): - badusers = self.getBadUsers(server_id) - return badusers.get(user_id, []) - - def updateChannel(self, server_id, channel_id): - server = self.getServer(server_id) - if channel_id is None: - if 'update_channel' in server: - server.pop('update_channel') - self.save_settings() - return - - server['update_channel'] = channel_id - self.save_settings() - - def getChannel(self, server_id): - server = self.getServer(server_id) - return server.get('update_channel') - - def getStrikesPrivate(self, server_id): - server = self.getServer(server_id) - return server.get('strikes_private', False) - - def setStrikesPrivate(self, server_id, strikes_private): - server = self.getServer(server_id) - server['strikes_private'] = strikes_private - self.save_settings() - - def bannedUsers(self): - return self.bot_settings['banned_users'] - - def addBannedUser(self, user_id: int, reason: str): - self.bannedUsers()[user_id] = reason - self.save_settings() - - def rmBannedUser(self, user_id: int): - self.bannedUsers().pop(user_id, None) - self.save_settings() - - def buEnabled(self): - return [int(gid) for gid in self.bot_settings['opted_in']] - - def addBuEnabled(self, gid: int): - self.bot_settings['opted_in'].append(gid) - self.save_settings() - - def rmBuEnabled(self, gid: int): - if str(gid) in self.bot_settings['opted_in']: - self.bot_settings['opted_in'].remove(str(gid)) - self.save_settings() - - def getUserData(self, uid): - o = { - "gban": "", - "baduser": 0, - } - if str(uid) in self.bot_settings['banned_users']: - o['gban'] = self.bot_settings['banned_users'][str(uid)] - for gid in self.bot_settings['servers']: - if str(uid) in self.bot_settings['servers'][gid]["badusers"]: - o['baduser'] += 1 - return o - - def clearUserData(self, uid): - # Do nothing - return - - def clearUserDataFull(self, uid): - if str(uid) in self.bot_settings['banned_users']: - del self.bot_settings['banned_users'][str(uid)] - for gid in self.bot_settings['servers']: - if str(uid) in self.bot_settings['servers'][gid]["badusers"]: - del self.bot_settings['servers'][gid]["badusers"][str(uid)] - self.save_settings() diff --git a/baduser/info.json b/baduser/info.json deleted file mode 100644 index 5e602bd4..00000000 --- a/baduser/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Allows for more specific punishments than regular discord including global strikes and positive/negative roles.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "General purpose punishment cog.", - "short": "A fancy python powered calculator", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["administration", "moderation"] -} diff --git a/calculator/__init__.py b/calculator/__init__.py deleted file mode 100644 index 1854399a..00000000 --- a/calculator/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .calculator import * - -__red_end_user_data_statement__ = "The most recent answer in each channel is stored." - -def setup(bot): - bot.add_cog(Calculator(bot)) diff --git a/calculator/calculator.py b/calculator/calculator.py deleted file mode 100644 index d3ce0d2b..00000000 --- a/calculator/calculator.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -A cloned and improved version of paddo's calculator cog. -""" - -import numbers -import os -import re -import shlex -import subprocess -import sys - -import discord -from redbot.core import commands, Config -from redbot.core.utils.chat_formatting import inline, box -from functools import reduce - -ACCEPTED_TOKENS = r'[\[\]\-()*+/0-9=.,% |&<>~_^]|>=|<=|==|!=|factorial|randrange|isfinite|copysign|radians|isclose|degrees|randint|lgamma|choice|random|round|log1p|log10|ldexp|isnan|isinf|hypot|gamma|frexp|floor|expm1|atanh|atan2|asinh|acosh|False|range|tanh|sqrt|sinh|modf|log2|fmod|fabs|erfc|cosh|ceil|atan|asin|acos|else|True|fsum|tan|sin|pow|nan|log|inf|gcd|sum|exp|erf|cos|for|not|and|ans|pi|in|is|or|if|e|x' - -ALTERED_TOKENS = {'^': '**', '_':'ans'} - -HELP_MSG = ''' -This calculator works by first validating the content of your query against a whitelist, and then -executing a python eval() on it, so some common syntax wont work. Here is the full symbol whitelist: -''' - - -class Calculator(commands.Cog): - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.config = Config.get_conf(self, identifier=857907) - self.config.register_user(ans={}) - - """ - CONFIG: Config - | GUILDS: Config - | | ans: channel_id -> Any - """ - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - udata = await self.config.user_from_id(user_id).ans() - - data = "You have previous answers stored in {} channels.\n".format(len(udata)) - - if not udata: - data = "No data is stored for user with ID {}.\n".format(user_id) - - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data.""" - await self.config.user_from_id(user_id).clear() - - @commands.group() - async def helpcalc(self, ctx): - '''Whispers info on how to use the calculator.''' - help_msg = HELP_MSG + '\n' + ACCEPTED_TOKENS - await ctx.author.send(box(help_msg)) - - @commands.command(aliases=['calc']) - async def calculator(self, ctx, *, inp): - '''Evaluate equations. Use helpcalc for more info.''' - inp = inp.lower() - unaccepted = list(filter(None, re.split(ACCEPTED_TOKENS, inp))) - for token in ALTERED_TOKENS: - inp = inp.replace(token, ALTERED_TOKENS[token]) - - if unaccepted: - err_msg = 'Found unexpected symbols inside the input: {}'.format(", ".join(unaccepted)) - help_msg = 'Use {0.prefix}helpcalc for info on how to use this command' - await ctx.send(inline(err_msg + '\n' + help_msg.format(ctx))) - return - - ans = (await self.config.user(ctx.author).ans()).get(str(ctx.channel.id)) - if re.search(r'\bans\b', inp) and ans is None: - await ctx.send("You don't have a previous result saved.") - return - - cmd = """{} -c "from math import *;from random import *;ans = {};print(eval('{}'), end='', flush=True)" """.format( - sys.executable, ans, inp) - - try: - if os.name != 'nt' and sys.platform != 'win32': - cmd = shlex.split(cmd) - output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=2) - calc_result = output.decode('utf-8').strip() - except subprocess.TimeoutExpired: - await ctx.send(inline('Command took too long to execute. Quit trying to break the bot.')) - return - except subprocess.CalledProcessError as e: - await ctx.send(inline(e.output.decode("utf-8").strip().split('\n')[-1])) - return - - - if len(str(calc_result)) > 1024: - await ctx.send(inline("The result is obnoxiously long! Try a request under 1k characters!")) - elif len(str(calc_result)) > 0: - if isinstance(calc_result, numbers.Number): - if calc_result > 1: - calc_result = round(calc_result, 3) - - em = discord.Embed(color=discord.Color.greyple()) - em.add_field(name='Input', value='`{}`'.format(inp)) - em.add_field(name='Result', value=calc_result) - async with self.config.user(ctx.author).ans() as ans: - if calc_result is not None: - ans[ctx.channel.id] = calc_result - await ctx.send(embed=em) - - @commands.command() - async def add(self, ctx, *, inp): - """Adds a string of numbers""" - em = discord.Embed(color=discord.Color.greyple()) - em.add_field(name='Input', value='`{}`'.format('+'.join(filter(None,re.split(r'\D', inp))))) - em.add_field(name='Result', value=sum(map(lambda x: int('0'+x), re.split(r'\D', inp)))) - await ctx.send(embed=em) - - @commands.command() - async def multiply(self, ctx, *, inp): - """Multiplies a string of numbers""" - em = discord.Embed(color=discord.Color.greyple()) - em.add_field(name='Input', value='`{}`'.format('*'.join(filter(None,re.split(r'\D', inp))))) - em.add_field(name='Result', value=reduce(lambda x, y: x*int(y) if y else x, re.split(r'\D', inp), 1)) - await ctx.send(embed=em) diff --git a/calculator/info.json b/calculator/info.json deleted file mode 100644 index c22bf216..00000000 --- a/calculator/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Calculate expressions via python exec, but safely so users can do it too!", - "end_user_data_statement": "The most recent answer in each channel is stored.", - "install_msg": "", - "short": "A fancy python powered calculator", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [], - "tags": ["calculator"] -} diff --git a/channelmod/__init__.py b/channelmod/__init__.py deleted file mode 100644 index 7df09ee4..00000000 --- a/channelmod/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .channelmod import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - n = ChannelMod(bot) - bot.add_cog(n) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py deleted file mode 100644 index 8c27b4f4..00000000 --- a/channelmod/channelmod.py +++ /dev/null @@ -1,396 +0,0 @@ -import io -import re -import logging -import time -import traceback -from datetime import datetime - -import aiohttp -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.utils.chat_formatting import inline - -from rpadutils import rpadutils, CogSettings, box - -# Three hour cooldown -ATTRIBUTION_TIME_SECONDS = 60 * 60 * 3 - -frMESSAGE_LINK = r'(https://discordapp\.com/channels/{0.guild.id}/{0.id}/(\d+)/?)' -MESSAGE_LINK = 'https://discordapp.com/channels/{0.guild.id}/{0.channel.id}/{0.id}' - - -class ChannelMod(commands.Cog): - """Channel moderation tools.""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = ChannelModSettings("channelmod") - self.channel_last_spoke = {} - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.group() - async def channelmod(self, ctx): - """Manage channel moderation settings""" - - @channelmod.command() - @checks.is_owner() - async def addmirror(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): - """Set mirroring between two channels.""" - if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): - await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) - return - self.settings.add_mirrored_channel(source_channel_id, dest_channel_id) - await ctx.send(inline('Done')) - - @channelmod.command() - @checks.is_owner() - async def rmmirror(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): - """Remove mirroring between two channels.""" - if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): - await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) - return - self.settings.rm_mirrored_channel(source_channel_id, dest_channel_id) - await ctx.send(inline('Done')) - - @channelmod.command() - @checks.is_owner() - async def mirrorconfig(self, ctx): - """List mirror config.""" - mirrored_channels = self.settings.mirrored_channels() - msg = 'Mirrored channels\n' - for mc_id, config in mirrored_channels.items(): - channel = self.bot.get_channel(mc_id) - channel_name = channel.name if channel else 'unknown' - msg += '\n{} ({})'.format(mc_id, channel_name) - for channel_id in config['channels']: - channel = self.bot.get_channel(channel_id) - channel_name = channel.name if channel else 'unknown' - msg += '\n\t{} ({})'.format(channel_id, channel_name) - await ctx.send(box(msg)) - - @channelmod.command() - async def countreactions(self, ctx, message: discord.Message): - """Count reactions on a message and all of its mirrors. - - The message can be a link, a message id (if used in the same - channel as the message), or the channel_id and message_id - separated by a dash (channel_id-message-id)""" - mirrored_messages = self.settings.get_mirrored_messages(message.channel.id, message.id) - if not mirrored_messages: - await ctx.send("This message isn't mirrored!") - reacts = {} - for react in message.reactions: - reacts[str(react)] = react.count - 1 - for (chid, mid) in mirrored_messages: - dest_channel = self.bot.get_channel(chid) - if not dest_channel: - print('could not locate channel {}'.format(chid)) - continue - dest_message = await dest_channel.fetch_message(mid) - if not dest_message: - print('could not locate message {}'.format(mid)) - continue - dest_reaction = discord.utils.find(lambda r: r == react, dest_message.reactions) - if not dest_reaction: - print('could not locate reaction {}'.format(react)) - continue - reacts[str(react)] += dest_reaction.count - 1 - o = "" - maxlen = len(str(max(reacts.values(), key=lambda x: len(str(x))))) - for r, c in reacts.items(): - o += "{{}}: {{:{}}}\n".format(maxlen).format(r,c) - await ctx.send(o) - - - @commands.Cog.listener('on_message') - async def mirror_msg(self, message): - if message.author.id == self.bot.user.id: - return - - prefixes = await self.bot.get_prefix(message) - for prefix in prefixes if isinstance(prefixes, list) else [prefixes]: - if message.content.strip().startswith(prefix): - return - - channel = message.channel - mirrored_channels = self.settings.get_mirrored_channels(channel.id) - - if not mirrored_channels: - return - - last_spoke, last_spoke_timestamp = self.settings.get_last_spoke(channel.id) - now_time = datetime.utcnow() - last_spoke_time = datetime.utcfromtimestamp( - last_spoke_timestamp) if last_spoke_timestamp else now_time - attribution_required = last_spoke != message.author.id - attribution_required |= (now_time - last_spoke_time).total_seconds() > ATTRIBUTION_TIME_SECONDS - self.settings.set_last_spoke(channel.id, message.author.id) - - attachment_bytes = None - filename = None - if message.attachments: - # If we know we're copying a message and that message has an attachment, - # pre download it and reuse it for every upload. - attachment = message.attachments[0] - if hasattr(attachment, 'url') and hasattr(attachment, 'filename'): - url = attachment.url - filename = attachment.filename - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - attachment_bytes = io.BytesIO(await response.read()) - - for dest_channel_id in mirrored_channels: - try: - dest_channel = self.bot.get_channel(dest_channel_id) - if not dest_channel: - continue - - if attribution_required: - msg = self.makeheader(message) - await dest_channel.send(msg) - - fmessage = await self.mformat(message.content, message.channel, dest_channel) - - if attachment_bytes and filename: - dest_message = await dest_channel.send(file=discord.File(attachment_bytes, filename), - content=fmessage) - attachment_bytes.seek(0) - elif message.content: - dest_message = await dest_channel.send(fmessage) - else: - print('Failed to mirror message from', channel.id, 'no action to take') - continue - - self.settings.add_mirrored_message( - channel.id, message.id, dest_channel.id, dest_message.id) - except Exception as ex: - print('Failed to mirror message from ', channel.id, 'to', dest_channel_id, ':', ex) - traceback.print_exc() - - if attachment_bytes: - attachment_bytes.close() - - @commands.Cog.listener('on_message_edit') - async def mirror_msg_edit(self, message, new_message): - await self.mirror_msg_mod(message, new_message_content=new_message.content) - - @commands.Cog.listener('on_message_delete') - async def mirror_msg_delete(self, message): - await self.mirror_msg_mod(message, delete_message_content=True) - - @commands.Cog.listener('on_reaction_add') - async def mirror_reaction_add(self, reaction, user): - message = reaction.message - if message.author.id != user.id: - return - await self.mirror_msg_mod(message, new_message_reaction=reaction.emoji) - - @commands.Cog.listener('on_reaction_remove') - async def mirror_reaction_remove(self, reaction, user): - message = reaction.message - if message.author.id != user.id: - return - await self.mirror_msg_mod(message, delete_message_reaction=reaction.emoji) - - async def mirror_msg_mod(self, message, - new_message_content: str = None, - delete_message_content: bool = False, - new_message_reaction=None, - delete_message_reaction=None): - if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): - return - - channel = message.channel - mirrored_messages = self.settings.get_mirrored_messages(channel.id, message.id) - for (dest_channel_id, dest_message_id) in mirrored_messages: - try: - dest_channel = self.bot.get_channel(dest_channel_id) - if not dest_channel: - print('could not locate channel to mod') - continue - dest_message = await dest_channel.fetch_message(dest_message_id) - if not dest_message: - print('could not locate message to mod') - continue - - if new_message_content: - fcontent = await self.mformat(new_message_content, channel, dest_message.channel) - await dest_message.edit(content=fcontent) - elif new_message_reaction: - await dest_message.add_reaction(new_message_reaction) - elif delete_message_content: - await dest_message.delete() - elif delete_message_reaction: - await dest_message.remove_reaction(delete_message_reaction, dest_message.guild.me) - except Exception as ex: - print('Failed to mirror message edit from ', - channel.id, 'to', dest_channel_id, ':', ex) - - def makeheader(self, message): - return 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name, - message.guild.name, - message.channel.name, - message.jump_url) - - async def mformat(self, text, from_channel, dest_channel): - # LINKS - for link, mid in re.findall(frMESSAGE_LINK.format(from_channel), text): - from_link = await from_channel.fetch_message(mid) - if not from_link: - print('could not locate link to copy') - continue - mirrored_messages = self.settings.get_mirrored_messages(from_link.channel.id, from_link.id) - to_link_id = [dmid for dcid, dmid in mirrored_messages if dcid == dest_channel.id] - if not to_link_id: - print('could not locate link to mod') - continue - to_link_id = to_link_id[0] - dest_link = await dest_channel.fetch_message(to_link_id) - - newlink = MESSAGE_LINK.format(dest_link) - text = text.replace(link, newlink) - # ROLES - for rtext, rid in re.findall(r'(<@&(\d+)>)', text): - target = from_channel.guild.get_role(int(rid)) - if target is None: - print('could not locate role to mod') - continue - dest = discord.utils.get(dest_channel.guild.roles, name=target.name) - if dest is None: - repl = "\\@"+target.name - else: - repl = "<@&{}>".format(dest.id) - text = text.replace(rtext, repl) - # MENTIONS - for utext, uid in re.findall(r'(<@!(\d+)>)', text): - target = from_channel.guild.get_member(int(uid)) - if target is None: - print('could not locate user to mod') - continue - text = text.replace(utext, target.name) - # CHANNELS - for ctext, cid in re.findall(r'(<#(\d+)>)', text): - target = from_channel.guild.get_channel(int(cid)) - if target is None: - print('could not locate channel to mod') - continue - text = text.replace(ctext, "\\#"+target.name) - #EVERYONE - text = re.sub(r"@everyone\b", "@\u200beveryone", text) - text = re.sub(r"@here\b", "@\u200bhere", text) - #EMOJI - # text = self.emojify(text) - return text - - def emojify(self, message): - emojis = list() - for guild in self.bot.guilds: - emojis.extend(guild.emojis) - message = rpadutils.replace_emoji_names_with_code(emojis, message) - return rpadutils.fix_emojis_for_server(emojis, message) - -class ChannelModSettings(CogSettings): - def make_default_settings(self): - config = { - 'servers': {}, - 'mirrored_channels': {}, - 'max_mirrored_messages': 20, - } - return config - - def servers(self): - return self.bot_settings['servers'] - - def get_server(self, server_id: int): - servers = self.servers() - if server_id not in servers: - servers[server_id] = {} - return servers[server_id] - - def max_mirrored_messages(self): - return self.bot_settings['max_mirrored_messages'] - - def mirrored_channels(self): - # Mirrored channels looks like: - # : { - # 'last_spoke: '', - # 'channels': [dest_channel_id_1, dest_channel_id2], - # 'messages': { - # : [ - # (dest_channel_id, dest_channel_msg], - # ] - # } - # } - return self.bot_settings['mirrored_channels'] - - def get_mirrored_channels(self, source_channel: str): - return self.mirrored_channels().get(source_channel, {}).get('channels', []) - - def get_last_spoke(self, source_channel: str): - spoke_values = self.mirrored_channels().get(source_channel, {}) - return spoke_values.get('last_spoke', None), spoke_values.get('last_spoke_timestamp', None) - - def set_last_spoke(self, source_channel: str, last_spoke: str): - spoke_values = self.mirrored_channels().get(source_channel) - spoke_values['last_spoke'] = last_spoke - spoke_values['last_spoke_timestamp'] = time.time() - self.save_settings() - - def add_mirrored_channel(self, source_channel: int, dest_channel: int): - channels = self.mirrored_channels() - if source_channel == dest_channel: - raise commands.UserFeedbackCheckFailure('Cannot mirror a channel to itself') - if dest_channel in channels: - raise commands.UserFeedbackCheckFailure('Destination channel is already a source channel') - if source_channel not in channels: - channels[source_channel] = { - 'channels': [], - 'messages': {}, - } - channels[source_channel]['channels'].append(dest_channel) - self.save_settings() - - def rm_mirrored_channel(self, source_channel: int, dest_channel: int): - channels = self.mirrored_channels() - config = channels.get(source_channel) - if config and dest_channel in config['channels']: - dest_channel_config = config['channels'] - dest_channel_config.remove(dest_channel) - if not dest_channel_config: - channels.pop(source_channel) - self.save_settings() - - def add_mirrored_message(self, source_channel: int, source_message: str, dest_channel: int, dest_message: str): - channel_config = self.mirrored_channels()[source_channel] - messages = channel_config['messages'] - if source_message not in messages: - messages[source_message] = [] - - targets = messages[source_message] - new_entry = [dest_channel, dest_message] - if new_entry not in targets: - targets.append(new_entry) - self.save_settings() - - if len(messages) > self.max_mirrored_messages(): - oldest_msg = min(messages.keys()) - messages.pop(oldest_msg) - self.save_settings() - - def get_mirrored_messages(self, source_channel: int, source_message: str): - """Returns [(channel_id, message_id), ...]""" - channel_config = self.mirrored_channels().get(source_channel, {}) - return channel_config.get('messages', {}).get(source_message, []) diff --git a/channelmod/info.json b/channelmod/info.json deleted file mode 100644 index e875485a..00000000 --- a/channelmod/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Allows you to mirror channels across servers.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Mirror channels across servers.", - "short": "A fancy python powered calculator", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["aiohttp"], - "tags": ["administration", "mirror"] -} diff --git a/datatransfer/__init__.py b/datatransfer/__init__.py deleted file mode 100644 index bc9ebb1a..00000000 --- a/datatransfer/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .datatransfer import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(DataTransfer(bot)) diff --git a/datatransfer/datatransfer.py b/datatransfer/datatransfer.py deleted file mode 100644 index 6d34bee1..00000000 --- a/datatransfer/datatransfer.py +++ /dev/null @@ -1,171 +0,0 @@ -import json -import os -import re -import base64 -import io -import aiohttp - -import discord -from redbot.core import checks, data_manager, commands -from redbot.core.utils.chat_formatting import inline, box, pagify - -R_MESSAGE_LINK = r"https://discordapp.com/channels/(\d+)/(\d+)/(\d+)" -R_ATTATCH_LINK = r"https://cdn.discordapp.com/attachments/\d+/\d+/.+" - -class DataTransfer(commands.Cog): - """Transfer cog data.""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.group(name="export") - @commands.guild_only() - @checks.mod_or_permissions(administrator=True) - async def _export(self, ctx): - """Get a .enc file to load data on another bot via the [p]import command.""" - - @commands.group(name="import") - @commands.guild_only() - @checks.mod_or_permissions(administrator=True) - async def _import(self, ctx): - """Load data from another bot via an attatched .enc file. (Obtain this with [p]export)""" - - @_import.command(name="alias") - async def import_alias(self, ctx, bot_mention: discord.User, link_or_attatchment=None): - if not bot_mention == ctx.me: - return - - if re.match(R_MESSAGE_LINK, link_or_attatchment): - m = await commands.MessageConverter().convert(ctx, link_or_attatchment) - raw_data = await m.attachments[0].read() - elif re.match(R_ATTATCH_LINK, link_or_attatchment): - async with aiohttp.ClientSession() as session: - async with session.get(link_or_attatchment) as response: - raw_data = await response.read() - elif ctx.message.attachments: - raw_data = await ctx.message.attachments[0].read() - elif link_or_attatchment is None: - await ctx.send(inline("Please supply the .enc file retrieved from export function.")) - return - else: - await ctx.send(inline("Invalid link to file. Please use a message or attachment link.")) - return - - try: - data = json.loads(base64.b64decode(raw_data).decode()) - - for k,v in data.items(): - await self.bot.get_cog("Alias").config.guild(ctx.guild).set_raw(k, value=v) - - await self.bot.get_cog("Core").reload(ctx, "alias") - await ctx.send(inline("Done.")) - except Exception as e: - print(e) - await ctx.send(inline("Invalid file.")) - - @_export.command(name="alias") - async def export_alias(self, ctx, bot_mention: discord.Member): - if not bot_mention == ctx.me: - return - - raw_data = (await self.bot.get_cog("Alias").config.all_guilds())[ctx.guild.id] - data = base64.b64encode(json.dumps(raw_data).encode()) - await ctx.send(file=discord.File(io.BytesIO(data), "alias_settings.enc")) - - @_import.command(name="customcom", aliases=["cc", "customcommands", "customcommand"]) - async def import_customcommand(self, ctx, bot_mention: discord.User, link_or_attatchment=None): - if not bot_mention == ctx.me: - return - - if re.match(R_MESSAGE_LINK, link_or_attatchment): - m = await commands.MessageConverter().convert(ctx, link_or_attatchment) - raw_data = await m.attachments[0].read() - elif re.match(R_ATTATCH_LINK, link_or_attatchment): - async with aiohttp.ClientSession() as session: - async with session.get(link_or_attatchment) as response: - raw_data = await response.read() - elif ctx.message.attachments: - raw_data = await ctx.message.attachments[0].read() - elif link_or_attatchment is None: - await ctx.send(inline("Please supply the .enc file retrieved from export function.")) - return - else: - await ctx.send(inline("Invalid link to file. Please use a message or attachment link.")) - return - - try: - data = json.loads(base64.b64decode(raw_data).decode()) - - for k,v in data.items(): - await self.bot.get_cog("CustomCommands").config.guild(ctx.guild).set_raw(k, value=v) - - await self.bot.get_cog("Core").reload(ctx, "customcom") - await ctx.send(inline("Done.")) - except Exception as e: - print(e) - await ctx.send(inline("Invalid file.")) - - @_export.command(name="customcom", aliases=["cc", "customcommands", "customcommand"]) - async def export_customcommand(self, ctx, bot_mention: discord.Member): - if not bot_mention == ctx.me: - return - - raw_data = (await self.bot.get_cog("CustomCommands").config.all_guilds())[ctx.guild.id] - data = base64.b64encode(json.dumps(raw_data).encode()) - await ctx.send(file=discord.File(io.BytesIO(data), "cc_settings.enc")) - - @_import.command(name="memes", aliases=["meme"]) - async def import_meme(self, ctx, bot_mention: discord.User, link_or_attatchment=None): - if not bot_mention == ctx.me: - return - - if re.match(R_MESSAGE_LINK, link_or_attatchment): - m = await commands.MessageConverter().convert(ctx, link_or_attatchment) - raw_data = await m.attachments[0].read() - elif re.match(R_ATTATCH_LINK, link_or_attatchment): - async with aiohttp.ClientSession() as session: - async with session.get(link_or_attatchment) as response: - raw_data = await response.read() - elif ctx.message.attachments: - raw_data = await ctx.message.attachments[0].read() - elif link_or_attatchment is None: - await ctx.send(inline("Please supply the .enc file retrieved from export function.")) - return - else: - await ctx.send(inline("Invalid link to file. Please use a message or attachment link.")) - return - - try: - data = json.loads(base64.b64decode(raw_data).decode()) - self.bot.get_cog("Memes").settings.bot_settings['configs'][ctx.guild.id] = data['configs'] - self.bot.get_cog("Memes").c_commands[ctx.guild.id] = data['commands'] - json.dump(self.bot.get_cog("Memes").c_commands, open(self.bot.get_cog("Memes").file_path, 'w+')) - await ctx.send(inline("Done.")) - except Exception as e: - print(e) - await ctx.send(inline("Invalid file.")) - - @_export.command(name="memes", aliases=["meme"]) - async def export(self, ctx, bot_mention: discord.Member): - if not bot_mention == ctx.me: - return - - raw_data = { - 'configs': self.bot.get_cog("Memes").settings.bot_settings['configs'].get(ctx.guild.id, {}), - 'commands': self.bot.get_cog("Memes").c_commands[ctx.guild.id], - } - data = base64.b64encode(json.dumps(raw_data).encode()) - await ctx.send(file=discord.File(io.BytesIO(data), "meme_settings.enc")) diff --git a/datatransfer/info.json b/datatransfer/info.json deleted file mode 100644 index 119055a8..00000000 --- a/datatransfer/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["Aradia Megido#2552"], - "description": "Export and import data for memes, customcom, and alias", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Exports/Imports data for some cogs", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["aiohttp"], - "tags": ["export", "import"] -} diff --git a/donations/__init__.py b/donations/__init__.py deleted file mode 100644 index 90050530..00000000 --- a/donations/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .donations import * - -__red_end_user_data_statement__ = "This cog stores your custom commands." - -def setup(bot): - n = Donations(bot) - bot.add_cog(n) - bot.loop.create_task(n.set_server_attributes()) diff --git a/donations/donations.py b/donations/donations.py deleted file mode 100644 index d0023273..00000000 --- a/donations/donations.py +++ /dev/null @@ -1,444 +0,0 @@ -import json -import random -import re - -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box - -from rpadutils import CogSettings, clean_global_mentions - -DONATE_MSG = """ -To donate to cover bot hosting fees you can use one of: - Patreon : https://www.patreon.com/tsubaki_bot - -Read the Patreon or join the Tsubaki Support Server for more details: - https://discord.gg/tVPmeG8 - -You permanently get some special perks for donating even $1. - -The following users have donated. Thanks! -{donors} -""" - -INSULTS_FILE = "data/donations/insults.json" -DEFAULT_INSULTS = { - 'miru_references': [ - 'Are you talking to me you piece of shit?', - ], - 'insults': [ - 'You are garbage.', - 'Kill yourself.', - ] -} -LOVE_FILE = "data/donations/love.json" -DEFAULT_LOVE = { - 'cute': ['xoxo'], - 'sexy': ['{}====>'], - 'perverted': ['{}===>()'], -} - - -def roll(chance: int): - return random.randrange(100) < chance - -def is_patron(ctx): - tsubaki_guild = ctx.bot.get_guild(746131494875168770) - patron_role = tsubaki_guild.get_role(749849451769757726) - author = tsubaki_guild.get_member(ctx.author.id) - if author is None or patron_role not in author.roles: - return False - return True - -def is_donor(ctx): - tsubaki_guild = ctx.bot.get_guild(746131494875168770) - donor_role = tsubaki_guild.get_role(749849518467580015) - patron_role = tsubaki_guild.get_role(749849451769757726) - author = tsubaki_guild.get_member(ctx.author.id) - if author is None or (donor_role not in author.roles and patron_role not in author.roles): - return False - return True - -class Donations(commands.Cog): - """Manages donations and perks.""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = DonationsSettings("donations") - - try: - insults_json = json.load(open(INSULTS_FILE, "r")) - except: - insults_json = {} - self.insults_miru_reference = insults_json.get( - 'miru_references', DEFAULT_INSULTS['miru_references']) - self.insults_list = insults_json.get('insults', DEFAULT_INSULTS['insults']) - try: - love_json = json.load(open(LOVE_FILE, "r")) - except: - love_json = {} - self.cute_list = love_json.get('cute', DEFAULT_LOVE['cute']) - self.sexy_list = love_json.get('sexy', DEFAULT_LOVE['sexy']) - self.perverted_list = love_json.get('perverted', DEFAULT_LOVE['perverted']) - - self.tsubaki_guild = None - self.donor_role = None - self.patron_role = None - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - udata = self.settings.getUserData(user_id) - - data = "Stored data for user with ID {}:\n".format(user_id) - if udata['command']: - data += " - You have setup the command '{}'.\n".format(udata['command']) - if udata['embed']: - data += " - You have setup the embed '{}'.\n".format(udata['embed']) - if udata['insult']: - data += " - You have asked the bot to insult you occasionally.\n" - - if not any(udata.values()): - data = "No data is stored for user with ID {}.\n".format(user_id) - - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data.""" - if requester not in ("discord_deleted_user", "owner"): - self.settings.clearUserData(user_id) - else: - self.settings.clearUserDataFull(user_id) - - async def set_server_attributes(self): - await self.bot.wait_until_ready() - self.tsubaki_guild = self.bot.get_guild(746131494875168770) - self.donor_role = self.tsubaki_guild.get_role(749849518467580015) - self.patron_role = self.tsubaki_guild.get_role(749849451769757726) - - @commands.command() - async def donate(self, ctx): - """Prints information about donations.""" - donor_names = set() - for user in self.tsubaki_guild.members: - if self.donor_role in user.roles or self.patron_role in user.roles: - donor_names.add(user.name) - - msg = DONATE_MSG.format(count=len(donor_names), donors=', '.join(sorted(donor_names))) - await ctx.send(box(msg)) - - @commands.command() - @commands.check(is_donor) - async def mycommand(self, ctx, command: str, *, text: str): - """Sets your custom command.""" - text = clean_global_mentions(text) - - self.settings.addCustomCommand(ctx.author.id, command, text) - await ctx.send(inline('I set up your command: ' + command)) - - @commands.command() - @commands.check(is_donor) - async def myembed(self, ctx, command: str, title: str, url: str, footer: str): - """Sets your custom embed command. - - This lets you create a fancier image message. For example you can set up - a simple inline image without a link using: - [p]myembed lewd "" "http://i0.kym-cdn.com/photos/images/original/000/731/885/751.jpg" "" - - Want a title on that image? Fill in the first argument: - [p]myembed lewd "L-lewd!" "" "" - - Want a footer? Fill in the last argument: - [p]myembed lewd "L-lewd!" "" "source: some managa i read" - """ - - self.settings.addCustomEmbed(ctx.author.id, command, title, url, footer) - await ctx.send(inline('I set up your embed: ' + command)) - - @commands.command() - @commands.check(is_donor) - async def spankme(self, ctx): - """You are trash.""" - await ctx.send(ctx.author.mention + ' ' + random.choice(self.insults_list)) - - @commands.command() - @commands.check(is_donor) - async def insultme(self, ctx): - """You are consistently trash.""" - user_id = ctx.author.id - - self.settings.addInsultsEnabled(user_id) - await ctx.send(ctx.author.mention + ' ' 'Oh, I will.\n' + random.choice(self.insults_list)) - - @commands.command() - @commands.check(is_donor) - async def plsno(self, ctx): - """I am merciful.""" - - self.settings.rmInsultsEnabled(ctx.author.id) - await ctx.send('I will let you off easy this time.') - - @commands.command() - @commands.check(is_donor) - async def kissme(self, ctx): - """You are so cute!.""" - await ctx.send(ctx.author.mention + ' ' + random.choice(self.cute_list)) - - @commands.command() - @commands.check(is_donor) - async def lewdme(self, ctx): - """So nsfw..""" - if 'nsfw' in ctx.channel.name.lower(): - await ctx.send(ctx.author.mention + ' ' + random.choice(self.sexy_list)) - else: - await ctx.send(ctx.author.mention + ' Oooh naughty...') - await ctx.author.send(random.choice(self.sexy_list)) - - @commands.command() - @commands.check(is_donor) - async def pervme(self, ctx): - """Hentai!!!.""" - if 'nsfw' in ctx.channel.name.lower(): - await ctx.send(ctx.author.mention + ' ' + random.choice(self.perverted_list)) - else: - await ctx.send(ctx.author.mention + ' Filthy hentai!') - await ctx.author.send(random.choice(self.perverted_list)) - - @commands.group() - @checks.admin_or_permissions(manage_guild=True) - async def donations(self, ctx): - """Manage donation options.""" - - @donations.command() - async def togglePerks(self, ctx): - """Enable or disable donor-specific perks for the server.""" - server_id = ctx.guild.id - if server_id in self.settings.disabledServers(): - self.settings.rmDisabledServer(server_id) - await ctx.send(inline('Donor perks enabled on this server')) - else: - self.settings.addDisabledServer(server_id) - await ctx.send(inline('Donor perks disabled on this server')) - - @donations.command() - @checks.is_owner() - async def info(self, ctx): - """Print donation related info.""" - patrons = [user for user in self.tsubaki_guild.members if self.patron_role in user.roles] - donors = [user for user in self.tsubaki_guild.members if self.donor_role in user.roles - and self.patron_role not in user.roles] - cmds = self.settings.customCommands() - embeds = self.settings.customEmbeds() - disabled_servers = self.settings.disabledServers() - - id_to_name = {m.id: m.name for m in self.bot.get_all_members()} - - msg = 'Donations Info' - - msg += '\n\nPatrons:' - for user in patrons: - msg += '\n\t{} ({})'.format(user.name, user.id) - - msg += '\n\nDonors:' - for user in donors: - msg += '\n\t{} ({})'.format(user.name, user.id) - - msg += '\n\nDisabled servers:' - for server_id in disabled_servers: - server = self.bot.get_guild(int(server_id)) - msg += '\n\t{} ({})'.format(server.name if server else 'unknown', server_id) - - msg += '\n\n{} personal commands are set'.format(len(cmds)) - msg += '\n{} personal embeds are set'.format(len(cmds)) - - await ctx.send(box(msg)) - - @commands.Cog.listener("on_message") - async def checkCC(self, message): - if len(message.content) < 2: - return - - prefix = (await self.bot.get_prefix(message))[0] - - user_id = message.author.id - if user_id not in [user.id for user in self.tsubaki_guild.members if self.donor_role in user.roles] and \ - user_id not in [user.id for user in self.tsubaki_guild.members if self.patron_role in user.roles]: - return - - if message.guild and message.guild.id in self.settings.disabledServers(): - return - - user_cmd = self.settings.customCommands().get(user_id) - user_embed = self.settings.customEmbeds().get(user_id) - - cmd = message.content[len(prefix):].lower() - if user_cmd is not None: - if cmd == user_cmd['command']: - await message.channel.send(user_cmd['text']) - return - if user_embed is not None: - if cmd == user_embed['command']: - embed = discord.Embed() - title = user_embed['title'] - url = user_embed['url'] - footer = user_embed['footer'] - if len(title): - embed.title = title - if len(url): - embed.set_image(url=url) - if len(footer): - embed.set_footer(text=footer) - await message.channel.send(embed=embed) - return - - @commands.Cog.listener("on_message") - async def check_insult(self, message): - # Only opted-in people - if message.author.id not in self.settings.insultsEnabled(): - return - - if message.guild and message.guild.id in self.settings.disabledServers(): - return - - content = message.clean_content - # Ignore short messages - if len(content) < 10: - return - - msg = message.author.mention - - # Pretty frequently respond to direct messages - mentions_bot = re.search(r'(miru|myr|tsubaki) bot', content, re.IGNORECASE) and roll(40) - # Semi-frequently respond to miru in msg - mentions_miru_and_roll = re.search( - r'\b(miru|myr|tsubaki)\b', content, re.IGNORECASE) and roll(20) - - if mentions_bot or mentions_miru_and_roll: - msg += ' ' + random.choice(self.insults_miru_reference) - msg += '\n' + random.choice(self.insults_list) - await message.channel.send(msg) - return - - # Semi-frequently respond to long messages - long_msg_and_roll = len(content) > 200 and roll(10) - # Occasionally respond to other messages - short_msg_and_roll = roll(1) - - if long_msg_and_roll or short_msg_and_roll: - msg += ' ' + random.choice(self.insults_list) - await message.channel.send(msg) - return - - # Periodically send private messages - if roll(7): - msg += ' ' + random.choice(self.insults_list) - await message.author.send(msg) - return - - -class DonationsSettings(CogSettings): - def make_default_settings(self): - config = { - 'custom_commands': {}, - 'custom_embeds': {}, - 'disabled_servers': [], - 'insults_enabled': [], - } - return config - - def customCommands(self): - return self.bot_settings['custom_commands'] - - def addCustomCommand(self, user_id, command, text): - cmds = self.customCommands() - cmds[user_id] = { - 'command': command.lower(), - 'text': text, - } - self.save_settings() - - def rmCustomCommand(self, user_id): - cmds = self.customCommands() - if user_id in cmds: - cmds.remove(user_id) - self.save_settings() - - def customEmbeds(self): - return self.bot_settings['custom_embeds'] - - def addCustomEmbed(self, user_id, command, title, url, footer): - embeds = self.customEmbeds() - embeds[user_id] = { - 'command': command.lower().strip(), - 'title': title.strip(), - 'url': url.strip(), - 'footer': footer.strip(), - } - self.save_settings() - - def rmCustomEmbed(self, user_id): - embeds = self.customEmbeds() - if user_id in embeds: - embeds.remove(user_id) - self.save_settings() - - def disabledServers(self): - return self.bot_settings['disabled_servers'] - - def addDisabledServer(self, server_id): - disabled_servers = self.disabledServers() - if server_id not in disabled_servers: - disabled_servers.append(server_id) - self.save_settings() - - def rmDisabledServer(self, server_id): - disabled_servers = self.disabledServers() - if server_id in disabled_servers: - disabled_servers.remove(server_id) - self.save_settings() - - def insultsEnabled(self): - return self.bot_settings['insults_enabled'] - - def addInsultsEnabled(self, user_id): - insults_enabled = self.insultsEnabled() - if user_id not in insults_enabled: - insults_enabled.append(user_id) - self.save_settings() - - def rmInsultsEnabled(self, user_id): - insults_enabled = self.insultsEnabled() - if user_id in insults_enabled: - insults_enabled.remove(user_id) - self.save_settings() - - # GDPR Compliance Functions - def getUserData(self, user_id): - o = { - 'command': "", - 'embed': "", - 'insult': False, - } - - if user_id in self.bot_settings['custom_commands']: - o['command'] = self.bot_settings['custom_commands'][user_id]["command"] - if user_id in self.bot_settings['custom_embeds']: - o['embed'] = self.bot_settings['custom_embeds'][user_id]["command"] - if user_id in self.bot_settings['insults_enabled']: - o['insult'] = True - - return o - - def clearUserData(self, user_id): - if user_id in self.bot_settings['custom_commands']: - del self.bot_settings['custom_commands'][user_id] - if user_id in self.bot_settings['custom_embeds']: - del self.bot_settings['custom_embeds'][user_id] - if user_id in self.bot_settings['insults_enabled']: - self.bot_settings['insults_enabled'].remove(user_id) - self.save_settings() - - def clearUserDataFull(self, user_id): - self.clearUserData(user_id) diff --git a/donations/info.json b/donations/info.json deleted file mode 100644 index d67449da..00000000 --- a/donations/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Tracks donors and gives them perks", - "end_user_data_statement": "This cog stores your custom commands.", - "install_msg": "", - "short": "Patreon/Donor perks", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["donations", "patreon"] -} diff --git a/fancysay/__init__.py b/fancysay/__init__.py deleted file mode 100644 index 8c372871..00000000 --- a/fancysay/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .fancysay import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(FancySay(bot)) diff --git a/fancysay/fancysay.py b/fancysay/fancysay.py deleted file mode 100644 index f15bf1a5..00000000 --- a/fancysay/fancysay.py +++ /dev/null @@ -1,156 +0,0 @@ -import asyncio - -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline - -from rpadutils import rpadutils -from rpadutils import char_to_emoji - - -class FancySay(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_messages=True) - async def fancysay(self, ctx): - """Make the bot say fancy things (via embeds).""" - - @fancysay.command() - async def pingrole(self, ctx, role: discord.Role, *, text): - """[p]fancysay pingrole rolename this is the text to ping - - 1) Converts a role to mentionable - 2) Posts the message + ping in the current channel - 3) Sets the role to unmentionable - 4) Deletes the input message - - The role must be unmentionable before this command for safety. - """ - if role.mentionable: - await ctx.send(inline('Error: role is already mentionable')) - return - - try: - await role.edit(mentionable=True) - except Exception as ex: - await ctx.send(inline('Error: failed to set role mentionable')) - if ex.text == "Missing Permissions": - message = await ctx.send(inline('Make sure this bot\'s role is higher than the one you\'re mentioning')) - await asyncio.sleep(3) - await message.delete() - return - - await ctx.message.delete() - await asyncio.sleep(1) - await ctx.send('From {}:\n{}\n{}'.format(ctx.author.mention, role.mention, text)) - - try: - await role.edit(mentionable=False) - except Exception as ex: - await ctx.send(inline('Error: failed to set role unmentionable')) - print(ex) - return - - @fancysay.command() - async def emoji(self, ctx, *, text): - """Speak the provided text as emojis, deleting the original request""" - await ctx.message.delete() - new_msg = "" - for char in text: - if char.isalpha(): - new_msg += char_to_emoji(char) + ' ' - elif char == ' ': - new_msg += ' ' - elif char.isspace(): - new_msg += char - - if len(new_msg): - await ctx.send(new_msg) - - @fancysay.command() - async def title_description_image_footer(self, ctx, title, description, image, footer): - """[title] [description] [image_url] [footer_text] - - You must specify a title. You can omit any of description, image, or footer. - To omit an item use empty quotes. For the text fields, wrap your text in quotes. - The bot will automatically delete your 'say' command if it can - - e.g. say with all fields: - fancysay title_description_image_footer "My title text" "Description text" "xyz.com/image.png" "source: xyz.com" - - e.g. say with only title and image: - fancysay title_descirption_image_footer "My title" "" "xyz.com/image.png" "" - """ - - embed = discord.Embed() - if len(title): - embed.title = title - if len(description): - embed.description = description - if len(image): - embed.set_image(url=image) - if len(footer): - embed.set_footer(text=footer) - - try: - await ctx.send(embed=embed) - await ctx.message.delete() - except Exception as error: - print("failed to fancysay", error) - - - - - - @commands.command(aliases = ["parrot", "repeat"]) - @checks.mod_or_permissions(manage_messages=True) - async def say(self, ctx, *, message): - """Make the bot parrot a phrase.""" - message = self.emojify(message) - await ctx.send(message) - - @commands.command(aliases = ["testparrot", "testrepeat"]) - @checks.mod_or_permissions(manage_messages=True) - async def testsay(self, ctx, *, message): - """Make the bot parrot a phrase without smart emoji replacements.""" - await ctx.send(message) - - @commands.command() - @checks.mod_or_permissions(manage_messages=True) - async def mask(self, ctx, *, message): - """Sends a message as the bot.""" - message = self.emojify(message) - await ctx.message.delete() - await ctx.send(message) - - @commands.command() - @checks.mod_or_permissions(manage_messages=True) - async def yell(self, ctx, *, message): - """Yells some text.""" - message = self.emojify(message) - await ctx.send(message.upper().rstrip(",.!?")+"!!!!!!") - - def emojify(self, message): - emojis = list() - for guild in self.bot.guilds: - emojis.extend(guild.emojis) - message = rpadutils.replace_emoji_names_with_code(emojis, message) - return rpadutils.fix_emojis_for_server(emojis, message) diff --git a/fancysay/info.json b/fancysay/info.json deleted file mode 100644 index 020063f3..00000000 --- a/fancysay/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Allows the user to make the bot say things in a variety of ways.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Say things with \u2728style\u2728~", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["announcement", "fancy"] -} diff --git a/globalban/__init__.py b/globalban/__init__.py deleted file mode 100644 index 7bb8347d..00000000 --- a/globalban/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .globalban import GlobalBan - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(GlobalBan(bot)) diff --git a/globalban/globalban.py b/globalban/globalban.py deleted file mode 100644 index bec67798..00000000 --- a/globalban/globalban.py +++ /dev/null @@ -1,106 +0,0 @@ -import asyncio -import discord -import datetime - -from redbot.core import commands, Config, checks, modlog -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline - - -class GlobalBan(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.config = Config.get_conf(self, identifier=1437847847) - self.config.register_global(banned={}, opted=[]) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.group() - async def globalban(self, ctx): - """Global ban related commands.""" - - @globalban.command() - @checks.mod_or_permissions(administrator=True) - async def optin(self, ctx): - """Opt your server in to the Global Ban system.""" - async with self.config.opted() as opted: - opted.append(ctx.guild.id) - await self.update_gbs() - await ctx.send(inline("Done")) - - @globalban.command() - @checks.mod_or_permissions(administrator=True) - async def optout(self, ctx): - """Opt your server out of the Global Ban system.""" - async with self.config.opted() as opted: - while ctx.guild.id in opted: - opted.remove(ctx.guild.id) - await self.remove_gbs_guild(ctx.guild.id) - await ctx.send(inline("Done")) - - @globalban.command() - @checks.is_owner() - async def ban(self, ctx, user, *, reason=""): - """Globally Ban a user across all opted-in servers.""" - async with self.config.banned() as banned: - banned[user] = reason - await self.update_gbs() - await ctx.send(inline("Done")) - - @globalban.command() - @checks.is_owner() - async def unban(self, ctx, user): - """Globally Unban a user across all opted-in servers.""" - async with self.config.banned() as banned: - if user in banned: - del banned[user] - await self.remove_gbs_user(user) - await ctx.send(inline("Done")) - - - async def update_gbs(self): - for gid in await self.config.opted(): - guild = self.bot.get_guild(int(gid)) - for uid, reason in (await self.config.banned()).items(): - if uid in [b.user.id for b in await guild.bans()]: - continue - m = guild.get_member(int(uid)) - if m is None: - try: - await guild.ban(discord.Object(id=uid), reason="GlobalBan", delete_message_days=0) - except discord.errors.NotFound: - pass - else: - await guild.ban(m, reason="GlobalBan", delete_message_days=0) - await modlog.create_case(bot=self.bot, - guild=guild, - created_at=datetime.datetime.now(), - action_type="globalban", - user=m, - reason='GlobalBan') - - async def remove_gbs_guild(self, gid): - guild = self.bot.get_guild(int(gid)) - for ban in await guild.bans(): - user = b.user - if user.id not in await self.config.banned(): - continue - await guild.unban(user) - - async def remove_gbs_user(self, uid): - for gid in await self.config.opted(): - guild = self.bot.get_guild(int(gid)) - users = [b.user for b in await guild.bans() if b.user.id == int(uid)] - if users: - await guild.unban(users[0]) diff --git a/globalban/info.json b/globalban/info.json deleted file mode 100644 index 5d73b968..00000000 --- a/globalban/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["Aradia Megido#2552"], - "description": "Allows bot owners to make global bans across servers.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Ban users across multiple servers", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [], - "tags": ["globalban", "moderation"] -} diff --git a/memes/__init__.py b/memes/__init__.py deleted file mode 100644 index ef8a0c2f..00000000 --- a/memes/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .memes import * - -__red_end_user_data_statement__ = "All stored data is anonymized." - -def setup(bot): - bot.add_cog(Memes(bot)) diff --git a/memes/info.json b/memes/info.json deleted file mode 100644 index d3598dcb..00000000 --- a/memes/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["Aradia Megido#2552"], - "description": "Allows users to setup custom commands only accessable by a certain role.", - "end_user_data_statement": "All stored data is anonymized.", - "install_msg": "", - "short": "Priviledged custom commands", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["customcommands", "memes"] -} diff --git a/memes/memes.py b/memes/memes.py deleted file mode 100644 index b0c00fca..00000000 --- a/memes/memes.py +++ /dev/null @@ -1,241 +0,0 @@ -import json -import os -import re -import base64 -import io - -import discord -from redbot.core import checks, data_manager, commands -from redbot.core.utils.chat_formatting import inline, box, pagify - - -from rpadutils import CogSettings, get_role_from_id, safe_read_json - - -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='memes')), file_name) - - -class Memes(commands.Cog): - """Custom memes.""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.file_path = _data_file('commands.json') - self.c_commands = safe_read_json(self.file_path) - self.c_commands = {int(k): v for k, v in self.c_commands.items()} # Fix legacy issues - self.settings = MemesSettings("memes") - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.command() - @commands.guild_only() - @checks.mod_or_permissions(administrator=True) - async def addmeme(self, ctx, command: str, *, text): - """Adds a meme - - Example: - [p]addmeme yourmeme Text you want - - Memes can be enhanced with arguments: - https://twentysix26.github.io/Red-Docs/red_guide_command_args/ - """ - guild = ctx.guild - command = command.lower() - if command in self.bot.all_commands.keys(): - await ctx.send("That meme is already a standard command.") - return - if not guild.id in self.c_commands: - self.c_commands[guild.id] = {} - cmdlist = self.c_commands[guild.id] - if command not in cmdlist: - cmdlist[command] = text - self.c_commands[guild.id] = cmdlist - json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("Custom command successfully added.") - else: - await ctx.send("This command already exists. Use editmeme to edit it.") - - @commands.command() - @commands.guild_only() - @checks.mod_or_permissions(administrator=True) - async def editmeme(self, ctx, command: str, *, text): - """Edits a meme - - Example: - [p]editmeme yourcommand Text you want - """ - guild = ctx.guild - command = command.lower() - if guild.id in self.c_commands: - cmdlist = self.c_commands[guild.id] - if command in cmdlist: - cmdlist[command] = text - self.c_commands[guild.id] = cmdlist - json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("Custom command successfully edited.") - else: - await ctx.send("That command doesn't exist. Use addmeme [command] [text]") - else: - await ctx.send("There are no custom memes in this server. Use addmeme [command] [text]") - - @commands.command() - @commands.guild_only() - @checks.mod_or_permissions(administrator=True) - async def delmeme(self, ctx, command: str): - """Deletes a meme - - Example: - [p]delmeme yourcommand""" - guild = ctx.guild - command = command.lower() - if guild.id in self.c_commands: - cmdlist = self.c_commands[guild.id] - if command in cmdlist: - cmdlist.pop(command, None) - self.c_commands[guild.id] = cmdlist - json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("Custom meme successfully deleted.") - else: - await ctx.send("That meme doesn't exist.") - else: - await ctx.send("There are no custom memes in this server. Use addmeme [command] [text]") - - @commands.command() - @commands.guild_only() - @checks.mod_or_permissions(administrator=True) - async def setmemerole(self, ctx, role: discord.Role): - """Sets the meme role - - Example: - [p]setmemerole Regular""" - - self.settings.setPrivileged(ctx.guild.id, role.id) - await ctx.send(inline("Done")) - - @commands.command() - @commands.guild_only() - async def memes(self, ctx): - """Shows custom memes list""" - guild = ctx.guild - if guild.id in self.c_commands and self.c_commands[guild.id]: - cmdlist = self.c_commands[guild.id] - i = 0 - msg = ["```Custom memes:\n"] - for cmd in sorted([cmd for cmd in cmdlist.keys()]): - if len(msg[i]) + len(ctx.prefix) + len(cmd) + 5 > 2000: - msg[i] += "```" - i += 1 - msg.append("``` {}{}\n".format(ctx.prefix, cmd)) - else: - msg[i] += " {}{}\n".format(ctx.prefix, cmd) - msg[i] += "```" - for cmds in msg: - await ctx.author.send(cmds) - else: - await ctx.send("There are no custom memes in this server. Use addmeme [command] [text]") - - @commands.Cog.listener('on_message') - async def checkCC(self, message): - if len(message.content) < 2 or isinstance(message.channel, discord.abc.PrivateChannel): - return - - guild = message.guild - prefix = await self.get_prefix(message) - - if not prefix: - return - - # MEME CODE - role_id = self.settings.getPrivileged(guild.id) - if role_id is not None: - role = get_role_from_id(self.bot, guild, role_id) - if role not in message.author.roles: - return - - # MEME CODE - if guild.id in self.c_commands and not message.author.bot: - cmdlist = self.c_commands[guild.id] - cmd = message.content[len(prefix):] - if cmd in cmdlist.keys(): - cmd = cmdlist[cmd] - cmd = self.format_cc(cmd, message) - await message.channel.send(cmd) - elif cmd.lower() in cmdlist.keys(): - cmd = cmdlist[cmd.lower()] - cmd = self.format_cc(cmd, message) - await message.channel.send(cmd) - - async def get_prefix(self, message): - for p in await self.bot.get_prefix(message): - if message.content.startswith(p): - return p - return False - - def format_cc(self, command, message): - results = re.findall(r"\{([^}]+)\}", command) - for result in results: - param = self.transform_parameter(result, message) - command = command.replace("{" + result + "}", param) - return command - - def transform_parameter(self, result, message): - """ - For security reasons only specific objects are allowed - Internals are ignored - """ - raw_result = "{" + result + "}" - objects = { - "message": message, - "author": message.author, - "channel": message.channel, - "server": message.guild - } - if result in objects: - return str(objects[result]) - try: - first, second = result.split(".") - except ValueError: - return raw_result - if first in objects and not second.startswith("_"): - first = objects[first] - else: - return raw_result - return str(getattr(first, second, raw_result)) - - -class MemesSettings(CogSettings): - def make_default_settings(self): - config = { - 'configs': {} - } - return config - - def guildConfigs(self): - return self.bot_settings['configs'] - - def getGuild(self, guild_id: int): - configs = self.guildConfigs() - if guild_id not in configs: - configs[guild_id] = {} - return configs[guild_id] - - def getPrivileged(self, guild_id: int): - guild = self.getGuild(guild_id) - return guild.get('privileged') - - def setPrivileged(self, guild_id: int, role_id: int): - guild = self.getGuild(guild_id) - guild['privileged'] = role_id - self.save_settings() diff --git a/modnotes/__init__.py b/modnotes/__init__.py deleted file mode 100644 index 4711ba81..00000000 --- a/modnotes/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .modnotes import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(ModNotes(bot)) diff --git a/modnotes/info.json b/modnotes/info.json deleted file mode 100644 index e120fb07..00000000 --- a/modnotes/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Keeps global tabs on iffy users.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Global user notes", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["moderation"] -} diff --git a/modnotes/modnotes.py b/modnotes/modnotes.py deleted file mode 100644 index 57f1bb07..00000000 --- a/modnotes/modnotes.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Utilities for managing moderator notes about users. -""" -import re - -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box, pagify - -from rpadutils import CogSettings - - -class ModNotes(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = ModNotesSettings("modnotes") - - @commands.group(aliases=["usernote"]) - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def usernotes(self, ctx): - """Moderator notes for users. - - This module allows you to create notes to share between moderators. - """ - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @usernotes.command() - @checks.mod_or_permissions(manage_guild=True) - async def get(self, ctx, user: discord.User): - """Sends the notes for a user.""" - notes = self.settings.getNotesForUser(ctx.guild.id, user.id) - if not notes: - await ctx.send(box('No notes for {}'.format(user.name))) - return - - for idx, note in enumerate(notes): - await ctx.send(inline('Note {} of {}:'.format(idx + 1, len(notes)))) - await ctx.send(box(note)) - - @usernotes.command() - @checks.mod_or_permissions(manage_guild=True) - async def add(self, ctx, user: discord.User, *, note_text: str): - """Add a note to a user.""" - timestamp = str(ctx.message.created_at)[:-7] - msg = 'Added by {} ({}): {}'.format(ctx.author.name, timestamp, note_text) - server_id = ctx.guild.id - notes = self.settings.addNoteForUser(server_id, user.id, msg) - await ctx.send(inline('Done. User {} now has {} notes'.format(user.name, len(notes)))) - - @usernotes.command() - @checks.mod_or_permissions(manage_guild=True) - async def delete(self, ctx, user: discord.User, note_num: int): - """Delete a specific note for a user.""" - notes = self.settings.getNotesForUser(ctx.guild.id, user.id) - if len(notes) < note_num: - await ctx.send(box('Note not found for {}'.format(user.name))) - return - - note = notes[note_num - 1] - notes.remove(note) - self.settings.setNotesForUser(ctx.guild.id, user.id, notes) - await ctx.send(inline('Removed note {}. User has {} remaining.'.format(note_num, len(notes)))) - await ctx.send(box(note)) - - @usernotes.command() - @checks.mod_or_permissions(manage_guild=True) - async def list(self, ctx): - """Lists all users and note counts for the server.""" - user_notes = self.settings.getUserNotes(ctx.guild.id) - msg = 'Notes for {} users'.format(len(user_notes)) - for user_id, notes in user_notes.items(): - user = ctx.guild.get_member(user_id) - user_text = '{} ({})'.format(user.name, user.id) if user else user_id - msg += '\n\t{} : {}'.format(len(notes), user_text) - - for page in pagify(msg): - await ctx.send(box(page)) - - -class ModNotesSettings(CogSettings): - def make_default_settings(self): - config = { - 'servers': {} - } - return config - - def servers(self): - return self.bot_settings['servers'] - - def getServer(self, server_id): - servers = self.servers() - if server_id not in servers: - servers[server_id] = {} - return servers[server_id] - - def getUserNotes(self, server_id): - server = self.getServer(server_id) - key = 'user_notes' - if key not in server: - server[key] = {} - return server[key] - - def getNotesForUser(self, server_id, user_id): - user_notes = self.getUserNotes(server_id) - return user_notes.get(user_id, []) - - def setNotesForUser(self, server_id, user_id, notes): - user_notes = self.getUserNotes(server_id) - - if notes: - user_notes[user_id] = notes - else: - user_notes.pop(user_id, None) - self.save_settings() - return notes - - def addNoteForUser(self, server_id, user_id, note: str): - notes = self.getNotesForUser(server_id, user_id) - notes.append(note) - self.setNotesForUser(server_id, user_id, notes) - return notes diff --git a/msgutils/__init__.py b/msgutils/__init__.py deleted file mode 100644 index bae0d4f1..00000000 --- a/msgutils/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .msgutils import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(MsgUtils(bot)) diff --git a/msgutils/info.json b/msgutils/info.json deleted file mode 100644 index 8736d91c..00000000 --- a/msgutils/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Allows users to see raw messages and manipulate bot messages in new ways.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Message manipulation", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [], - "tags": ["administration"] -} diff --git a/msgutils/msgutils.py b/msgutils/msgutils.py deleted file mode 100644 index 25314f63..00000000 --- a/msgutils/msgutils.py +++ /dev/null @@ -1,97 +0,0 @@ -import asyncio -import datetime -import re - -import discord -from redbot.core.bot import Red - -from redbot.core import checks, modlog -from redbot.core import commands -from redbot.core.utils.chat_formatting import inline, box, pagify - - -class MsgUtils(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.command() - @checks.mod_or_permissions(manage_guild=True) - async def editmsg(self, ctx, channel: discord.TextChannel, msg_id: int, *, new_msg: str): - """Given a channel and an ID for a message printed in that channel, replaces it. - - To find a message ID, enable developer mode in Discord settings and - click the ... on a message. - """ - try: - msg = await channel.fetch_message(msg_id) - except discord.NotFound: - await ctx.send(inline('Cannot find that message, check the channel and message id')) - return - except discord.Forbidden: - await ctx.send(inline('No permissions to do that')) - return - if msg.author.id != self.bot.user.id: - await ctx.send(inline('Can only edit messages I own')) - return - - await msg.edit(content=new_msg) - await ctx.send(inline('done')) - - @commands.command() - @checks.mod_or_permissions(manage_guild=True) - async def dumpchannel(self, ctx, channel: discord.TextChannel, msg_id: int = None): - """Given a channel and an ID for a message printed in that channel, dumps it - boxed with formatting escaped and some issues cleaned up. - - To find a message ID, enable developer mode in Discord settings and - click the ... on a message. - """ - await self._dump(ctx, channel, msg_id) - - @commands.command() - async def dumpmsg(self, ctx, msg_id: int = None): - """Given an ID for a message printed in the current channel, dumps it - boxed with formatting escaped and some issues cleaned up. - - To find a message ID, enable developer mode in Discord settings and - click the ... on a message. - """ - await self._dump(ctx, ctx.channel, msg_id) - - async def _dump(self, ctx, channel: discord.TextChannel = None, msg_id: int = None): - if msg_id: - msg = await channel.fetch_message(msg_id) - else: - msg_limit = 2 if channel == ctx.channel else 1 - async for message in channel.history(limit=msg_limit): - msg = message - content = msg.content.strip() - content = re.sub(r'<(:[0-9a-z_]+:)\d{18}>', r'\1', content, flags=re.IGNORECASE) - content = box(content.replace('`', u'\u200b`')) - await ctx.send(content) - - @commands.command() - async def dumpmsgexact(self, ctx, msg_id: int): - """Given an ID for a message printed in the current channel, dumps it - boxed with formatting escaped. - - To find a message ID, enable developer mode in Discord settings and - click the ... on a message. - """ - msg = await ctx.channel.fetch_message(msg_id) - content = msg.content.strip() - content = box(content.replace('`', u'\u200b`')) - await ctx.send(content) diff --git a/playground/__init__.py b/playground/__init__.py deleted file mode 100644 index 27cd12eb..00000000 --- a/playground/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .playground import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(Playground(bot)) diff --git a/playground/info.json b/playground/info.json deleted file mode 100644 index b1b9c022..00000000 --- a/playground/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["Aradia Megido#2552"], - "description": "This cog doesn't do anything.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "\u00af\\_(\u30c4)_/\u00af", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [], - "tags": [] -} diff --git a/playground/playground.py b/playground/playground.py deleted file mode 100644 index f7f3095d..00000000 --- a/playground/playground.py +++ /dev/null @@ -1,19 +0,0 @@ -from redbot.core import checks, commands -from redbot.core.utils.chat_formatting import box, pagify - -class Playground(commands.Cog): - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return diff --git a/rpadutils/__init__.py b/rpadutils/__init__.py deleted file mode 100644 index 89dcf2b5..00000000 --- a/rpadutils/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .rpadutils import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(RpadUtils(bot)) diff --git a/rpadutils/info.json b/rpadutils/info.json deleted file mode 100644 index 6f32d4a6..00000000 --- a/rpadutils/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Helper functions for most other Tsubaki cogs.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Helper functions (LOAD THIS FIRST)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["pytz", "aiohttp", "backoff"], - "tags": ["PAD", "sharedlibrary"] -} diff --git a/rpadutils/rpadutils.py b/rpadutils/rpadutils.py deleted file mode 100644 index 4b49b3cf..00000000 --- a/rpadutils/rpadutils.py +++ /dev/null @@ -1,725 +0,0 @@ -import asyncio -import concurrent.futures -import errno -import inspect -import json -import os -import re -import io -import signal -import time -import unicodedata -from functools import wraps -from typing import List, Optional - -import aiohttp -import backoff -import discord -import pytz -from discord.ext.commands import CommandNotFound -from redbot.core import commands, data_manager -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import box, pagify - -RPADCOG = None - -class RpadUtils(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - global RPADCOG - RPADCOG = self - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - -# TZ used for PAD NA -# NA_TZ_OBJ = pytz.timezone('America/Los_Angeles') -NA_TZ_OBJ = pytz.timezone('US/Pacific') - -# TZ used for PAD JP -JP_TZ_OBJ = pytz.timezone('Asia/Tokyo') - -# https://gist.github.com/ryanmcgrath/982242 -# UNICODE RANGE : DESCRIPTION -# 3000-303F : punctuation -# 3040-309F : hiragana -# 30A0-30FF : katakana -# FF00-FFEF : Full-width roman + half-width katakana -# 4E00-9FAF : Common and uncommon kanji -# -# Non-Japanese punctuation/formatting characters commonly used in Japanese text -# 2605-2606 : Stars -# 2190-2195 : Arrows -# u203B : Weird asterisk thing - -JP_REGEX_STR = r'[\u3000-\u303F]|[\u3040-\u309F]|[\u30A0-\u30FF]|[\uFF00-\uFFEF]|[\u4E00-\u9FAF]|[\u2605-\u2606]|[\u2190-\u2195]|\u203B' -JP_REGEX = re.compile(JP_REGEX_STR) - - -def containsJp(txt): - return JP_REGEX.search(txt) - - -def get_role(roles, role_string): - if role_string.lower() == "everyone": - role_string = "@everyone" - - role = discord.utils.find( - lambda r: r.name.lower() == role_string.lower(), roles) - - if role is None: - print("Could not find role named " + role_string) - raise commands.UserFeedbackCheckFailure("Could not find role named " + role_string) - - return role - - -def get_role_from_id(bot, guild, roleid): - try: - roles = guild.roles - except AttributeError: - guild = get_server_from_id(bot, guild) - try: - roles = guild.roles - except AttributeError: - raise ValueError("Role with id {} not found.".format(roleid)) - - role = discord.utils.get(roles, id=roleid) - if role is None: - raise commands.UserFeedbackCheckFailure("Could not find role id {} in guild {}".format(roleid, guild.name)) - return role - - -def get_server_from_id(bot, serverid): - return discord.utils.get(bot.get_guild, id=serverid) - - -def normalizeServer(server): - server = server.upper() - return 'NA' if server == 'US' else server - - -def should_download(file_path, expiry_secs): - if not os.path.exists(file_path): - print("file does not exist, downloading " + file_path) - return True - - ftime = os.path.getmtime(file_path) - file_age = time.time() - ftime - #print("for " + file_path + " got " + str(ftime) + ", age " + - # str(file_age) + " against expiry of " + str(expiry_secs)) - - if file_age > expiry_secs: - print("file {} too old, download it".format(file_path)) - return True - else: - return False - - -def writeJsonFile(file_path, js_data): - with open(file_path, "w") as f: - json.dump(js_data, f, indent=4) - - -def readJsonFile(file_path): - with open(file_path, "r") as f: - return json.load(f) - - -def safe_read_json(file_path): - try: - return readJsonFile(file_path) - except Exception as ex: - print('failed to read', file_path, 'got exception', ex) - return {} - - -def ensure_json_exists(file_dir, file_name): - if not os.path.exists(file_dir): - print("Creating dir: ", file_dir) - os.makedirs(file_dir) - file_path = os.path.join(file_dir, file_name) - try: - readJsonFile(file_path) - except: - print('File missing or invalid json:', file_path) - writeJsonFile(file_path, {}) - - -@backoff.on_exception(backoff.expo, aiohttp.ClientError, max_time=60) -@backoff.on_exception(backoff.expo, aiohttp.ServerDisconnectedError, max_time=60) -async def async_cached_dadguide_request(file_path, file_url, expiry_secs): - if should_download(file_path, expiry_secs): - async with aiohttp.ClientSession() as session: - async with session.get(file_url) as resp: - assert resp.status == 200 - with open(file_path, 'wb') as f: - f.write(await resp.read()) - - -def writePlainFile(file_path, text_data): - with open(file_path, "w", encoding='utf-8') as f: - f.write(text_data) - - -def readPlainFile(file_path): - with open(file_path, "r", encoding='utf-8') as f: - return f.read() - - -async def makeAsyncPlainRequest(file_url): - async with aiohttp.ClientSession() as session: - async with session.get(file_url) as resp: - return await resp.text() - - -async def makeAsyncCachedPlainRequest(file_path, file_url, expiry_secs): - if should_download(file_path, expiry_secs): - resp = await makeAsyncPlainRequest(file_url) - writePlainFile(file_path, resp) - return readPlainFile(file_path) - - -async def boxPagifySay(say_fn, msg): - for page in pagify(msg, delims=["\n"]): - await say_fn(box(page)) - - -def default_check(payload): - return payload.user_id != RPADCOG.bot.user.id and not (payload.guild_id and payload.member.bot) - - -class EmojiUpdater(object): - # a pass-through class that does nothing to the emoji dictionary - # or to the selected emoji - def __init__(self, emoji_to_embed, **kwargs): - self.emoji_dict = emoji_to_embed - self.selected_emoji = None - - def on_update(self, ctx, selected_emoji): - self.selected_emoji = selected_emoji - return True - - -class Menu(): - def __init__(self, bot): - self.bot = bot - - # Feel free to override this in your cog if you need to - self.emoji = { - 0: "0⃣", - 1: "1⃣", - 2: "2⃣", - 3: "3⃣", - 4: "4⃣", - 5: "5⃣", - 6: "6⃣", - 7: "7⃣", - 8: "8⃣", - 9: "9⃣", - 10: "🔟", - "next": "➡", - "back": "⬅", - "yes": "✅", - "no": "❌", - } - - # for use as an action - async def reaction_delete_message(self, bot, ctx, message): - await message.delete() - - async def custom_menu(self, ctx, emoji_to_message, selected_emoji, **kwargs): - """Creates and manages a new menu - Required arguments: - Type: - 1- number menu - 2- confirmation menu - 3- info menu (basically menu pagination) - 4- custom menu. If selected, choices must be a list of tuples. - Messages: - Strings or embeds to use for the menu. - Pass as a list for number menu - Optional arguments: - page (Defaults to 0): - The message in messages that will be displayed - timeout (Defaults to 15): - The number of seconds until the menu automatically expires - check (Defaults to default_check): - The same check that wait_for_reaction takes - is_open (Defaults to False): - Whether or not the menu can take input from any user - emoji (Defaults to self.emoji): - A dictionary containing emoji to use for the menu. - If you pass this, use the same naming scheme as self.emoji - message (Defaults to None): - The discord.Message to edit if present - """ - return await self._custom_menu(ctx, emoji_to_message, selected_emoji, **kwargs) - - async def show_menu(self, - ctx, - message, - new_message_content): - if message: - if isinstance(new_message_content, discord.Embed): - return await message.edit(embed=new_message_content) - else: - return await message.edit(content=new_message_content) - else: - if isinstance(new_message_content, discord.Embed): - return await ctx.send(embed=new_message_content) - else: - return await ctx.send(new_message_content) - - async def _custom_menu(self, ctx, emoji_to_message, selected_emoji, - allowed_action=True, **kwargs): - timeout = kwargs.get('timeout', 15) - message = kwargs.get('message', None) - - reactions_required = not message - new_message_content = emoji_to_message.emoji_dict[selected_emoji] - if allowed_action: - if not message: - message = await self.show_menu(ctx, message, new_message_content) - else: - await self.show_menu(ctx, message, new_message_content) - - if reactions_required: - for e in emoji_to_message.emoji_dict: - try: - await message.add_reaction(e) - except Exception as e: - # failed to add reaction, ignore - pass - - def check(payload): - return (kwargs.get('check', default_check)(payload) and - str(payload.emoji.name) in list(emoji_to_message.emoji_dict.keys()) and - payload.user_id == ctx.author.id and - payload.message_id == message.id) - - if not message: - raise ValueError(message, ctx) - return None, None - - try: - p = await self.bot.wait_for('raw_reaction_add', check=check, timeout=timeout) - except asyncio.TimeoutError: - p = None - - if p is None: - try: - await message.clear_reactions() - except Exception as e: - # This is expected when bot doesn't have manage messages - pass - return message, new_message_content - - react_emoji = p.emoji.name - react_action = emoji_to_message.emoji_dict[p.emoji.name] - - if inspect.iscoroutinefunction(react_action): - message = await react_action(self.bot, ctx, message) - elif inspect.isfunction(react_action): - message = react_action(ctx, message) - - # user function killed message, quit - if not message: - return None, None - - try: - await message.remove_reaction(react_emoji, p.member) - except: - # This is expected when bot doesn't have manage messages - pass - - # update the emoji mapping however we need to, or just pass through and do nothing - - allowed_action = emoji_to_message.on_update(ctx, react_emoji) - return await self._custom_menu( - ctx, emoji_to_message, emoji_to_message.selected_emoji, - timeout=timeout, - check=check, - message=message, - allowed_action=allowed_action) - - -def char_to_emoji(c): - c = c.lower() - if c >= '0' and c <= '9': - names = { - '0': '0⃣', - '1': '1⃣', - '2': '2⃣', - '3': '3⃣', - '4': '4⃣', - '5': '5⃣', - '6': '6⃣', - '7': '7⃣', - '8': '8⃣', - '9': '9⃣', - } - return names[c] - if c < 'a' or c > 'z': - return c - - base = ord('\N{REGIONAL INDICATOR SYMBOL LETTER A}') - adjustment = ord(c) - ord('a') - return chr(base + adjustment) - - -def fix_emojis_for_server(emoji_list, msg_text): - """Finds 'emoji-looking' substrings in msg_text and corrects them. - - If msg_text has something like '<:emoji_1_derp:13242342343>' and the server - contains an emoji named :emoji_2_derp: then it will be swapped out in - the message. - - This corrects an issue where a padglobal alias is created in one server - with an emoji, but it has a slightly different name in another server. - """ - # Find all emoji-looking things in the message - matches = re.findall(r'<:[0-9a-z_]+:\d{18}>', msg_text, re.IGNORECASE) - if not matches: - return msg_text - - # For each unique looking emoji thing - for m in set(matches): - # Create a regex for that emoji replacing the digit - m_re = re.sub(r'\d', r'&', m).rstrip("~") - for em in emoji_list: - # If the current emoji matches the regex, force a replacement - emoji_code = str(em) - if re.match(m_re, emoji_code, re.IGNORECASE): - msg_text = re.sub(m_re, emoji_code, msg_text, flags=re.IGNORECASE) - break - return msg_text - - -def replace_emoji_names_with_code(emoji_list, msg_text): - """Finds emoji-name substrings in msg_text and corrects them. - - If msg_text has something like ':emoji_1_derp:' and emoji_list contains - an emoji named 'emoji_1_derp' then the value will replaced with the full - emoji id. - - This allows a padglobal admin without nitro to create entries with emojis - from other servers. - """ - # First strip down actual emojis to just the names - msg_text = re.sub(r'<(:[0-9a-z_]+:)\d{18}>', r'\1', msg_text, flags=re.IGNORECASE) - - # Find all emoji-looking things in the message - matches = re.findall(r':[0-9a-z_]+:', msg_text, re.IGNORECASE) - if not matches: - return msg_text - - # For each unique looking emoji thing - finished_names = [] - for m in set(matches): - emoji_name = m.strip(':') - for e in emoji_list: - if e.name == emoji_name and e.name not in finished_names: - finished_names.append(e.name) - msg_text = msg_text.replace(m, str(e)) - return msg_text - - -def is_valid_image_url(url): - url = url.lower() - return url.startswith('http') and (url.endswith('.png') or url.endswith('.jpg')) - - -def extract_image_url(m): - if is_valid_image_url(m.content): - return m.content - if m.attachments and len(m.attachments) and is_valid_image_url(m.attachments[0].url): - return m.attachments[0].url - return None - - -def rmdiacritics(input): - ''' - Return the base character of char, by "removing" any - diacritics like accents or curls and strokes and the like. - ''' - output = '' - for c in input: - try: - desc = unicodedata.name(c) - cutoff = desc.find(' WITH ') - if cutoff != -1: - desc = desc[:cutoff] - output += unicodedata.lookup(desc) - except: - output += c - return output - - -def clean_global_mentions(content): - """Wipes out mentions to @everyone and @here.""" - return re.sub(r'(@)(\w)', '\\g<1>\u200b\\g<2>', content) - - -def intify(input): - if isinstance(input, dict): - return {intify(k): intify(v) for k, v in input.items()} - elif isinstance(input, (list, tuple)): - return [intify(x) for x in input] - elif isinstance(input, str) and input.isdigit(): - return int(input) - elif isinstance(input, str) and input.replace('.', '', 1).isdigit(): - return float(input) - else: - return input - - -class CogSettings(object): - SETTINGS_FILE_NAME = "legacy_settings.json" - - def __init__(self, cog_name, bot=None): - self.folder = str(data_manager.cog_data_path(raw_name=cog_name)) - self.file_path = os.path.join(self.folder, CogSettings.SETTINGS_FILE_NAME) - - self.bot = bot - - self.check_folder() - - self.default_settings = self.make_default_settings() - if not os.path.isfile(self.file_path): - print("CogSettings config for {} not found. Creating default...".format(self.file_path)) - self.bot_settings = self.default_settings - self.save_settings() - else: - current = intify(readJsonFile(self.file_path)) - updated = False - for key in self.default_settings.keys(): - if key not in current.keys(): - current[key] = self.default_settings[key] - updated = True - - self.bot_settings = current - if updated: - self.save_settings() - - def check_folder(self): - if not os.path.exists(self.folder): - print("Creating " + self.folder) - os.makedirs(self.folder) - - def save_settings(self): - writeJsonFile(self.file_path, self.bot_settings) - - def make_default_settings(self): - return {} - - -async def get_prefix(bot: Red, message: discord.Message, text: str = None) -> Optional[str]: - text = text or message.content or '' - for p in await get_prefixes(bot, message): - if text.startswith(p): - return p - return None - - -async def get_prefixes(bot: Red, message: discord.Message) -> List[str]: - prefixes = await bot.get_prefix(message) # This returns all server prefixes - if isinstance(prefixes, str): - prefixes = [prefixes] - - # In case some idiot sets a null prefix - if "" in prefixes: - prefixes.remove("") - - return prefixes - - -def strip_right_multiline(txt: str): - """Useful for prettytable output where there is a lot of right spaces,""" - return '\n'.join([x.strip() for x in txt.splitlines()]) - - -# This was overwritten by voltron. PDX opted to copy it +10,000 ids away -CROWS_1 = {x: x + 10000 for x in range(2601, 2635 + 1)} -# This isn't overwritten but PDX adjusted anyway -CROWS_2 = {x: x + 10000 for x in range(3460, 3481 + 1)} - -PDX_JP_ADJUSTMENTS = {} -PDX_JP_ADJUSTMENTS.update(CROWS_1) -PDX_JP_ADJUSTMENTS.update(CROWS_2) - - -def get_pdx_id(m): - pdx_id = m.monster_no_na - if int(m.monster_no) == m.monster_no_jp: - pdx_id = PDX_JP_ADJUSTMENTS.get(pdx_id, pdx_id) - return pdx_id - - -def get_pdx_id_dadguide(m): - pdx_id = m.monster_no_na - if int(m.monster_id) == m.monster_no_jp: - pdx_id = PDX_JP_ADJUSTMENTS.get(pdx_id, pdx_id) - return pdx_id - - -async def await_and_remove(bot, react_msg, listen_user, delete_msgs=None, emoji="❌", timeout=15): - try: - await react_msg.add_reaction(emoji) - except Exception as e: - # failed to add reaction, ignore - return - - def check(payload): - return str(payload.emoji.name) == emoji and \ - payload.user_id == listen_user.id and \ - payload.message_id == react_msg.id - - try: - p = await bot.wait_for('add_reaction', check=check, timeout=timeout) - except asyncio.TimeoutError: - # Expected after {timeout} seconds - p = None - - if p is None: - try: - await react_msg.remove_reaction(emoji, react_msg.guild.me) - except Exception as e: - # failed to remove reaction, ignore - return - else: - msgs = delete_msgs or [react_msg] - for m in msgs: - await m.delete_message() - - -loop_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) - - -async def run_in_loop(bot, task, *args): - event_loop = asyncio.get_event_loop() - running_task = event_loop.run_in_executor(loop_executor, task, *args) - return await running_task - - -def validate_json(fp): - try: - json.load(open(fp)) - return True - except: - return False - - -def timeout_after(seconds=10, error_message=os.strerror(errno.ETIME)): - def decorator(func): - def _handle_timeout(signum, frame): - raise TimeoutError(error_message) - - def wrapper(*args, **kwargs): - signal.signal(signal.SIGALRM, _handle_timeout) - signal.setitimer(signal.ITIMER_REAL, seconds) - try: - result = func(*args, **kwargs) - finally: - signal.alarm(0) - return result - - return wraps(func)(wrapper) - - return decorator - - -async def confirm_message(ctx, text, yemoji = "✅", nemoji = "❌", timeout = 10): - msg = await ctx.send(text) - await msg.add_reaction(yemoji) - await msg.add_reaction(nemoji) - def check(reaction, user): - return (str(reaction.emoji) in [yemoji, nemoji] - and user.id == ctx.author.id - and reaction.message.id == msg.id) - - ret = False - try: - r, u = await ctx.bot.wait_for('reaction_add', check=check, timeout=timeout) - if r.emoji == yemoji: - ret = True - except asyncio.TimeoutError: - pass - - await msg.delete() - return ret - -class CtxIO(io.IOBase): - def __init__(self, ctx): - self.ctx = ctx - super(CtxIO, self).__init__() - - def read(self): - raise io.UnsupportedOperation("read") - - def write(self, data): - asyncio.ensure_future(self.ctx.send(data)) - -def corowrap(coro, loop): - def func(*args, **kwargs): - fut = asyncio.run_coroutine_threadsafe(coro, loop) - try: - fut.result() - except: - pass - return func - -def fawait(coro, loop): - fut = asyncio.run_coroutine_threadsafe(coro, loop) - try: - fut.result() - except: - pass - -async def doubleup(ctx, message): - lmessage = await ctx.history().__anext__() - fullmatch = re.escape(message) + r"(?: x(\d+))?" - match = re.match(fullmatch, lmessage.content) - if match and lmessage.author == ctx.bot.user: - n = match.group(1) or "1" - await lmessage.edit(content=message+" x"+str(int(n)+1)) - else: - await ctx.send(message) - - -async def repeating_timer(seconds, condition=lambda:True, start_immediately=True): - if start_immediately: - yield - while True and condition(): - await asyncio.sleep(seconds) - yield - -def deepget(mapping, keys, default): - o = mapping - for key in keys: - try: - o = o[key] - except KeyError: - return default - return o - -class aobject(object): - """Inheriting this class allows you to define an async __init__.""" - async def __new__(cls, *args, **kwargs): - self = super().__new__(cls) - await self.__init__(*args, **kwargs) - return self - - async def __init__(self): - pass diff --git a/selfroleoverride/__init__.py b/selfroleoverride/__init__.py deleted file mode 100644 index dd86057e..00000000 --- a/selfroleoverride/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .selfroleoverride import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - pdb = SelfRoleOverride(bot) - bot.add_cog(pdb) diff --git a/selfroleoverride/info.json b/selfroleoverride/info.json deleted file mode 100644 index 27f6a3b8..00000000 --- a/selfroleoverride/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["Aradia Megido#2552"], - "description": "Makes [p]selfrole case insensitive.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Case-insensitive Selfrole", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"admin": "https://github.com/Cog-Creators/Red-DiscordBot"}, - "requirements": [], - "tags": ["patch"] -} diff --git a/selfroleoverride/selfroleoverride.py b/selfroleoverride/selfroleoverride.py deleted file mode 100644 index eb89cd78..00000000 --- a/selfroleoverride/selfroleoverride.py +++ /dev/null @@ -1,124 +0,0 @@ -import json -import re -import discord.utils -import asyncio - -from redbot.core import checks, Config -from redbot.core import commands -from redbot.core.utils.chat_formatting import box, pagify - -class SelfRoleConverterOverride(commands.Converter): - async def convert(self, ctx: commands.Context, arg: str) -> discord.Role: - admin = ctx.command.cog.bot.get_cog("Admin") - if admin is None: - raise commands.BadArgument("The Admin cog is not loaded.") - - selfroles = await admin.config.guild(ctx.guild).selfroles() - role_converter = commands.RoleConverter() - - pool = [] - for rid in selfroles: - role = ctx.guild.get_role(rid) - if role is None: - continue - if role.name.lower() == arg.lower(): - pool.append(role) - if len(pool) == 0: - role = await role_converter.convert(ctx, arg) - - else: - if len(pool) > 1: - await ctx.send("This selfrole has more than one capitalization" - "possibilities. Please inform a moderator.") - role = pool[0] - - - - if role.id not in selfroles: - raise commands.BadArgument("The provided role is not a valid selfrole.") - return role - -class SelfRoleOverride(commands.Cog): - """Overrides of builtin commands""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - self.old_cmds = [] - for cmd in self.__cog_commands__: - old_cmd = bot.get_command(cmd.name) - if old_cmd: - bot.remove_command(old_cmd.name) - self.old_cmds.append(old_cmd) - - def cog_unload(self): - for cmd in self.old_cmds: - try: - self.bot.remove_command(cmd.name) - except: - pass - self.bot.add_command(cmd) - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.guild_only() - @commands.group() - async def selfrole(self, ctx: commands.Context): - """Apply selfroles.""" - pass - - @selfrole.command(name="add") - async def selfrole_add(self, ctx: commands.Context, *, selfrole: SelfRoleConverterOverride): - """ - Add a selfrole to yourself. - - Server admins must have configured the role as user settable. - """ - self = self.bot.get_cog("Admin") - if self is None: - await ctx.send("Admin cog not loaded.") - - await self._addrole(ctx, ctx.author, selfrole, check_user=False) - - @selfrole.command(name="remove") - async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRoleConverterOverride): - """ - Remove a selfrole from yourself. - - Server admins must have configured the role as user settable. - """ - self = self.bot.get_cog("Admin") - if self is None: - await ctx.send("Admin cog not loaded.") - - await self._removerole(ctx, ctx.author, selfrole, check_user=False) - - @selfrole.command(name="list") - async def selfrole_list(self, ctx: commands.Context): - """ - Lists all available selfroles. - """ - self = self.bot.get_cog("Admin") - if self is None: - await ctx.send("Admin cog not loaded.") - - selfroles = await self._valid_selfroles(ctx.guild) - fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles]) - - if not fmt_selfroles: - await ctx.send("There are currently no selfroles.") - return - - msg = "Available Selfroles:\n{selfroles}".format(selfroles=fmt_selfroles) - await ctx.send(box(msg, "diff")) diff --git a/seniority/__init__.py b/seniority/__init__.py deleted file mode 100644 index f69dc5f8..00000000 --- a/seniority/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .seniority import * - -__red_end_user_data_statement__ = "Activity data is stored." - -def setup(bot): - n = Seniority(bot) - bot.add_cog(n) - bot.loop.create_task(n.init()) diff --git a/seniority/info.json b/seniority/info.json deleted file mode 100644 index 3c25a3ed..00000000 --- a/seniority/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Assigns a role to active members.", - "end_user_data_statement": "Activity data is stored.", - "install_msg": "", - "short": "Activity Tracker", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["aioodbc", "prettytable", "pytz"], - "tags": ["moderation", "administration"] -} diff --git a/seniority/seniority.py b/seniority/seniority.py deleted file mode 100644 index 91ef4ae9..00000000 --- a/seniority/seniority.py +++ /dev/null @@ -1,1000 +0,0 @@ -import os -import re -import sys -import timeit -from collections import deque -from datetime import datetime, timedelta - -import aioodbc -import discord -import prettytable -import pytz -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.commands import Context -from redbot.core.utils.chat_formatting import inline, pagify, box - -import rpadutils -from rpadutils import CogSettings, NA_TZ_OBJ - -CREATE_TABLE = ''' -CREATE TABLE IF NOT EXISTS seniority( - record_date STRING NOT NULL, - server_id STRING NOT NULL, - channel_id STRING NOT NULL, - user_id STRING NOT NULL, - points REAL DEFAULT 0, - PRIMARY KEY (record_date, server_id, channel_id, user_id)) -''' - -CREATE_INDEX_1 = ''' -CREATE INDEX IF NOT EXISTS idx_server_id_record_date_user_id -ON seniority(server_id, record_date, user_id) -''' - -# Why? -CREATE_INDEX_2 = ''' -CREATE INDEX IF NOT EXISTS idx_record_date_server_id_user_id -ON seniority(record_date, server_id, user_id) -''' - -# Why? record date should never come first -CREATE_INDEX_3 = ''' -CREATE INDEX IF NOT EXISTS idx_record_date_server_id_channel_id_user_id -ON seniority(record_date, server_id, channel_id, user_id) -''' - -CREATE_INDEX_4 = ''' -CREATE INDEX IF NOT EXISTS idx_server_id_user_id_record_date -ON seniority(server_id, user_id, record_date) -''' - -GET_USER_POINTS_QUERY = ''' -SELECT record_date, round(sum(points), 2) as points -FROM seniority INDEXED BY idx_server_id_user_id_record_date -WHERE server_id = ? - AND user_id = ? -GROUP BY 1 -ORDER BY 1 DESC -LIMIT ? -''' - -GET_LOOKBACK_POINTS_QUERY = ''' -SELECT user_id, sum(points) as points -FROM seniority INDEXED BY idx_server_id_user_id_record_date -WHERE server_id = ? - AND record_date >= ? -GROUP BY 1 -''' - -GET_DATE_POINTS_QUERY = ''' -SELECT channel_id, points -FROM seniority INDEXED BY idx_server_id_record_date_user_id -WHERE record_date = ? - AND server_id = ? - AND user_id = ? -''' - -GET_NEWMESSAGE_POINTS_QUERY = ''' -SELECT SUM(points) as points -FROM seniority INDEXED BY idx_record_date_server_id_channel_id_user_id -WHERE record_date = ? - AND server_id = ? - AND channel_id = ? - AND user_id = ? -''' - -GET_NEWMESSAGE_SERVER_POINTS_QUERY = ''' -SELECT SUM(points) as points -FROM seniority INDEXED BY idx_record_date_server_id_user_id -WHERE record_date = ? - AND server_id = ? - AND user_id = ? -''' - -REPLACE_POINTS_QUERY = ''' -REPLACE INTO seniority(record_date, server_id, channel_id, user_id, points) -VALUES(?, ?, ?, ?, ?) -''' - -DELETE_DAY_QUERY = ''' -DELETE FROM seniority -WHERE record_date = ? - AND server_id = ? -''' - -DELETE_USER_DATA = ''' -DELETE FROM seniority -WHERE user_id = ? -''' - -GET_USER_DATA = ''' -SELECT * FROM seniority -WHERE user_id = ? -''' - -class Seniority(commands.Cog): - """Automatically promote people based on activity.""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = SenioritySettings("seniority") - self.db_path = self.settings.folder + '/log.db' - self.lock = True - self.pool = None - self.insert_timing = deque(maxlen=1000) - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(GET_USER_DATA, user_id) - rows = await cur.fetchall() - guilds = len({r[1] for r in rows}) - data = "You have activity data stored in {} guilds.\n".format(guilds) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data.""" - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(REPLACE_POINTS_QUERY, user_id) - - def cog_unload(self): - print('Seniority: unloading') - self.lock = True - if self.pool: - self.pool.close() - self.bot.loop.create_task(self.pool.wait_closed()) - self.pool = None - else: - print('unexpected error: pool was None') - print('Seniority: unloading complete') - - async def init(self): - print('Seniority: init') - if not self.lock: - print('Seniority: bailing on unlock') - return - - if os.name != 'nt' and sys.platform != 'win32': - dsn = 'Driver=SQLite3;Database=' + self.db_path - else: - dsn = 'Driver=SQLite3 ODBC Driver;Database=' + self.db_path - self.pool = await aioodbc.create_pool(dsn=dsn, autocommit=True) - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(CREATE_TABLE) - await cur.execute(CREATE_INDEX_1) - await cur.execute(CREATE_INDEX_2) - await cur.execute(CREATE_INDEX_3) - await cur.execute(CREATE_INDEX_4) - self.lock = False - - print('Seniority: init complete') - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def seniority(self, ctx): - """Automatically promote people based on activity.""" - - @seniority.command() - @checks.is_owner() - async def rawquery(self, ctx, *, query: str): - await self.queryAndPrint(ctx, ctx.guild, query, []) - - @seniority.command() - @checks.is_owner() - async def inserttiming(self, ctx): - size = len(self.insert_timing) - avg_time = round(sum(self.insert_timing) / size, 4) - max_time = round(max(self.insert_timing), 4) - min_time = round(min(self.insert_timing), 4) - await ctx.send(inline('{} inserts, min={} max={} avg={}'.format(size, min_time, max_time, avg_time))) - - @seniority.command() - @checks.is_owner() - async def togglelock(self, ctx): - self.lock = not self.lock - await ctx.send(inline('Locked is now {}'.format(self.lock))) - - @seniority.command() - @commands.guild_only() - async def printconfig(self, ctx): - """Print the configuration for the server.""" - server = ctx.guild - server_id = server.id - msg = 'Config:' - announce_channel = self.get_announce_channel(server_id) - msg += '\n\tannounce_channel: {}'.format( - announce_channel.name if announce_channel else '') - msg += '\n\tauto_grant: {}'.format(self.settings.auto_grant(server_id)) - msg += '\n\tmessage_cap: {}'.format(self.settings.message_cap(server_id)) - msg += '\n\tserver_point_cap: {}'.format(self.settings.server_point_cap(server_id)) - msg += '\n\tgrant_lookback: {}'.format(self.settings.grant_lookback(server_id)) - msg += '\n\tremove_lookback: {}'.format(self.settings.remove_lookback(server_id)) - msg += '\n\n' - msg += 'Acceptability:' - msg += '\n\tignore_commands: {}'.format(self.settings.ignore_commands(server_id)) - msg += '\n\tignore_emoji: {}'.format(self.settings.ignore_emoji(server_id)) - msg += '\n\tignore_mentions: {}'.format(self.settings.ignore_mentions(server_id)) - msg += '\n\tignore_room_codes: {}'.format(self.settings.ignore_room_codes(server_id)) - msg += '\n\tmin_length: {}'.format(self.settings.min_length(server_id)) - msg += '\n\tmin_words: {}'.format(self.settings.min_words(server_id)) - msg += '\n\n' - msg += 'Ignored Users:' - for user_id in self.settings.blacklist(server_id): - member = server.get_member(int(user_id)) - msg += '\n\t{} ({})'.format(member.name if member else 'unknown', user_id) - msg += '\n\n' - msg += 'Channels and max ppd:' - for channel_id, config in self.settings.channels(server_id).items(): - channel = self.bot.get_channel(int(channel_id)) - msg += '\n\t{} : {}'.format(channel.name if channel else channel_id, config['max_ppd']) - msg += '\n\n' - msg += 'Roles:' - for role_id, config in self.settings.roles(server_id).items(): - try: - role = rpadutils.get_role_from_id(self.bot, server, role_id) - role_name = role.name - except: - role_name = role_id - msg += '\n\t{}'.format(role_name) - msg += '\n\t\tremove_amount : {}'.format(config['remove_amount']) - msg += '\n\t\twarn_amount : {}'.format(config['warn_amount']) - msg += '\n\t\tgrant_amount : {}'.format(config['grant_amount']) - - for page in pagify(msg): - await ctx.send(box(page)) - - def get_announce_channel(self, server_id): - announce_channel_id = self.settings.announce_channel(server_id) - return self.bot.get_channel(announce_channel_id) - - @seniority.group() - @commands.guild_only() - async def grant(self, ctx): - """Seniority grant and remove actions.""" - - @grant.command() - @commands.guild_only() - async def listbelow(self, ctx): - """List users below the remove amount.""" - lookback_days = self.settings.remove_lookback(ctx.guild.id) - await self.do_print_overages(ctx, ctx.guild, lookback_days, 'remove_amount', False) - - @grant.command() - @commands.guild_only() - async def listnear(self, ctx): - """List users above the warn amount.""" - lookback_days = self.settings.grant_lookback(ctx.guild.id) - await self.do_print_overages(ctx, ctx.guild, lookback_days, 'warn_amount', True) - - @grant.command() - @commands.guild_only() - async def listover(self, ctx): - """List users above the grant amount.""" - lookback_days = self.settings.grant_lookback(ctx.guild.id) - await self.do_print_overages(ctx, ctx.guild, lookback_days, 'grant_amount', True) - - @grant.command() - @commands.guild_only() - async def grantnow(self, ctx): - """List users above the grant amount.""" - guild = ctx.guild - lookback_days = self.settings.grant_lookback(guild.id) - for role_id, role, amount in self.roles_and_amounts(guild, 'grant_amount'): - if role is None or amount <= 0: - continue - - msg = 'Granting for role {} (point cutoff {})'.format(role.name, amount) - await ctx.send(inline(msg)) - - grant_users, ignored_users = await self.get_grant_ignore_users( - guild, role, amount, lookback_days, True) - grant_users = [guild.get_member(int(x[0])) for x in grant_users] - - cs = 5 - user_chunks = [grant_users[i:i + cs] for i in range(0, len(grant_users), cs)] - for chunk in user_chunks: - msg = 'Granting to users: ' + ','.join([m.name for m in chunk]) - await ctx.send(inline(msg)) - for member in chunk: - try: - await member.add_roles(role) - except Exception as ex: - raise commands.UserFeedbackCheckFailure(str(ex)) - - @grant.command() - @commands.guild_only() - async def removenow(self, ctx): - """List users below the remove amount.""" - server = ctx.guild - lookback_days = self.settings.remove_lookback(server.id) - for role_id, role, amount in self.roles_and_amounts(server, 'remove_amount'): - if role is None or amount <= 0: - continue - - msg = 'Removing for role {} (point cutoff {})'.format(role.name, amount) - await ctx.send(inline(msg)) - - grant_users, ignored_users = await self.get_grant_ignore_users( - server, role, amount, lookback_days, False) - grant_users = [server.get_member(int(x[0])) for x in grant_users] - - cs = 5 - user_chunks = [grant_users[i:i + cs] for i in range(0, len(grant_users), cs)] - for chunk in user_chunks: - msg = 'Removing from users: ' + ','.join([m.name for m in chunk]) - await ctx.send(inline(msg)) - for member in chunk: - try: - await member.remove_roles(role) - except Exception as ex: - raise commands.UserFeedbackCheckFailure(str(ex)) - - async def do_print_overages(self, - ctx: Context, - server: discord.Guild, - lookback_days: int, - check_name: str, - points_greater_than: bool): - await ctx.send(inline('Printing info for all roles')) - - for role_id, role, amount in self.roles_and_amounts(server, check_name): - if role is None: - await ctx.send(inline('Cannot find role with id ' + role_id)) - continue - - if amount <= 0: - await ctx.send(inline('Skipping role {} (disabled)'.format(role.name))) - continue - - grant_users, ignored_users = await self.get_grant_ignore_users( - server, role, amount, lookback_days, points_greater_than) - - def process_userlist(user_list): - r = '' - for userid_points in user_list: - user_id = userid_points[0] - member = server.get_member(int(user_id)) - member_name = member.name if member else user_id - points = round(userid_points[1], 2) - r += '\n\t{} ({}) : {}'.format(member_name, user_id, points) - return r - - msg = 'Modified users for role {} (point cutoff {})'.format(role.name, amount) - msg += process_userlist(grant_users) - msg += '\n\nIgnored users' - msg += process_userlist(ignored_users) - - for page in pagify(msg): - await ctx.send(box(page)) - - def roles_and_amounts(self, server: discord.Guild, check_name: str): - for role_id, role_config in self.settings.roles(server.id).items(): - amount = role_config[check_name] - try: - role = rpadutils.get_role_from_id(self.bot, server, role_id) - except: - role = None - yield role_id, role, amount - - async def get_grant_ignore_users(self, - server: discord.Guild, - role: discord.Role, - amount: int, - lookback_days: int, - points_greater_than: bool): - - if points_greater_than: - def point_check_fn(p): - return p >= amount - else: - def point_check_fn(p): - return p < amount - - users_and_points = await self.get_lookback_points(server, lookback_days) - grant_users, ignored_users = self.check_users_for_role( - users_and_points, server, point_check_fn, role, points_greater_than) - - return grant_users, ignored_users - - async def get_lookback_points(self, server: discord.Guild, lookback_days: int): - lookback_date = datetime.now(rpadutils.NA_TZ_OBJ) - timedelta(days=lookback_days) - lookback_date_str = lookback_date.date().isoformat() - - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(GET_LOOKBACK_POINTS_QUERY, server.id, lookback_date_str) - rows = await cur.fetchall() - return [(int(x[0]), x[1]) for x in rows] - - def check_users_for_role(self, - users_and_points, - server: discord.Guild, - point_check_fn, - role: discord.Role, - adding_role: bool): - blacklisted_ids = self.settings.blacklist(server.id).keys() - - grant_users = [] - ignored_users = [] - - userid_to_points = {x[0]: x[1] for x in users_and_points} - for user in server.members: - points = userid_to_points.get(user.id, 0) - if role in user.roles and adding_role: - continue - if role not in user.roles and not adding_role: - continue - if not point_check_fn(points): - continue - - if user.id in blacklisted_ids: - ignored_users.append((user.id, points)) - else: - grant_users.append((user.id, points)) - - return grant_users, ignored_users - - @seniority.command() - @commands.guild_only() - async def userhistory(self, ctx, user: discord.User, limit=30): - """Print the points per day for a user.""" - limit = min(limit, 90) - server = ctx.guild - args = [server.id, user.id, limit] - await self.queryAndPrint(ctx, server, GET_USER_POINTS_QUERY, args, reverse=True, total=True) - - @seniority.command() - @commands.guild_only() - async def usercurrent(self, ctx, user: discord.User): - """Print the current day's points for a user.""" - server = ctx.guild - args = [now_date(), server.id, user.id] - await self.queryAndPrint(ctx, server, GET_DATE_POINTS_QUERY, args) - - @seniority.command() - @commands.guild_only() - async def blacklist(self, ctx, user: discord.User, reason: str): - """Ensure a user never gets a role auto granted.""" - server = ctx.guild - by = ctx.author - self.settings.add_blacklist(server.id, user.id, by.id, reason) - await ctx.send(inline('Set blacklist')) - - @seniority.command() - @commands.guild_only() - async def unblacklist(self, ctx, user: discord.User): - """Remove the blacklist for a user.""" - server = ctx.guild - if self.settings.remove_blacklist(server.id, user.id): - await ctx.send(inline('Removed blacklist')) - else: - await ctx.send(inline('That user was not blacklisted')) - - @seniority.command() - @commands.guild_only() - async def checktext(self, ctx, text: str): - """Check if text is considered significant by the current config. - """ - is_good, cleaned_text, reason = await self.check_acceptable(ctx.message, text) - if is_good: - await ctx.send(box('Message accepted, cleaned text:\n{}'.format(cleaned_text))) - else: - await ctx.send(box('Message rejected ({}), cleaned text:\n{}'.format(reason, cleaned_text))) - - @seniority.group() - @commands.guild_only() - async def config(self, ctx): - """Toggle Seniority configuration settings.""" - pass - - @config.command() - @commands.guild_only() - async def announcechannel(self, ctx, channel: discord.TextChannel): - """Set the announcement channel.""" - self.settings.set_announce_channel(ctx.guild.id, channel.id) - await ctx.send(inline('Done.')) - - @config.command() - @commands.guild_only() - async def messagecap(self, ctx, cap_amount: int): - """Set the number of messages required to reach maximum points for a channel.""" - self.settings.set_message_cap(ctx.guild.id, cap_amount) - await ctx.send(inline('Done.')) - - @config.command() - @commands.guild_only() - async def serverpointcap(self, ctx, cap_amount: int): - """Set the maximum number of points per day a user can receive in the server.""" - self.settings.set_server_point_cap(ctx.guild.id, cap_amount) - await ctx.send(inline('Done.')) - - @config.command() - @commands.guild_only() - async def toggleautogrant(self, ctx): - """Enable or disable the automatic granting of roles.""" - server_id = ctx.guild.id - new_setting = not self.settings.auto_grant(server_id) - self.settings.set_auto_grant(server_id, new_setting) - await ctx.send(inline('Auto grant set to {}.'.format(new_setting))) - - @config.command() - @commands.guild_only() - async def grantlookback(self, ctx, days: int): - """Number of days to look back when computing points for granting a role.""" - server_id = ctx.guild.id - self.settings.set_grant_lookback(server_id, days) - await ctx.send(inline('Grant lookback set to {}.'.format(days))) - - @config.command() - @commands.guild_only() - async def removelookback(self, ctx, days: int): - """Number of days to look back when computing points for removing a role.""" - server_id = ctx.guild.id - self.settings.set_remove_lookback(server_id, days) - await ctx.send(inline('Remove lookback set to {}.'.format(days))) - - @config.command() - @commands.guild_only() - async def channel(self, ctx, channel: discord.TextChannel, max_points_per_day: int): - """Maximum points per day a user can earn in a channel (0 disables).""" - server_id = ctx.guild.id - self.settings.set_channel(server_id, channel.id, max_points_per_day) - if max_points_per_day: - await ctx.send(inline('Max points set to {}.'.format(max_points_per_day))) - else: - await ctx.send(inline('Channel disabled')) - - @config.command() - @commands.guild_only() - async def role(self, ctx, role_name: str, remove_amount: int, warn_amount: int, grant_amount: int): - """Set the configuration for a role. - - role_name: Role name to configure - remove_amount: Automatic role removal threshold. Set to 0 to disable. - warn_amount: Print a warning in the announcement channel when exceeded. Set to 0 to disable. - grant_amount: Automatic role grant threshold. Set to 0 to disable. - - remove_amount must be less than grant_amount. - Set all three to 0 to delete the entry. - """ - server = ctx.guild - role = rpadutils.get_role(server.roles, role_name) - self.settings.set_role(server.id, role.id, remove_amount, warn_amount, grant_amount) - - if remove_amount == 0 and grant_amount == 0: - await ctx.send(inline('Role configuration deleted')) - return - - msg = 'Done.' - if remove_amount <= 0: - msg += '\nThis role will not be automatically removed.' - else: - msg += '\nThis role will be removed when points in remove window drop below {}.'.format( - remove_amount) - if grant_amount <= 0: - msg += '\nThis role will not be automatically granted.' - else: - if warn_amount <= 0: - msg += '\nNo early warning will be given for this role.' - else: - msg += '\nI will warn when a user exceeds {} points in the grant window.'.format( - warn_amount) - msg += '\nThis role will be granted when points in grant window exceed {}.'.format( - grant_amount) - - await ctx.send(box(msg)) - - @seniority.group() - @commands.guild_only() - async def acceptable(self, ctx): - """Toggle Seniority text acceptability settings.""" - - @acceptable.command() - @commands.guild_only() - async def togglecommands(self, ctx): - """Toggle rejection of bot commands.""" - server_id = ctx.guild.id - new_setting = not self.settings.ignore_commands(server_id) - self.settings.set_ignore_commands(server_id, new_setting) - await ctx.send(inline('ignore_commands set to {}.'.format(new_setting))) - - @acceptable.command() - @commands.guild_only() - async def toggleemoji(self, ctx): - """Toggle deletion of emoji from text.""" - server_id = ctx.guild.id - new_setting = not self.settings.ignore_emoji(server_id) - self.settings.set_ignore_emoji(server_id, new_setting) - await ctx.send(inline('ignore_emoji set to {}.'.format(new_setting))) - - @acceptable.command() - @commands.guild_only() - async def togglementions(self, ctx): - """Toggle deletion of mentions from text.""" - server_id = ctx.guild.id - new_setting = not self.settings.ignore_mentions(server_id) - self.settings.set_ignore_mentions(server_id, new_setting) - await ctx.send(inline('ignore_mentions set to {}.'.format(new_setting))) - - @acceptable.command() - @commands.guild_only() - async def toggleroomcodes(self, ctx): - """Toggle rejection of text containing room codes.""" - server_id = ctx.guild.id - new_setting = not self.settings.ignore_room_codes(server_id) - self.settings.set_ignore_room_codes(server_id, new_setting) - await ctx.send(inline('ignore_room_codes set to {}.'.format(new_setting))) - - @acceptable.command() - @commands.guild_only() - async def minlength(self, ctx, length: int): - """Set the minimum length of text.""" - server_id = ctx.guild.id - self.settings.set_min_length(server_id, length) - await ctx.send(inline('Min text length set to {}.'.format(length))) - - @acceptable.command() - @commands.guild_only() - async def minwords(self, ctx, words: int): - """Set the minimum number of words in text.""" - server_id = ctx.guild.id - self.settings.set_min_words(server_id, words) - await ctx.send(inline('Min word count set to {}.'.format(words))) - - async def check_acceptable(self, message: discord.Message, text: str): - server = message.guild - server_id = server.id - if self.settings.ignore_commands(server_id): - if await rpadutils.get_prefix(self.bot, message): - return False, text, 'Ignored command' - - if self.settings.ignore_room_codes(server_id): - if re.match(r'.*\d{4}\s?\d{4}.*', text): - return False, text, 'Ignored room code' - - if self.settings.ignore_emoji(server_id): - text = re.sub(r'<:[0-9a-z_]+:\d{18}>', '', text, re.IGNORECASE) - - if self.settings.ignore_mentions(server_id): - text = re.sub(r'<@\d{18}>', '', text, re.IGNORECASE) - - if len(text) < self.settings.min_length(server_id): - return False, text, 'Min length' - - if len(text.split()) < self.settings.min_words(server_id): - return False, text, 'Min words' - - return True, text, 'Passed!' - - @commands.Cog.listener("on_message") - async def on_message(self, message: discord.Message): - if message.guild is None: - return - now_date_str = now_date() - await self.process_message(message, now_date_str) - - async def process_message(self, message: discord.Message, now_date_str: str): - if self.lock: - return - - guild = message.guild - channel = message.channel - user = message.author - - if guild is None or message.author.id == self.bot.user.id: - return - - channel_config = self.settings.channels(guild.id).get(channel.id, None) - if not channel_config: - return - - acceptable, _, _ = await self.check_acceptable(message, message.content) - if not acceptable: - return - - max_points = channel_config['max_ppd'] - current_points = await self.get_current_channel_points(now_date_str, guild, channel, user) - current_points = current_points or 0 - - if current_points >= max_points: - return - - server_point_cap = self.settings.server_point_cap(guild.id) - current_server_points = await self.get_current_server_points(now_date_str, guild, user) - current_server_points = current_server_points or 0 - - if current_server_points >= server_point_cap: - return - - message_cap = self.settings.message_cap(guild.id) - incremental_points = max_points / message_cap - new_points = current_points + incremental_points - new_points = min(new_points, max_points) - - before_time = timeit.default_timer() - await self.save_current_points(now_date_str, guild, channel, user, new_points) - execution_time = timeit.default_timer() - before_time - self.insert_timing.append(execution_time) - - return incremental_points - - async def get_current_channel_points(self, now_date_str: str, server: discord.Guild, channel: discord.TextChannel, - user: discord.User): - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(GET_NEWMESSAGE_POINTS_QUERY, now_date_str, server.id, channel.id, user.id) - results = await cur.fetchone() - return results.points if results else 0 - - async def get_current_server_points(self, now_date_str: str, server: discord.Guild, user: discord.User): - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(GET_NEWMESSAGE_SERVER_POINTS_QUERY, now_date_str, server.id, user.id) - results = await cur.fetchone() - return results.points if results else 0 - - async def save_current_points(self, now_date_str: str, server: discord.Guild, channel: discord.TextChannel, - user: discord.User, new_points: int): - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(REPLACE_POINTS_QUERY, now_date_str, server.id, channel.id, user.id, new_points) - - async def queryAndPrint(self, ctx, server, query, values, max_rows=100, reverse=False, total=False): - before_time = timeit.default_timer() - async with self.pool.acquire() as conn: - async with conn.cursor() as cur: - await cur.execute(query, *values) - rows = await cur.fetchall() - columns = [x[0] for x in cur.description] - execution_time = timeit.default_timer() - before_time - - if reverse: - rows.reverse() - - tbl = prettytable.PrettyTable(columns) - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.NONE - tbl.align = 'l' - - grand_total = 0 - - for idx, row in enumerate(rows): - if idx > max_rows: - break - - table_row = list() - for cidx, col in enumerate(columns): - raw_value = row[cidx] - value = str(raw_value) - if col == 'timestamp': - # Assign a UTC timezone to the datetime - raw_value = raw_value.replace(tzinfo=pytz.utc) - # Change the UTC timezone to PT - raw_value = NA_TZ_OBJ.normalize(raw_value) - value = raw_value.strftime("%F %X") - elif col == 'channel_id': - channel = server.get_channel(int(value)) if server else None - value = channel.name if channel else value - elif col == 'user_id': - member = server.get_member(int(value)) if server else None - value = member.name if member else value - elif col == 'server_id': - server_obj = self.bot.get_guild(int(value)) - value = server_obj.name if server_obj else value - elif cidx + 1 == len(columns): - grand_total += force_number(value) - table_row.append(value) - - tbl.add_row(table_row) - - result_text = "{} results fetched in {}s\n{}".format( - len(rows), round(execution_time, 2), tbl.get_string()) - - if total: - result_text += '\n\nTotal: {}'.format(grand_total) - - for p in pagify(result_text): - await ctx.send(box(p)) - - -def ensure_map(item, key, default_value): - if key not in item: - item[key] = default_value - return item[key] - - -def now_date(): - return datetime.now(rpadutils.NA_TZ_OBJ).date().isoformat() - - -class SenioritySettings(CogSettings): - def make_default_settings(self): - config = { - 'servers': {} - } - return config - - def servers(self): - return self.bot_settings['servers'] - - def server(self, server_id: str): - servers = self.servers() - return ensure_map(servers, server_id, {}) - - def config(self, server_id: str): - server = self.server(server_id) - config = ensure_map(server, 'config', {}) - ensure_map(config, 'announce_channel', '') - ensure_map(config, 'auto_grant', False) - ensure_map(config, 'message_cap', 20) - ensure_map(config, 'server_point_cap', 5) - ensure_map(config, 'grant_lookback', 90) - ensure_map(config, 'remove_lookback', 90) - return config - - def announce_channel(self, server_id: str): - return self.config(server_id)['announce_channel'] - - def auto_grant(self, server_id: str): - return self.config(server_id)['auto_grant'] - - def message_cap(self, server_id: str): - return self.config(server_id)['message_cap'] - - def server_point_cap(self, server_id: str): - return self.config(server_id)['server_point_cap'] - - def grant_lookback(self, server_id: str): - return self.config(server_id)['grant_lookback'] - - def remove_lookback(self, server_id: str): - return self.config(server_id)['remove_lookback'] - - def set_announce_channel(self, server_id: str, channel_id): - self.config(server_id)['announce_channel'] = channel_id - self.save_settings() - - def set_auto_grant(self, server_id: str, auto_grant: bool): - self.config(server_id)['auto_grant'] = auto_grant - self.save_settings() - - def set_message_cap(self, server_id, message_cap: int): - self.config(server_id)['message_cap'] = message_cap - self.save_settings() - - def set_server_point_cap(self, server_id, server_point_cap: int): - self.config(server_id)['server_point_cap'] = server_point_cap - self.save_settings() - - def set_grant_lookback(self, server_id: str, lookback: int): - self.config(server_id)['grant_lookback'] = lookback - self.save_settings() - - def set_remove_lookback(self, server_id: str, lookback: int): - self.config(server_id)['remove_lookback'] = lookback - self.save_settings() - - def roles(self, server_id): - server = self.server(server_id) - return ensure_map(server, 'roles', {}) - - def set_role(self, server_id: str, role_id: str, remove_amount: int, warn_amount: int, grant_amount: int): - roles = self.roles(server_id) - if remove_amount == 0 and grant_amount == 0: - roles.pop(role_id, None) - elif remove_amount >= grant_amount: - raise commands.UserFeedbackCheckFailure('remove_amount must be less than grant_amount') - elif warn_amount >= grant_amount: - raise commands.UserFeedbackCheckFailure('warn_amount must be less than grant_amount') - elif remove_amount < 0 or warn_amount < 0 or grant_amount < 0: - raise commands.UserFeedbackCheckFailure('role values must be >= 0') - else: - roles[role_id] = { - 'role_id': role_id, - 'remove_amount': remove_amount, - 'warn_amount': warn_amount, - 'grant_amount': grant_amount, - } - self.save_settings() - - def utterances(self, server_id: str): - server = self.server(server_id) - utterances = ensure_map(server, 'utterances', {}) - ensure_map(utterances, 'ignore_commands', True) - ensure_map(utterances, 'ignore_emoji', True) - ensure_map(utterances, 'ignore_mentions', True) - ensure_map(utterances, 'ignore_room_codes', True) - ensure_map(utterances, 'min_length', 30) - ensure_map(utterances, 'min_words', 5) - return utterances - - def ignore_commands(self, server_id: str): - return self.utterances(server_id)['ignore_commands'] - - def ignore_emoji(self, server_id: str): - return self.utterances(server_id)['ignore_emoji'] - - def ignore_mentions(self, server_id: str): - return self.utterances(server_id)['ignore_mentions'] - - def ignore_room_codes(self, server_id: str): - return self.utterances(server_id)['ignore_room_codes'] - - def min_length(self, server_id: str): - return self.utterances(server_id)['min_length'] - - def min_words(self, server_id: str): - return self.utterances(server_id)['min_words'] - - def set_ignore_commands(self, server_id: str, ignore: bool): - self.utterances(server_id)['ignore_commands'] = ignore - self.save_settings() - - def set_ignore_emoji(self, server_id: str, ignore: bool): - self.utterances(server_id)['ignore_emoji'] = ignore - self.save_settings() - - def set_ignore_mentions(self, server_id: str, ignore: bool): - self.utterances(server_id)['ignore_mentions'] = ignore - self.save_settings() - - def set_ignore_room_codes(self, server_id: str, ignore: bool): - self.utterances(server_id)['ignore_room_codes'] = ignore - self.save_settings() - - def set_min_length(self, server_id: str, min_length: int): - self.utterances(server_id)['min_length'] = min_length - self.save_settings() - - def set_min_words(self, server_id: str, min_words: int): - self.utterances(server_id)['min_words'] = min_words - self.save_settings() - - def blacklist(self, server_id: str): - server = self.server(server_id) - return ensure_map(server, 'blacklist', {}) - - def add_blacklist(self, server_id: str, user_id: str, by_id: str, reason: str): - blacklist = self.blacklist(server_id) - blacklist[user_id] = { - 'user_id': user_id, - 'by_id': by_id, - 'ignore_date': now_date(), - 'reason': reason, - } - self.save_settings() - - def remove_blacklist(self, server_id: str, user_id: str): - blacklist = self.blacklist(server_id) - result = blacklist.pop(user_id, None) - self.save_settings() - return result - - def channels(self, server_id: str): - server = self.server(server_id) - return ensure_map(server, 'channels', {}) - - def set_channel(self, server_id: str, channel_id: str, max_ppd: int): - channels = self.channels(server_id) - if max_ppd == 0: - channels.pop(channel_id, None) - else: - channels[channel_id] = { - 'channel_id': channel_id, - 'max_ppd': max_ppd, - } - self.save_settings() - - -def force_number(s): - try: - return float(s) - except ValueError: - return 0 diff --git a/speech/__init__.py b/speech/__init__.py deleted file mode 100644 index 425372ba..00000000 --- a/speech/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .speech import Speech - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(Speech(bot)) diff --git a/speech/info.json b/speech/info.json deleted file mode 100644 index 49410046..00000000 --- a/speech/info.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Audio helper functions for other cogs + a few commands of its own.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Audio helper commands", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [ - "google-api", - "google-api-python-client", - "google-cloud", - "google-cloud-texttospeech" - ], - "tags": ["audio"] -} diff --git a/speech/speech.py b/speech/speech.py deleted file mode 100644 index aa2c8c68..00000000 --- a/speech/speech.py +++ /dev/null @@ -1,158 +0,0 @@ -import asyncio -import logging -import ctypes.util - -import discord -from google.cloud import texttospeech -from google.oauth2 import service_account -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline - -from rpadutils import CogSettings, corowrap - -logger = logging.getLogger('red.tsubaki.speech') - -try: - if not discord.opus.is_loaded(): - discord.opus.load_opus(ctypes.util.find_library('opus')) -except: # Missing opus - logger.warning('Failed to load opus') - opus = None -else: - opus = True - -SPOOL_PATH = "data/speech/spool.mp3" - - -class Speech(commands.Cog): - """Speech utilities.""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = SpeechSettings("speech") - - self.service = None - self.try_setup_api() - self.busy = False - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - def try_setup_api(self): - api_key_file = self.settings.get_key_file() - if api_key_file: - try: - credentials = service_account.Credentials.from_service_account_file(api_key_file) - self.service = texttospeech.TextToSpeechClient(credentials=credentials) - except: - print('speech setup failed') - - @commands.group() - @checks.is_owner() - async def speech(self, ctx): - """Speech utilities.""" - - @commands.command() - @checks.is_owner() - async def vcsay(self, ctx, *, text): - """Speak into the current user's voice channel.""" - if not self.service: - await ctx.send(inline('Set up an API key file first!')) - return - - voice = ctx.author.voice - if not voice: - await ctx.send(inline('You must be in a voice channel to use this command')) - return - - channel = voice.voice_channel - - if len(text) > 300: - await ctx.send(inline('Command is too long')) - return - - await self.speak(ctx, channel, text) - - async def speak(self, ctx, channel, text: str): - if self.busy: - await ctx.send(inline('Sorry, saying something else right now')) - return False - else: - self.busy = True - - try: - voice = texttospeech.types.VoiceSelectionParams( - language_code='en-US', name='en-US-Wavenet-F') - - audio_config = texttospeech.types.AudioConfig( - audio_encoding=texttospeech.enums.AudioEncoding.MP3) - - synthesis_input = texttospeech.types.SynthesisInput(text=text) - response = self.service.synthesize_speech(synthesis_input, voice, audio_config) - - with open(SPOOL_PATH, 'wb') as out: - out.write(response.audio_content) - - await self.play_path(channel, SPOOL_PATH) - return True - finally: - self.busy = False - return False - - async def play_path(self, channel, audio_path: str): - existing_vc = channel.guild.voice_client - if existing_vc: - await existing_vc.disconnect(force=True) - - voice_client = None - try: - voice_client = await channel.connect() - await asyncio.sleep(.5) - b_options = '-guess_layout_max 0 -v 16' - a_options = '' - - audio_source = discord.FFmpegPCMAudio(audio_path, options=a_options, before_options=b_options) - voice_client.play(audio_source, after = corowrap(voice_client.disconnect(), self.bot.loop)) - return True - except Exception as e: - print(e) - if voice_client: - try: - await voice_client.disconnect() - except: - pass - return False - - @speech.command() - @checks.is_owner() - async def setkeyfile(self, ctx, api_key_file): - """Sets the google api key file.""" - self.settings.set_key_file(api_key_file) - await ctx.send("done, make sure the key file is in the data/speech directory") - - -class SpeechSettings(CogSettings): - def make_default_settings(self): - config = { - 'google_api_key_file': '' - } - return config - - def get_key_file(self): - return self.bot_settings.get('google_api_key_file') - - def set_key_file(self, api_key_file): - self.bot_settings['google_api_key_file'] = api_key_file - self.save_settings() diff --git a/sqlactivitylog/__init__.py b/sqlactivitylog/__init__.py deleted file mode 100644 index 75dde5eb..00000000 --- a/sqlactivitylog/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .sqlactivitylog import * - -__red_end_user_data_statement__ = "All message edits/deletions less than 3 weeks old are saved." - -def setup(bot): - n = SqlActivityLogger(bot) - bot.add_cog(n) diff --git a/sqlactivitylog/info.json b/sqlactivitylog/info.json deleted file mode 100644 index 6fcd63fd..00000000 --- a/sqlactivitylog/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Stores message edits and deletions.", - "end_user_data_statement": "All message edits/deletions less than 3 weeks old are saved.", - "install_msg": "", - "short": "Message deletion log", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["prettytable", "pytz"], - "tags": ["moderation"] -} diff --git a/sqlactivitylog/sqlactivitylog.py b/sqlactivitylog/sqlactivitylog.py deleted file mode 100644 index ec679476..00000000 --- a/sqlactivitylog/sqlactivitylog.py +++ /dev/null @@ -1,469 +0,0 @@ -import os -import sqlite3 as lite -import textwrap -import timeit -from collections import deque -from datetime import datetime, timedelta - -import discord -import prettytable -import pytz -from redbot.core import checks, commands, data_manager -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box, pagify - -import rpadutils - - -def mod_help(self, ctx, help_type): - hs = getattr(self, help_type) - return self.format_text_for_context(ctx, hs).format(ctx) if hs else hs - - -commands.Command.format_help_for_context = lambda s, c: mod_help(s, c, "help") -commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, "short_doc") - - -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='sqlactivitylog')), file_name) - - -TIMESTAMP_FORMAT = '%Y-%m-%d %X' # YYYY-MM-DD HH:MM:SS -DB_FILE = _data_file("log.db") - -ALL_COLUMNS = [ - ('timestamp', 'Time (PT)'), - ('server_id', 'Server'), - ('channel_id', 'Channel'), - ('user_id', 'User'), - ('msg_type', 'Type'), - ('clean_content', 'Message'), -] - -CREATE_TABLE = ''' -CREATE TABLE IF NOT EXISTS messages( - rowid INTEGER PRIMARY KEY ASC AUTOINCREMENT, - timestamp TIMESTAMP NOT NULL, - server_id STRING NOT NULL, - channel_id STRING NOT NULL, - user_id STRING NOT NULL, - msg_type STRING NOT NULL, - content STRING NOT NULL, - clean_content STRING NOT NULL) -''' - -CREATE_INDEX_1 = ''' -CREATE INDEX IF NOT EXISTS idx_messages_server_id_channel_id_user_id_timestamp -ON messages(server_id, channel_id, user_id, timestamp) -''' - -CREATE_INDEX_2 = ''' -CREATE INDEX IF NOT EXISTS idx_messages_server_id_user_id_timestamp -ON messages(server_id, user_id, timestamp) -''' - -CREATE_INDEX_3 = ''' -CREATE INDEX IF NOT EXISTS idx_messages_server_id_clean_content -ON messages(server_id, clean_content) -''' - -CREATE_INDEX_4 = ''' -CREATE INDEX IF NOT EXISTS idx_messages_server_id_timestamp -ON messages(server_id, timestamp) -''' - -CREATE_INDEX_5 = ''' -CREATE INDEX IF NOT EXISTS idx_messages_server_id_channel_id_timestamp -ON messages(server_id, channel_id, timestamp) -''' - -MAX_LOGS = 500 - -USER_QUERY = ''' -SELECT * FROM ( - SELECT timestamp, channel_id, msg_type, clean_content - FROM messages INDEXED BY idx_messages_server_id_user_id_timestamp - WHERE server_id = :server_id - AND user_id = :user_id - ORDER BY timestamp DESC - LIMIT :row_count -) -ORDER BY timestamp ASC -''' - -CHANNEL_QUERY = ''' -SELECT * FROM ( - SELECT timestamp, user_id, msg_type, clean_content - FROM messages INDEXED BY idx_messages_server_id_channel_id_timestamp - WHERE server_id = :server_id - AND channel_id = :channel_id - AND user_id <> :bot_id - ORDER BY timestamp DESC - LIMIT :row_count -) -ORDER BY timestamp ASC -''' - -USER_CHANNEL_QUERY = ''' -SELECT * FROM ( - SELECT timestamp, msg_type, clean_content - FROM messages INDEXED BY idx_messages_server_id_channel_id_user_id_timestamp - WHERE server_id = :server_id - AND user_id = :user_id - AND channel_id = :channel_id - ORDER BY timestamp DESC - LIMIT :row_count -) -ORDER BY timestamp ASC -''' - -CONTENT_QUERY = ''' -SELECT * FROM ( - SELECT timestamp, channel_id, user_id, msg_type, clean_content - FROM messages INDEXED BY idx_messages_server_id_clean_content - WHERE server_id = :server_id - AND lower(clean_content) LIKE lower(:content_query) - AND user_id <> :bot_id - ORDER BY timestamp DESC - LIMIT :row_count -) -ORDER BY timestamp ASC -''' - -DELETE_BEFORE_QUERY = ''' -DELETE -FROM messages -WHERE timestamp < :before -''' - -GET_USER_DATA_QUERY = ''' -SELECT * FROM ( - SELECT timestamp, channel_id, msg_type, clean_content - FROM messages INDEXED BY idx_messages_server_id_user_id_timestamp - WHERE user_id = :user_id - ORDER BY timestamp DESC -) -ORDER BY timestamp ASC -''' - -DELETE_USER_DATA_QUERY = ''' -DELETE -FROM messages -WHERE user_id = :user_id -''' - -class SqlActivityLogger(commands.Cog): - """Log activity seen by bot""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.lock = False - self.con = lite.connect(DB_FILE, detect_types=lite.PARSE_DECLTYPES) - self.con.row_factory = lite.Row - self.con.execute(CREATE_TABLE) - self.con.execute(CREATE_INDEX_1) - self.con.execute(CREATE_INDEX_2) - self.con.execute(CREATE_INDEX_3) - self.con.execute(CREATE_INDEX_4) - self.con.execute(CREATE_INDEX_5) - self.insert_timing = deque(maxlen=1000) - self.purge() - - def cog_unload(self): - self.lock = True - self.con.close() - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - values = { - 'user_id': user.id, - } - column_data = [ - ('timestamp', 'Time (PT)'), - ('channel_id', 'Channel'), - ('msg_type', 'Type'), - ('clean_content', 'Message'), - ] - - data = ("NOTE: Data stored by this cog is essential for moderator use " - "so data deletion requests can only be made by the bot owner and " - "Discord itself. If you need your data deleted, please contact a " - "bot owner.\n\n\n") - data += await self.queryAndSave(None, None, USER_QUERY, values, column_data) - - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - The personal data stored in this cog is for essential moderation use, - so some data deletion requests can only be made by the bot owner and - Discord itself. If this is an issue, please contact a bot owner. - """ - if requester not in ("discord_deleted_user", "owner"): - return - cursor = self.con.execute(DELETE_USER_DATA_QUERY, {'user_id': user_id}) - self.con.commit() - - @commands.command() - @checks.is_owner() - async def rawquery(self, ctx, *, query: str): - await self.queryAndPrint(ctx, ctx.guild, query, {}, {}) - - @commands.command() - @checks.is_owner() - async def inserttiming(self, ctx): - size = len(self.insert_timing) - avg_time = round(sum(self.insert_timing) / size, 4) - max_time = round(max(self.insert_timing), 4) - min_time = round(min(self.insert_timing), 4) - await ctx.send(inline('{} inserts, min={} max={} avg={}'.format(size, min_time, max_time, avg_time))) - - @commands.command() - @checks.is_owner() - async def togglelock(self, ctx): - """Prevents the bot from inserting into the db. - - This is useful if you need to use sqlite3 to access the database for some reason. - Will keep the bot from locking up trying to insert records. - """ - self.lock = not self.lock - await ctx.send(inline('Locked is now {}'.format(self.lock))) - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def exlog(self, context): - """Extra log querying tools. - - Uses the bot's local SQL message storage to retrieve deleted/edited messages. - """ - - @exlog.command() - async def user(self, ctx, user: discord.User, count=10): - """exlog user "{0.author.name}" 100 - - List of messages for a user across all channels. - Count is optional, with a low default and a maximum value. - """ - count = min(count, MAX_LOGS) - server = ctx.guild - values = { - 'server_id': server.id, - 'row_count': count, - 'user_id': user.id, - } - column_data = [ - ('timestamp', 'Time (PT)'), - ('channel_id', 'Channel'), - ('msg_type', 'Type'), - ('clean_content', 'Message'), - ] - - await self.queryAndPrint(ctx, server, USER_QUERY, values, column_data) - - @exlog.command() - async def channel(self, ctx, channel: discord.TextChannel, count=10): - """exlog channel #general_chat 100 - - List of messages in a given channel. - Count is optional, with a low default and a maximum value. - The bot is excluded from results. - """ - count = min(count, MAX_LOGS) - server = channel.guild - values = { - 'server_id': server.id, - 'bot_id': self.bot.user.id, - 'row_count': count, - 'channel_id': channel.id, - } - column_data = [ - ('timestamp', 'Time (PT)'), - ('user_id', 'User'), - ('msg_type', 'Type'), - ('clean_content', 'Message'), - ] - - await self.queryAndPrint(ctx, server, CHANNEL_QUERY, values, column_data) - - @exlog.command() - async def userchannel(self, ctx, user: discord.User, channel: discord.TextChannel, count=10): - """exlog userchannel "{0.author.name}" #general_chat 100 - - List of messages from a user in a given channel. - Count is optional, with a low default and a maximum value. - """ - count = min(count, MAX_LOGS) - server = channel.guild - values = { - 'server_id': server.id, - 'row_count': count, - 'channel_id': channel.id, - 'user_id': user.id, - } - column_data = [ - ('timestamp', 'Time (PT)'), - ('msg_type', 'Type'), - ('clean_content', 'Message'), - ] - - await self.queryAndPrint(ctx, server, USER_CHANNEL_QUERY, values, column_data) - - @exlog.command() - async def query(self, ctx, query, count=10): - """exlog query "4 whale" 100 - - Case-insensitive search of messages from every user/channel. - Put the query in quotes if it is more than one word. - Count is optional, with a low default and a maximum value. - The bot is excluded from results. - """ - if query[0] in ('%', '_'): - await ctx.send('`You cannot start this query with a wildcard`') - return - - count = min(count, MAX_LOGS) - server = ctx.guild - values = { - 'server_id': server.id, - 'bot_id': self.bot.user.id, - 'row_count': count, - 'content_query': query, - } - column_data = [ - ('timestamp', 'Time (PT)'), - ('channel_id', 'Channel'), - ('user_id', 'User'), - ('msg_type', 'Type'), - ('clean_content', 'Message'), - ] - - await self.queryAndPrint(ctx, server, CONTENT_QUERY, values, column_data) - - async def queryAndPrint(self, ctx, server, query, values, column_data, max_rows=MAX_LOGS * 2, verbose=True): - result_text = queryAndSave(self, ctx, server, query, values, column_data, max_rows, verbose) - for p in pagify(result_text): - await ctx.send(box(p)) - - async def queryAndSave(self, ctx, server, query, values, column_data, max_rows=MAX_LOGS * 2, verbose=True): - before_time = timeit.default_timer() - cursor = self.con.execute(query, values) - rows = cursor.fetchall() - execution_time = timeit.default_timer() - before_time - - if len(column_data) == 0: - column_data = ALL_COLUMNS - - results_columns = [d[0] for d in cursor.description] - column_data = [r for r in column_data if r[0] in results_columns] - for missing_col in [col for col in results_columns if col not in [c[0] for c in column_data]]: - column_data.append((missing_col, missing_col)) - - column_names = [c[0] for c in column_data] - column_headers = [c[1] for c in column_data] - - tbl = prettytable.PrettyTable(column_headers) - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.NONE - tbl.align = 'l' - - for idx, row in enumerate(rows): - if idx > max_rows: - break - - table_row = list() - for col in column_names: - if col not in row.keys(): - table_row.append('') - continue - raw_value = row[col] - value = str(raw_value) - if col == 'timestamp': - # Assign a UTC timezone to the datetime - raw_value = raw_value.replace(tzinfo=pytz.utc) - # Change the UTC timezone to PT - raw_value = rpadutils.NA_TZ_OBJ.normalize(raw_value) - value = raw_value.strftime("%F %X") - if col == 'channel_id': - channel = server.get_channel(value) if server else None - value = channel.name if channel else value - if col == 'user_id': - member = server.get_member(value) if server else None - value = member.name if member else value - if col == 'server_id': - server_obj = self.bot.get_guild(value) - value = server_obj.name if server_obj else value - if col == 'clean_content': - value = value.replace('```', '~~~') - value = value.replace('`', '\\`') - value = '\n'.join(textwrap.wrap(value, 60)) - table_row.append(value) - - tbl.add_row(table_row) - - result_text = "" - if verbose: - result_text = "{} results fetched in {}s\n{}".format( - len(rows), round(execution_time, 2), tbl.get_string()) - return result_text - - @commands.Cog.listener("on_message_edit") - async def on_message_edit(self, before, after): - self.log('EDIT', before, after.edited_at) - - @commands.Cog.listener("on_message_delete") - async def on_message_delete(self, message): - self.log('DELETE', message, datetime.utcnow()) - - def log(self, msg_type, message, timestamp): - if self.lock: - return - - if message.author.id == self.bot.user.id: - return - - stmt = ''' - INSERT INTO messages(timestamp, server_id, channel_id, user_id, msg_type, content, clean_content) - VALUES(:timestamp, :server_id, :channel_id, :user_id, :msg_type, :content, :clean_content) - ''' - timestamp = timestamp or datetime.utcnow() - server_id = message.guild.id if message.guild else -1 - channel_id = message.channel.id if message.channel else -1 - - msg_content = message.content - msg_clean_content = message.clean_content - if message.attachments: - extra_txt = '\nattachments: ' + str(message.attachments) - msg_content = (msg_content + extra_txt).strip() - msg_clean_content = (msg_clean_content + extra_txt).strip() - - if message.embeds: - extra_txt = '\nembeds: ' + str(message.embeds) - msg_content = (msg_content + extra_txt).strip() - msg_clean_content = (msg_clean_content + extra_txt).strip() - - values = { - 'timestamp': timestamp, - 'server_id': server_id, - 'channel_id': channel_id, - 'user_id': message.author.id, - 'msg_type': msg_type, - 'content': msg_content, - 'clean_content': msg_clean_content, - } - - before_time = timeit.default_timer() - self.con.execute(stmt, values) - self.con.commit() - execution_time = timeit.default_timer() - before_time - self.insert_timing.append(execution_time) - - def purge(self): - before = datetime.today() - timedelta(days=(7 * 3)) - values = dict(before=before) - print('Purging old logs') - cursor = self.con.execute(DELETE_BEFORE_QUERY, values) - self.con.commit() - print('Purged', cursor.rowcount) diff --git a/stickers/__init__.py b/stickers/__init__.py deleted file mode 100644 index 82c0c12d..00000000 --- a/stickers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .stickers import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(Stickers(bot)) diff --git a/stickers/info.json b/stickers/info.json deleted file mode 100644 index 206c94dd..00000000 --- a/stickers/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Custom commands to pull up images.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Sticker commands", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["customcommands"] -} diff --git a/stickers/stickers.py b/stickers/stickers.py deleted file mode 100644 index 19cdcebc..00000000 --- a/stickers/stickers.py +++ /dev/null @@ -1,198 +0,0 @@ -import re -from collections import defaultdict - -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import box, pagify - -from rpadutils import CogSettings - -STICKER_COG = None - - -async def is_sticker_admin_check(ctx): - return STICKER_COG.settings.check_admin(ctx.author.id) or await ctx.bot.is_owner(ctx.author) - - -def is_sticker_admin(): - return commands.check(is_sticker_admin_check) - - -class Stickers(commands.Cog): - """Sticker commands.""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = StickersSettings("stickers") - - global STICKER_COG - STICKER_COG = self - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.group() - @is_sticker_admin() - async def sticker(self, context): - """Global stickers.""" - - @sticker.command() - @is_sticker_admin() - async def add(self, ctx, command: str, *, text): - """Adds a sticker - - Example: - !stickers add "whale happy" link_to_happy_whale - """ - command = command.lower() - if command in self.bot.all_commands.keys(): - await self.bot.say("That is already a standard command.") - return - - self.settings.update_sticker(command, text) - await ctx.send("Sticker successfully added.") - - @sticker.command() - @is_sticker_admin() - async def delete(self, ctx, command: str): - """Deletes a sticker - - Example: - !stickers delete "whale happy" """ - command = command.lower() - if command in self.settings.get_stickers(): - self.settings.rm_sticker(command) - await ctx.send("Sticker successfully deleted.") - else: - await ctx.send("Sticker doesn't exist.") - - @commands.command() - async def stickers(self, ctx): - """Shows all stickers""" - if not self.settings.get_stickers(): - await ctx.send("There are no stickers yet") - return - - stickers = list(self.settings.get_stickers()) - - prefixes_list = defaultdict(list) - other_list = list() - - for c in stickers: - m = re.match(r'^(.+)[ ](.+)$', c) - if m: - grp = m.group(1) - prefixes_list[grp].append(m.group(2)) - else: - other_list.append(c) - - msg = "Stickers:\n" - for cmd in sorted(other_list): - msg += " {}{}\n".format(ctx.prefix, cmd) - - msg += "\nSticker Packs:\n" - - for prefix in sorted(prefixes_list): - msg += " {}{} [...]\n ".format(ctx.prefix, prefix) - - for suffix in sorted(prefixes_list[prefix]): - msg += " {}".format(suffix) - msg += "\n\n" - - for page in pagify(msg): - await msg.author.send(box(page)) - - @sticker.command() - @checks.is_owner() - async def addadmin(self, ctx, user: discord.Member): - """Adds a user to the stickers admin""" - self.settings.add_admin(user.id) - await ctx.send("done") - - @sticker.command() - @checks.is_owner() - async def rmadmin(self, ctx, user): - """Removes a user from the stickers admin""" - try: - u = await commands.MemberConverter().convert(ctx, user) - self.settings.rmAdmin(u.id) - except commands.BadArgument as e: - try: - u = int(user) - self.settings.rmAdmin(u) - except ValueError: - await ctx.send(inline("Invalid user id.")) - return - await ctx.send("done") - - @commands.Cog.listener("on_message") - async def check_for_sticker_request(self, message): - if len(message.content) < 2: - return - - prefix = (await self.bot.get_prefix(message))[0] - - cmdlist = self.settings.get_stickers() - image_url = None - cmd = message.content[len(prefix):] - if cmd in cmdlist.keys(): - image_url = cmdlist[cmd] - elif cmd.lower() in cmdlist.keys(): - image_url = cmdlist[cmd.lower()] - - if image_url: - footer_text = message.content + ' posted by ' + message.author.name - embed = discord.Embed().set_image(url=image_url).set_footer(text=footer_text) - await message.channel.send(embed=embed) - await message.delete() - - -class StickersSettings(CogSettings): - def make_default_settings(self): - config = { - 'admins': [], - 'c_commands': {} - } - return config - - def admins(self): - return self.bot_settings['admins'] - - def check_admin(self, user_id): - admins = self.admins() - return user_id in admins - - def add_admin(self, user_id): - admins = self.admins() - if user_id not in admins: - admins.append(user_id) - self.save_settings() - - def rm_admin(self, user_id): - admins = self.admins() - if user_id in admins: - admins.remove(user_id) - self.save_settings() - - def get_stickers(self): - return self.bot_settings['c_commands'] - - def update_sticker(self, key, value): - self.bot_settings['c_commands'][key] = value - self.save_settings() - - def rm_sticker(self, key): - self.bot_settings['c_commands'].pop(key, None) - self.save_settings() diff --git a/streamcopy/__init__.py b/streamcopy/__init__.py deleted file mode 100644 index 1b5c94cb..00000000 --- a/streamcopy/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .streamcopy import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - n = StreamCopy(bot) - bot.add_listener(n.check_stream, "on_member_update") - bot.loop.create_task(n.refresh_stream()) - bot.add_cog(n) diff --git a/streamcopy/info.json b/streamcopy/info.json deleted file mode 100644 index ad20c27f..00000000 --- a/streamcopy/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Show which members are streaming and opt members in and out of being public for the guild", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Streaming Utils", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["streaming"] -} diff --git a/streamcopy/streamcopy.py b/streamcopy/streamcopy.py deleted file mode 100644 index c9da2d49..00000000 --- a/streamcopy/streamcopy.py +++ /dev/null @@ -1,215 +0,0 @@ -import asyncio -import random -import traceback - -import discord -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, box - -from rpadutils import CogSettings, get_role, get_role_from_id - - -class StreamCopy(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = StreamCopySettings("streamcopy") - self.current_user_id = None - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - async def refresh_stream(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('StreamCopy'): - try: - await self.do_refresh() - await self.do_ensure_roles() - except Exception as e: - traceback.print_exc() - - await asyncio.sleep(60 * 3) - print("done refresh_stream") - - @commands.group() - @checks.mod_or_permissions(manage_guild=True) - async def streamcopy(self, ctx): - """Utilities for reacting to users gaining/losing streaming status.""" - - @streamcopy.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def setStreamerRole(self, ctx, *, role_name: str): - try: - role = get_role(ctx.guild.roles, role_name) - except: - await ctx.send(inline('Unknown role')) - return - - self.settings.set_streamer_role(ctx.guild.id, role.id) - await ctx.send(inline('Done. Make sure that role is below the bot in the hierarchy')) - - @streamcopy.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def clearStreamerRole(self, ctx): - self.settings.clear_streamer_role(ctx.guild.id) - await self.bot.say(inline('Done')) - - @streamcopy.command(name="adduser") - @checks.is_owner() - async def addUser(self, ctx, user: discord.User): - self.settings.add_user(user.id) - await ctx.send(inline('Done')) - - @streamcopy.command(name="rmuser") - @checks.is_owner() - async def rmUser(self, ctx, user): - try: - u = await commands.MemberConverter().convert(ctx, user) - self.settings.rm_user(u.id) - except commands.BadArgument as e: - try: - u = int(user) - self.settings.rm_user(u) - except ValueError: - await ctx.send(inline("Invalid user id.")) - return - await ctx.send(inline('Done')) - - @streamcopy.command(name="list") - @checks.is_owner() - async def list(self, ctx): - user_ids = self.settings.users().keys() - members = {x.id: x for x in self.bot.get_all_members() if x.id in user_ids} - - output = "Users:" - for m_id, m in members.items(): - output += "\n({}) : {}".format('+' if self.is_playing(m) else '-', m.name) - - await ctx.send(box(output)) - - @streamcopy.command(name="refresh") - @checks.is_owner() - async def refresh(self, ctx): - other_stream = await self.do_refresh() - if other_stream: - await ctx.send(inline('Updated stream')) - else: - await ctx.send(inline('Could not find a streamer')) - - async def check_stream(self, before, after): - streamer_role_id = self.settings.get_streamer_role(before.guild.id) - if streamer_role_id: - await self.ensure_user_streaming_role(after.guild, streamer_role_id, after) - - try: - tracked_users = self.settings.users() - if before.id not in tracked_users: - return - - if self.is_playing(after): - await self.copy_playing(after.activity) - return - - await self.do_refresh() - except Exception as ex: - print("Stream checking failed", ex) - - async def ensure_user_streaming_role(self, server, streamer_role_id: discord.Role, user: discord.Member): - user_is_playing = self.is_playing(user) - try: - streamer_role = get_role_from_id(self.bot, server, streamer_role_id) - user_has_streamer_role = streamer_role in user.roles - if user_is_playing and not user_has_streamer_role: - await user.add_roles(streamer_role) - elif not user_is_playing and user_has_streamer_role: - await user.remove_roles(streamer_role) - except Exception as ex: - pass - - async def do_refresh(self): - other_stream = self.find_stream() - if other_stream: - await self.copy_playing(other_stream) - else: - await self.bot.change_presence(activity=None) - return other_stream - - async def do_ensure_roles(self): - servers = self.bot.guilds - for server in servers: - streamer_role_id = self.settings.get_streamer_role(server.id) - if not streamer_role_id: - continue - for member in server.members: - await self.ensure_user_streaming_role(member.guilds, streamer_role_id, member) - - def find_stream(self): - user_ids = self.settings.users().keys() - members = {x.id: x for x in self.bot.get_all_members() if x.id in user_ids and self.is_playing(x)} - games = [x.activity for x in members.values()] - random.shuffle(games) - return games[0] if len(games) else None - - def is_playing(self, member: discord.Member): - return member and member.activity and member.activity.type == 1 and member.activity.url - - async def copy_playing(self, stream: discord.Streaming): - new_stream = discord.Game(name=stream.name, url=stream.url, type=stream.type) - await self.bot.change_presence(activity=new_stream) - - -class StreamCopySettings(CogSettings): - def make_default_settings(self): - config = { - 'users': {}, - 'servers': {} - } - return config - - def users(self): - return self.bot_settings['users'] - - def add_user(self, user_id): - users = self.users() - users[user_id] = {'priority': 100} # This is for stupid legacy reasons - self.save_settings() - - def rm_user(self, user_id): - users = self.users() - if user_id in users: - users.pop(user_id) - self.save_settings() - - def get_guild(self, guild_id): - guilds = self.bot_settings['servers'] - if guild_id not in guilds: - guilds[guild_id] = {} - return guilds[guild_id] - - def set_streamer_role(self, server_id, role_id): - server = self.get_guild(server_id) - server['role'] = role_id - self.save_settings() - - def get_streamer_role(self, server_id): - server = self.get_guild(server_id) - return server.get('role', None) - - def clear_streamer_role(self, server_id): - server = self.get_guild(server_id) - if 'role' in server: - server.pop('role') - self.save_settings() diff --git a/timecog/__init__.py b/timecog/__init__.py deleted file mode 100644 index 88e8321c..00000000 --- a/timecog/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .timecog import * - -__red_end_user_data_statement__ = "Reminders are stored." - -def setup(bot): - n = TimeCog(bot) - bot.add_cog(n) - bot.loop.create_task(n.reminderloop()) diff --git a/timecog/info.json b/timecog/info.json deleted file mode 100644 index 6a83b09a..00000000 --- a/timecog/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["Araida Megido#2552"], - "description": "A cog to track time-related things such as reminders and schedules.", - "end_user_data_statement": "Reminders are stored.", - "install_msg": "", - "short": "remindme and scheduler", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["pytz", "python-dateutil"], - "tags": ["remindme", "timer", "scheduler"] -} diff --git a/timecog/timecog.py b/timecog/timecog.py deleted file mode 100644 index 2f3573c8..00000000 --- a/timecog/timecog.py +++ /dev/null @@ -1,686 +0,0 @@ -import asyncio -import re -import time -import traceback -from datetime import timedelta, datetime -from dateutil.relativedelta import relativedelta - -import discord -import pytz -from redbot.core import commands, Config, checks -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline, pagify, box - -import rpadutils - -tz_lookup = dict([(pytz.timezone(x).localize(datetime.now()).tzname(), pytz.timezone(x)) - for x in pytz.all_timezones]) - -time_at_regeces = [ - r'^\s*(?P\d{4})[-/](?P\d+)[-/](?P\d+) (?P\d+):(?P\d\d) ?(?Ppm|am)? ?(?P.*)$', - r'^\s*(?P\d{4})[-/](?P\d+)[-/](?P\d+) ?(?P.*)$', - r'^\s*(?P\d+)[-/](?P\d+) ?(?P.*)$', - r'^\s*(?P\d+):(?P\d\d) ?(?P\d?pm|am)? ?(?P.*)$', - r'^\s*(?P\d+) ?(?P\d?pm|am) ?(?P.*)$', -] - -time_in_regeces = [ - r'^\s*((?:-?\d+ ?(?:m|h|d|w|y|s)\w* ?)+)\b (.+)$', # One tinstr - r'^\s*((?:-?\d+ ?(?:m|h|d|w|y|s)\w* ?)+)\b\s*\|\s*((?:-?\d+ ?(?:m|h|d|w|y|s)\w* ?)+|now)\b (.*)$', #Unused -] - -exact_tats = [ - r'^\s*(?P\d{4})[-/](?P\d+)[-/](?P\d+) (?P\d+):(?P\d\d) ?(?Ppm|am)?\s*$', - r'^\s*(?P\d{4})[-/](?P\d+)[-/](?P\d+)\s*$', - r'^\s*(?P\d+)[-/](?P\d+)\s*$', - r'^\s*(?P\d+):(?P\d\d) ?(?P\d?pm|am)?\s*$', - r'^\s*(?P\d+) ?(?P\d?pm|am)\s*$', -] - -exact_tins = [ - r'^\s*((?:-?\d+ ?(?:m|h|d|w|y|s)\w* ?)+)$', # One tinstr -] - -DT_FORMAT = "%A, %b %-d, %Y at %-I:%M %p" -SHORT_DT_FORMAT = "%b %-d, %Y at %-I:%M %p" - - - -class TimeCog(commands.Cog): - """Utilities pertaining to time""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.config = Config.get_conf(self, identifier=7173306) - self.config.register_user(reminders=[], tz='') - self.config.register_guild(schedules={}) - - """ - CONFIG: Config - | USERS: Config - | | reminders: list - | | | REMINDER: tuple - | | | | TIME: int (timestamp) - | | | | TEXT: str - | | | | INTERVAL: Optional[int] - | | tz: str (tzstr) - | CHANNELS: Config - | | schedules: dict - | | | NAME: dict - | | | | start: int (timestamp) (unused) - | | | | time: int (timestamp) - | | | | end: int (timestamp) - | | | | interval: int (seconds) - | | | | enabled: bool - | | | | channels: list (cids) - | | | | message: str - """ - - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - udata = await self.config.user_from_id(user_id).reminders() - - data = "You have {} reminder(s) set.\n".format(len(udata)) - - if not udata: - data = "No data is stored for user with ID {}.\n".format(user_id) - - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data.""" - await self.config.user_from_id(user_id).clear() - - @commands.group(aliases=['remindmeat', 'remindmein'], invoke_without_command=True) - async def remindme(self, ctx, *, time): - """Reminds you to do something at a specified time - - [p]remindme 2020-04-13 06:12 Do something! - [p]remindme 5 weeks Do something! - [p]remindme 4:13 PM Do something! - [p]remindme 2020-05-03 Do something! - [p]remindme 04-13 Do something! - """ - - user_tz_str = await self.config.user(ctx.author).tz() - user_timezone = tzstr_to_tz(user_tz_str or 'UTC') - - for ar in time_at_regeces: - match = re.search(ar, time, re.IGNORECASE) - if not match: - continue - match = match.groupdict() - - if not user_tz_str: - await ctx.send( - "Please configure your personal timezone with `{0.clean_prefix}settimezone` first.".format(ctx)) - return - - now = datetime.now(tz=user_timezone) - defaults = { - 'year': now.year, - 'month': now.month, - 'day': now.day, - 'hour': now.hour, - 'minute': now.minute, - 'merid': 'NONE' - } - defaults.update({k: v for k, v in match.items() if v}) - input = defaults.pop('input') - for key in defaults: - if key not in ['merid']: - defaults[key] = int(defaults[key]) - if defaults['merid'] == 'pm' and defaults['hour'] <= 12: - defaults['hour'] += 12 - elif defaults['merid'] == 'NONE' and defaults['hour'] < now.hour: - defaults['hour'] += 12 - if defaults['hour'] >= 24: - defaults['day'] += int(defaults['hour'] // 24) - defaults['hour'] = defaults['hour'] % 24 - del defaults['merid'] - try: - rmtime = user_timezone.localize(datetime(**defaults)) - except ValueError as e: - await ctx.send(inline(str(e).capitalize())) - return - if rmtime < now: - rmtime += timedelta(days=1) - rmtime = rmtime.astimezone(pytz.utc).replace(tzinfo=None) - break - else: - ir = time_in_regeces[0] - match = re.search(ir, time, re.IGNORECASE) - if not match: # Only use the first one - raise commands.UserFeedbackCheckFailure("Invalid time string: " + time) - tinstrs, input = match.groups() - rmtime = datetime.utcnow() - try: - rmtime += tin2tdelta(tinstrs) - except OverflowError: - raise commands.UserFeedbackCheckFailure( - inline("That's way too far in the future! Please keep it in your lifespan!")) - - if rmtime < (datetime.utcnow() - timedelta(seconds=1)): - raise commands.UserFeedbackCheckFailure(inline("You can't set a reminder in the past! If only...")) - - async with self.config.user(ctx.author).reminders() as rms: - rms.append((rmtime.timestamp(), input)) - - response = "I will tell you " + format_rm_time(rmtime, input, user_timezone) - if not user_tz_str: - response += '. Configure your personal timezone with `{0.clean_prefix}settimezone` for accurate times.'.format( - ctx) - await ctx.send(response) - - @remindme.command(hidden=True) - async def now(self, ctx, *, input): - await ctx.author.send(input) - - @remindme.command() - @checks.is_owner() #Command is unfinished - async def every(self, ctx, *, text): - await ctx.send("Test") - match = re.search(time_in_regeces[1], text, re.IGNORECASE) - if match: - tinstrs, tinstart, input = match.groups() - else: - match = re.search(time_in_regeces[0], text, re.IGNORECASE) - if not match: - await ctx.send("Invalid interval") - return - tinstart = "now" - tinstrs, input = match.groups() - start = (datetime.utcnow() + tin2tdelta(tinstart)).timestamp() - async with self.config.channel(ctx.channel).schedules() as scs: - scs.append((start, tin2tdelta(tinstrs).seconds, input)) - m = await ctx.send("Schedule created. I will send a message on my next cycle (10s or less)") - await asyncio.sleep(5.) - await m.delete() - - @remindme.command(aliases=["list"]) - async def get(self, ctx): - """Get a list of all pending reminders""" - rlist = sorted((await self.config.user(ctx.author).reminders()), key=lambda x: x[0]) - if not rlist: - await ctx.send(inline("You have no pending reminders!")) - return - tz = tzstr_to_tz(await self.config.user(ctx.author).tz()) - o = [] - for c, (timestamp, input) in enumerate(rlist): - o.append(str(c + 1) + ": " + format_rm_time(datetime.fromtimestamp(float(timestamp)), input, tz)) - o = "```\n" + '\n'.join(o) + "\n```" - await ctx.send(o) - - @remindme.command(name="remove") - async def remindme_remove(self, ctx, no: int): - """Remove a specific pending reminder""" - rlist = sorted(await self.config.user(ctx.author).reminders(), key=lambda x: x[0]) - if len(rlist) < no: - await ctx.send(inline("There is no reminder #{}".format(no))) - return - async with self.config.user(ctx.author).reminders() as rms: - rms.remove(rlist[no - 1]) - await ctx.send(inline("Done")) - - @remindme.command() - async def purge(self, ctx): - """Delete all pending reminders.""" - await self.config.user(ctx.author).reminders.set([]) - await ctx.send(inline("Done")) - - - @commands.group(invoke_without_command=True) - @checks.mod_or_permissions(administrator=True) - async def schedule(self, ctx, name, *, text): - """Sets up a schedule to fire at the specified interval - - [p]schedule name 1d Do something! - [p]schedule name 5 hours Do something! - """ - match = re.search(time_in_regeces[0], text, re.IGNORECASE) - if match: - tinstart = "now" - tinstrs, input = match.groups() - else: - match = re.search(time_in_regeces[1], text, re.IGNORECASE) - if not match: - if name.isdigit() or re.search(time_in_regeces[0], name+" .", re.IGNORECASE): - await ctx.send("Invalid interval. Remember to put 'name' as the first argument.") - else: - await ctx.send_help() - return - tinstart, tinstrs, input = match.groups() - start = (datetime.utcnow() + tin2tdelta(tinstart)).timestamp() - - async with self.config.guild(ctx.guild).schedules() as schedules: - if name in schedules: - await ctx.send("There is already a schedule with this name.") - return - schedules[name] = { - "start": start, - "time": start, - "end": 2e11, - "interval": tin2tdelta(tinstrs).seconds, - "enabled": True, - "channels": [ctx.channel.id], - "message": input, - } - m = await ctx.send("Schedule created.") - await asyncio.sleep(5.) - await m.delete() - - @schedule.command() - async def begin(self, ctx, name, *, time): - """Sets the start time for a schedule.""" - time = await self.exact_tartintodt(ctx, time) - if time is None: - return - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - schedules[name]['start'] = time.timestamp() - schedules[name]['time'] = time.timestamp() - await ctx.send(inline("Done.")) - - @schedule.command() - async def end(self, ctx, name, *, time): - """Sets the end time for a schedule.""" - time = await self.exact_tartintodt(ctx, time) - if time is None: - return - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - schedules[name]['end'] = time.timestamp() - await ctx.send(inline("Done.")) - - @schedule.command() - async def interval(self, ctx, name, *, time): - """Sets the interval for a schedule.""" - if not re.match(exact_tins[0], time): - await ctx.send("Invalid interval.") - return - interval = tin2tdelta(time) - if interval is None: - return - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - schedules[name]['interval'] = interval.seconds - await ctx.send(inline("Done.")) - - @schedule.command() - async def message(self, ctx, name, *, message): - """Sets the message for a schedule.""" - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - schedules[name]['message'] = message - await ctx.send(inline("Done.")) - - @schedule.group() - async def channel(self, ctx): - """Set or remove channels for a schedule.""" - - @channel.command() - async def add(self, ctx, name, channel: discord.TextChannel): - """Add a channel.""" - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - if channel.id in schedules[name]['channels']: - await ctx.send("This channel is already registered.") - return - schedules[name]['channels'].append(channel.id) - await ctx.send(inline("Done")) - - @channel.command(name="remove") - async def channel_remove(self, ctx, name, channel): - """Remove a channel.""" - c = bot.get_channel() - if c is None and channel.isdigit(): - channel_id = channel - elif c: - channel_id = channel.id - else: - await ctx.send("Invalid channel.") - return - - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - if channel_id not in schedules[name]['channels']: - await ctx.send("This channel is not registered.") - return - schedules[name]['channels'].remove(channel_id) - await ctx.send(inline("Done")) - - @channel.command(name="list") - async def channel_list(self, ctx, name): - """List the registered channels.""" - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - if not schedules[name]['channels']: - await ctx.send("This schedule has no channels.") - return - o = "" - for c in schedules[name]['channels']: - ch = self.bot.get_channel(c) - if ch: - o += "{} ({})\n".format(ch.name, c) - else: - o += "deleted-channel ({})\n".format(c) - for p in pagify(o): - await ctx.send(box(p)) - - @schedule.command() - async def enable(self, ctx, name): - """Enable a schedule.""" - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - schedules[name]['enabled'] = True - await ctx.send(inline("Done.")) - - @schedule.command() - async def disable(self, ctx, name): - """Disable a schedule.""" - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - schedules[name]['enabled'] = False - await ctx.send(inline("Done.")) - - @schedule.command(name="remove") - async def schedule_remove(self, ctx, name): - """Remove a schedule.""" - async with self.config.guild(ctx.guild).schedules() as schedules: - if name not in schedules: - await ctx.send("There is no schedule with this name.") - return - del schedules[name] - await ctx.send(inline("Deleted schedule {}.".format(name))) - - @schedule.command(name="list") - async def schedule_list(self, ctx): - """List the guild's schedules.""" - async with self.config.guild(ctx.guild).schedules() as schedules: - o = "" - for n,s in schedules.items(): - o += " - {}{}\n".format(n, " (disabled)" if not s['enabled'] else " (expired)" if s['end'] < datetime.utcnow().timestamp() else "") - o += " - Start Time: {}\n".format(datetime.fromtimestamp(s['start']).strftime(SHORT_DT_FORMAT)) - o += " - Next Time: {}\n".format(datetime.fromtimestamp(s['time']).strftime(SHORT_DT_FORMAT)) - o += " - End Time: {}\n".format(datetime.fromtimestamp(s['end']).strftime(SHORT_DT_FORMAT)) - o += " - Interval: {}\n".format(timedelta(seconds=s['interval'])) - o += " - Message: {}\n".format(s['message']) - if not o: - await ctx.send(inline("There are no schedules for this guild.")) - for page in pagify(o): - await ctx.send(box(page)) - - @commands.command(aliases=['settz']) - async def settimezone(self, ctx, tzstr): - """Set your timezone.""" - try: - v = tzstr_to_tz(tzstr) - await self.config.user(ctx.author).tz.set(tzstr) - await ctx.send(inline("Set personal timezone to {} ({})".format(str(v), get_tz_name(v)))) - except IOError as e: - await ctx.send(inline("Invalid tzstr: " + tzstr)) - - async def reminderloop(self): - await self.bot.wait_until_ready() - async for _ in rpadutils.repeating_timer(10, lambda: self == self.bot.get_cog('TimeCog')): - urs = await self.config.all_users() - gds = await self.config.all_guilds() - now = datetime.utcnow() - for u in urs: - for rm in urs[u]['reminders']: - if datetime.fromtimestamp(float(rm[0])) < now: - async with self.config.user(self.bot.get_user(u)).reminders() as rms: - rms.remove(rm) - await self.bot.get_user(u).send(rm[1]) - for g in gds: - for n, sc in gds[g]['schedules'].items(): - if datetime.fromtimestamp(sc['end']) < now or not sc['enabled']: - continue - if datetime.fromtimestamp(float(sc['time'])) < now: - async with self.config.guild(self.bot.get_guild(g)).schedules() as scs: - scs[n]['time'] += scs[n]['interval'] - for ch in sc['channels']: - await self.bot.get_channel(ch).send(sc['message']) - - @commands.command() - async def time(self, ctx, *, tz: str): - """Displays the current time in the supplied timezone""" - try: - tz_obj = tzstr_to_tz(tz) - except Exception as e: - await ctx.send("Failed to parse tz: " + tz) - return - - now = datetime.now(tz_obj) - msg = "The time in " + now.strftime('%Z') + " is " + fmt_time_short(now).strip() - await ctx.send(inline(msg)) - - @commands.command() - async def timeto(self, ctx, tz: str, *, time: str): - """Compute the time remaining until the [timezone] [time]""" - try: - tz_obj = tzstr_to_tz(tz) - except Exception as e: - await ctx.send("Failed to parse tz: " + tz) - return - - try: - time_obj = timestr_to_time(time) - except Exception as e: - await ctx.send("Failed to parse time: " + time) - return - - now = datetime.now(tz_obj) - req_time = now.replace(hour=time_obj.tm_hour, minute=time_obj.tm_min) - - if req_time < now: - req_time = req_time + timedelta(days=1) - delta = req_time - now - - msg = ("There are " + fmt_hrs_mins(delta.seconds).strip() + - " until " + time.strip() + " in " + now.strftime('%Z')) - await ctx.send(inline(msg)) - - async def exact_tartintodt(self, ctx, time, allowtat=True): - user_tz_str = await self.config.user(ctx.author).tz() - user_timezone = tzstr_to_tz(user_tz_str or 'UTC') - - for ar in exact_tats: - if not allowtat: - continue - match = re.match(ar, time, re.IGNORECASE) - if not match: - continue - match = match.groupdict() - - if not user_tz_str: - await ctx.send( - "Please configure your personal timezone with `{0.clean_prefix}settimezone` first.".format(ctx)) - return - - now = datetime.now(tz=user_timezone) - defaults = { - 'year': now.year, - 'month': now.month, - 'day': now.day, - 'hour': now.hour, - 'minute': now.minute, - 'merid': 'NONE' - } - defaults.update({k: v for k, v in match.items() if v}) - for key in defaults: - if key not in ['merid']: - defaults[key] = int(defaults[key]) - if defaults['merid'] == 'pm' and defaults['hour'] <= 12: - defaults['hour'] += 12 - elif defaults['merid'] == 'NONE' and defaults['hour'] < now.hour: - defaults['hour'] += 12 - if defaults['hour'] >= 24: - defaults['day'] += int(defaults['hour'] // 24) - defaults['hour'] = defaults['hour'] % 24 - del defaults['merid'] - try: - rmtime = user_timezone.localize(datetime(**defaults)) - except ValueError as e: - await ctx.send(inline(str(e).capitalize())) - return - if rmtime < now: - rmtime += timedelta(days=1) - rmtime = rmtime.astimezone(pytz.utc).replace(tzinfo=None) - break - else: - ir = exact_tins[0] - match = re.search(ir, time, re.IGNORECASE) - if not match: # Only use the first one - raise commands.UserFeedbackCheckFailure("Invalid time string: " + time) - tinstrs, = match.groups() - rmtime = datetime.utcnow() - try: - rmtime += tin2tdelta(tinstrs) - except OverflowError: - raise commands.UserFeedbackCheckFailure( - inline("That's way too far in the future! Please keep it in your lifespan!")) - - return rmtime - - -def timestr_to_time(timestr): - timestr = timestr.replace(" ", "") - try: - return time.strptime(timestr, "%H:%M") - except: - pass - try: - return time.strptime(timestr, "%I:%M%p") - except: - pass - try: - return time.strptime(timestr, "%I%p") - except: - pass - raise commands.UserFeedbackCheckFailure("Invalid Time: " + timestr) - - -def fmt_hrs_mins(seconds): - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - return '{}hrs {}mins'.format(int(hours), int(minutes)) - - -def fmt_time_short(dt): - return dt.strftime("%I:%M %p") - - -def tzstr_to_tz(tz): - tz = tz.lower().strip() - if tz in ['edt', 'est', 'et']: - tz = 'America/New_York' - elif tz in ['mdt', 'mst', 'mt']: - tz = 'America/North_Dakota/Center' - elif tz in ['pdt', 'pst', 'pt']: - tz = 'America/Los_Angeles' - elif tz in ['jp', 'jt', 'jst']: - return tz_lookup['JST'] - elif tz.upper() in tz_lookup: - return tz_lookup[tz.upper()] - else: - for tzo in pytz.all_timezones: - if tz.lower() in tzo.lower().split("/"): - tz = tzo - break - else: - for tzo in pytz.all_timezones: - if tz.lower() in tzo: - tz = tzo - break - try: - return pytz.timezone(tz) - except Exception as e: - raise commands.UserFeedbackCheckFailure("Invalid timezone: " + tz) - - -def tin2tdelta(tinstr): - if tinstr.lower().strip() == "now": - return timedelta(0) - tins = re.findall(r'(-?\d+) ?([a-z]+) ?', tinstr.lower()) - o = timedelta() - for tin, unit in tins: - try: - tin = int(tin) - if unit[:2] == 'mo': - o += relativedelta(months=+1) - elif unit[0] == 'm': - o += timedelta(minutes=tin) - elif unit[0] == 'h': - o += timedelta(hours=tin) - elif unit[0] == 'd': - o += timedelta(days=tin) - elif unit[0] == 'w': - o += timedelta(weeks=tin) - elif unit[0] == 'y': - o += relativedelta(years=+1) - elif unit[0] == 's': - raise commands.UserFeedbackCheckFailure( - "We aren't exact enough to use seconds! If you need that precision, try this: https://www.timeanddate.com/timer/") - else: - raise commands.UserFeedbackCheckFailure(inline( - "Invalid unit: {}\nPlease use minutes, hours, days, weeks, months, or, if you're feeling especially zealous, years.".format( - unit))) - except OverflowError: - raise commands.UserFeedbackCheckFailure(inline("Come on... Be reasonable :/")) - return o - - -def ydhm(seconds): - y, seconds = divmod(seconds, 60 * 60 * 24 * 365) - d, seconds = divmod(seconds, 60 * 60 * 24) - h, seconds = divmod(seconds, 60 * 60) - m, seconds = divmod(seconds, 60) - y, d, h, m = [int(ydhm) for ydhm in (y, d, h, m)] - ydhm = [] - if y: ydhm.append("{} yr" .format(y) + ("s" if y > 1 else '')) - if d: ydhm.append("{} day".format(d) + ("s" if d > 1 else '')) - if h: ydhm.append("{} hr" .format(h) + ("s" if h > 1 else '')) - if m: ydhm.append("{} min".format(m) + ("s" if m > 1 else '')) - return " ".join(ydhm) or "<1 minute" - - -def format_rm_time(rmtime, input, D_TZ): - return "'{}' on {} {} ({} from now)".format( - input, - D_TZ.fromutc(rmtime).strftime(DT_FORMAT), - get_tz_name(D_TZ, rmtime), - ydhm((rmtime - datetime.utcnow()).total_seconds() + 2) - ) - - -def get_tz_name(tz, dt=None): - if dt is None: - dt = datetime.utcnow() - else: - dt = dt.replace(tzinfo=None) - tzname = tz.tzname(datetime(year=dt.year, month=1, day=1)) - tznowname = tz.tzname(dt) - if tzname != tznowname and tznowname: - return "{} ({})".format(tzname, tznowname) - return tzname diff --git a/translate/__init__.py b/translate/__init__.py deleted file mode 100644 index 56afed9e..00000000 --- a/translate/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .translate import Translate - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - n=Translate(bot) - bot.add_cog(n) - bot.loop.create_task(n.build_service()) diff --git a/translate/info.json b/translate/info.json deleted file mode 100644 index d0627224..00000000 --- a/translate/info.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Translates text between langauges and writing systems.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Translation Cog", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [ - "romkan", - "google-api", - "google-api-python-client", - "google-cloud" - ], - "tags": ["translation"] -} diff --git a/translate/translate.py b/translate/translate.py deleted file mode 100644 index 4c39c2bd..00000000 --- a/translate/translate.py +++ /dev/null @@ -1,87 +0,0 @@ -import discord -import romkan - -from googleapiclient.discovery import build -from redbot.core import commands, Config, checks -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import inline - - -class Translate(commands.Cog): - """Translation utilities.""" - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.config = Config.get_conf(self, identifier=724757473) - self.config.register_global(api_key=None) - - self.service = None - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.group() - @checks.is_owner() - async def translate(self, context): - """Translation utilities.""" - - async def build_service(self): - api_key = await self.config.api_key() - try: - assert api_key - self.service = build('translate', 'v2', developerKey=api_key) - except: - print("Google API key not found or invalid") - - @commands.command(aliases=['jaus', 'jpen', 'jpus']) - async def jaen(self, ctx, *, query): - """Translates from Japanese to English""" - if not self.service: - await ctx.send(inline('Set up an API key first!')) - return - - em = self.translate_to_embed("ja", "en", query) - await ctx.send(embed=em) - - @commands.command(aliases=['zhus']) - async def zhen(self, ctx, *, query): - """Translates from Chinese to English""" - if not self.service: - await ctx.send(inline('Set up an API key first!')) - return - - em = self.translate_to_embed("zh", "en", query) - await ctx.send(embed=em) - - @commands.command() - async def kanrom(self, ctx, *, query): - """Transliterates Kanji to Romanji""" - await ctx.send(romkan.to_roma(query)) - - def translate_lang(self, source, target, query): - result = self.service.translations().list(source=source, target=target, format='text', q=query).execute() - return result.get('translations')[0].get('translatedText') - - def translate_to_embed(self, source, target, query): - translation = self.translate_lang(source, target, query) - return discord.Embed(description='**Original**\n`{}`\n\n**Translation**\n`{}`'.format(query, translation)) - - @translate.command() - async def setkey(self, ctx, api_key): - """Sets the google api key.""" - await self.config.api_key.set(api_key) - await ctx.send(inline("done")) - - @translate.command() - async def getkey(self, ctx): - await ctx.author.send(await self.config.api_key()) diff --git a/trigger/__init__.py b/trigger/__init__.py deleted file mode 100644 index 643916a6..00000000 --- a/trigger/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .trigger import Trigger - -__red_end_user_data_statement__ = "Triggers are stored persistantly." - -def setup(bot): - n = Trigger(bot) - bot.loop.create_task(n.save_stats()) - bot.loop.create_task(n.load_triggers()) - bot.add_cog(n) diff --git a/trigger/info.json b/trigger/info.json deleted file mode 100644 index e0561290..00000000 --- a/trigger/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["Twentysix26", "Aradia Megido#2552"], - "description": "Setup commands that trigger certain behaviors.", - "end_user_data_statement": "Triggers are stored persistantly.", - "install_msg": "", - "short": "You're an eyesore!", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [], - "tags": ["general", "customcommands"] -} diff --git a/trigger/trigger.py b/trigger/trigger.py deleted file mode 100644 index 1e432b0a..00000000 --- a/trigger/trigger.py +++ /dev/null @@ -1,621 +0,0 @@ -import discord -import datetime -import os -import asyncio -import re - -from redbot.core import checks, commands, Config -from redbot.core.utils.chat_formatting import box, pagify, escape -from random import choice - - -class TriggerError(Exception): - pass - - -class Unauthorized(TriggerError): - pass - - -class NotFound(TriggerError): - pass - - -class AlreadyExists(TriggerError): - pass - - -class Trigger(commands.Cog): - """Custom triggers""" - - def __init__(self, bot): - self.bot = bot - self.config = Config.get_conf(self, identifier=7173306) - self.config.register_global(triggers=[]) - - self.triggers = [] - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - c = 0 - for trigger in self.triggers: - if trigger.owner == user_id: - c += 1 - data = "You own {} triggers.\n".format(c) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data.""" - for trigger in self.triggers: - if trigger.owner == user_id: - trigger.owner = None - - @commands.group() - @commands.guild_only() - async def trigger(self, ctx): - """Trigger creation commands""" - - @trigger.command() - @checks.admin_or_permissions(administrator=True) - async def create(self, ctx, trigger_name : str, *, triggered_by : str): - """Creates a trigger""" - try: - self.create_trigger(trigger_name, triggered_by, ctx) - except AlreadyExists: - await ctx.send("A trigger with that name already exists.") - else: - await self.save_triggers() - await ctx.send("Trigger created. Entering interactive " - "add mode...".format(ctx.prefix)) - trigger = self.get_trigger_by_name(trigger_name) - await self.interactive_add_mode(trigger, ctx) - await self.save_triggers() - - @trigger.command() - @checks.admin_or_permissions(administrator=True) - async def delete(self, ctx, trigger_name : str): - """Deletes a trigger""" - try: - self.delete_trigger(trigger_name, ctx) - await self.save_triggers() - except Unauthorized: - await ctx.send("You're not authorized to delete that trigger.") - except NotFound: - await ctx.send("That trigger doesn't exist.") - else: - await ctx.send("Trigger deleted.") - - @trigger.command() - @checks.admin_or_permissions(administrator=True) - async def add(self, ctx, trigger_name : str, *, response : str=None): - """Adds a response to a trigger - - Leaving the response argument empty will enable interactive mode - - Owner only: - Adding a response as 'file: filename.jpg' will send that file as - response if present in data/trigger/files""" - trigger = self.get_trigger_by_name(trigger_name) - - if trigger is None: - await ctx.send("That trigger doesn't exist.") - return - if not trigger.can_edit(ctx.author): - await ctx.send("You're not allowed to edit that trigger.") - return - - if response is not None: - trigger.responses.append(response) - await ctx.send("Response added.") - else: # Interactive mode - await self.interactive_add_mode(trigger, ctx) - await self.save_triggers() - - @trigger.command() - @checks.admin_or_permissions(administrator=True) - async def remove(self, ctx, trigger_name : str): - """Lets you choose a response to remove""" - trigger = self.get_trigger_by_name(trigger_name) - - if trigger is None: - await ctx.send("That trigger doesn't exist.") - return - if trigger.responses == []: - await ctx.send("That trigger has no responses to delete.") - return - if not trigger.can_edit(ctx.author): - await ctx.send("You're not allowed to do that.") - return - - msg = None - current_list = None - past_messages = [] - quit_msg = "\nType 'exit' to quit removal mode." - - while self.get_n_trigger_responses(trigger) is not None: - r_list = self.get_n_trigger_responses(trigger, truncate=100) - if current_list is None: - current_list = await ctx.send(r_list + quit_msg) - else: - if r_list != current_list.content: - await current_list.edit(content=r_list + quit_msg) - msg = await self.bot.wait_for("message", timeout=15.0, check=lambda m:m.author==ctx.author) - if msg is None: - await ctx.send("Nothing else to remove I guess.") - break - elif msg.content.lower().strip() == "exit": - past_messages.append(msg) - await ctx.send("Removal mode quit.") - break - try: - i = int(msg.content) - del trigger.responses[i] - except: - pass - past_messages.append(msg) - - if not trigger.responses: - await ctx.send("No more responses to delete.") - - past_messages.append(current_list) - await self.attempt_cleanup(past_messages) - - async def attempt_cleanup(self, messages): - try: - for message in messages: - await message.delete() - except: - pass - - @trigger.command() - async def info(self, ctx, trigger_name : str): - """Shows a trigger's info""" - trigger = self.get_trigger_by_name(trigger_name) - if trigger: - msg = "Name: {}\n".format(trigger.name) - owner_name = discord.utils.get(self.bot.get_all_members(), id=trigger.owner) - owner_name = owner_name if owner_name is not None else "not found" - msg += "Owner: {} ({})\n".format(owner_name, trigger.owner) - trigger_type = "all responses" if trigger.type == "all" else "random response" - msg += "Type: {}\n".format(trigger_type) - influence = "server" if trigger.server is not None else "global" - msg += "Influence: {}\n".format(influence) - cs = "yes" if trigger.case_sensitive else "no" - msg += "Case Sensitive: {}\n".format(cs) - regex = "yes" if trigger.regex else "no" - msg += "Regex: {}\n".format(regex) - msg += "Cooldown: {} seconds\n".format(trigger.cooldown) - msg += "Triggered By: \"{}\"\n".format(trigger.triggered_by.replace("`", "\\`")) - msg += "Payload: {} responses\n".format(len(trigger.responses)) - msg += "Triggered: {} times\n".format(trigger.triggered) - await ctx.send(box(msg, lang="xl")) - else: - await ctx.send("There is no trigger with that name.") - - @trigger.command() - async def show(self, ctx, trigger_name : str): - """Shows all responses of a trigger""" - trigger = self.get_trigger_by_name(trigger_name) - if trigger: - payload = self.elaborate_payload(trigger.responses, truncate=9999) - if payload: - payload = "\n\n".join(payload) - if len(payload) > 2000: - for page in pagify(payload, delims=[" "]): - await ctx.author.send(page) - else: - await ctx.send(payload) - else: - await ctx.send("That trigger has no responses.") - else: - await ctx.send("That trigger doesn't exist.") - - @trigger.command(name="list") - async def _list(self, ctx, trigger_type="local"): - """Lists local / global triggers - - Defaults to local""" - server = ctx.guild - results = [] - if trigger_type == "local": - for trigger in self.triggers: - if trigger.server == server.id: - results.append(trigger) - elif trigger_type == "global": - for trigger in self.triggers: - if trigger.server is None: - results.append(trigger) - else: - await ctx.send("Invalid type.") - return - if results: - results = ", ".join([r.name for r in results]) - for page in pagify(results, delims=[" ", "\n"]): - await ctx.send("```\n{}\n```".format(page.lstrip(" "), results)) - else: - await ctx.send("I couldn't find any trigger of that type.") - - @trigger.command() - async def search(self, ctx, *, search_terms : str): - """Returns triggers matching the search terms""" - result = self.search_triggers(search_terms.lower()) - if result: - result = ", ".join(sorted([t.name for t in result])) - await ctx.send("Triggers found:\n\n{}".format(result)) - else: - await ctx.send("No triggers matching your search.") - - @commands.group() - @commands.guild_only() - @checks.admin_or_permissions(administrator=True) - async def triggerset(self, ctx): - """Edits the settings of a trigger""" - - @triggerset.command() - async def cooldown(self, ctx, trigger_name : str, seconds : int): - """Sets the trigger's cooldown""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - if seconds < 1: - seconds = 1 - trigger.cooldown = seconds - await self.save_triggers() - await ctx.send("Cooldown set to {} seconds.".format(seconds)) - - @triggerset.command() - async def phrase(self, ctx, trigger_name : str, *, triggered_by : str): - """Sets the word/phrase by which the trigger is activated by""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - if not triggered_by: - await ctx.send("Invalid setting.") - return - trigger.triggered_by = triggered_by - await self.save_triggers() - await ctx.send("The trigger will be activated by `{}`.".format(triggered_by)) - - @triggerset.command() - async def response(self, ctx, trigger_name : str, _type : str): - """Sets the response type for the trigger. - - Available types: all, random - - All will show all responses in order - Random will pick one at random""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - _type = _type.lower() - if _type not in ("all", "random"): - await ctx.send("Invalid type.") - return - trigger.type = _type - await self.save_triggers() - await ctx.send("Response type set to {}.".format(_type)) - - @triggerset.command() - @checks.is_owner() - async def influence(self, ctx, trigger_name : str, _type : str): - """Sets the influence of the trigger. - - Available types: server, global""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - _type = _type.lower() - if _type not in ("server", "global"): - await ctx.send("Invalid type.") - return - trigger.server = ctx.guild.id if _type == "server" else None - await self.save_triggers() - await ctx.send("Influence set to {}.".format(_type)) - - @triggerset.command() - async def channels(self, ctx, trigger_name : str, *channels : discord.TextChannel): - """Sets the channel(s) in which the trigger will be active - - Not entering any channel will revert the trigger to server-wide""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - if channels: - channels = [c.id for c in channels] - trigger.channels[ctx.guild.id] = list(channels) - await self.save_triggers() - if trigger.server is not None: - await ctx.send("The trigger will be enabled only on " - "those channels.") - else: - await ctx.send("In this server the trigger will be " - "enabled only on those channels") - else: - trigger.channels[ctx.guild.id] = [] - await self.save_triggers() - await ctx.send("The trigger will be active in all channels.") - - @triggerset.command() - async def casesensitive(self, ctx, trigger_name : str, - true_or_false : bool): - """Toggles the trigger's case sensitivity. - - Can be true or false""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - trigger.case_sensitive = true_or_false - await self.save_triggers() - await ctx.send("Case sensitivity set to {}.".format(true_or_false)) - - @triggerset.command() - async def regex(self, ctx, trigger_name : str, true_or_false : bool): - """Toggles the trigger's case capabilities. - - Can be true or false""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - trigger.regex = true_or_false - await self.save_triggers() - await ctx.send("Regex set to {}.".format(true_or_false)) - - @triggerset.command() - async def active(self, ctx, trigger_name : str, true_or_false : bool): - """Toggles the trigger on/off. - - Can be true or false""" - trigger = self.get_trigger_by_name(trigger_name) - if not await self.settings_check(ctx, trigger, ctx.author): - return - trigger.active = true_or_false - await self.save_triggers() - await ctx.send("Trigger active: {}.".format(true_or_false)) - - async def settings_check(ctx, self, trigger, author): - if not trigger: - await ctx.send("That trigger doesn't exist.") - return False - if not trigger.can_edit(author): - await ctx.send("You're not authorized to edit that triggers' " - "settings.") - return False - return True - - def get_trigger_by_name(self, name): - for trigger in self.triggers: - if trigger.name.lower() == name.lower(): - return trigger - return None - - def search_triggers(self, search_terms): - results = [] - for trigger in self.triggers: - if search_terms in trigger.name.lower(): - results.append(trigger) - continue - for payload in trigger.responses: - if search_terms in payload.lower(): - results.append(trigger) - break - else: - if search_terms in trigger.triggered_by.lower(): - results.append(trigger) - return results - - def create_trigger(self, name, triggered_by, ctx): - trigger = self.get_trigger_by_name(name) - if not trigger: - trigger = TriggerObj(bot=self.bot, - name=name, - triggered_by=triggered_by, - owner=ctx.author.id, - server=ctx.guild.id - ) - self.triggers.append(trigger) - else: - raise AlreadyExists() - - def delete_trigger(self, name, ctx): - trigger = self.get_trigger_by_name(name) - if trigger: - if not trigger.can_edit(ctx.author): - raise Unauthorized() - self.triggers.remove(trigger) - else: - raise NotFound() - - def elaborate_payload(self, payload, truncate=50, escape=True): - shortened = [] - for p in payload: - if escape: - p = escape(p) - if len(p) < truncate: - shortened.append(p) - else: - shortened.append(p[:truncate] + "...") - return shortened - - async def interactive_add_mode(self, trigger, ctx): - msg = "" - await ctx.send("Everything you type will be added as response " - "to the trigger. Type 'exit' to quit.") - while msg is not None: - msg = await self.bot.wait_for("message", timeout=60.0, check=lambda m:m.author==ctx.author) - if msg is None: - await ctx.send("No more responses then. " - "Your changes have been saved.") - break - if msg.content.lower().strip() == "exit": - await ctx.send("Your changes have been saved.") - break - trigger.responses.append(msg.content) - - def get_n_trigger_responses(self, trigger, *, truncate=2000): - msg = "" - responses = trigger.responses - i = 0 - for r in responses: - if len(r) > truncate: - r = r[:truncate] + "..." - r = r.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_") - msg += "{}. {}\n".format(i, r) - i += 1 - if msg != "": - return box(msg) - else: - return None - - async def is_command(self, msg): - prefixes = await self.bot.get_prefix(msg) - if not isinstance(prefixes, list): - prefixes = [prefixes] - for p in prefixes: - if msg.content.startswith(p): - return True - return False - - def elaborate_response(self, trigger, r): - is_owner = trigger.owner in self.bot.owner_ids - if not is_owner: - return "text", r - if not r.startswith("file:"): - return "text", r - else: - path = r.replace("file:", "").strip() - path = os.path.join("data", "trigger", "files", path) - if os.path.isfile(path): - return "file", path - else: - return "text", r - - @commands.Cog.listener('on_message') - async def on_message(self, message): - channel = message.channel - author = message.author - - if message.guild is None: - return - - if author == self.bot.user: - return - - if await self.is_command(message): - return - - for trigger in self.triggers: - if not trigger.check(message): - continue - payload = trigger.payload() - for p in payload: - resp_type, resp = self.elaborate_response(trigger, p) - if resp_type == "text": - await channel.send(resp) - elif resp_type == "file": - await channel.send(file=resp) - - async def save_stats(self): - """Saves triggers every 10 minutes to preserve stats""" - await self.bot.wait_until_ready() - try: - await asyncio.sleep(60) - while True: - await self.save_triggers() - await asyncio.sleep(60 * 10) - except asyncio.CancelledError: - pass - - async def load_triggers(self): - triggers = await self.config.triggers() - for trigger in triggers: - trigger["bot"] = self.bot - self.triggers.append(TriggerObj(**trigger)) - - async def save_triggers(self): - triggers = [t.export() for t in self.triggers] - await self.config.triggers.set(triggers) - - -class TriggerObj: - def __init__(self, **kwargs): - self.bot = kwargs.get("bot") - self.name = kwargs.get("name") - self.owner = kwargs.get("owner") - self.triggered_by = kwargs.get("triggered_by") - self.responses = kwargs.get("responses", []) - self.server = kwargs.get("server") # if it's None, the trigger will be implicitly global - self.channels = kwargs.get("channels", {}) - self.type = kwargs.get("type", "all") # Type of payload. Types: all, random - self.case_sensitive = kwargs.get("case_sensitive", False) - self.regex = kwargs.get("regex", False) - self.cooldown = kwargs.get("cooldown", 1) # Seconds - self.triggered = kwargs.get("triggered", 0) # Counter - self.last_triggered = datetime.datetime(1970, 2, 6) # Initialized - self.active = kwargs.get("active", True) - - def export(self): - data = self.__dict__.copy() - del data["bot"] - del data["last_triggered"] - return data - - def check(self, msg): - if not self.active: - return False - - channels = self.channels.get(msg.guild.id, []) - if channels: - if msg.channel.id not in channels: - return False - - content = msg.content - triggered_by = self.triggered_by - - if (self.server == msg.guild.id or self.server is None) is False: - return False - - if not self.case_sensitive: - triggered_by = triggered_by.lower() - content = content.lower() - - if not self.regex: - if triggered_by not in content: - return False - else: - found = re.search(triggered_by, content) - if not found: - return False - - timestamp = datetime.datetime.now() - passed = (timestamp - self.last_triggered).seconds - if passed > self.cooldown: - self.last_triggered = timestamp - return True - else: - return False - - def payload(self): - if self.responses: - self.triggered += 1 - if self.type == "all": - return self.responses - elif self.type == "random": - if self.responses: - return [choice(self.responses)] - else: - return [] - else: - raise RuntimeError("Invalid trigger type.") - - def can_edit(self, user): - server = user.guild - # Using the is_owner_check would be better but I don't always have - # context here, nor I feel like mocking it - is_owner = user.id in self.bot.owner_ids - is_admin = user.guild_permissions.administrator - is_trigger_owner = user.id == self.owner - trigger_is_global = self.server is None - if trigger_is_global: - return is_trigger_owner or is_owner - else: - return is_trigger_owner or is_owner or is_admin diff --git a/trutils/__init__.py b/trutils/__init__.py deleted file mode 100644 index 5dbf0f51..00000000 --- a/trutils/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .trutils import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(TrUtils(bot)) diff --git a/trutils/info.json b/trutils/info.json deleted file mode 100644 index 213fcbd9..00000000 --- a/trutils/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Commands for bot owners.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Owner Utils (and credits)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["utility", "moderation", "administration", "developer"] -} diff --git a/trutils/trutils.py b/trutils/trutils.py deleted file mode 100644 index b2d148f1..00000000 --- a/trutils/trutils.py +++ /dev/null @@ -1,526 +0,0 @@ -import asyncio -import datetime -import re -import sys - -import discord -from redbot.core import checks, modlog, commands -from redbot.core.utils.chat_formatting import inline, box, pagify - -from rpadutils import CogSettings - - -USER_HELP = """ -Bot user help -This command gives you an overview of the most commonly used user-focused -commands, with an emphasis on the ones unique to this bot. - -Join the Tsubaki Support Server for info, update, and bot support: -https://discord.gg/tVPmeG8 - -Use {0.prefix}help to get a full list of help commands. Execute any command with no -arguments to get more details on how they work. - -Info commands: -{0.prefix}credits some info about the bot -{0.prefix}donate info on how to donate to cover hosting fees -{0.prefix}userhelp this message -{0.prefix}modhelp bot help specifically for mods - -General: -{0.prefix}pad lists the pad-specific global commands -{0.prefix}padfaq lists pad-specific FAQ commands -{0.prefix}boards lists common leader optimal board commands -{0.prefix}glossary looks up a pad term in the glossary -{0.prefix}customcommands lists the custom commands added by the administrators of your server -{0.prefix}memes works the same way, but is restricted per-server to a privileged memer-only group -{0.prefix}serverinfo stats for the current server -{0.prefix}userinfo stats for a specific user - -Monster Info: -{0.prefix}id search for a monster by ID, full name, nickname, etc -{0.prefix}helpid gets more info on how monster lookup works, including the nickname submission link -{0.prefix}evolist list all the evolutions for a monster, tabbed -{0.prefix}pic display the image for a monster - -Profile: -Tsubaki will store your personal PAD details, and provide them on request. -Use the series of commands starting with {0.prefix}profile to configure your own profile. - -Use one of the following commands to retrieve data. -{0.prefix}idme send your profile to the current channel -{0.prefix}idfor get profile data for a specific user -{0.prefix}idto have Tsubaki DM your profile to a user -{0.prefix}profile search search the list of configured (visible) profiles - -Time conversion: -{0.prefix}time get the current time in a different timezone -{0.prefix}timeto calculate the how long until another time in another timezone - -Translation: -{0.prefix}[jpen|jpus|jaen|jaus] translate text from japanese to english -""" - -MOD_HELP = """ -Bot Moderator Help -~~~~~~~~~~~~~~~~~~~~~~ - -Tsubaki is a set of plugins inside the Red Discord bot, running on discord.py. There -are some custom ones, but a lot of them are generic to all Red Discord bots, so -things you've used elsewhere will probably also work here. - -If there is a feature you're missing, let me know and I can check to see if it's -already available in some public plugin. If not, and I think it's valuable, I might -write it. - -~~~~~~~~~~~~~~~~~~~~~~ - -Check out the {0.prefix}help command from inside your server. You'll see a wider list of -commands than normal users do. - -If you've just added Tsubaki to your server, start with the {0.prefix}modset command. You -might want to configure an Admin and a Mod role (they can be the same thing). - -~~~~~~~~~~~~~~~~~~~~~~ -Interesting features -~~~~~~~~~~~~~~~~~~~~~~ - -Self applied roles: -You can configure which roles a user can add to themself using {0.prefix}selfrole via {0.prefix}adminset - -Message logs: -Discord doesn't save deleted/edited messages anywhere. Using {0.prefix}exlog you can pull -messages for a user, channel, or search for a term. - -Auto Moderation: -The {0.prefix}automod2 command allows you to configure a set of rules (defined as regular expressions) -that match messages. You can then apply these rules as either a blacklist or a whitelist to -a specific channel. This allows you to force users to format their messages a specific way, -or to prevent them from saying certain things (the bot deletes violators, and notifies them -via DM). - -Bad user tools: -Allows you to specify a set of roles that are applied as punishments to users, generally -restricting them from seeing or speaking in certain channels. If a punishment role is -applied to a user, the last 10 things they said (and where they said it) are recorded, and -a strike is added to their record. - -You can configure a channel where Tsubaki will log when these moderation events occur, and ping -@here asking for an explanation. She will also track when a user with a strike leaves the -server, and when they rejoin the server (as this is generally done to evade negative roles). - -Custom commands: -Tsubaki supports three types of custom commands, you can find the list of associated commands via {0.prefix}help. -* CustomCommands: Added by server mods, executable by anyone -* Memes: Added by server mods, executable only by people with a specific Role (configured by mods) -* Pad: Added by Tsubaki PAD admins and executable by users in any server - -Limiting command execution: -The '{0.prefix}p' command can be used to prevent users from executing specific commands on the server, -in specific channels, or unless they have specific roles. Read the documentation carefully. -""" - - -class TrUtils(commands.Cog): - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = TrUtilsSettings("trutils") - - self.sessions = set() - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.command() - async def revertname(self, ctx): - """Unsets your nickname""" - await ctx.author.edit(nick=None) - await ctx.send(inline('Done')) - - @commands.command() - @checks.is_owner() - async def loadallcogs(self, ctx): - cogs = ['RpadUtils', 'AutoMod2', 'ChannelMod', 'Donations', 'FancySay', 'Memes', - 'PadBoard', 'Profile', 'Stickers', 'StreamCopy', 'Translate', 'VoiceRole', - 'Dadguide', 'PadGlobal', 'PadInfo'] - - owner_cog = self.bot.get_cog('Core') - - for cog_name in cogs: - cog = self.bot.get_cog(cog_name) - if cog is None: - await ctx.send('{} not loaded, trying to load it...'.format(cog_name)) - try: - await ctx.invoke(owner_cog.load, cog_name.lower()) - except Exception as e: - await ctx.send(box("Loading cog failed: {}: {}".format(e.__class__.__name__, str(e)))) - await ctx.send('Done!') - - @commands.command(hidden=True) - async def traceback2(self, ctx: commands.Context, public: bool = False): - """Sends to the owner the last command exception that has occurred - - If public (yes is specified), it will be sent to the chat instead""" - if ctx.author.id not in [144250811315257344, 86605480876601344]: - return - - if not public: - destination = ctx.author - else: - destination = ctx.channel - - if self.bot._last_exception: - for page in pagify(self.bot._last_exception, shorten_by=10): - await destination.send(box(page, lang="py")) - else: - await ctx.send("No exception has occurred yet") - - @commands.command() - async def userhelp(self, ctx): - """Shows a summary of the useful user features""" - for page in pagify(USER_HELP.format(ctx), delims=['\n'], shorten_by=8): - await ctx.author.send(box(page)) - - @commands.command() - @checks.mod_or_permissions(manage_guild=True) - async def modhelp(self, ctx): - """Shows a summary of the useful moderator features""" - for page in pagify(MOD_HELP.format(ctx), delims=['\n'], shorten_by=8): - await ctx.author.send(box(page)) - - @commands.command() - async def credits(self, ctx): - """Shows info about this bot""" - author_repo = "https://github.com/Twentysix26" - red_repo = author_repo + "/Red-DiscordBot" - rpad_invite = "https://discord.gg/pad" - - about = ( - "This is an instance of [the Red Discord bot]({}), " - "use the '{}info' command for more info. " - "The various PAD related cogs were created by Aradia Megido and tactical_retreat. " - "Massive overhaul of the bot to Red v3 by Aradia Megido. " - "This bot was created for the [PAD Community Server Discord]({}) but " - "is available for other servers on request." - "".format(red_repo, ctx.prefix, rpad_invite)) - - avatar = ( - "Bot avatars supplied by:\n" - "\t[Tsubaki]({}): {}").format("https://twitter.com/_violebot", "Violebot") - - using = ( - "You can use `{0.prefix}help` to get a full list of commands.\n" - "Use `{0.prefix}userhelp` to get a summary of useful user features.\n" - "Use `{0.prefix}modhelp` to get info on moderator-only features." - ) - - embed = discord.Embed() - embed.add_field(name="Instance owned by", value='The Tsubaki Team') - embed.add_field(name="About the bot", value=about, inline=False) - embed.add_field(name="Using the bot", value=using.format(ctx), inline=False) - embed.add_field(name="Avatar credits", value=avatar, inline=False) - embed.set_thumbnail(url=self.bot.user.avatar_url) - - try: - await ctx.send(embed=embed) - except discord.HTTPException: - await ctx.send("I need the `Embed links` permission to send this") - - @commands.command(hidden=True) - @checks.is_owner() - async def supersecretdebug(self, ctx, *, code): - """Executes superdebug and then deletes the message that triggered it. - - This is useful if you want to make the bot appear to say or do something that can't - otherwise be easily done. - """ - await self._superdebug(ctx, code=code) - await ctx.message.delete() - - @commands.command(hidden=True) - @checks.is_owner() - async def superdebug(self, ctx, *, code): - """Evaluates code, with a helper for asynchronous results (like ctx.send()). - - If you just want to evaluate variables, repl is a better alternative. - """ - await self._superdebug(ctx, code=code) - - async def _superdebug(self, ctx, *, code): - def check(m): - if m.content.strip().lower() == "more": - return True - - author = ctx.author - channel = ctx.channel - - code = code.strip('` ') - result = None - - global_vars = globals().copy() - global_vars['bot'] = self.bot - global_vars['ctx'] = ctx - global_vars['message'] = ctx.message - global_vars['author'] = ctx.author - global_vars['channel'] = ctx.channel - global_vars['guild'] = ctx.guild - - local_vars = locals().copy() - local_vars['to_await'] = list() - - try: - eval(compile(code, '', 'exec'), global_vars, local_vars) - to_await = local_vars['to_await'] - except Exception as e: - await ctx.send(box('{}: {}'.format(type(e).__name__, str(e)), - lang="py")) - return - - for result in to_await: - if asyncio.iscoroutine(result): - try: - result = await result - except Exception as e: - await ctx.send(box('{}: {}'.format(type(e).__name__, str(e)), - lang="py")) - else: - await ctx.send(result) - - @commands.command() - @checks.is_owner() - async def superfuckingban(self, ctx, user: discord.User, *, reason: str): - """Really fucking bans someone. - - This will ban a user from every guild that the bot can ban them from. Use with caution. - """ - msg = 'Ban report for {} ({}):'.format(user.name, user.id) - for guild in self.bot.guilds: - try: - ban_list = await guild.bans() - if user.id in [x.user.id for x in ban_list]: - msg += '\n\tUser already banned from {}'.format(guild.name) - continue - except: - msg += '\n\tNot allowed to ban in {}; nothing I can do here'.format(guild.name) - continue - - m = guild.get_member(user.id) - if m is None: - try: - await self.bot.http.ban(user.id, guild.id, 0) - msg += '\n\tUser not in {}; added to hackban'.format(guild.name) - await modlog.create_case(bot=self.bot, - guild=guild, - created_at=datetime.datetime.now(), - action_type="hackban", - moderator=ctx.author, - user=user, - reason='SuperBan by bot owner: {}'.format(reason)) - except Exception as ex: - msg += '\n\tUser not in {}; hackban failed: {}'.format(guild.name, ex) - continue - try: - await m.ban(delete_message_days=0) - msg += '\n\tBanned from {}'.format(guild.name) - await modlog.create_case(bot=self.bot, - guild=guild, - created_at=datetime.datetime.now(), - action_type="ban", - moderator=ctx.author, - user=user, - reason='SuperBan by bot owner: {}'.format(reason)) - except Exception as ex: - msg += '\n\tFailed to ban from {} because {}'.format(guild.name, ex) - - for page in pagify(msg): - await ctx.send(box(page)) - - async def _send_feedback(self, ctx, message: str, feedback_channel, success_message: str): - if feedback_channel is None: - raise commands.UserFeedbackCheckFailure("Feedback channel not set") - - guild = ctx.guild - author = ctx.author - footer = "User ID: " + str(author.id) - - if guild: - source = "from {}".format(guild) - footer += " | Guild ID: " + str(guild.id) - else: - source = "through DM" - - description = "Sent by {} {}".format(author, source) - - e = discord.Embed(description=message) - if author.avatar_url: - e.set_author(name=description, icon_url=author.avatar_url) - else: - e.set_author(name=description) - e.set_footer(text=footer) - - try: - await feedback_channel.send(embed=e) - except: - await ctx.send(inline("I'm unable to deliver your message. Sorry.")) - else: - await ctx.send(inline("Your message has been sent." - " Abusing this feature will result in a blacklist." - + success_message)) - - @commands.command() - @commands.cooldown(1, 60, commands.BucketType.user) - async def feedback(self, ctx, *, message: str): - """Provide feedback on the bot. - - Use this command to provide public feedback on the bot. - """ - feedback_channel = self.bot.get_channel(int(self.settings.get_feedback_channel())) - await self._send_feedback(ctx, message, feedback_channel, - " Join the Tsubaki Server to see any responses ({0.prefix}tsubakiserver).".format(ctx)) - - @commands.command() - @commands.guild_only() - @checks.is_owner() - async def setfeedbackchannel(self, ctx, channel: discord.TextChannel): - """Set the feedback destination channel.""" - self.settings.set_feedback_channel(channel.id) - await ctx.send(inline('Done')) - - @commands.command(aliases=['mamafeedback', 'renifeedback']) - @commands.cooldown(1, 60, commands.BucketType.user) - async def blogfeedback(self, ctx, *, message: str): - """Provide feedback on Reni's blog or translations. - - Use this command to submit feedback on https://pad.protic.site or the JP translations. - """ - feedback_channel = self.bot.get_channel(int(self.settings.get_blog_feedback_channel())) - await self._send_feedback(ctx, message, feedback_channel, - " Join the PDX Server to see any responses ({0.prefix}pdx).".format(ctx)) - - @commands.command() - @commands.guild_only() - @checks.is_owner() - async def setblogfeedbackchannel(self, ctx, channel: discord.TextChannel): - """Set the blog feedback destination channel.""" - self.settings.set_blog_feedback_channel(channel.id) - await ctx.send(inline('Done')) - - @commands.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def mentionable(self, ctx, role: discord.Role): - """Toggle the mentionability of a role.""" - try: - new_mentionable = not role.mentionable - await role.edit(mentionable=new_mentionable) - await ctx.send(inline('Role is now {}mentionable'.format('' if new_mentionable else 'un'))) - except Exception as ex: - await ctx.send(inline('Error: failed to alter role')) - - @commands.command() - @checks.is_owner() - async def freload(self, ctx, cmd, *, args=""): - """Run a command after reloading its base cog.""" - full_cmd = "{}{} {}".format(ctx.prefix, cmd, args) - cmd = self.bot.get_command(cmd) - if cmd is None: - await ctx.send("Invalid Command: {}".format(full_cmd)) - return - _send = ctx.send - async def fakesend(*args, **kwargs): pass - - ctx.send = fakesend - await self.bot.get_cog("Core").reload(ctx, cmd.cog.__module__.split('.')[0]) - - ctx.send = _send - ctx.message.content = full_cmd - await self.bot.process_commands(ctx.message) - - @commands.command() - @checks.is_owner() - async def pipupdate(self, ctx, module="Red-DiscordBot", updatepip=True): - async with ctx.typing(): - process = await asyncio.create_subprocess_exec( - sys.executable, "-m", "pip", "install", "-U", module, - - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - stdout, stderr = (bts.decode() if bts else "" for bts in await process.communicate()) - - if stderr.startswith("WARNING: You are using pip version"): - if updatepip: - await ctx.send(stderr.split('You should consider')[0]+'\n\nUpdating pip...') - await self.pipupdate(ctx, 'pip', False) - stderr = "" - - if stderr: - await ctx.author.send("Error updating:\n" + stderr) - await ctx.send(inline("Error (sent via DM)")) - else: - await ctx.send(inline('Done')) - - @commands.command(hidden=True, aliases=["make_aa", "makearadia"]) - async def makeaa(self, ctx, *, task): - await self.bot.get_user(144250811315257344).send(task) - - @commands.command() - @checks.mod_or_permissions(manage_guild=True) - async def onlinecount(self, ctx): - gonline = gmobile = conline = cmobile = 0 - for member in ctx.guild.members: - gonline += member.status.name == "online" - gmobile += member.is_on_mobile() - for member in ctx.channel.members: - conline += member.status.name == "online" - cmobile += member.is_on_mobile() - await ctx.send(box("There are {} members online ({} online on mobile).\n" - "There are {} members online in this channel ({} online on mobile).") - .format(gonline, gmobile, conline, cmobile)) - - @commands.command() - async def servercount(self, ctx): - await ctx.send("{} is in {} servers.".format(self.bot.user.name, len(self.bot.guilds))) - - - -class TrUtilsSettings(CogSettings): - def make_default_settings(self): - config = { - 'servers': {}, - } - return config - - def guilds(self): - return self.bot_settings['servers'] - - def get_guild(self, server_id): - servers = self.guilds() - if server_id not in servers: - servers[server_id] = {} - return servers[server_id] - - def get_feedback_channel(self): - return self.bot_settings.get('feedback_channel') - - def set_feedback_channel(self, channel_id: int): - self.bot_settings['feedback_channel'] = str(channel_id) - self.save_settings() - - def get_blog_feedback_channel(self): - return self.bot_settings.get('blog_feedback_channel') - - def set_blog_feedback_channel(self, channel_id: int): - self.bot_settings['blog_feedback_channel'] = str(channel_id) - self.save_settings() diff --git a/voicerole/__init__.py b/voicerole/__init__.py deleted file mode 100644 index 5ef141b3..00000000 --- a/voicerole/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .voicerole import * - -__red_end_user_data_statement__ = "No personal data is stored." - -def setup(bot): - bot.add_cog(VoiceRole(bot)) diff --git a/voicerole/info.json b/voicerole/info.json deleted file mode 100644 index 244f211e..00000000 --- a/voicerole/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": ["The Tsubaki Bot Team"], - "description": "Only allow people in a voice channel to join a specific text channel.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Muted text for voice channels", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": [], - "tags": ["utility"] -} diff --git a/voicerole/voicerole.py b/voicerole/voicerole.py deleted file mode 100644 index 7d0f5c9d..00000000 --- a/voicerole/voicerole.py +++ /dev/null @@ -1,122 +0,0 @@ -from redbot.core import checks -from redbot.core import commands -from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import * - -from rpadutils import CogSettings, get_role_from_id - - -class VoiceRole(commands.Cog): - """Gives a custom to anyone who enters a voice channel. - - THIS ROLE MUST EXIST AND THE BOT MUST HAVE THE RIGHTS TO CHANGE ROLES FOR IT TO WORK! - """ - - def __init__(self, bot: Red, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = VoiceRoleSettings("voicerole") - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.Cog.listener("on_voice_state_update") - async def _on_voice_state_update(self, member, before, after): - guild = member.guild - guild_id = guild.id - channel_id = (before.channel or after.channel).id - - channel_roles = self.settings.get_channel_roles(guild_id) - if channel_id not in channel_roles: - return - - role_id = channel_roles[channel_id] - try: - role = get_role_from_id(self.bot, guild, role_id) - if member.voice: - await member.add_roles(role) - else: - await member.remove_roles(role) - except Exception as ex: - print('voicerole failure {} {} {}'.format(guild_id, channel_id, role_id)) - print(ex) - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(administrator=True) - async def voicerole(self, ctx): - """Automatic role adjustment on VC enter/exit.""" - pass - - @voicerole.command() - async def set(self, ctx, channel: discord.VoiceChannel, role: discord.Role): - """Associate a channel with a role. - - To reference a voice channel, use this syntax: - <#328254327321919489> - - Get the ID by enabling developer tools, right-clicking on the VC, and - selecting 'copy id'. - - To reference a role, make it pingable. - """ - self.settings.add_channel_role(ctx.guild.id, channel.id, role.id) - await ctx.send('done') - - @voicerole.command() - async def clear(self, ctx, channel: discord.VoiceChannel): - """Clear the role associated with a channel""" - self.settings.rm_channel_role(ctx.guild.id, channel.id) - await ctx.send('done') - - @voicerole.command() - async def list(self, ctx): - """List the channel/role associations for this server.""" - msg = 'Channel -> Role:' - for channel_id, role_id in self.settings.get_channel_roles(ctx.guild.id).items(): - msg += '\n\t{} : {}'.format(channel_id, role_id) - await ctx.send(box(msg)) - - -class VoiceRoleSettings(CogSettings): - def make_default_settings(self): - config = { - 'servers': {} - } - return config - - def guild_configs(self): - return self.bot_settings['servers'] - - def get_guild(self, guild_id): - configs = self.guild_configs() - if guild_id not in configs: - configs[guild_id] = {} - return configs[guild_id] - - def get_channel_roles(self, guild_id): - key = 'channel_ids' - guild = self.get_guild(guild_id) - if key not in guild: - guild[key] = {} - return guild[key] - - def add_channel_role(self, server_id, channel_id, role_id): - channel_roles = self.get_channel_roles(server_id) - channel_roles[channel_id] = role_id - self.save_settings() - - def rm_channel_role(self, guild_id, channel_id): - channel_roles = self.get_channel_roles(guild_id) - if channel_id in channel_roles: - channel_roles.pop(channel_id) - self.save_settings() From 9ab76c968967b064e02db145615ac6e4477d3f43 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 23:20:29 -0700 Subject: [PATCH 0096/1220] Update info.json --- info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.json b/info.json index 4965c11a..90b10de9 100644 --- a/info.json +++ b/info.json @@ -1,5 +1,5 @@ { "author": ["The Tsubaki Bot Team"], - "description": "A collection of cogs, primarily related to the mobile game Puzzle and Dragons, along with miscellaneous moderation and other utilities.", + "description": "A collection of cogs related to the mobile game Puzzle and Dragons, along with a few unrelated games.", "short": "The main Tsubotki repo." } From 8dc7abac67e5ab8f4dce4f94c08c5169dffe48ea Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 23:40:00 -0700 Subject: [PATCH 0097/1220] rpadutils repo fix --- azurelane/info.json | 2 +- chronomagia/info.json | 2 +- dadguide/info.json | 2 +- padbuildimg/info.json | 2 +- padbuilds/info.json | 2 +- padevents/info.json | 2 +- padglobal/info.json | 2 +- padguidedb/info.json | 2 +- padinfo/info.json | 2 +- padinfo/padinfo.py | 1 + padmonitor/info.json | 2 +- padsearch/info.json | 2 +- profile/info.json | 2 +- schoolidol/info.json | 2 +- 14 files changed, 14 insertions(+), 13 deletions(-) diff --git a/azurelane/info.json b/azurelane/info.json index 3d740a86..de8e6992 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, "requirements": ["aiohttp"], "tags": ["azurlane", "azurelane"] } diff --git a/chronomagia/info.json b/chronomagia/info.json index 5fa1b88a..28eb9f35 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, "requirements": ["aiohttp"], "tags": ["chronomagia"] } diff --git a/dadguide/info.json b/dadguide/info.json index 4f3bfab6..053d556a 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, "requirements": ["pytz", "romkan"], "tags": ["PAD"] } diff --git a/padbuildimg/info.json b/padbuildimg/info.json index b3d00b27..1796d4aa 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, diff --git a/padbuilds/info.json b/padbuilds/info.json index 0978625f..7bef9ac1 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, "requirements": [], "tags": ["PAD"] } diff --git a/padevents/info.json b/padevents/info.json index 67c1550a..a98c549b 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": ["pytz", "prettytable"], diff --git a/padglobal/info.json b/padglobal/info.json index 5f75dec1..25283a59 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, diff --git a/padguidedb/info.json b/padguidedb/info.json index 12d66bd4..7643e57c 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs" + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs" }, "requirements": ["pymysql"], "tags": ["PAD"] diff --git a/padinfo/info.json b/padinfo/info.json index a94f0339..ae47b7a6 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a69c0d34..876817f6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -688,6 +688,7 @@ async def _findMonster(self, query, server_filter=ServerFilter.any): elif server_filter == ServerFilter.jp: monster_index = self.index_jp else: + print(server_filter) raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster(query) diff --git a/padmonitor/info.json b/padmonitor/info.json index 709561eb..6496394e 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [], diff --git a/padsearch/info.json b/padsearch/info.json index 288a4a8c..88b04d13 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs", + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": ["ply"], diff --git a/profile/info.json b/profile/info.json index 001ccfc2..cf5499c9 100644 --- a/profile/info.json +++ b/profile/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, "requirements": [], "tags": ["PAD"] } diff --git a/schoolidol/info.json b/schoolidol/info.json index 7c980c12..471f0049 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, "requirements": ["aiohttp"], "tags": ["schoolidol"] } From 977039bc385c6c860005826812de07539a014564 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Sep 2020 23:40:41 -0700 Subject: [PATCH 0098/1220] whoops --- padinfo/padinfo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 876817f6..a69c0d34 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -688,7 +688,6 @@ async def _findMonster(self, query, server_filter=ServerFilter.any): elif server_filter == ServerFilter.jp: monster_index = self.index_jp else: - print(server_filter) raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster(query) From bc21e20eaf9bc13c197be277c0b98f0bf6a5fc0c Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 11 Sep 2020 00:11:48 -0700 Subject: [PATCH 0099/1220] same thing as in misc-cogs with the data fix --- azurelane/azurelane.py | 2 +- chronomagia/chronomagia.py | 2 +- dadguide/dadguide.py | 2 +- damagecalc/damagecalc.py | 2 +- padboard/padboard.py | 2 +- padbuildimg/padbuildimg.py | 2 +- padbuilds/padbuilds.py | 2 +- padevents/padevents.py | 2 +- padglobal/padglobal.py | 2 +- padguidedb/padguidedb.py | 2 +- padinfo/padinfo.py | 2 +- padmonitor/padmonitor.py | 2 +- padsearch/padsearch.py | 2 +- padvision/padvision.py | 2 +- schoolidol/schoolidol.py | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 6d988fde..fafd06bb 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -26,7 +26,7 @@ def __init__(self, bot: Red, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index f2b48377..5a4a2c84 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -54,7 +54,7 @@ def __init__(self, bot: Red, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index a11b725f..a7581d29 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -90,7 +90,7 @@ async def wait_until_ready(self): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/damagecalc/damagecalc.py b/damagecalc/damagecalc.py index a6b794ed..c0dd510d 100644 --- a/damagecalc/damagecalc.py +++ b/damagecalc/damagecalc.py @@ -194,7 +194,7 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padboard/padboard.py b/padboard/padboard.py index 9ea5e285..b7cb87be 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -29,7 +29,7 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index dda9c29e..50e21f2d 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -175,7 +175,7 @@ def make_default_build_img_params(self): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index 97dbd22f..1d6f3f69 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -21,7 +21,7 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padevents/padevents.py b/padevents/padevents.py index f2dbbcb6..88e1f2ec 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -36,7 +36,7 @@ def __init__(self, bot: Red, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 21e232fd..befb889d 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -117,7 +117,7 @@ def __init__(self, bot: Red, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index c5d0fddb..c99f7442 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -42,7 +42,7 @@ def __init__(self, bot: Red, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a69c0d34..1950cc1f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -205,7 +205,7 @@ def cog_unload(self): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 04fa520d..6f94fd01 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -15,7 +15,7 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index dacb2d7c..0ef3120b 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -726,7 +726,7 @@ def __init__(self, bot: Red, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/padvision/padvision.py b/padvision/padvision.py index f5703dfb..3ee6e9b9 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -15,7 +15,7 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index 9bf9784d..45e13fe2 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -26,7 +26,7 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO("data".encode())} + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data. From 09aa3054a4acda3a7c6d26c90f7c7b4d1dd74f92 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 11 Sep 2020 11:36:22 -0700 Subject: [PATCH 0100/1220] fix killers --- padbuildimg/padbuildimg.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 50e21f2d..59139e2d 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -131,13 +131,13 @@ } REVERSE_LATENTS_MAP = {v: k for k, v in LATENTS_MAP.items()} TYPE_TO_KILLERS_MAP = { - 'God': [7], # devil - 'Devil': [5], # god - 'Machine': [5, 1], # god balanced - 'Dragon': [8, 3], # machine healer - 'Physical': [8, 3], # machine healer - 'Attacker': [7, 2], # devil physical - 'Healer': [4, 6], # dragon attacker + 'God': [207], # devil + 'Devil': [205], # god + 'Machine': [205, 201], # god balanced + 'Dragon': [208, 320], # machine healer + 'Physical': [208, 203], # machine healer + 'Attacker': [207, 202], # devil physical + 'Healer': [204, 206], # dragon attacker } AWK_CIRCLE = 'circle' @@ -604,6 +604,7 @@ def combine_latents(self, latents): return latents_bar def combine_portrait(self, card, show_stats=True, show_supers=False): + if card['ID'] == DELAY_BUFFER: if card['ID'] == DELAY_BUFFER: return Image.open(self.params.ASSETS_DIR + DELAY_BUFFER + '.png') if 'http' in self.params.PORTRAIT_DIR: From e40f1767c52595393aa4933118fff525b769ce53 Mon Sep 17 00:00:00 2001 From: RheingoldRiver <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Sep 2020 15:39:17 -0500 Subject: [PATCH 0101/1220] remove duplicated line --- padbuildimg/padbuildimg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 59139e2d..dba1c197 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -604,7 +604,6 @@ def combine_latents(self, latents): return latents_bar def combine_portrait(self, card, show_stats=True, show_supers=False): - if card['ID'] == DELAY_BUFFER: if card['ID'] == DELAY_BUFFER: return Image.open(self.params.ASSETS_DIR + DELAY_BUFFER + '.png') if 'http' in self.params.PORTRAIT_DIR: From cad8911a48388b53eb636098ab62f0a43bcc9421 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Sep 2020 02:00:12 -0700 Subject: [PATCH 0102/1220] PriceCheck --- pricecheck/__init__.py | 6 ++ pricecheck/info.json | 20 ++++++ pricecheck/pricecheck.py | 127 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 pricecheck/__init__.py create mode 100644 pricecheck/info.json create mode 100644 pricecheck/pricecheck.py diff --git a/pricecheck/__init__.py b/pricecheck/__init__.py new file mode 100644 index 00000000..1a4dff78 --- /dev/null +++ b/pricecheck/__init__.py @@ -0,0 +1,6 @@ +from .pricecheck import * + +__red_end_user_data_statement__ = "No personal data is stored." + +def setup(bot): + bot.add_cog(PriceCheck(bot)) diff --git a/pricecheck/info.json b/pricecheck/info.json new file mode 100644 index 00000000..9490b7d6 --- /dev/null +++ b/pricecheck/info.json @@ -0,0 +1,20 @@ +{ + "author": ["Aradia Megido#2552"], + "description": "A cog that enables the display of pricing information for evolution material gems.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Price Check", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", + "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [], + "tags": ["PAD"] +} diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py new file mode 100644 index 00000000..e74e5300 --- /dev/null +++ b/pricecheck/pricecheck.py @@ -0,0 +1,127 @@ +import re + +import discord +from redbot.core import commands, checks, Config +from redbot.core.utils.chat_formatting import inline, box, pagify + +from rpadutils import auth_check + +PC_TEXT = """{name} - Stamina Cost: {stam_cost} + +{name} - Plus Point Value: {pp_val} +({points} 297 Plus Points) + +{foot} +""" + + +class PriceCheck(commands.Cog): + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.config = Config.get_conf(self, identifier=9213337337) + self.config.register_global(pcs={}) + self.bot.get_cog("GlobalAdmin").register_perm("pricecheck") + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + + @commands.group(invoke_without_command=True) + async def pricecheck(self, ctx, *, query): + """Displays pricing data for a tradable non-collab gem.""" + padinfo_cog = self.bot.get_cog('PadInfo') + if padinfo_cog is None: + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + return + if "gem" not in query.lower(): + query += " gem" + nm, err, debug_info = await padinfo_cog._findMonster(query) + if not nm: + await ctx.send(err) + return + async with self.config.pcs() as pcs: + if str(nm.base_monster_no) not in pcs: + await ctx.send("{} does not have PC data.".format(nm.name_na)) + return + sc, foot = pcs[str(nm.base_monster_no)] + pct = PC_TEXT.format(name = nm.name_na, + stam_cost = sc, + pp_val = sc*83/50, + points = sc*83/50/297, + foot = foot) + for page in pagify(pct): + await ctx.send(box(page)) + + @commands.group() + @auth_check('pricecheck') + async def pcadmin(self, ctx): + """Creates custom commands for [p]pricecheck.""" + + @pcadmin.command(aliases=['add']) + async def set(self, ctx, stam_cost: float, *, query): + """Adds stamina cost data to a card.""" + padinfo_cog = self.bot.get_cog('PadInfo') + if padinfo_cog is None: + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + return + if "gem" not in query.lower(): + query += " gem" + nm, err, debug_info = await padinfo_cog._findMonster(query) + if not nm: + await ctx.send(err) + return + async with self.config.pcs() as pcs: + foot = "" + if str(nm.base_monster_no) in pcs: + foot = pcs[str(nm.base_monster_no)][1] + pcs[str(nm.base_monster_no)] = (stam_cost, foot) + await ctx.send(box("Set {} ({}) to {}".format(nm.name_na, nm.base_monster_no, stam_cost))) + + @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) + async def setfooter(self, ctx, query, *, footer=""): + """Adds notes regarding the stamina cost of a card.""" + padinfo_cog = self.bot.get_cog('PadInfo') + if padinfo_cog is None: + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + return + if "gem" not in query.lower(): + query += " gem" + nm, err, debug_info = await padinfo_cog._findMonster(query) + if not nm: + await ctx.send(err) + return + async with self.config.pcs() as pcs: + sc = -1 + if str(nm.base_monster_no) in pcs: + sc = pcs[str(nm.base_monster_no)][0] + pcs[str(nm.base_monster_no)] = (sc, footer.strip('`')) + await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_na, nm.base_monster_no, footer))) + + @pcadmin.command(aliases=['delete', 'del', 'rm']) + async def remove(self, ctx, *, query): + """Removes stamina cost data from a card.""" + padinfo_cog = self.bot.get_cog('PadInfo') + if padinfo_cog is None: + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + return + if "gem" not in query.lower(): + query += " gem" + nm, err, debug_info = await padinfo_cog._findMonster(query) + if not nm: + await ctx.send(err) + return + async with self.config.pcs() as pcs: + if str(nm.base_monster_no) not in pcs: + await ctx.send("{} does not have PC data.".format(nm.name_na)) + return + del pcs[str(nm.base_monster_no)] + await ctx.send(inline("Removed PC data from {}.".format(nm.name_na))) From 789cddc778801d9e8480530fd19527ba1a96588d Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Sep 2020 02:13:55 -0700 Subject: [PATCH 0103/1220] set demon ly --- pricecheck/pricecheck.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index e74e5300..a28f1079 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -21,6 +21,7 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.config = Config.get_conf(self, identifier=9213337337) self.config.register_global(pcs={}) + self.config.register_channel(dm=False) self.bot.get_cog("GlobalAdmin").register_perm("pricecheck") async def red_get_data_for_user(self, *, user_id): @@ -53,11 +54,15 @@ async def pricecheck(self, ctx, *, query): await ctx.send("{} does not have PC data.".format(nm.name_na)) return sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name = nm.name_na, - stam_cost = sc, - pp_val = sc*83/50, - points = sc*83/50/297, - foot = foot) + pct = PC_TEXT.format(name = nm.name_na, + stam_cost = sc, + pp_val = sc*83/50, + points = sc*83/50/297, + foot = foot) + if await self.config.channel(ctx.channel).dm(): + for page in pagify(pct): + await ctx.author.send(box(page)) + else: for page in pagify(pct): await ctx.send(box(page)) @@ -125,3 +130,9 @@ async def remove(self, ctx, *, query): return del pcs[str(nm.base_monster_no)] await ctx.send(inline("Removed PC data from {}.".format(nm.name_na))) + + @pcadmin.command(aliases=['set-demon-ly']) + async def setdmonly(self, ctx, value: bool = True): + """Tells a channel to send [p]pricecheck in dms.""" + await self.config.channel(ctx.channel).dm.set(value) + await ctx.tick() From dd916aecd1082277180d61823f9bf997fa558391 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Sep 2020 02:20:43 -0700 Subject: [PATCH 0104/1220] pcadmin --- pricecheck/pricecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index a28f1079..a08a2f06 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -67,7 +67,7 @@ async def pricecheck(self, ctx, *, query): await ctx.send(box(page)) @commands.group() - @auth_check('pricecheck') + @auth_check('pcadmin') async def pcadmin(self, ctx): """Creates custom commands for [p]pricecheck.""" From 749b3749238b4af40abe59e7f24bacbbe34e0c4c Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Sep 2020 02:21:50 -0700 Subject: [PATCH 0105/1220] pcadmin (again) --- pricecheck/pricecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index a08a2f06..fe3deb23 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -22,7 +22,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9213337337) self.config.register_global(pcs={}) self.config.register_channel(dm=False) - self.bot.get_cog("GlobalAdmin").register_perm("pricecheck") + self.bot.get_cog("GlobalAdmin").register_perm("pcadmin") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" From 49a302a3e6b6d41a1faa4c1bd0eb78d5aa069c42 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Sep 2020 02:31:37 -0700 Subject: [PATCH 0106/1220] assets --- padbuildimg/assets/lat/aak.png | Bin 6778 -> 1900 bytes padbuildimg/assets/lat/ah.png | Bin 12290 -> 1497 bytes padbuildimg/assets/lat/all.png | Bin 11031 -> 2817 bytes padbuildimg/assets/lat/atk+.png | Bin 6741 -> 1867 bytes padbuildimg/assets/lat/atk.png | Bin 15229 -> 1673 bytes padbuildimg/assets/lat/attr.png | Bin 0 -> 2145 bytes padbuildimg/assets/lat/awk.png | Bin 7152 -> 1991 bytes padbuildimg/assets/lat/bak.png | Bin 6832 -> 1858 bytes padbuildimg/assets/lat/bres+.png | Bin 6563 -> 1842 bytes padbuildimg/assets/lat/bres.png | Bin 12830 -> 1486 bytes padbuildimg/assets/lat/dek.png | Bin 6629 -> 1840 bytes padbuildimg/assets/lat/dres+.png | Bin 6649 -> 1872 bytes padbuildimg/assets/lat/dres.png | Bin 13506 -> 1512 bytes padbuildimg/assets/lat/drk.png | Bin 6873 -> 1874 bytes padbuildimg/assets/lat/emp.png | Bin 0 -> 625 bytes padbuildimg/assets/lat/enk.png | Bin 7413 -> 2041 bytes padbuildimg/assets/lat/evk.png | Bin 6633 -> 1814 bytes padbuildimg/assets/lat/gok.png | Bin 7058 -> 1971 bytes padbuildimg/assets/lat/gres+.png | Bin 6572 -> 1848 bytes padbuildimg/assets/lat/gres.png | Bin 12879 -> 1498 bytes padbuildimg/assets/lat/hek.png | Bin 6971 -> 1948 bytes padbuildimg/assets/lat/hp+.png | Bin 6634 -> 1888 bytes padbuildimg/assets/lat/hp.png | Bin 15033 -> 1604 bytes padbuildimg/assets/lat/jsf.png | Bin 0 -> 2101 bytes padbuildimg/assets/lat/lres+.png | Bin 6584 -> 1857 bytes padbuildimg/assets/lat/lres.png | Bin 12699 -> 1492 bytes padbuildimg/assets/lat/ls.png | Bin 0 -> 2011 bytes padbuildimg/assets/lat/mak.png | Bin 6643 -> 1817 bytes padbuildimg/assets/lat/phk.png | Bin 7188 -> 1966 bytes padbuildimg/assets/lat/psf.png | Bin 0 -> 2177 bytes padbuildimg/assets/lat/rcv+.png | Bin 6601 -> 1868 bytes padbuildimg/assets/lat/rcv.png | Bin 14117 -> 1547 bytes padbuildimg/assets/lat/rek.png | Bin 6923 -> 1930 bytes padbuildimg/assets/lat/rres+.png | Bin 6567 -> 1847 bytes padbuildimg/assets/lat/rres.png | Bin 12762 -> 1471 bytes padbuildimg/assets/lat/sdr.png | Bin 12451 -> 1362 bytes padbuildimg/assets/lat/te+.png | Bin 6320 -> 1732 bytes padbuildimg/assets/lat/te.png | Bin 13407 -> 1497 bytes padbuildimg/assets/lat/vdp.png | Bin 0 -> 2166 bytes 39 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 padbuildimg/assets/lat/attr.png create mode 100644 padbuildimg/assets/lat/emp.png create mode 100644 padbuildimg/assets/lat/jsf.png create mode 100644 padbuildimg/assets/lat/ls.png create mode 100644 padbuildimg/assets/lat/psf.png create mode 100644 padbuildimg/assets/lat/vdp.png diff --git a/padbuildimg/assets/lat/aak.png b/padbuildimg/assets/lat/aak.png index 2e58f09a9d6eee9173b690a45b3e5071ed10df63..a8fcf2d5bba47234f344c2f1cedfad82b600d0db 100644 GIT binary patch delta 1898 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 literal 6778 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 diff --git a/padbuildimg/assets/lat/ah.png b/padbuildimg/assets/lat/ah.png index 9f4d0a4ee89b88e0824a0d7ac909f62ae7bee280..1f396d71a2f40348bce789cf5382b694553cefd7 100644 GIT binary patch delta 1480 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 cpF07*qoM6N<$f;lv15&!@I diff --git a/padbuildimg/assets/lat/all.png b/padbuildimg/assets/lat/all.png index c26229aac22ae3d5a5b3d244e2ff383b54ab196f..ac4d24614c5e5430981b0cbb6b63874df36304f4 100644 GIT binary patch literal 2817 zcmV+c3;y(pP)X1^@s6b5wmq00009a7bBm000ie z000ie0hKEb8vp;eU)@ELua>I~$&#)l8%r2Rwy}$a ztvbXGZXF0AAt8`VJSELG(_t`4n`R~u=sIbKBz9Rs2w7~v*ifd9i4_qHHb}Oy7R%OV z$&!7gyXo%terx|wMmBXJ^qdyugPS35a<*7Vn%BwlX>oypZ~Nz5{kME-4H)&k{5IQY2;u0B{CcusM7my4EmsGNCaF=?J35fv@^{G7Gbe%#53F zlI7bZSypQD-c{I4zNO2YYJwuO8Aa1bU_&$8F{S2E@+s11o~Ei+<lq8a_m^^&AU1A6mutv3SbK<3d)HN(s?~V0C~Bx~3vu9;PjM zoYlTk!kH|;H>!wvucN5{8DbNCT-S7#N4}}@@ZUO^nZE}tE{e(;DTF8KaGb_wGc%u% zNfgY43^(bDjUd^`MrMhgKFl{h@iH~b1$OVL!DjK|D6T_rIMH>L%lZbSeT$m9~#B>K7i=39}6=1Gn( zkQq(Us+952_4o7k=n4LD_XJ=6Mx104K6j^^A3p}mE6ySVnrJ2(jUq`B7K?@9!9gTR zqQf#op^)R&o5tAt!^^aK+@G49y86Nkfz*=)vQv9NyK zI{x+BUq{Vncxd+w-`kx;6kY7tVdu%mFVkLroWkW1fUAULlYFwN8F$J8ih{mSz(}Uh zC&M_#XQ?qOu<527_aHPP~;R9G1xgn$5-h{5*mnFg-oZz~CSyB_*tCsY9OV zBRrd9U?4#<1+uL1*0DTihqGwd$j4{dxvqtZ@^VB`L=Xf-LB!#3u)cE*mEK~~v3at| z6d)3bC`?{15RK#n1(`Kh%zX44JBp2bi^TgwE!r?GppO089MtgfZ^}R1Ks^7x%uMJS;pTZ;x>^o#5 zVcU$&--4D&u(s`b%mot^U^+U@@Ynb%!S5S(P}U>q7a^6OV%PF zIxDUSK>=hio6IN^DO53wRWlG}1KrRup>u2HP0TH{@kr;{BhQ9P&jo)V@=@GSOW%vwfsu7Pyj__ zc;Xzo%R%_?5RtQE6fy;nH7v4>+i=sMdKi7z$M+x6xjgH~TvCIxsvQb|t%9R(o#L0z zA4HPOc)eb{UN7V0<9xh(BQ?%69)A-rzF5TGC&1aT9cR@I2zD=;ArKx7Va#V(7@OhW zj-2G2dnbqD6@2H%hj>F>&9asEb0N~mo~I1X4|({L#v8Hw+X3fQjsPaRn`e(6mQ`UOsSuq*RP5nV0GmQ9@|BoxR}{ z@rxJvazP|v7SD{;*a(UncnjKI^e zIDM5Zm`WOu5#KAsU~!gUvfyagut>_|p|Sg8xLmF)_8JDWv$KqjjS-Tn7Hh78mkJJIv-ioL^4;$|h_37Cy3WkZ3_slS z4AF%&$<_z2DNY7TJvpITxf1H8gQ4)$5!N^ly5kQ*GT;nK!GuX{D zp&@om&9c>9LuhV-GFLV4NO=w{yAFFr+kZ#-S|NfUkj>_Z#^UVnJ;z`EMRYM1g#t&8 z9O2cOQY@95aJwt7IV@MZnipRE1-EU=;PH4EJadNgCr@)K=f@q~fv@pnOkaG0v8gDj z?)!MZ4}($O#!I#U7vKI7k2Y@S;i*^o-Rdtu(Y1Fj@&4^(%J_$UY0jms0BHFn^=BVt z!Pdrn>3V9sRUW<=owe$|G7u`r8WnWSKCz;Ks?aIN%aSAoC!vWw*~)i!GvyD)MR32 zc>3bL=k`1$00KVWjoZ3E@vZBED?eqDB+Exl^20z=Re5q`;Dvub^6EbmkN5p2WaXIl T9eyZn00000NkvXXu0mjfB!qHm literal 11031 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 Rt}y@r002ovPDHLkV1i8P*+&2X diff --git a/padbuildimg/assets/lat/atk+.png b/padbuildimg/assets/lat/atk+.png index c40e7fdfde6c37907b9d65880bd7c7a4c7bbcdd2..5af04640f50db7c8b4b321d87528ab0bfdaa39b5 100644 GIT binary patch delta 1865 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 diff --git a/padbuildimg/assets/lat/atk.png b/padbuildimg/assets/lat/atk.png index b2dd72750c5d65a2960f667366e96ce7095ecc17..5445babc1aef18de8946dd2941fa2294a32d5622 100644 GIT binary patch delta 1657 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

X1^@s6$6kv|00009a7bBm000XU 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 Y2Oz6@$M@@=Jpcdz07*qoM6N<$f_m@8MgRZ+ literal 7152 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 Xa8PU5l@e!k00000NkvXXu0mjfakJ0L literal 12830 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 diff --git a/padbuildimg/assets/lat/dek.png b/padbuildimg/assets/lat/dek.png index 9f527caab0d567cf1de300df382bfa42d7c794b4..266fd0542c5f6c6491cce281bcbc8e43158584b4 100644 GIT binary patch delta 1838 zcmV+}2hsTDGq4Vj7k?QD1^@s6b5wmq00009a7bBm000ie000ie0hKEb8vpk7 zR9J=0m|1L8*BQru=Pq++w`az?u?^lmW^qHp(nJbKF}P~dR87;W4QkZw4XLW~RMGY& ztyHP1+Lylcfl5`9W}!-efQmx|4G^bLgIS6T7_ZnKJjV8TJb!m)?%D@C!9Y;)fZG0F z?$vkh`M&>O_dDPD&Jh4>n%mmCUi|j1r=EHCtCh9&t&${3{2#-yZF_QT=*p?1hxY$< z-y5$DUG47|^)2!EcVGGGtF~ornj9I6WETvd5Mr4Scj+mX-o^jO&hjzG7u50sR-PAAkSiH@^$VDqk3xlPePos_e*tWfLAP%xoIl$RdP5QZ-axxIj-XlAWO< zXxghIZbF~Cbx^$1(?9&t>Fed!^&)S^Q;8agfOzym^yCQc@ELsR2~>}dVG*Qy^9u-B z;f@4&ARR7dW|n|4NmF3K*tWT8T8Wlb);Uohl1D?8w10v~3S1N%%Kft>G9gS?MJ!up zW_F&rOWoA;A0(a`A>a#fBUi+Hq=G>01{8l0LXuYyP@o2*NKODNnD>OE#Z?lLjPUqC zRtgdUAmdO%nwOi7QS97A*aoU(qe>Ps2C3m*IP*H&^(*WS=h!Vz@gvVPgTMb4>7ici zypHR-|9?5qJ%N=)?uty|?3ckOQnY)z*|u(mcg~zAUL9d;Q;s5YoW6^b{QQl5{8mk| zIcSp08BACMA*O><^fZOKJOdgrXMAK>@G<6EF&NVmStKZUEhl@RM)cK!`CM zE632an;bdV!^>)tYQIYo7sCcgk~o{r^Aaw}?0*nv{{ALrrwL(a*gXY$CZgYdw)IUTY#7%5h7JBDa4TU&~HB#SJUZZE1=7JwojJ&A1c7HSuc} zUNKL|8X#)^6In_StLxzCOb37aM=xKT?tkZp6(IZ)MNOMHZ=}d&29dJUge1Yi3>f8p z;_J5{MXE4u2iIBQej?v3WQ7Ri`*>x?6+Gqu|LUJ-V8BOO_R&@~z@FL!PN%@<4^b4; z0XY2jQ-H#u!=G^E$VJA-O&Z&(`Th^eX>C8j2j`|aF%T!M)fYmia3KkO75TZ-G=J2) zl$MkeD-E;n&)`_XVZ+#m6394XVew~q_Bz4wV)&)<}+L7W{`@2Y0JdYeKrNVoeiHQV%?rvdX^>-i; zLOevD_bDFAgYfvsm$!2zzki0ammkBg%ureyqrSF*I%JwOoi9Bz$nnuiu8J_@b1tVR zs`yxbk>L8rAL*%QQHTL#S!}opfP{}w$p)|>GSSV`v2Hp$Mp#p(LdxQ$*1f#`@ebzg zE^k)=$OXy3s z(3k7P@;Bfre#9d>eTMxKt+P%P+* zXV3L~(s%NM_xB1wAj@qvtLvYSZ`k;ihWNUTsz)pQsPz6O~UOwM@U~G7B cKepZfZ_vxf&$BQT_y7O^07*qoM6N<$g7fZ@IsgCw literal 6629 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 j*l_ORp0GIYJB9=Q^${- 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 diff --git a/padbuildimg/assets/lat/dres.png b/padbuildimg/assets/lat/dres.png index 35439b8618aaf70937060ca8281591a2da68eef6..2498ac54eedd77e65ecaa4b4dd6aed9689bbd50b 100644 GIT binary patch delta 1495 zcmV;|1t|K$Y3K`(BYyx1a7bBm000ie000ie0hKEb8vpc0+kI#$gB8lgy0O7vgHg&p z6dEX73uBkCZcPGh@VYkZC3WiJ*p6)ZZuu5Vw(dtq(z)z|)PJtAvoY`k=fimq|L6C< z@AKyf0N1w72H>emrShVwn+@zyf<;JMt=3-x=YW)OZQE?nH0|}ZrG%G&I%f)p zqkzj}Qf~ny6(wCHt3xg$kuUjB@S>Qlr1KgZNdr-G;qW%23Ic}95F`W<>^4xU9Sn8N z(HO{)(bg!e=Gk1`CcTj(ZB}YMmJ!7=?nWo!kc`zPVSmR?Qp*#YUSf7+0aN}ip07Vq z^%X>-pT_X7b<$%VAfk6s(sdU6qpThNjP?O1&7FQ^r-R~F ziOtnb)I@@U`e7APlLTPa9)PlzqLeEVyBcTYoh43Y@8@V?0NEj9eaN6RxJ>lDBm~?+i=(Ghnxz2$W4`X>9OareVkTU->$&TAq zPk&x3Be@hLuNwe83nN#e1Ou%U_X~UtAE}uPnyw+OHd49dWj-`cQBPtj3R+f!eU4jP zcU|Gtya&yq;_awzQp{T^MC+*Byc>_dNT=*4(=$b(pfm7|9>SFn(w0dqa)aEGftu4H zaPan!FQ%Kn*^ZhSC33Gq;tjAXcG`1IM1P)LK^^XIeYPqL@qAoKzCL#6IQTGDi1NSHGjJ>;r3qnf-K+PBQpNkS(W^#H$i*e}BA- z>5mr)oEfM0J{h^wPsMqYnUw{ukB#%!H{YicIf2k|ntWa3|IWk^Z6rb`dGpWL@C0HE zoEfBLFhJt>8GKPMm8%&Pr--F{iSg+v;xm(+Kld5SdYIthXHbIuJ1uuZ08Fu-mB^F4 z{PQ1CdS0Qg{~p3W3)6-L(=4%`pMPg+c8d7o0+%mdCpurj|IP0sgzx!sMY|z7P}=z= z!bZmrdFAC_aq?Re?CTBF;`gEJ0`bk8EX}U+!G$p<*8K#AUP8X(vE3fE|B9rl3IUHD z=5}`=xb7s|{saF0_HS@pI8R%|g~eiFWi3Z+G>N6-ER845A~keXQ&w(0AAj)JQMZy< z_V*q8N!O7>!i|**`QlEBfZ=Gy>Dz}jx4`0fj`3NMOsbi{osS|Pe-=@WfMD6xV8{>m z4>mCQ@jnb{W_tX&-lNBU`}C<+rj`|wX>->B=!YYJx=sD|3N#4xv^jB><3uM%UzCO~ zzx4-;E}iZ@cIfnpgLf&mKv0YG|91N?DkoQz==eXKS4KX+^!|Gn&k2Bl$KyTF)%9RF x6!~#|pmE<`A-QB?esz8E7dK{R{zfK~`ZvM%UQ~`R7KH!+002ovPDHLkV1j59>Ouej literal 13506 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 w@`L(7n+a diff --git a/padbuildimg/assets/lat/drk.png b/padbuildimg/assets/lat/drk.png index 4b246ad629cd320fc936e4f4c26bb704e93936e4..b21e28d44046bad6aa683c9f7c68e7f320529566 100644 GIT binary patch delta 1872 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 diff --git a/padbuildimg/assets/lat/evk.png b/padbuildimg/assets/lat/evk.png index 6adc6928463c9afa14a0ab79ef2b76dff1a98475..a550c26ac8cfdd2f81e93228e466ff82ec6072b1 100644 GIT binary patch delta 1812 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 diff --git a/padbuildimg/assets/lat/gok.png b/padbuildimg/assets/lat/gok.png index b601ab6e30d3b17b1abd4f20e533af8fadb5793a..f32f974ef4bb8acffac7ddeea104462efb9bccf7 100644 GIT binary patch delta 1970 zcmV;j2Tk~rH?t3r7k?QD1^@s6b5wmq00009a7bBm000ie000ie0hKEb8vp|k){cOq$H$DN`V+K5DZ=l7_TqfynnXW-plOn>|FX_0tOP* zYgD!Wml>VWcfRwR?>lGCd;*}kZTa#qKDBN8hRu(DxvrsQnIuUP|HpD{+rBz7_^(3; z_Pz19SO56(;Q2&Cw6u40?0EVo&)AmLeRXIgT*#Ta5MmaHd*do@+>8H_o!QSE-R~(y z>*M2!N9%p@*?(t#EPnKh=bjHPtb1~3O0K(>Q)Nf?%^EO&NsfhN7jP+ItJMIM3t4Ql zfDi&n^`Pp(d3tw|>^xC_*=`EE*Nmx&H^uHFiR3?y4%7}9p{l%Ahw2N1xOWh*lOt@W zkfbal@+yqf5N`4qdiDycrjrzY;txEDkQHv*0CVDSDSzkF_{^&;_2tZmA6PoBL>ARG zIuRX7^Pozeiu?)JMz}7HkU&VdwuuK5TPQ@-3>|?hs7j7YR~yNk*-yOxO*-!3B?H%W|Kogj zNmgcJS7a)WewM1n-)BYmB8}~B>`ks?LW6qu?>zb$ot4Y|6pTrFdXilG!?Qf^Ezuot zC|U*=ESv3qhvU@&-Z}7V$_~_Yeh#6i^NvLUuz!IvU{*NzioTZmpu&pS5S!MNd29a| zgBNU)Lj^P`%gE?uUfFe=Z)nrRyb3A6v_X<2PGn1b%d0Y78sb>bpD{`$;@yu`G{1Z! z_S-;ApjaV@#}I?ikRFQOFQ=H%ri1VSojlh+xWm?k}KFnqblc*aC?Ej9#H`~t?x zHh(Q%g_p+iTy!nUK@*QGRXqP)Ag%+p4Yo6PCjyh!P#!Joa5focWiZInB|3_nC7Uyu z7#C#oUL;=&XG%jX4k=t$Wh7k%S7tC%pcjo)q#dQ(r_9#2bHu{^^yYGe&C04wU&()aF8CI=5QQ11iE(H8a4-GxfC?zj~pbKfRVC zZ&tDXtDAZ5mvO=&gW-dhh{!%t&f@umGk=iM&@q(U1bID6$C4}^O*#jUw(-*L1S1zF z7#S~7vdZXE8Csh%8bl z7YbDSWiBKpIX}_PxKumm6#ZBpZl(2Y<+n6P2Ueh-y@^(EKjNXzG_^H43x7n3+9nv( z9R|~aP_Z7hrW1gm^Z~J2hxP_RFl2$8Ls%L^i4=c180U1pp1dbQrT-2d?kvAI40Eo2 zs+W@!>kxfYxK@IP*BC?uSdxj6HPFl?KDiZ?zzj&S4>Na-a?U|$9voFAHD$8reLwGw ztRu6a8O5ttydpNY8vJ>;D1TKot(-{-%+nq$<1k-Zt>JiVg1SU&tVmKa=uJBy1f8`R zq6=Jv>ypnpWJ)&sj~VQ`xC3YLDil3<&mH&tdo5W;iZ10`ZUF;5HN3s+H|*RQ#XD_q z<-#DtR~x}!0I!#Gr~7FL%S3Afoais{+gG74yaT;`(>(=$H;4%on15-wD&?UXC;A1> z`d0FtpT5Emp6aKuEkv^(;nZanGBzu^f-I~luzR1*{tFK>5`ToMPz>_-7`@8P3Z)c` zEPjuIM~HcW2n4RHA?Yzjqu=Dfhrg#QHh?M#a20R`x+OSzIK-*x&yxy2grw9UYqpdftJlI^T$E<%zw?#C*}UlXSNfra^1#| z;i$$?YVW?k?okfC_twkJO`XeEug`R?uU{Ni0t@}Fm|X*ApK5&KiN;-atFQ)eQFH@v zDK&3F3j`3d%}ip<&Cl)jTO`7@Tst?J8y_A{pFHy6seit^_eB8+WO;dgQ_JHWtJiI5 z?O3@^)japU_ACEImCaIlpuexT|D)bLBgso|VB3j*0|BBE2o+s_2><{907*qoM6N<$ Eg3~9ceE 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 diff --git a/padbuildimg/assets/lat/gres+.png b/padbuildimg/assets/lat/gres+.png index 8ef447dcc04aad491b284357feccaa7adc2fd1b3..64e9bd2699b4661800b11f3d15e3fb60e61da565 100644 GIT binary patch delta 1846 zcmV-62g&%XGq?_r7k?QD1^@s6b5wmq00009a7bBm000ie000ie0hKEb8vpPbXF zR9J=0m`Q9^=M~3)-*<=i?i>A{$L9eX;}Yxy1A#&rf`W*uC_-t}sH&7ks@m?ls9JT= zMY~IPs8Tmww3V_-D^-e=gqR4mw5lLL2$;cy7;ud7gpJ>O*nfV0#X0JhM%Y-pNmx`_sFt{Se*+s{>__MZ4}T@VGr3dLO7qOwGg{IvHokml z*Xw$^DbwkvEu^uBVCTE)QiW1!a|!-;2eECfa@jr60qv6`7k*R|?M4PjcK$<C8t8QR>LcKl$0WyyT%Jvh0cs*Q_>_a75?xS^k_#J8FeTv z^+Ek%tnLHB0?>4XDM8oJgK0jDny1K@3XIL&pyXcT^uRT~xHL(jU8iubLT=zLcnYOp z_kJK5!A(GU&ZCem@w=a%z!!>edYH`KD4juxtbc(H-~kR`2UJ!{;rK3UuCghN|H>6kXePWJ<Y3R!t06;1#*qL5_$X6kB6{zPcPHdl?3GyEm<^0>q*teuo?wzuQo8PIb;ipiD{S7yDs;N5PyrA zY<+D5$&`t18VJ)^PblkazaYsM1(Wm8-=D<1F1dXpOZifkmUe?ZTk05c7ch2*IsONO zk5AYLPhp02I{TB9W{b?vl!-RCW5m1GPsC?6y8=N7a`QU3RfIjgDYhEjTuOY!Ex*iM zDNO5J1G`&xP`A@({>S&38(Uy;p?`|h1nsE^qeFK=Sa|VHs#XRkp!*Q3W&Ke54Vt1L z@Vyu_`FcjqkML6e4(3ZXBl;AYVbWyOlX47hPt4%zU_Dn)vowkN8fvl1$?HS-$sM>2 zTPa(apuL6=kG9_xLBQ^}OhijC?{x6_l|g>AJ;lM+9kix0gfxwwa1*v$W`8NSh-rmM zHO6VO47gupcr45PVv1PLLA=JTxJELtcaPPnYMnI#z80aNI=R6iu3R|H*Y|8@ciT2> zU&r?)Qc9k0*pC!8u3hBr%mjm@Q=C0Hj^1+w+4W6`cR5lK(0PIpVCdiCPDN&V!9fBEp+KWuyZg_fR{xD}T3RZXC@ z)yjMHqdmF@cua00RevAC?MAs(TwmcUfu>1>5dl2}XwP8h3DG}z=lQ9yQk}`NZetB*1 k%d3OOChy$-2+te-7jNR=+2kNZ1^@s607*qoM6N<$f(UGob^rhX literal 6572 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 XXJ=_P000>X1^@s6#OZ}&001mSdQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3>tmZUk3Z2xl=vjl3f9LjTM2XFcB8tD-!O0=ly z>`LL`&W{8EgBb|A+5hi%@T)0|&7U{q-)!`G-K9Ub-L=p!yY}PBzx*IgKkU!{ z`^k3ata{FVtLrgiscb%r`dZ5U8*gNg81Bb1zY6~){$B2{#;?i}Cp)&-&B3pUA32Y; zkBe@(?vC5{&*>&pjDCB}?Y*B^T(4Ek-~7CrBJ)Rgd|`W*36))$>{{Su|2-FXpWE*9 zrmH-8Io|0VgE>C&hhOH;Ui@!^jZERo%(=~Htf!&Tb#7ORkZ-u+hf%hdFjeViv%K8j|iFkHmGA^(KHoK5qcD8t5 zoWqWl{prlyhv)}8;8OC-nZw?MklCE-^=v*<+JQfg_X*HB|kHP=#WZM8Sw0uYv3ZpF0P zTI-#Mc82bJy7P|ShaX|YkwzY6)X_$txX+9;%{Wl* z_Ras*&KYgp|HaN3ZQW<*{{vTLi~j1uk!DGFs%8xFvBa_+wN+%Rn4Jn^^{Yw51{ zR910JVyDycs&khxd4Hd0)D`n?es%293cKc==Y4Ls6FXBHZiKk@Ve*BEu*$=%!Xe(9 zUFA&QvF`SHr5WqAY3V2DG?Q~X+eLsYrV*CKIwuT$kPnv?(yCRwQt;znj z_CI@<@%3*EtHuAc_P^Tz^JhEo2LsGMf7j3JH_u1@+yvJCYN+q`xj*i$Ie%q{|Ii7{ zw;{q#uje61;-j=H?5czfD>n} zaFRKDw?1lgz@Y3NTju#PU%n2o!eHodUs<`=y7+Nsu4%LS2^~r?%-YPrN80s~b$k|< zlBu!F7~mEU-ktd@zZZ*}k)#kn8_(KO(fT@X%>ZX6)>5xy_9m}uqwEw*OI}`#DJHW-8O%ZPE@sbB)l zB^Y*;y(#?M{W6ovXgFcpcvT}NQtBA|z&1bA`fXC?d#zuS`cJ3yCbZ4pPY7%L<&=K4 zlzFE5=aw>`nf+r+neYAkhn6z`&V>FQOPQaY>c6+tgdahAY!O`;k!WtcF&+@P+qy93 zZj|XGA=e%kIlN@Tba^b((AUhKB`B{A@FV_Y1YJQyT&+qP0z*uKoB#8uMR5W+Mx{nI8( z^Sob(z)IQ~i#6 zZd@0>W351*&STbEt$e_@=f+8+4b+dbBpj3R0(e5A~Qeh~aC^^`S^|N~;<=G)R_+GKY zF7(f+IZyNnD=qQNX7Gd5eOt5bMBE&_J|CpfP_T)Q4c-R1K+QWe15;$zQ#_0eugMq# zB4qnCC_}q*&vOx-3WCZeS-~D6{>Gs7*S{>ghsy)V=z1v3N(0r4Y&rKwCJ*QqoMu9% zN^{^YB+9cb*(1>ap6FK2gehd_;+AFS2-3Qia8wp@kNUaK^U#qFvM;bs))}4i#3^D#_PvGN(QJTIA0-m;ZiBybmWSt+`8DyM6^J@ zOLQVU5ENn^uM~eV3z)zcdqg~fjWt9t6}$$;S=fq~R)hN|?^^8@wjj_W?4Tzbpv8o~64YCXh4DaQf3_x{Q9=v!!Otcb07$W{N?z&B!s`iWAC)oisEH3BR6yxW zi&c3;Yy*%dy3G294RQn<_KqlYT+DUo#k%=NQBF44(k6L$GFxIV*!nrhV1_gZ?r9R) z1J|Um1e5|%B~2_f_`E*YL%^mC!d%a1uIxTQPyE^3y(weW9!UZMHGT{a0T>7|K#_;Xz6vQ~7c^2M-v#W*^6bG0aq1e9uZfsT z7GrB(^?IF0@M#CgadDu+U^{YCD>D~}+q}L@*3M5*k1PC`FFOI$Lf4#VSr^2-H}uB@ zdX|OL|fX7{jGQf5OX}>-O zk0E}p1DkITCXp=EVV~kR(-GvINx>O067&6RC(%MEByu($E1lMX0(R6HObcemQEEfQ zc``VV3;O{9>eK$y57Icy4C0r0PY+S`d13d6O3n47vf0XxR+rCr7*2%dMsN?$9SB*q48injWjsqt^nSBW=$%(0*tP}K9RL@NE5+=%mE zF%Tc;Kg@y2bZ9f6Wry2o6*2V0?nA~%5KIyW0kv^;93C%a0kGPmfGnanM6`wcuQ(yT z8K+^kKt;_Cv-$`mKNJ&AFl@l^cu@k5I`4?-SCm=;Amqm()&gixltDJga61Eun_nPsT1`@N0G_k zo>+sWVa4)o#W>=7{`YE2YEQLo$X_6l;uz3(;WiLn{8*|N_e7@c8-OM~j>98Uov=;^ zJT?-*v+B1;;n6t|01RR^)D{?kB1~Wx7Dm2Lf z0gDZG945v_Nl>CeFna%q2f|Lo1HV~d&NV#+*OdFu51f#M729zlQego&b^gNe+#N=T z02*eDuVJbj@}mUX7V~o#0pJJ#IB5f^89|bj#rclPh*~6xk*p8QbkVO%x|VTx6Dof7 z;7U;ijue}g?H3K3Nf7Vuydd^Ju%fL1}uS0tOX;xr;Z1)GI^F1<;*1Ss5**Uim^T+Q`|cN*&PB8 zG0XwpEh4@c7bz1MNj1w`l9mY;a$PQ^a*Kjg1=7^fCMmG4p`)#Q9G+Lmy#CG=y$Ep+ zvx|}i2{OVOh%^X^Y;`u1A4|MNe86DeP0gL@YFF0@F^iFvP+=eyQjL)zOla*P+%(~S z02mdeVuOI!jjkzvLEupvp_0r`sz{VF=41p3ZN(;#x(5O!4v#`K2Uv)O(5QJrQJiek ziB*&_Bn$6l7QQ$%jW}fz18#Bp$QV|R*Y()ggCVAHGKS4Kihdy@1~k3kLjVuFWjsK+ zY5Ea{3p6mTsAR|Ukm1dYODC2p)V@0~t${@7hXIwG;x!NhG9mb(+WlZ+NQ)k0V~x|R zP-RInX&5r~(rFoHWH@r0XsHM#IZR;*$;oSds@d3f0UD^Xs$C!&gXSQ+nR4lzSdXle zE@q6z*6GuL%tbgf;u)j#j?CIB)!ldQe8(UtkmL;gY7-6GFIKRd z&P9>3EYek>`Uz5Cgb`k9;_Z_d!UpmI$#itTJ^TZzmqD+%7}a;5kY6?|P=~b;tP|{S zN;^la%~eyOdGnscWFfh59MgIBmw*|hhut2aT2Ykq@c!#v`c!*t!2kzj28*0zwjD_- zAOZB}Ux9x474*L_-+Y0-5MQ8=;<=!YGL991{I1_!=GS3<;cky;eB$2+`8M^R1CBdijDQKY4&igl1gAjTmHyv9;$bapns8=KA^#?z+N`7uf9Lzzj) z;xyRn0kn_irQUiyMWhQN(uQio$T|-{cdAHTxS!_{Rhdf1-}rM+ga=Sl$h0u(pyQ+p z&XB$)PYkz$0W<;PNF1qh7J*R9cnao2qN24#B!oO@4CyPwV)u|LhYXfRFtHE>naYE_ ztPU#jW_q(h%t_3AXB@T^ovkRnrh8*BTtkFh-%1xYnTDcNWM zx51BOEm$Hp1_LTVRw9Vm&XPJ)V$lIz5K@E`Rc2G@0%mn!2OVjW{{VP{q4$2wpOjR$ z-vzvL62m1$L{4POKsFeEB-IiAm5(tI4b1NVppGq?VVUC{;Ewl0h>ScblAuykz7Gj8gzb!XEr`bpGbME4er*8{VHP90l9_Jbl27bgXe;7%ba!Db>v zrGQS5d%KMSGTjOh67rT$f5xXy?23@{7G6|wLfEGnJ}rVs6trM<1MIOAb}iSDG&_4d zXV#AQz{$5-AqdXs-!i4Xv{#KtLcay(i&82+*KIaDt0c**Zarz<_VUgIklVN#?D~pX z#h{MGH`GMot7%Xq49bM9bCiNatZuT#1-P@VQrsd zhjoRY9gkD9Wz4t^ax@7(!A~dAK~g>M=|c+Vr(+Ba^>u<_*01t6W;ay&Z3F(XRW0^Y z`a_07JLcbB8B3*viTpAjrR$TphN#tu-!d0OKyy|%D~zY&m_I{WzL2(#h%0+(Mru_J zU8;v2+>mgCT9f#J5KIi4+h}aI4v2%A<{JY#XGfEVj*GYqcdPROn*^-9!6oawNxALGsRS4H2^e{ydYG;7AJjUAf1sY+~8DY`?uO*GJr%(PuLLt9;Dg z&iYPcFP<@L5^|mR=0YjzxJ@`M_$!381^lp!Ds$!4DR^KvIv^`EHlYokI`4_42+pSv zB$L_FnZ6i(l#oSe*Z?nx%Kys)5je5Y@W_vjUcrj5?f~p6fI-=$je-WKME`|cS-Bsr zPXPb)4)gO{u;VLf*s-8pK?_Ay0iv$kTcO*}%qkeELI?B_BcwVQTMXrQa))EFpJfM; z5@hIKvb3*(0JA&L&x&4kBLWisM&P{jj~^P{kyQTp+9{iiY~Un2t#^2%ayQ(ssu2>$ zA-)MRA8Q3UorsNiSVczi-;`w6)WHg35uQy(RTEHqElc-6QSCzmK!%j~Xvv6s2pU6N z%i`)v(*&||NknpH2Ml%7nI;Ng=yaoqZt47z`9dBXNCBLXSHKt`i}+QuU(X^m4l=Dn z1N9L~jGNQchoxgwoh;m=@TQ~i4Sj@Z%{EbI;-C`BIQxM7dbON`YiU}i6F<3+LFU6) zT~J6CHDIcau~nsH3>_YNY%o*=LFcflGNgSETisNx-XgKn&Smlf>!yM_I~{`CLLYzyM!Mnb{5|p<_v$u6HRu5K0HB2EwTW#zj6*^~$Z=_oQR2^jdJ{>riN_ zx-(J1Ari&Gy(i#TrL%Sl!yr4?4qj#Yd9@JFtUBJqM|y!iigbZ6FpW&kg3&e`3jpho z&&o2qPG0kd6hP`GlffBBh0{h@u_{2*qZev$HmWNOnFLA+D3}O_%=h3Ts)l+i#Ln!Y z6PBaCOilZ?(Lt*6=Y*Tw7@0kjle>!kzIwn?Uq!h+W49_(%^EuAf*UOrs)-l1W(XgU zhRCFI56M-jc%x2WR~1}ebM*kgvRyT%lptC`sxv7=k}4dl?69A#(hfA0cwhIK>c*Io z(AIs=g9Ji;BhT#<47o7hv=JPxDp}&HlXlQyYEj?Ug62Yi7XLn6Su`9}%QTg;)KIpbTPTSvL`7++~p|r{{_x;e?ibS0k9?^%% zI8D5~x=_TLuq1eZ&da}E!^3aog$*SagG5!GFop@pb}q}`y$ z{4FHj&O7vq3T;-@B5(&b;{#;h%3XG&Nc5tAkXJ6PpmW1=8)-ks**F z!a8qKGC{IYEt|I-wJ#c8w%&BQ_k-n=etWs3x>t3BK#WzOBC|)G(*~1j8bLKy=p~<6 z_iRxXi;hn^^~AGyP`nm_l=Q}D8$4JJ@r015q0V@c1_~YcZ)&X&4+6p^A=8tPNYmSA zPvUJ71;mz46CSTx0(4R-;u!wS6HxDJs>^`UMEmDr)zGhpS~XdbhvV`(Z^Yd&_0$>s znk-mn`kOc+Wg{Q3A(}m4aD*?}1_sb7K0s#eLX7|~#-y{cd5xEvREiow1iDSI^p3?W z@q<(&qoyp!jxMUEk)cPw)r^9AXvsb5{?buaP?dm%bvPt^T!$z=k`hkzQ2e46SKn?$ zyT5Kct5TL4^F^ zSci7O^j6hfKu*o4aI@R#yMZsVr9n3(O1eFi|4&?G*fSd};!AbZvp8HU){vF7j ze*tnvD0f?qrn+?QvgUYkDpR}uDE#n`#byyBE0iK}?Uj zM;+AJ9jrm8uMNGXG+vNWSp;^}Wgsm&S%6AZk+HJZ%38L z>&W*I>L^3M`X;l8uS4Trd8+6;0%2D*K8*aD86|ey%C92|IZiriQFRG}gFr?mAwS5P z`XSNmnfWMSLBZ#ApjP@lBO@k;x?Pz~AfM=jH%a%SpAC7NEU;0atyHZT+BdI8DrtBN zY>FxA3J!8YB|D;5)mxXD91>?r%tMI`JRB>@h8@b0PzNqIO>PRBFo@1)k9r4iOsszk z_3IvakTZOZ_8ql?kjho}L~DAlsx9i_2~&{e{;2{mQ`(X;8RYj}lqKTo@Nj2UxO6~m z?{)waL2ZldI8+WJ=!BZ#*ce!6a6o8aaaD*5jdLiSGoAsEu+UFkJVAD(nN(H6 zk_NzPU6UlM;LENnQj=m<_XB`t7%piTx`p%!uV=#yrCQyYxSbs&3}31Yh&BDu>0oJA zmm9{{j|*+6w5?D29bmOxypXBe#lbw^=B?xq?a1syt_zuq=L93DWz zB+~_y5_3A&vo(EkQx1-q`IlIo2&WC}RaVu3(BL?t zyXb)%UAk)qjV&ahSntsR%HKg&R3t;_Lmc7bYDB`7ike4RpwZon^f`#STE8Qn`%(wbQ9SI09Qp5wT55ej{99B^4-i^E(JvpG*w5CKB1%udQW0zq7h-zi}GGY zMM;O>&UCd0+){&hFahjT^H;g9=+r@$;ZrmJMhUSiabs`p^at3HJ-&Z6LbE*?Eo&wiU2Jjax8&`4LMfvEymgi;9E6OZWUN zbb9Ie1kfbPVKx&9iCe+-N<~LvARr~SO7x=pAL>cGEYa*ehC45wI0qsUr7p*ghZ}*j-g3_5qVsxXtR6j95U; zsc^%9=DdAY-Ttr?Sv+>tueb4Riw!_Fx=m4&Q8IJ=%!Jgy9(R~96vziWfT-w@2TW1KGSGUE_B^y-cv;c)RdD?o1!aklF{W+%gUOdTI`G5G?J-I`m36!$_PJ&;4dXlby}}?o|E%C&oP%6 z{@H;JKI$%;ato)GrWp<4n7cZ( z$nZ`qqM>7;cMIqqFvIs%9ficPP4Hj?DaNh%d<~zYmiO%B8?=m=$E!8G z%3X+_%35#JOwutE1WGp{RAn?|`2s2YzC&w$N=l~mM_vvof;cik!y zB2>uBEqS)6`Vnh-{8YW2S#>H>7Vv=2>)K1RA?tKfM`5qs>fJ=%F0`AD!I5>0YU?@; z)z=OrlhJs@bBO1DeoVQw>S`~17lCv6I^11AVp zw6LhNs)v1mR}$*3z;1#?A#L<)V&jPYC6O469k3H!3Q8WF-gxEOQNl}Zo=}#{W}WXt zTeN+vQlYF4Z|vNcy>EEn4qq;RZ_mbKSEqJiM8nj#~Mpo*Qo*= zm{bTaCPw2DT7pJ8L1^tli+jI=J`bo=1t96F*m6-EhrI7lxwiv+ctze;7p9Y_x1CXa z%1m7rkZ=HBet`Tht>;4dz8sR7jfW`?eFMf)`c;W%2~9u+X1`0&tx&;!XV-P7CQCmc z3P|AOHOB2Ts8vNLs2yx`xQBwIPd4V$}8gC+g$2z*i% z8LHVhRo!2RM3VejNmnB zKCn9o66m*`YJi1m+^8_Ek*o2>IvH163rg$e#ez{=gmcnsqO6NBo|H=_Q8$82=1 zgl$N{8R(sk3ZdXzH!`AXg=C(D+Ytb%>QY{Yd)%A$UWY>jr!)s@zNwob>$1mstyMag zeJu*-9Z-;Cw)563Z0avj$PgU#o>d#K?sSUmY4!LgFxw6EW&rQ4ugX_A^rW{ zmgeL3D(HcGTi=ngZu3y~5B7DG2Qva%cnX}!H&fu1Doqq!w|@uZ#GFz=TKZywi_@wT zKB_UNO3Eur^gJybw6~LV)Y`qKnkt4Sfcnw3QkKeU-*N6&=+SCwycLB=B`vAh3mkQ(f#J z@70>1%M7H30yKwvIcH!dRjSs#=Qe>lg$RHGdcDL9Gu73ouU;Fk14kJDPPsw5Jc4bzPB)k2lGHbR zZ`uL>-GlAzcCa7D3hP zaL>yzno&RghQ?N%iUCCF$;Ymq)1d~6`_&iUt@YOkD)FkGlhhQt8a`!E@gwM;`DSesB>s#dq z#PN_3nP~v+nl3vN(&pZXzeUXN%f>(Ts^FW58*sin5e@TEZ~Z&hiC(R9I+D?)iuc7E zqPx+*A}T*g;#8_WfpuQ^@U=!F;-@uYZjynRRD#yr75>Ky{H%|a+%H7PQQxJ3$0aqS z`>j$EWKIQO_PY^|Ul)dtE_xnS7R3^_u3i25VDm4YOs0nZzjFZeK2$0P^+EaF)J#yw zf^c-4!T4JeZ5FTYB-MfY=?qzSYVWh>`1clVzMq};m4p1qs?MqaBrmp_-WTy~ zotQwMeI*ksEjzUvHn^T4cjtY3``?|zNw&NYqLZcex&ZsR-(931+?Uncr>kK6YmG#D zYZk^fbpup!8(;h1aoXo`dAUnK9I)*@Q(_+ysRwMee&{HRNYVF6ylW5FMknphcMg63 z#6OadpaRf#r^+vj@?awZX0_WB`NLhEZfD4`f4KE0X8)rF>bmPNUoJepqvpR08uQ1f z`R{_p{DKT0Tu@T}Id1+KAb<2NbAANQSJ)_OU|J9d_avUag+TYY(0C<$piv0*0Y@F- zFp!1st3O}2BZy;idH*a(CDW*~th!Xoy8?ewBY*4av2GZ3wP>hvVKd-Q4iP-xYK=~p zpDVN+$NkpsqSkDY^-p5#K>Esm}9PbRu`m z&%rd=L*ak3hrT_@-6l-QD&0OY1QU6R=uxpmcmJW9z32qEwbWo8i2d*|Api#U%^HRz(5TGBb3+Ti>^Us6fumMHZRrqt$)*;F^jp9Rj2) z=xQI}^ZHUM9bw-uV$!iaxOtZZAyLWR=_{(B{KjFzQ|+yy&m3=MDC-m^-lY6hfny>4 zHiNw5AhkHa-l3jGfBVmdGVhxw{`pYmF7*FkDD$z|KOV|_oBtmU zWq#Vpe>9Z&#VP*5Q07-}`0s}@zdP2y8j8Sa-+^@4Ca5wr%9*aVytAgtz>!&@b47IAqwWn#{Vt}dE_g5T2wAeqy=2Yv zP6321X6eg5)FJvB<5X-Puj4)L{!xwPb0C0Lhf)yNZY98B`=shaP#?ZGD6pdoJ!{(I zb!_DHm6TgeZpb)Q(7Fh_(9;j^(`(YxmpZYixQ4#&7?#1VF(LE+0oK4xBZfZ@m;e9( z24YJ`L;&;v-T;kI>8eEl000SaNLh0L01m?d01m?e$8V@)00007bV*G`2jc?>0xUGe z5!%K800oOlL_t(Y$E}rJY*XhQ#((eev3=~=Id*I}i9?J71PB2(%105Ul#W%cXtz$& zc2TJnOe*`hSnsL|Y1($NshTS7Y8TU5sW;O)Z8zJRAx)^Gw6vuKg{FxE`5+$}FRre-01@E(&1UngYr7FPSi&PTuh;tq z@HS8sS6AJLWm)g!=4ZckJu^l*Tiz%K?LF-b4DKemx$m4T%P)(otM1Ql-JJZ{TfcpW zV&nuu-LMecl15Z}pEE!F9;vO{-xQzRsoO*EotHjbNuxxQ{J+X9<~SCcwl8jOBph}dg+lyvc+$lIn zatI}w#1sN2)WfIOaD{~Ypv>vTMe5Ql6XRcU@%#icvv=@*vmJSR3{?mKDhAkB zH4>k}p|AB3ONXhg)>+OjW9IWz%#YVdS|tOxtp(7qij?&dS1yh*nO!9IT8dpgy`1pu zBc=4RT+UOd%rW%D1pXxxTo=Dn$FOW{%LN;-3in~kuy(Gs4r)~>H|xyL773p2r8f~m zEqeHH@e@`q&yn>yh(H^=`ob*N9kRZR;kSS$1ibf0Am7bW1Kb8~ zT}DybPzNLEhBeXjX%S!tjiiLgWQ^939hwa4furCqJmf zUE8`YC;=}@%!lFDNGTDzw%A;@%QTCS_D9$m=%UiJ3B^^ustgc}`vgPJ^~a8=Z+5KB)<&Nv>3&v1GC%*TbXwHdNb7-ON!h)^TMA zt876)S;ww>Dr=}%{z>AVFbnzu*G8t%$GtqhFTzJskxHe)ko^Vi#t4U3Hqq75g&dH` zSMumf2BvO9e9!tM|5i7%v>9`0n8T^9TpF8Xq_)82aY2_8I5+zty07&$ns%2GLr$>)KWECAl;;!1mz`-*|jKsYEaD zO?^zQ(!j2{{Bdl8`Q-vnZQjE(g9q?hH8Rr~&Yv4a+VL!+?<7WB;=gC&NIvr2$NBxA zFQY2M96fTJ<6EA%q`X^0V_yEzI$^KU`vbLL>InSwzo~yB0l6VgvOD zZ_?=fuk+UL-r&f1hOR-Ccp{2q*C{TU++M6QJoF`Fi<^ia`xSw$-+dUP^{>dLStYLe z;pRHCI<%L1|7%>h@N1M0u8~TI@OV5dPV{o)R+WP0uH)(<*#D>hdGy93f}N=#!NzT_XNJ$p@1Ot6 zyB=FUIe2j2$tU-GrRs}s3T|}1Hwp{Y>!Tk8Ke}@5+{Fuje_H?oRgLcI?|-4EJN=Vz tJh5e?Bzhq~lU=ax@5b4a z$BusZ_QBWt{@K+fDr@TMp8wuYUNO_@=Fx#cUwp=J3LzE{k@HtEe=q(=<1C!BoF03! zu(UX=*=?=+e}D1HkHvR>{;OYStqANMm{bF|XLQw4-3u1{XOW6WF^xDv2o&9p?#x=I zC5d9rQs_yU|KC69E$EG9J7#SGbhi&grbHwPCTsG;`0Yt1V;Un- z4-q39fKuluvlC$=H?Clf2hnYA`h|z$M|LArje7*JB!733)a(RqW0ckI8Dr}stHYW< zKTu}*ohUXtx?<0e0H~O_v;+b71W#38r_4LeaKypI;bJ<&#aIY7`P=E}IFF;fh1}p6 zE~lTNL=MxwLfk7G(OlUGMP0g6A<#X36w6K8leA^|bBZ(+6=8FNs@#?0QXwTCElz`P zfUm6kgnw0eQ&^@!MUI2=oFFxQJtRy)uDndknRj_?a*7>UDo!iJN?V#Az4}$kU-~0i zRi8z5co3N?LDdj81vC{c!_XHIF{$f8v^W_s>jq~rLqlWeLPqq8%__g_BD zPZJhR4u~fb45dx>dL$iX0giq2ds3Dpul{ogO@Ci@C>nqXqyQts#ZD>7(qnjC5vHv) zLer3_%uGSbFbM`H*uVcg+h$|rIdwu33=FyQi3b2(^{$t37D8^1Isi33rkh$x;jnE&1(9`bI}wNw>3f{ ztA_sgYxo@+WkRLYts=285KYpSvJv)IqLpmIk-Hi*of#2RnT!1n5z@?U$r6c3DeU$TppdaPA5}2I}!O6q|-^lk$(}SnI>Ivm9QMAQ$$cS6Td^_{lOU~bRWLD z&mgKdBeE+q*S0br`+Y>rf45>0EJ>B;G7-ImX+tMGo~CAnlL@mCoF%B13v?G+fe(P9 zJ-?-`tB(^EHjZ~Y7#)4M@^0Cy zdqy;}Jd)<7JaY1Cc=-nx*}Ud9rS5;=6r=Y&?@RWv?@1?np01{}vVncye38wKHp2ZD zQ=x@5*7Bgi+^6xWx|RYd1bsm}(SO)9O;uxTZd}2c?&ta4pRUkY-9=rl&^KkIwa?@}cy5sW5sIM+5# zo;u8?8jEi{yDH0O^}mQzkLsZnjaKF5=PpI9rM?F*Y#8!4n<^)e;gOFX}R+WpcNwT6T$|d;JVq+c?>8 zCw24$^>x$it~gG#shRen3V*_vuCl>%8k??iu>V^WRXF*4Z4c#IC+FIS=(rlEW%Apo zE7zdfJ8Qadan=6@o=LqYZr}^%4*C{C7$w$G*IW)NuW-ZEA1$e3W z0PQE*c&$Uyr!|n%upMXhhKDBbDa5J1s66o37#(8@Q>hF}>3K*bSbrAMG?0@c_>JqV zcePQH=fcX{h-nG5;2WIl-@vf4mV~zq;r1ak9U(IJw-DL7$+`lIecN1U@mLg(UBfQK zT;-S{=5mdKz=8CX5H)?ApK`H{OT5@{7#y(sm+MHc*ox^WMA008hDbmBDb#Kgcx;mR zcnGzwsrkA5q9TtW+<*F143eo#4<&PSKp+$isXB>EH+m|BZTtd5gJF(Oy@28^N7&p5 zp^|x`y&qXFg*C+*b%9u7xa(}6cJjzuM|QnXytkpSs?h6nC=&@)Al17+%Edkx_W>R- zE@c2$w29ux1X^O2Cmw4+QS^I{&`Yp$AMVVVmd_z&S$53QV}Apo501V2o_6-sUtX^$ zudi)KG;8}3018vEw6m0u5shjtLoNn)NS_VAJzT~NEyjg@3kwf*Dkld hH`sskD5lxn1dvE>n2P`a002ovPDHLkV1kTbxkLZ} literal 6971 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 O0000P000>X1^@s6#OZ}&001;ZdQ@0+Qek%> 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 P00000NkvXXu0mjf3f~sZ diff --git a/padbuildimg/assets/lat/jsf.png b/padbuildimg/assets/lat/jsf.png new file mode 100644 index 0000000000000000000000000000000000000000..7db08f619fe11ba638c7d30eca78af22b282efac GIT binary patch literal 2101 zcmV-52+H?~P)X1^@s6$6kv|00009a7bBm000XU z000XU0RWnu7ytkV=Sf6CRA_TqceSgPWXY0^Y|G#$cFHT55=<$91k#~E z$t2U}Lr7*ilT1jPNiwCKPFj*?+NPP#G;Nt_Gwp{oOA zywS@IM+WJ(Am9e$F}9rrZTYSOnER8bW@Ok&}F;*kOLp3^LH`H%`sTs(S?+LQmo)p`b9 z3lpwAg(EYUxaQ?t^Cy(8`yuwCxmc786uXOLs)LvSVUlqa%tuHaBxGI4m?Tim5J|Ob zSCdF2@HL#Er+OEI-5q!etSs92O`PsLYJ0P|@M$fLje9X+X4V%U!C5{B@7cq&)P6+k z$6G+LGQ(5CthG||XJ|kHDHj(ptF-IcUOgb~lN-4|B zMK)p-&-n`3nI7`;%JF=mlBRP9XstUz+!RF%cu@yqc+6f(VrLLxZ>qzYG%^SbAdzZM zt|97gVcA_C&L`L5nX`y&cL=w|f~uPEcAuxKtAYHx*3wn8gG2BAfM_5_R_ScAGG;Ju z(K^i5EasJkD0K;Tz5QFdT551tJd9`AqevM}5Vxs%yh%ouRT&I4Ae2gP#vmgrgQexm zX=#6t<9pv?@#>`rF*JE>7KNVX(}?y{$c@KX37i0A(@ZLsew~uSY3$tk7d++#Ts%-q zVE;~*Njk?`&f@QEBLBf>QQSpom6VZ6UgGvCja$NMwK8Y+Op1yMi3Iuyc6EVaAOL%g z8^s)Au<;l_mEcVqydpt=(=qBk`5Q$JKSk5+?Ai7%7k2OF$Fj~JY_Lg$kdFTy?~d0e zC4o|b30wSpMTFq(p{AVjojm>1VxAK|_4_d(@V1osGgM<9O>YuQB6J+E~E}%QXaLpkD z2m?soKsXzt13g||ZfVwC)4wo-P(X4Bj9@!Ny_YklX1XxDvU&GXH(#XrfWsPWi-UIe8nEP&R`u|3>^G&J46aYgR4xHm^(9GIrp-h) zxj`RVvhJRlgZUe)`S7nnp7nR|3p1=TgJOcU3J4qI>hNXjP!};^cNAWZLR0A?3Ri8M zLT{&*%L)0s1A3>{U`4Spf93P!3Ao=3 z62pZ`47O|VYC_ObP)Wf@1VZTPi9UIxs_|EuG-0y(IEc_z@?sfKu2CBrH{XXExQ;)Na!hz z?s`ThxC~%bQ|QS!gmgQBP?WeYMBB+jWJ_~tZ0*EsQHaMAI8+CbjxK!d7qROR9stVV zsz+BgU>;BjydS6~*l>hcMK&g@GbJ%i#7NnDSs11px}Kz^{t^SB1gb?P9uHGfa}Kkr zqF7`i1HH7J*@x(Bw%|%TtEZ7!0Uh;_!i&*W&ke$ za=4d&@7s(aWU`lkJ1zGSIUR+e0mPCjr_OYd2nTRxmlBU>aj~Y4c)&}Gw*}4L%<~w0 z|B7~vz;A$El8i{|bYSpZpn#z#$ zNB#uGZo*m^xx0^0-z7B72a=#V3H3lB&8HV`r+Ceeh=hXl)gQut;*Tiy95QT9G|k5me>1-}bk+hp zfFnX6duEfhWFxA3Hocd&vHS2_bPaU!Fz^mgXV1sA@)d+(O{-514y#Hc)PHlr@J=F` zBrFaWo{Dw&UB&cVcn4xhY)7=saE8LtpvD--dgaAkqBqa%F;e4`7<`2DQ0WiQm ze=#I94AnuFEdz^u2@cS+S19~P1$d(1FEcnhtW0kXZhE+=1G1%2?9Q^PDarD6c z-p`tAUX(!)BdtCSjEQ0bWt?lV?T_IDd|E#n-yHwfr5para|8Q{+T5&dJQ|IMV}t)~ f^Ll?pFnH?!iHIYN8N_b100000NkvXXu0mjf(WTkf literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/lres+.png b/padbuildimg/assets/lat/lres+.png index ff93af27bfa639305aaecd67d6413ca3e94cf683..986b3d9d12046e4a963e36323178ee5df67c3765 100644 GIT binary patch delta 1855 zcmV-F2f+BaGrkcK7oQ)Y!)-PrUo# z+kbg$>gMRE=-JWV|Knf&=2h2m_Rddd>dX}@B!t*NM6O-M+P(N6$=UeM3q_4uOGjHy zGXleJzV_;`#eXk;`-j)#_37_T-&NBKE4u2bkqrv}8CYhK(!^yd3u8#f#7L%zwGR?* z+l~+$1AJOY6t_%E)ONe-;cp!~eSV^0iG*sV(&&*o)~x$744`3RXjOum zLs0iH0-$Sf^WqZ8d$;JWjS&=8?pDi;EpK7Vv%3(Q&VK_5@Ko+3wQ_+aYhF!QD~->; z@YR>JR8zXsONCGZ2D)PSvZo-41s#%M3yFo{<%kA~M0As=VG#~Wi~!6m9H*;vlEd{? z(h-FV#T@5GYxEr%qGswiUKF$>N-+L(j)g#vrck^HPP7(?r;@E2ii!w?KvmXL@hK6~ zJONuhFZ%#;pmP z^1XhjKTN3SkiP&_4G~a4Q_=isK8Ttt$(Kt^Wyh#+jm6v@rY>cf8C&M|jV!Zc%Tx;v zQo!B=Kq8D2hsu&mDO=``KmQO@n zHpBAxJZ`B+#ChA|yg8Z82zyD+E*+N~!#_Cx#GkVXDmAmvhXO^1l76Yq38- z!~&{54Y`2jNjyg)D;~P-aA2E?SyEY;5(G=IYpX&-R7G3O008PDf<39#hkO-`t5B^; z-+%reAtF6+U2r|0mOKZ?vau`|tL)O4RO#;MM4APh@*O5;%UqnTk%-tdCZVHACmd4A zmt00OHUMK;4_gE=j1aEl&%LXx#(tj&4y(V!ClTq`IMo_8)23Xg5iu-Wqk(v^Mr(&b z_D+CL=L6Vog&plKUr#GE$KlqZ#T);!h<~PnEZotksxcC=sQ=tn)?$Bzi2EdsCovIt zE;yElRdDf&CCosPU`S4<>7nNT)I1QZG@g6VmMw$=tF=f;^|wrNW)u`N}>)xpf2 z<RfM4W+Stx;I!a z>j&De-xLLb=SG>%$GLFk6JGqr9ul5Fu}oHKVcejKCMCoh*s`sQmMz^>lrVWqGBGpG zG@Y(%ViKuawYaM*{S&;qvsx&ul!@XC;qkoE29K1jS zLg*;rR${3b!t$7zm}30KEX((jM7Iv%HV)wG3BMB(F6&d(rVmh_8m6rEFzX%T^r;)n z6_*h8QDV&2h#5Z{17%->}zE7 zFy+?g_|u6C`z9Bb^9)b0<+FAczJeWF-Ji6*{7g%4 zODq&r3RP7g)%D7I_RB=3sCk3jiAoMV32B1ELoySCf|DEgA z1*PmvR6M;hojrcy@9$|J|Ko$Vy1Ty8xBEb8@IXgR{^U~J-fjxS_*xcar tUxsbf?1}3m!`H74zn8f^`7W+I`fp4O2bvGDHOBw|002ovPDHLkV1nYGk@5fl literal 6584 zcmV;p8As-cP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*ta$~!Wg#U9Dy#$URxg2nccsuCj_XkQI3ocfJ`KGq1p%F)&!*6W_kv zPZ$5q*ZV-{7<5ChV9L0$VqMVy$TBQ*_R3wPAe?7R%Z=~-a_?#V_-BbtWU$;YH#Vq# zJcbx0d|)fxJSXleK0E(Na;WP$03l-T!eT-q171QZL4vO_))0teBR|T@W6E)mflx|Z z!5wmwl8~GhZEikO!@W0___z$Ukcg5bRZ>HtkgS{w_^G*pAuXw7S4xpmODnyoB*{`l zq>6Er)>LyXRcfuR_Npzl+)9&HTWh^(k3B(QrAx1_x8A!dlPW_ipH}Wr9dV?QM;S8e zXrm9Ez-Q)Jrp!9q>{C}8De)K(S z&#b;?Equw`pR*R8S+k7gdtd#?8kcK*8zLB;6w4VIi#d?-Y8e2~(Q@Wn$T?c(EN8w) zilRia7K$Zj1B_MQO=TF8cZ*}LePS89vYf6mhF_E!{jJLCjSc#+#&S9!#7byNSQ={| zu}|^rD(Ofk#E~V~%yF-3-%s5uonwpRwUB^R&pgAGV{fnXSm=&AdOmx$5z-QG*JfSg zOle%L<6h~TvD+C0Zq@rap+scwK@@xFb%B^8^{hH4p|*j8l-<@j=`M&Xj|I^cRMUBX zn|74GEqRx8M%a`40M8+nir4&jsEa)K6`9weX(5tL+}P;Exjw* zB%T>VVZ_%rSp;u7t&b#@slU(pLu|rX{gg5E?6GM|jsX&vxUC#?!?^qZNJhCcK5{`O z+DoQBC(3=w)X(!0etu8$hByH76A1H2djMI>3uxo>u7Va8CzK|4j(C6_#USI(HjdoY zPGDx=-)>;Kdkd?5+%n;8dhoCF{+PV_4jKP}825zCCq#S)#3#V~gr|!a$DH+bPI`}w znBa$Yx79+jaK(xO(NX2GmSchWh)Z#Q-uWXme~4VA;buRC>Tf|R1ifdusOc6bq-i`6 zPVUfWL440zx0f_lJ}Yk-d&M)@N>MFm>fGJ^vx@p2zmvj}mXl-jT;UaBw%0PU=|+mW ztCl{B(S2`{pkfd8B&fEBw$CJ@qs&3y*Ub95vn6g8NXi*Lv;i&tn^n4(Ws%x z?dPh0D|+{h6+TnOS4#Lw2)8WREbDhbP1Xw~NIGwn@J0x~RQxUY_TUTm8)1ARgirH+ zCIrH4rFjd}l>bkh@Jb1Pg?#rL^gk%!b>`nl!TrJt|G1C1pJ4xQ`iNWWpZAf|#*SGU zrGa)c6*9X+NUZXPnw!e+xak3LGjdybt1IX)?`|Gu>NwUWrQU?&#A#!YULq zm|r`NMtxwI@?`tW7m*Q>F-8rg;YGGte;8#T$qofF2URr7PE|6ceY%*A@zfr5eez&OhRj6B9@3%hZYLsAR(ue&~>2FHkQt)#=P|PM4u^LW02q0|b^h&~!aeb0c5;;g5+~`snjDIi`z$l>FV%q!UWF!eyFX`q?QD0%P z#k!?gkV2a(JvRt-2Narzvk6kYg^(wOGNKY&z}W$p&6h3>iRsau*D370SCtqLtd@C!MK| zPAmXo3Qeg4#>bfuoRKH3ENKY4)v@TmsB@=8Z5*Fe16NU@yX#(bz%dd!en9=F&l@2c zHj6{&(#)`wM15{>R$x1}Mh^F&QF%&=Wm2?-0>dE`Z&~gX6I97q7#IdwcnaDb@mmy0 zu8kU+6y9`N5dD~m3NQCn!F;Y2{cmkkfHlyuM0YFB&aerm(duUVaC;w<_>VIrIidhn zq_QYeOL#vs4ct|a!?>aPTIO00zBYnRtt{~i9+euOjz(e zFbkzZ4DDB=x%B8d!lc&f9rbz&rBNb&sD?_pwY+nj)Uj4P&^!XnC~Djm?ttM-+1_Y$ zFCYuxy4h7H)Td&gNJvPn*-uTaa9_XI823)hN)~-F9fgB}T=y*?-_Vdm0t8_2?oGGq zx{oGFroWq@jLWWK9m9ewRO!|NfO8Gh+HD+VIqMRQ>cU=vut9{dumxTPaB+iqwQh1Q zdjSPDi{}%hkvC9b2?b^uIl8>qYnBZ)wglA_eZ1~NQ);XgR{r@8JaPJX+x?gK|M2!C z8ngx94Tl>sdLXi^^(brDR8pa)(0M?cwu9ni_v9EOK5p+7I1uWhKFA&mrwk4;@*Rl6 znP{*T$t6`w)QB)mqZq@%v;HKixM0B7>4`{PlC>h$N_~t58XO;lpP>32d~P5nn!!$j zcGd~Lq}FH8pbdJHej9nN1v*2+1Ei)6cU>Kn$SPSLIuW=-74ZBR(PTAHv9f_yXy8FQ zowL=5Ag?>cVsdFv9SH4m)KMzP4$TzRL8eihi>|-JYLWg4;Vh;2B8#PwN-S`cnTU+R z$F3vcr=4o<>;@DKCPIw})L8=??{F6+N^6RIO@cAB)5(LKdce;J07bbXZt4AS9Lg}O z8=fp%wHmYoC5lBj=|Gkw1?u0utp(d&XrWjOlB_y78B?D{Ma zHsQV8Mutu6`&cfiU2c0Y+wg<7Te}qQ2_KvOqM1V)jI5%<9c3zyO&3} zMUgh4k?`%kGKlDGMFfYlO;IPQ+4`dgW;jf07-d5@WMG;Tm{CvlKO_{)b-E%!6F}^7 zpwiGcM)lc)@$?Z;Ij@l3hpat!W3}DnWu{_trK$t;J zQlMx(NSklbB!|(kdT?%e9LXuA&?0noJ5)*Z1oW~|LRX4vcX~9<76F@Z(gf~Z=`Ucm?=59&l~O~yOGn+}>Vth6;CMN)h(mBJ3li|mjR)y`S&xn=1{ zLnK6`eob_lbL$X%%Etl=q){=%ns$pGM5A)Q=><%r!xR^DI`kUcTI%MO06C)N2)z_d z78Ny2fTbroEfK&@(X~l)g^}UhW2ZWHG-huxw3xUuRk&zRLbilI$O`4_n1Fy+ehXH6YMN33IVUh?fosr_tL{T(kqRgI)&90MkxwlPUgsy925Z z>d@i*G{t5m;oFF~u%|kvaktiMlA)%E(G{W=6z{dYG&t90ag8~juH)GW*z^O zeZEauO^&IDflTS1(J!W2j+ixTY6K0TRWgFiI+)!HA3ci>Jqy$ST9e;OGT5UvptP<+ zo5@`wpfx{E(@ZR7v4bG8hgh;LXR-_IXY=yTK9_t_iieM|B6OfzsO(Aqi~FI~3ZJ8d z=bQclHNYPfLB6*SJ+^H_bQ=61yJ4g%D zk|cJ&zt46daW>wSPK#u=2MnnMatOC{gmy8F(eZlrfXOI<57aE?4i2QJc3@$E!=5r8 zGf>5tnou%{YLyCXh+vN}sKyT+D?k-ain1pk4zf0PPczJ@V>5q?W)cuO z;F@}czYY!%lM>N$(Ton3R((B&ynFU<^^3hcf+!)pb{Aqt9jXBAVM3BUR3Jq(-K?0Q zjR(PIkUXhyt%Z+b6=FP1`l9z4IvKWdUjSTMWPy&LcnxFGmM^SSzl9|5HbIlGZ zpjr!E=LI>4t_mB$L9%injKMf!Ti>q`In4=if$K?L>?o41LJvlM_(_?%7EvXzH^kbw zF;L~WIVuK(L%%eSN3O^PG=M>3@AlLSnxlMyGOiL5%1%rIPupslh7PQ1VP6?(uuR#L z0IU8+L$@m?bdz=}%&XVYX8j&ir5rBUu zd7A^u!5pJWutlA3Sk^7*B)o}0_E=(?u>*#7W;Q|ygWlJW^K>@!HI~W;rhzG=AY=-~ zomd!cWzyA}87iV23~L9Mtp{mhR3NxY@QPF-^AKjA<{Q=m_Y{boU6A_LkOAI~sy)r6 zk?x4w!N+Z9kDk>QhYcUejz1h{9ESxYa(824?$Du_E0}_O$ z>80QikxVAm%?0>QxMGs9)y_u@^BWpL{F+Lnul5!v;-W)zcuW!#d~e>kKr+~|q8Iz? z059elSPq5&Y!nPi)9kE0fJ6JEI$%mtDrAhDhm#>O=mbz5yt@%zL*2D=AA8)V16+pk z%sBb!qE4JDjc0CT2pF;xIiw)Q_T(myGg13%EBjzTN5Z5iY=kMSuCic7Ep%J?2P^Kq$WNG4Cy&wvSZkgpHG@$V0W6oHv?~aJY;1LC_Bx3Q<=!@ zUsjOo+dPk1&i$(~rm^Gf2-pl7Ba_woCJ)n7=!Ib}<#2%~^ zdpc!jv=9K5h$h7S)~}oRw1@C9kGm7yX&4f>i8!ZvbODYXzJ}|$jgMWthpM~BB=(nY z-Tq+i=Ri?sCV7@H?cJbQ;+H*_rV=LJ@A))KiY>Ul2OY-|kIu!a61u^gPX%Eu*ZWLr z-ZKI3ACK$;N~_w6Qdsf1!Ss29XdRKCg4Mt7!b0VK&dE!t;@^bbeaY{?4!cXgF8FWo z`2imH8KgfJ!{6YPeYodF+Hb}9M~r^jg?ntnSMu$9&RXUyrk?3xNE5;09eW@|!05ns zyu0*TvU-zm5AuQ)dvgnU$uL3H{&a^O8hL^gUpDq1BJ^4ic+zn=!RVG=W%QRSU4fm9 zif`}(3&Dqs4k6x!@(#s5Vt>8H_&Y(?+Hvd2d>Vd+)+jJ{SC(kzT!TMF0Q*32;bRa{vGi!vFvd!vV){ zsAK>D00(qQO+^Re0|){qGq{14t^fc8rb$FWR9M5+ms?B}R~*KFGdl~r>;<;0EEipa zcPids0o#UHswQe1lU_`lHnz4eYAQ`^(#G_y4^0FgNQ`Ndn$$M2R;pAJuaO6|Hbo0n z6u}EODNtAzmR**aU3O=7`e3U2?Cc9)a*~tDqwUz^7?iT}+N`&~;s8 z4nPouq{U{rW-^%$0PRA1P_K|>x#h%>2HVKM2uUqD7jz(+M2d=wcz@r1#b&eB2sixk zwqr+*_&T)JI2No0G5v-_!=ns0A0ya(5qrTh3f6ywYh^Wp5w2vC$WRAsO?UZtPi>c^ z%qYIF=M5ZYcN&hIB#Quf&Hx^pLVhmTZE*hkCVWFj*;$^=qsT*QTNJXFmSeKm4Ok-L zSXf3l;YCs8m69%+5UdVRpFd`9rpo7=dQqh@R5?g|CeD;HO*oh|OGBDmpgq!#${^h~-QF=Q4+-n^| z8Bd@Kuzd@V4N(Qc{uvtfo+l~lI2VjkW*HKl4Sykch<^d+VALCTv z6?cL|wGLca64{x64T^K&aeWQ|=Q^hN;&iVeZZ%a%0+0;^({#`@&_g=883G@?nL}rf zLc1Rd@?oCy*}s-7Sm1iH&lvzkF&pAeqzWOP^5uDiL_!T`Y^j|tEl^Ch`7~;b5Kd`wyOuNbqm57?^ zhz3LrECmtSwuZ*Tf8s9mvbbV5!-Hl*+9-+S1d29ENS?>5JNDqtTf$g4!2PxnGFRDXe+~EAD;K>(J~!r z<9^nD<#dG0z`A@FCr|Xycl#@^L7pI{>1euuS&|T(`9LP=8XPaqx3G7M$epX_t_#Iw zWt9iMJlwLhqQn|A=b-AMFc)5kl91Fbi4Z+)_oD~vKdTV{5tqyN`sQu>i&w0A+ayWm qxgbPS)x^Mq&ZY~^7aN$GYX1)&ln3(=+q(4t0000J6^nLoH%xzHm&18sT`#tg$59a$BnA3)YA38 zcmHuo`e?HYHGkT^Yrq#W@~aHR=K0ppSx<2tS29EzHJSuqpw%FtxY%f@R-QefJgQou zDeTcK1B!vJRVg}9Dkzj|`_PG^d!1-yo4Yv|Nohijwqpo^+vExbAwj?odVPYS?pYG? z5(Rq|cdLS~SJ^C<*qGK?u2=|R5omJR8}ZSR4C3=E1b+ySv&-C>T43h$3|{a@gunbk z-B(B`+hfkGtx=OQEE&^m&1IRrk!4|e6Jxzhx>d(RVmj-mr9DRKSb}tKj9`mOb*sk4 z$_7R@OU3x44rxdM@EQ+*Y3H#VgKTD%kKS3Kytv5Wl!V(C{6QUuYb4W*x=Y6IDnmdz*B*GU27Dodc9D-tJ7>2`Sw|rs_SRUi*fjfNlx=&?NvF| zuX0p_D~mZg=R^1-5j@MnsG0;Ce$M@nd#Y5h5`YE4u*#Wdk_;T`BVLe$=w=Kf+=ITngaG0+?{S@)G7%BN6f_OIyLK`^quG;5jx1ea0J6J z$mQmk&CHQqUE8Shgrvw|`RvuG&s$ z-vNB34D(|p#%3f6`F7&Ren9Z#PmqFLAmkkbCS&l;ry`7B{+FvvPmTSwZ{Wo5UOCgr zP000>X1^@s6#OZ}&001kNdQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3>vmSnlDr2lgjZwZ`-<#0TwchJl4&q$BRp|Yfs zUMp3Z5$@OTB?-V_27+$(|NTG5{LlaVk8?gn6H}?VpJ_wV;h z`@H9VecgP&;dv?WZ+O4v{kgvLdiwE!5qZ(-f%roaCl3yZN5F9vV?4!q9aSx*U!Ahgi=H{8FAF5mvQN4r`ODegPQcEknh8kQ)ZT96?ScLz|tC&?+TYbBYEA6<` z&b#co+wKQnJK@BWPCn(-(@y`k+Q+KDTFw1txqodn_pzER7VmZCht+twl&@Pj!AX+M zu$Yeyix*h{hxW3Wtu96{%gJW8d4eK&tmcx8vz#o31>+_u9QSRzAC~)k?B5``>WmlV71k6MhVwIibAca4F_O5Id@-sZY*ryJn^^{Yw51{R90~e zVyDycs&khxxxUXc>WXt?)i1^duNvFb8nC#n6RAx4B~Gt`|}XG4b8P( z8!V))qs9K?Wr17xvU1RiED=kI;Xp);ej>{`<~~1PV}4wuxqRkyXQbtQbF#n9{ZCgJ zU;o9zYVtqL{a+ct{K*1;S%CTHtA3upc|P*zC9w9_h5EkE{nfVS{M|zQhfZL=7b1)m zIs@6^xRCqK5n$V=daVHNE?eGpT4Qx?Jl}0LY2o9LamEcCtywp?lZVagBsY2)8C2+L zHip|yi=~(4NAxpqpW2x71Olqg9ID%f5ZiIpL4?2vd}LSbBNi zsW5)7%`S5ZDKcR?Y#I%r9ALJoS^^E03dq1rO2s6@pUrQ(mYq*)%xkB<7x&DE#pCDV z&Md5H%*!}}ya1uqkA`JBLBDN?V~3j4sAt#$jqz!XE!=bqjy1+^nlUo~C(c^oBy;v| zebnfHL9KgindfWy@^yd}2Dc9Pm6dy~i?=glO`F+I=unDb)@BAi(yos{_Oh^)OwGEC z0dDc&-5JmFdoj5gNeTh9@ysn1&98H51~@aZmUW-8O(};20p|AuROE6dz z+Z5h+zl@}EH=M9-ys8lsDRmb7z&1a_`fX6=d#+!D`cH@S2DHt;9uTYXw?q1MrOaEZ z|NTmt&&dAQD`mdz_rF{z^G^o!PgctO>{S2#N=^6?q{kM~g%OG7)*JT&B6nLC#@vlE zeI(@C<06NbOqedu$}|@=dZwVfHo&7ez5=SQ>exSX>15#8XEp-lc{;G z7a}l|cE(~3J0XHsfY+?vW9y5&Qej&AgbuL-z+ox@aK~I@rOK;oP6Jm#pTT%s^I|{6 z?FvETJ}d@22INIH1PJt)(Y=&3dFH9;LqOhxH^68z5H-#1@t>q5Xm2t?kfgpsEdkCH zJRsWkiWo07;T&t-S>ry_fe5hxreThON_1QJ4^Sdx&H%A}2ZQZ_T8CIqV6Da}6^M2~ zKR|D1%npRF%Zl1{<;<96BhhlN-x)fspumTMI7C@ zE_`RT0(m-*S!GDd@QsX{BhEJ)(RvwN-^W<1SAdb0Kx{#7o32+opmY64KY9X-eB+d`9Z z6I>69zBgGCqLRtF7zA#B@XL1`gs^X9kh~OBD{~9I)Jc9Liio2P7G!=+*m#9R`vRL! zwg#7%tz2aym&57t(Rpu4k|hie!lh+CE5xb%n{|(*doZZEtU}DH9Kn^KK&<}G%uh?N1 z`e)RfC;Eh$mUw0}_(AHvt=V=WZVq0b57KBT*u=*Mmq9L2^A63x5V3lShdaY-GK&Eb zvV9tqq20OXxrj~$LB&W`u!o4hF=+kuFN^hXc>oz*4~1E2pn8!l=e}j~fNsHQCSsrE5t&n@v&wZYUj&z`1@VyOJwTSS#1Hy1; zF`RmTS&2m9u({hP%~MxJd=6Nw$ygb$9|I{FppxKxh1`WprMT$G6;Zi$Var6cK)y?K zB0LZjVjZs(e=!S~z!!T&Jc5liL@*V+2F01!ilWtc1=4tQ*k<_gYLr4<4#&bbj?vO4 z&~lvGAnc&feWmb$OvEJe2En@=&dU@$ri3@&{6cObC+U{Vahtiojj?dzEI!VsPOjK{ zNiOmhStVZczBn5MAJhS7H{cONZoNR@!9vv)l-%z) z+gwLPR|yg<+96FMeFtEq%$CoZs9t7nFFJ0JzDY>p^I5TH$6ckd-Sz(3dtr@28C7#9 zj{#t)5a@GYMQ3@E#&tlo5qe9j^LZ>Wrs)RQGPup6nj#k@BH-xKhD`HD*zxyY!G%8P zaT#s~5<&Fznt$Prnczv*>l2(0ji1*?>51T`^Q6ey7{xFMu&y0oK zyg~$9AnOQ$n~uKciQqwkc@I*MtxE$LJ1N6@+>~jCcHRAHG@ny1f=P@7EbnBv3-=)2 z)3Y%OqN4W_P_*%r4r^FoW(oLAZ>&4v`d^|HLR?HJ>kQ*GHK#L$kL6DLx2I4TOFupVg!RW3- znPL;@IOlUkOtAW4QaI!Ia>fNYW6`FF&~(tS&ae!2wigs~{EBN3MXMOeNJpBk!%>Jr zR90!{MNanM*nz*JBpSkEkSmm;c}lNO}+H>kuKr z7^@{4(A#n$X<9x&06<@`dUt{$Qr!&@#ca2YT;Cy%GnOebMJ}=(*Cwt{vvXaVCx;Zv zTi`JyTjQ%gl7>yv_b+5nL)btZn)q(WqEEt60-yEST!-6C zTziQcxJ@EEgLJ}EN$L|c^nr;2kjHGF^H9iNnSj&u4X}Y_XbD0AJ_I9y1zSlMS#sBl z3rUQ`Lke7iCC0nK;JJ!SOxy&dqtD2e52 zf=V+gI12%IMjzp#ARcs6J~V$o9FKUTt4SLlhnOn5BkT=lCO4JI8lC9-+7a<|DN4V$ zOyJsvRzcZ|%T4Q(n93^5Wh*CRywV~dLdYkERgJ*4?TUta>Z=P9;lHu!b3APymb^gL zN>^>`5nggFQ{=u&tbGt;(Blm^Bj-0CgoWbmr@sqA{L zRfi|wqFHugzY`^0mhQim2&E%BfX0J2tN?{nP)c`sZ;ebZrxhWTPo1#X<$U|E&hz^233_T zWsZmnY-nEH&dam*sJ=W6mi#Jjl!Sd3#z8)Im>yd?p*+jyU&!8^OVADlZTJe>zlnV^ zVErxj!!NO4j`_s?`kUAL!yE6)u1k_nDX-V)+I2(+V zA;Xgp4k(k6apjC`A_ArF2qY{J#9L6A(326eLCZ2YdtHl6K|qk4fdW)0$qgL}HO**; zX_qRaU^uIuR>8OtwF!m8hzrcd(IJ|!A=1Yp9tv~CTN57vmYdDYVQ3NJ2Qp6GEY%(o zxo4<00@q&Fu*R$r;B{evlmVMyDBCG4dihjilMIi?y5xgh5HkoEY60%m@HrU4GDv%9 z1z;V){fL%#>pXw3L@(+9Ct9&+?QI=z-3MxHg;^(XoLCBuVwg z!N{kxT8;(*f`B{Whp(8|r-+y(IR>Z=~Jp?>wlFdT8lJns( zBKJ2i{oBycE*Zgkkr4&PnjX0CKBmbL#skcY&}jqA5^Mo94BB+y$GWur=K_L5X5>MG z;D8?^={F2)ZlmJT)vzcnC@X>h%Bg*UrKE5W?gT?h!uc?-_mf2xNfTL!s6syt4@WV3 zvRW;`6;;75Ji2b_(S9P>=k$233@Snuvmi1FhMZ50313SAF4Y$>LSAofqEk1PVwoch z95@xIUu-Ak#>(iFGpw@A7qDxyZf97kaNS&BSLqJfMVfyCSd(hFfvA=CIGt)Ukn9<^ zl}du;#9ln0fP^~8hCR#zUkA#ZP^m;yaIOpG-v-Zg;k!&WAiqRO(-tIXC4~(&zEecO z1@qH|H$**D&&q)@v%gG=98P)@VqUI^5B&t~A0gJQdDvd(-xm$Y`%3BjA8aWDy$gld z=m8#HzpH=7v$B2r@?H5`zf}Gp=MtJr%r6eegcEaDSu=K4+xxx3R6!CFQJ+unc~-}% z7P^+U(@~y>i~vU+x}Dev_=XO*q)>Dh(-M8}16>G=;La($s#bsG5Sr=7eB=h5sFpF6rN?55L^bdMcW>Ya31|*5E<&B8dc>m0+of#+K57ejv}zIx z4A%t8Nn9cg7$KxeBF_xy{+8GWwmMKV?a`{2 zCys&h{Py{P6BX{{LByQX!=Y0NiAfIkxtOXtg&ky@Kw2OQQz1b^-(iSbaA$)p{_eyF zhzSY2C9H^4!gS%$=A_(z0}9LW7P!6@qkV(pTU?V7Ag=@tT9+|hRW^_pNyj44fD3}# zHwPZgTX^$Kzbc2t2BK^&l%NK`P;o^|2tve3=m!@nmcsM0426O&vbKRQVhNN5ZmTs< z8Hs!Y1T0Lu<=f>%2!Xg&1Pf0Kl_7NHFUWY&9`Gq93#nA(Q@vDp6$7b4g!8FYl?7ra zF+q9>_d3Co3GQON)unnAX)L?ZCSpQXcJPFifF8(a;D@+A{87wx+8+o96FE6QGFl%Q z^;fGHtvcX;B^89csaEtD$VIt_ahNg^70d`=Z*#(pTytb?qz4qx6LLm94hn#MKQ5++ zIgnI2N@2we(K{GiXPmgE{U_E~x+; z+OB-Iw@djHqrZxr<+gR~8zlF3ug%~Jd=YYtSj6KXqoEyXfgjr)RmIQWUV#L#kX1;x zN0HuUH56`97phjC3~GT@g-Tlm1itVSzV#=5p)GK{hESN5$ZOk{wVx`eCFRI?x;9v# zW#yGTV38GX0$c;MosZ$ zZC6yr-rHZ@8S*s%a+2ScdljuvUQPoh@4p7TxK&a`bWB^?29%%LUx{MLq;BqT5wqboa(q~3d7KY7Amm1d zi>T?;xuK3SU|iMRWWizk0I){`$!I;m*Q>HRd}Rkn3KJht7k4rgYs_`GS5>MbXWAS6 z&BJHF7QF+58Rl-8fSaP%7XEY9J`KtccE@X53 z2HK1OB-$$8Z?yUrN((gH+NHLkRNR-IR&0}Goam+s-Cganz@r#tRw{Zm1=SOR=?kHG zZV?nRPTDEOYELHsHWCn>XUmS{hB%O;Yux(p1?GC3=G?WvWmsL+^(AROXi65r<`e$# zF8S*f(Ei5^;DA!z*_~-X2EZfWW=Fj5Q&;#ZD*)PXc6+(!{;jNlLDcvMsn9Z*TUWRg z5-|OlLf*o{yU~45xB9iLi1@84E0SuHGcQM6N(8+ODk-|(Uy~fi(ml)pIdg~}B!14x z*{Nm$wOUPs%Iyp(4s9+*N9Haz5RP^e;E;_dQpVH$09kU`ZZ@?Rs88`iNhh7D4rsc_ zag5ex3Eiez1^MJSFk{E5-GQUMzgiXPD&;$mu=fEu1wFT_-YH3B2s>b!FUU@4T%1f= zn+7n;DA!dhQ?suq-GE3{Br1M8KS1k*i6=CpT?fYAhEn|iz0$Tl{`hd>N}Q~$TN7r~ z5xx`s);^l{j8$rYw=@Hbo+ZG)3fM*ls}f-_zCzw_Q&m#j)HOAouh?p@cyzH@%~Z-y}!|gi{m7V&@wgfU*?rRRzYYO59>X#RAJJ^}a%i*MkhRr=;}{TtYzeWzCn{WgN<%ELP3?yh zmWV=Y%JJy>7Fk5KaHv-Vny@DsKy}=d@$7z!5BUBAE3o$NW|DuZ+I8VeO0Jik!?a2L zN!Ln_tYMd$LO{^wv-WclC(DM*?@J(OOc$zPQuv(@4C z%CJT+LdsZ9)gsBZKCXsGd_)~S^0rgr-Q6QAt5wml?nJ7#0i;r~$uw}(3cFNEvHI0! z=EG=Ms4eLqsD#xf?}@m)Z!@eTt_A~}N5#M7PX_tXJVx7eOU)RN;i~$^n@?2V$Td}2 zZ>vY85f+Sf1UdHPiYAA@^aj;#aN?KKZ#DexE(G#(J6U_I&=j~@a$3@Pchivp2yE}x z83I3y#?uKw#IYile2yVHSH!v8h%X`YRIJ!k-p_H3fW`(F%d5y~4;fI=rceg=%uz*} z0l+tKc7PJzP5`5@kz)-GN?HmN6l`OtxoX#~*oG#lq>jv-YWRmkdTkjExIA=$06xWH zd2Ud3tRO@c!$(Fb+6|C4^{C-cFi5b&S2%X50``XK_dyorFIb$RK$WsYHoZl&HDSKafA_Gbmhr4Z8lJj>D5nV|-SD8$^>g_4HiqJBLU}K`N|E?mw zn3Kw958}>?!bOy7D@yhCEJ~>)h&PW4Oap*9UX|T4zA6D9bef_eW}yjJ5+hoxVPZY{$NeU-y+kvdp&5erNVg~>WNwMf19zNjj`fCq+}Id5^4zm zibQjU-62sCo6e|X(05O)s^WQ6*uz9R|=obzYKJ8zG|Y z_^@zuza+O8v0cQaBZj%i_D)?S#bN%Dj=zDA*K$)0E(_x;56XC(o zE<_{itbG+#n=wmJ0ad{_xAgYiND8TeirK3_C3j=@tMH-KLrO@-sbeGV9x6~(yTn(! z(YB$ahkP13cJ7&t(33>Q68J}zd9@9InTgOKg}2LRQ6-F7N$!w98;uUPgc=jJ>gE9o zfGiBGUO#Owsc9VPK$4hcqi3MlOEnqTA<6S+)kn72Lwp@bYVR`KaQu~D&3osgt9blNM*gn45v!7dTNRxk@!Enbz= z-3X*CM%$Zu%w`km)h^}QFhwypNhqhyXp+vX2A(rmb*9^vRo4W#!otX(-;wQ~WM2G&aDJeHt>#$)OziWx;+yl`GpIC9++8_Y|M zz-n`QTbx~hdDH0S0bN>L4!h;BuMAYR1+A`|bWiU%0i;CoXCq;#luan#sqZ$`VW$eV z&h{?#*hKYlFhvk@!1M@3q#7|aZxxHV%p>9-AiBr~|BeNf*En!{t4Q~#FS+?pwU3b_ zvjVUAs^U5NOH8DwanORepz>;Bcg(noG%rlMbi~rCW30?e!tl>Nw(+5%p?gG?`$=*G zWR^~_snovKxg8Bt97WX(ZRp43Q>jdj1js_5m{RxJsoD6f3W7=0cj&|rO3~iv1&|;4 z6blKT27>&->UFWr<7Vns1|ffi>ECLj=L@L+P#XoP+kSKu5B|vY@ON}{{R29>OG)7) z*VB{cYFo7HLK(U0i<54Z>~Nvm2>=+Yv()M|pC4)`P@~)_M!gZE-&P^@lB*ouocy}( ztcH$PAk#KGo>NBa@YY6@Mlb?it@aviGvL{HGq++)1?mahlbJ!iu!fk&QClb~HEd=r z{DsVfX*X#;cXTNbI~iSjJ8?VE)pM zx|m251bv0PL1U4SSdF0fc)sg+`WqbZ#ir&ss-S! z6RDG6_L)`oEz1Lk-T9BPX#Nt6vnUt?qW1=)|A(sVB0=}^VGnyr4-tZl%y zM2~}RboWR<6e_4Lz11590{SlowFXjyS;U(CQQaOkQi{6Sy&6D-p9i4`i&;JvbbL)9 zW0scGVLj5L8ujA+JB=kXMl~kIs#3k3T(NsPO6*}h7q)fqi)YM%LNC{P!IS(jiM4UD3o2t5?`afli}2q0NfJTlhh5b62=p2?q{XB~mEvB(*MD&58FL;B9bJgh) zucGX1kg@|)9jLnwMMK)ERmS-VbTpAEs63P+Pow5hb^e{Zt0q3zZZGXrnURclU1Q;u zNxh;Z6iYVL!SANh+Xiy)?m##96s^ii`mo>kWCl(nnK~VMaH~Cbr=Zd~TD`fuvp#23 zHwFNz9Vj852~m5(MHM7u_ILkvS*x@=;RI8`X`a$l|LXi>WEw-3Kdp%TS zqxl*KWCIn!;4$NSb49y-S;f}14Tv1>i#p;5E~;(3oYp>3&YZMOBxl7CYp=0?v1%c0~^o9)wSlY^kroB-GBgW!J6j8)4u-eQ7Gc*8caR1IKZjfp5 zFsS7qWx#Ep5-#@&j8S`q6a@6I)%oBgceE1)fmX;syiku1;{?V0v7(2!v0kuBi^JJ) zx967X*Zh~X0i<1L29LVbA;_aGXyO$NyL#@OYP!v->UZfPpKx7RZI6U+%uQwN?4YB_ zNQ*QA=o1vVy}ahFg7u9DdN#MpD_&K7ZQ0`EvDqlHofphTGIk|ZuF+FcI$%~tPN~}m z?Ng+v-*Jk|?oU=j{yW~89)sKM<^`WXd!>V=3|b{lOPzC@N8tueuODic&2Cne@pbY} zU-9zx37RN!kZ4K6Is*5WdY+`0^X=7wmg!Vxsw^_`b9Y>!d}JeiS&6C! z3EAk56q+NiKs?~tjWv}<$*QDALL{NXnGf_%C1}V4T$Y7YGu18$;Xbn0H`O4y?spdP z1OGR{J~dF0MbHz^Qt(u@?8AZ~I?NLsLOpGeUfb_m)KPRfzJ7V~@bud7{piD|SC@|H z7pGI2%%P~8n)q%JUUUj3anA|tZ+T#r^%#*Hk& z7e@ocbn0wMmV>qOE_Dts7II#71PtuES2OQ%yW3Anf?BP$+B`3*0Hh5J8Dr~c8VK&6 zx*o3_w85jG)-+}asRhM`+LnjD#z4Cxx3B^$$!dF;U0?N5$`w4?{+rrP($Q+>Z)DKt zwxrwvjSBDk6SDS1Zk0{xUR5^LJ5;z?s&sS@nzv6$y{TtZy_@>hhPS{m*ot`8Hzzn| zwzLUQbnJrMoP=S%1!6vK{y915Ya2qo#xg-+b5wXoRg$Tec&&gZJte804ieB1OKIbU z!|kqD8&oCpmG=!6CQm|o7wv`|wZe|1)&o*Da!dMjoqO;b_xwIlTh`s1 zP~5(51b`iYRUdbM-ytDs7VmpzkJol{-@EUqrOB)BnKzO)#pb$ar@ZjsYmT@3?|Y6c zIBQhYgpmIGDZUvRm6HQm11EBL9Ej85&nl-@?ZDNE#T#n#o{EL;yin4&27KCd#_gPc zi~Y}@Ty=~^hep7Z=+%;3HyUiW2>mXG)E5XP;z_NJzFD1lA(0Lo@Er`&(b3P-=lI)c zn?FAtWvUjV<*izcb-->_HLhP{*Oczpv4wt5q}HX47^CJ{^lq~tyn|=8OQ5PWfGBf3 zNbttdF<8=Bvq02TN1)o1o?1Na`SQ5EfGz=Tz_s^PCmhgI zs(uBFV18dXLAdCf6QqayJII*!{YshdmsLl;;RBGw1wqr7>J?wemjjnol;E=FhtIl? zYm3@^Iroj+e>6$+HOrKKa^vxzhmHAD-269TWBw91A8b$}xRLW~-25IM-+pC&cdVbG z;VW`rSC9p#vWgDdtB2j*LzWL7FE*M7j$a*53{w3hLfJvax;0rHNIvRbQos2vMu%IB zR{6Au2_5yR7L^wkE^c}KeVelZr#M!MHG=nNzcQAXV0ww#?faEm2*zQ0ixN1y5;dqw z)u|kuQU{KpbuBeW2Dt>K4qMT8BdM3;73y^i4+jBppzTJxVkhWSO^eEofh6+Dj_zj1 zTc2uoiPS3=)8}hL)YOo4tjO^oH;uLe)H$H0vbU5NO()w!$3jS^ssiG6RU~f-$H+`m zXJZf#+VodNj=LNFNldndI1vWgip9Ga44=OnL^|-K?-zQt?55pTC<|;cAE$3#g4jD% z&?H6e{#=&h8&!cW#PgRu?!I2@lFG5`ojQbuGOga){(Nhdx^8Q^rKrz0oxQKP+8$Xr zb>>;eOPan@!(l6x0h7LRiTSOf4%BMYhM>NmrPJoFq0Urs%%79HIvkFsy!GN2aZZf` zwcX%&x7VJaj-7e?edm+(Q@-vP z?|FlNenazh$N%jO&F@3}Z*OS+ZqEPd4b8t>#D982^G~hv?`~-RQ#1eF4b6XQ=D)k4 z`EUK^_Zymjv4})j`;M|#QBs_hpW-ap_f2*PzZq_~(Pz6&!t)+|PiiGlx01eFNMFt} zdxmYP?Tm4*dFy11D}{UV!xiPV2J=?Qc#M1gzFa6$uZYv;_Yk!b5Dfv5wzrRtB&oMd zN;k*-NQ5>EKU)JRcO7Z$JTrkF+b0zyLPS8J=+Lk_E~m{l)#03anHjjc9oKCd?2pY0 zZ2j;k4}HH#ADWEz^FaKl1il(i{eJ_v4RS?hBGUi>00v@9M??Vh0NwzNQ0b~g00009 za7bBm000id000id0mpBsWB>pF2XskIMF-;p2m&nz6y3T9000GsNkl?H(fP#4rUwuKUbZaj4bSl+>{3D&n$PEZ3wRqSh>=Y%V%zrlm4(cAZ;#)iv|QRP zgS{PlInsBO_AjO1QxxSnF|z6Xa&h*~n}7WMc?yv;i14lx!7FM+wfA}H)fY%Ta_HCM zv%7WY-G5w?KHlg;jkfdul2KS;C^pZxhR%A5>$s93(x}lS00XTC0ma2eL$wO*2^CP) zDotUJW*JZnbgf3ofpSryQs0M86y57YtJvJlyGTkCYP20g2;3%DCFFuAu*lv)6yOzbu2-;H%72UrM6jT zeR&-tm!oQYx&>)S0q`0hfN2-79D`hDg^%A^rn0!m;gp2i7vtH-416NU@Q6yOu*}tq zIqY(p?ytAw*x=Y6IDnmhz*B*(xYh{N^sPYUu1>RA;@f9cYObFtFUH{`CV9<=wO8d- zzsgY!t}N#1oDboTMDQ#Nqh=Cl_&N7O?x|YEN&prF!zyQdHj>Gy| ziKSJYYwwP7Yjr>Ip*IOW^23Kc8t;l?7&`HA0A_cX)lJ8!cfZc?@NZB*{FGEz6S6Fm zT`6(-MHX~6iv=cw}1A-@if)wlmAwP7$WDLIfRD|)%AGpf&)Ywn^22T9$l{1}8F6!hp zZ-=T5!V?$%=g}LF2zpW>Lgo^;$FIGiynpF$e~}&KT;GY~=T7xMs{7;nL%W^tWX1^@s6$6kv|00009a7bBm000XU z000XU0RWnu7ytkVjY&j7RA_FY5 zG@w3|v`VWYLKD$SlvGiH(o_wi^bw*qO(M~N3Mr5_G)P)CF>dG(@rsd{dvDs`7i?U|<{wJk}=Kgud z-w3FaAbrID-I>qmgODGuS;6vUk1_MUc^_)waQObR3(Xi)9%EGYRD`M}+e84Ql;{nW z)E2(Tm5TR~meHwJ#hp13$LI+Js!m}=FDIV3_J>f@Tx8Ap2BYUciRSVq!~9!Dng*fz zGnAdb%H&Dm*_shGg>X;CGd4Tfwspabg{eFC7tFH1@~~;*=vH6bTuntJi=_nxxaE|@ zO({VL8XFoYE!j>+)@;1977V%Vw}VhMJUKHEN*#t~3Jp~OhaK9IZCY2Xc$7$U15-?s zbwKCm+z%2(-MS@8#YpQ=unroQYJYA7yudwBw*ZBbZ9Z904c5oLjoqC12a{X>24dJ z+Tu){G?iC(?&Q^-JDD+K2FsQ$!=WfR6a@<~q{IkbrSb3^G@aUu@6IJ;&3zOl%?o0X zGRS^lwU-)5qCha2*7Sg3TA&-a06S0*oC0P6vw@%ce3UO-$k|woceichZs0dSU0ogd zd3mTd8?EseO?4Nk+4~0^AOC~7N*fEdGkSy{=i6jfNjHRvqJK zT8yPmcwG{5a@JQb}L1<@^L z9EtF9+27$SYane@Kb#r_)I^~H2+8%ASWk5C)PZn|x&(I8zp$9rIkg^q6Nl)R z7J_qR-+m7z?fsD&tH55cr;Q{#??)K3{4^ig!FX2<`Sa(aXc~+i%Lt#3vV|3p}+%*fO&RMXirvR@NN_~7=lYVx#T>u0nf;3h!mO7JEK8j z)LM`dixP{j(@VlO1wwa9CGh^r{fK-0JysEOC%A6L_j)zF(9O9D5LM>+{FeC7Z)R? zWY%4Gv0=>`(x*?S1ZV~Ffk}|?uH&67VLFftC@r7Tdf^BydXU!kHmUOKNP86!STZ$6 zM+dU|wW6SUM-YsvY~8w*+KLKXE*IX19%9e5Y3u>M2G_P&!aySlN5eoCuo_q#4dQ?A zIReLbVMaqk>eH>Ffh1z3-n%pom{Ou!CZXfIi5=U6t)+@FEhd50tI6EBk@efR^Lk++ zUjv44wxT0egX?LXhKd5HWRJzVTR7zs3F&+h_>YvjlhpIF~+}%=y1WNLR{ycJR0y z7{LJI;KiWab)cT|W?!;x>OwSXVnqB@7rlU0T?|$Xgo+RXsbr##zX!*Jd4$WiU^Se@ zX_@Gn4TK%lJ`DR87m+#R>!|LDw4UEb;K*hip?dUTlgWHw4c-ZP2qgtZhXUC?h41Ct zX!&TrOxb`y;sc=7X=_7Ky4J;FiF;}G5W>)rjx@ZkG+Z|I23>lfkx--sw|h8l?=74g zxZQ&l{XB?~`8kCLCpfXSe!|X|{&-NMe|m-lArhOuz12CLiR-9L$L;bk%&oBM5f7@; zQ<{=KXm$>SNW(>W=wP8J{ZGl2s*9)BY9NTGTAFp67zb7o2viXOp5E5`(WRRn<++J` tUv2s;8`t%CI2L*Pzm1K*qqX(K{{ZvbEA7!_?+E|^002ovPDHLkV1hfkv9|yK literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/mak.png b/padbuildimg/assets/lat/mak.png index 9006d370d46aea04192c16ed94a7c74d72c30d32..50dbb83aa37193f6801a8cf0b49f64841e360f39 100644 GIT binary patch delta 1815 zcmV+y2k7|oGno#M7k?QD1^@s6b5wmq00009a7bBm000ie000ie0hKEb8vp{1+^k5O%pK2m~e@0Fg3Or2YS|8v$_`yv1yS=ZLK>-l{LzVghs_k>#_k7}Bx@qY|gN;w%Fy?o-s zBk%t0%|maD4i629$ols77heA5E3#PJHaQ**W>W>P5Mq%KHGdWJ_u_wKXYo7N>$mfv z=B6pbw)$Ux?SGYDh?frj`Zs~v@ZRwmJ$xf&>aJd~$l>9_$tID7EJ6r0)5i1$?jrx7 zP-rqu$nVJ3pt_NnnSNjVrDtgDpQlF}Mlw~Nv=zpz2!eR{LNf7j)L0+h z{qY|Otbgf3_xKT|W`b@YEDa1DqkQTM@RDY|`x#yBao!x+K~peJV{M$dWQyL?qa6PI zQ4X30Tl|nK7PwNB9PleHtO|4Fqu=4Uin@-c5r$bd2LnI?4p1o7Te-B@3A#g9d9Evp z&J6AAjuNn3OgGEGV4OpTF0$8zu+Jrqq98%jG=I(}^Xx|v&yLd9dzef}4$5Vi=N)wNOm?Me! z4E_*H@-J1usVrdYTFLS65u#j>5~N$&K*|-wGJ-*=aHT^o<8tC;j`4AwM79Ddb=o(t z=YPUqzaZ?uAi9+3=Ji z%JPJOq|#RSEd`E)g@c2Kpf8D06~URV!_XX7c&2%D!z7`a4DG8jh^;26@ecZjJ?#H^ zn(EF<&TWl!HYd1rUQ+9sp(7M$AeP57)6a{0 zCcw?p5`b%CIS-qKvR=}DOG#1J6Xb0C3BI`LVNKmM zPqfYOZ0A)D9kKA}V{B}kV)Mo-jM)+DeQDC!9DYAkGC7a_U&*Ox@OV)xxrh<(>Q63T{{i3vqfZHVio|` zK@^s;u)7bIwG&jX%gvD+T=>TYRIrHc7SL`^v9Te+3ws1V*tdc)rv}G0F{(`L6&9M$ zq$oAKw)3zw0RQ7*9b7DZ6;3y%Tvesv?OshfpsZK!fqjB*WcfL8D$diX{>29h=j7a^JjXZ z#)t3x>7DQXu<5{Kp*5jOuSZMdbb-ZfPTH>>wQ|CTC_QC4{5a@bab8F-~?VCEE zTiw2))3ogJw`%_dIEB13GI+6n@O=M!(Xp%VBIVG(0U_AleGo8AWWWFb002ovPDHLk FV1kL}jJN;* literal 6643 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 z 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 literal 14117 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 diff --git a/padbuildimg/assets/lat/rek.png b/padbuildimg/assets/lat/rek.png index 6e3d7ea953d41457c25c3fa0d4542cdbfa9cbfc7..e8938d978d53b78181c0baa7dd41496c3c98ecbc 100644 GIT binary patch delta 1929 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 R%8dX3002ovPDHLkV1f-zAJPB- diff --git a/padbuildimg/assets/lat/rres+.png b/padbuildimg/assets/lat/rres+.png index 3063e59d9af1e5deead40a37f3f979cc8ed67737..2ae763c0b5e25c89745140d47d76c129a98e6e0a 100644 GIT binary patch delta 1845 zcmV-52g>-TGq()z!Je^a=6}xI<<|#W*lZFvZqNp&5t9UGG-!0v~_?` z%XWm==-`t=qPRtxBDUL+l=G#f+^5>ULodB{;-8as3&hoOO$t4nWKFv-#Q+**OwA&o zIRtbM(*RwAYv&h8EKjhtGEP8PELlay?`~nsGdmHQ&VPLY@I>Y$mEs}`)j2g@U8z6# z!n3bvH4UkDuO^6MnCOb>OP_)y8gNL2sz|DsUY4+)C#05$1gbF94`V|0woZ8GQ&ckM`}>KFixam#A*`F8j6ZAf}kqvq4hBu}VI_!b-7-U34i*NhW)R9lqE55z+_F(@s#6f#Br^ycSyLK3JMBQ+SN-G48U4bT8wzy_*57fUIzJ%@5>nH#s# ze0ls|Skna>vsDH{9s>h{pM57pJgCwVg@%}dp~8V0m49?b5dzLl7y0mFmf1qiw|#ZZ z_xp%c0n4W%7pQs?*Opj!JqD+iF_tUbbtLmDL}ReCOQ9~JqOC>%0Lidmcg^ZU&I0o? zSbr7i+x|mDqzA4GuIE#e=OA4d!_=v-j}eO+G*>)ssgk9N$DKl%^S3I*!#4E^Xlu|3 z1yyndm$9@Bz<9>P76D8%i0k+>?<%Xl-y?#<>M!w0L^?K#>tN_AMo`6xXw*0+AtXx$ z$x_Z>NPfp^NZEEUNT2d-|0d#|^m0_U-GqVm=Pk)fH z1a)me4!^#gFFv?}aCC&BZ^o6)5dnqViXffSIXuuq<;5JghcBS!EzAIDYy0aOjOrSHkqe#OK_EBoCXZhwF zVt$r*{todNBsGI0d%M`1Y{G7=M}Ln;Fl!A)&s@eMfE#Vc3Z}4qnm1Wb>-);D-w*|X z=SD~`#5g;AmhZp12c;)TwOk;dF>tCu>?N0FS7T=?OxxC43T+J(0y?vqEMucnc(Fd5 zx_(N*l;2u|$%Ey0P2jWotrAgV-377S*YLhtvo*JTPbaBi45of;qfc-<;i0$Yn z){{W11gM~3NDrx0@T!XxZe3^U`UK}rr_s8Ppme+l(Ke*t4)_6Vnh1cWhbT5b$NR_5 zQ7(SX(B3?q+d7FhB>-?19e>i zZTBDmoxrxA^0yQ3F!9wTy0@RBsXIYKLzv~v3e!`|TpLX@byJWto~Qo6FA+}U+r;xa zbnaCstz23rVrrN|Y?25FC5deLF>{NxWKSH&`|JV&2g`yIaqQqONPi6xYutw#Zb#Ug zJBgZxAYw?$xeVH+GbhJ7TlaP3y?D?{dlX83YsW@D8Zy*@rUxz33PSaL{@I9FNTIt~qqC`OT|PgO)`ma&%SW&OqUDVpP2Ek=U_e>4RDo32 zGw;EV_TU`gA-RDf`+pElC(^NSJc%a-s;VIL(AqAkK7pP`cz^Gm>m>tHw#Q4JUb>k% zdhD;CXs7@F*?U_%p6=Z>SQr>=YfWm=+K3bzYrw{-#v>;hkCCfD^IwKtt=N;_Tp9i5 j^5`e&8&e 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 diff --git a/padbuildimg/assets/lat/rres.png b/padbuildimg/assets/lat/rres.png index 74992c27fe201687c55a68a5d0e73141edd7f0bb..6a8064a3f8a5d3948b493286d52aaa3939c8f76e 100644 GIT binary patch delta 1454 zcmV;f1yTCiW4{ZKBYyx1a7bBm000ie000ie0hKEb8vp)o}-ti7?Fb)0qMI8BwtcAE;)Gy$~=Mzj*N4}c1YLi+&Xr4JDZkU**s5-;Te zL8J=tfRvXO2$jGpNz+TNN@yHA7u&HN+iS0P?OE^6&d$!xg?|Tct~L^q|I&H+&Yb`6 zJOA%!1i+Z8y#qB|M_&^?Kg|jsvQ=wCct!%X%j}JN@GIv1^nTN?T>n zw5^H0{yn6&cfT*o^0&mLRreQnrf&WGkH3EhHGTvU+ww(l*NP;HAMo1i-=V9g_c!9x zrJ8;6?<3Ngm47sWL~6@`yE4=)2U}(sKKLb9c5Fv7L^pv%I{*Vo1HbHGQ6x}T35Qey zfeMkBOVk6%270kd$%gWpj8WdPU8l@N$lxwulouK z`O#}mE-q4&rkPurU?rF5>X|Is`5A`l79Rmopclvs!D3&MeP8UPy`zzBY!zQ7K4IZ$VW090nKEzvxy}KY8EUg zQB+l@Cm6tp1exvdVwI}64j2~D6m9rD2)R2LWLy{2e1*!#1qSjJ9?QUB$e=e@B<=-C zf`nRTpQEuW1ACns{?jV+<0Yz97hH#g5cdbNAugh5mPvC;;!tl3(vpf+wrETG=~RHg z`hQo0X}}w%n0KffaG)c|rA&h5TnVdW!H&QtaXp{~Eru(}biI5Sttr9OY#rUW4*HU$wP`pPgpoja)+UW~F0%Hx|u?moHOx$WfVYb17MNFP6j=fxx3$;YS{1a2b& zy9AS2Kl*|FZ2RSp@$~lK)L`uFb)=XVRe#y@U|Q>OIoyB3Q9H=_D|x!Qf~4A$$iura zngg8s=Xo-icx8vFRT<|Cy~smPupF<{6Ohe>uSWJN2I?Gg?b351f&ITG`^EaxtK#>k0Jksf;i(R~yx+<*Fz z=LU!)`OrRhm=o`w#haMn@bgd6KKvE@FMN+sBVB=uD~c@4jI(fciVxo%L-Xw>`Q#hu z$)QIrH$nj1P?XA(uk)w3-$b%1>>n)B+SY~;5*RL8Rb%o_f!jC68F}vtH*cA=Jog4- z+cWnf+6b|M+M_#ZjNL!s&wqG}sekdm(bgYBX-y#6HPoEJ+@j9qlUKR6*nx8J*93dM zzR{!cUy%(%M~V4iYGVf?vX@%sO-`NqErHWl=t@WMcs$H!%iOwLz|;LIt?KT&CwtA4-Ee92S+-&aYv_6bT=G;A=rOnlltaT;(}dW zAwuRH*T*g&lRp@F?=K!(KH5LH@949SKcV}Sj?n+@_CI76=Jo5Nr$ZlIyz>5sr~YwV z0D@R7u{)Fb>bCauPoqlfjwh``%7y&xg~gd)jZaMcm9;hXzX<(J`EDftO8@`>07*qo IM6N<$f*icV#sB~S literal 12762 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 diff --git a/padbuildimg/assets/lat/sdr.png b/padbuildimg/assets/lat/sdr.png index f1c9450dff248d377a26303df0ef01302ca25f9a..753cbc9e6d88d9a95bf95e04483a9cf99ec6fe90 100644 GIT binary patch delta 1344 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 diff --git a/padbuildimg/assets/lat/te+.png b/padbuildimg/assets/lat/te+.png index ad8aefb3a71ab1dd154d50439491fa84f7c1101e..bba0100baa39b992fd044761a752bec4543801fd 100644 GIT binary patch delta 1729 zcmV;y20r<)F~kj!7k?QD1^@s6b5wmq00009a7bBm000ie000ie0hKEb8vp zB2}uY_90Exs;Vjysd;Hp7aEzM7@V*g9KhJdV~jl>&+5B zkCj`e^d{w}BY&%$Jso*Yjl$SRICE2IyM_vc33Y5G(y|31VQhMc3d90k(@~k{? z@P*@sV>_?jxvv|i{fw%nTQMUk5KkARl)FN7?hZ{0HwaUu=)|}_870vFO{5uEhN*`` zvslhEZBME3`?aQhw*9hU+fJ7q+lmwcOc_{LpjvD2eSf^l9QZyW5J8$jG@w0~(!?d6 zo%}n8!sB!#V6+n8=R-N%7rukiK2li-r9MB95SY<4(vPqhU9@b+$r?z7utK2ZN-EYX z-$z%=*w_DscB>ez{fKlIp6BA0=BQ5GrXgIRFKf}44iKA_L`UYS%-q5V#!-Kk6};JDwru$}i9~|f=?_SZ z{)ImKB5%B+NCs_MBVpPi3e$qVM#Zl)F)9?9tbY`E>)c&NM-td|f~e)#*MQhSZ9O1j zxkjosmSJF7CJ}Ryq*0(rmKf-b&=@x`9Eqs~LO@sub~P(}G@~;vsgG*7PNnYk9|6%m zcpi9ueU7yD5!F1*++eXd&wQakDxD_NlqQ)@v2af^S@EbUMcD92MI=E}QgJl71rM!Z zW`EJcw&LhO63?w~eNQgM{s07*r6Z{m1kWRB=gC?*oUKjV7#*dnyPLMQ4&se%oF6eb zbwOZBo9$a*-wunGwB+uL=DiCxMK?ytk8v;5ht=3ue{Rd=*dGD$p7e{yOS?Xs8iE}8 zaxa~oU*qJ-lN>yFkW3~+bGC=P)52e`hJUzu^CoY+=Fq-Nk!n!P%=-NK1BV-WJ2Kuz zs`n*QT?b&XP7w7l$Obe$F~Q~1Wd;TYxSz|hP${Atte{E;?U(3l)eUq}pg08wZ)0Tj27&afFn_5()D8JlVE3T3T9YZf-^h!GF=? z$NAY$C;7+w*N8<+RP6}k3mvp?IQ$hEuv8kKW>-DZ2;xDDw3Wm(3^L74RI61S$3dx; zUEaHQFUO9bV0ic}L$~r=FSZic)Q@9@@Pu6VWEWtitk<;PdQ()RQK2A-L>y%pG_`kN zdoH%);Ij1J1Q1H6dH&EL!jUMs;D0O^r@OEkI&h@1q46}p!|iw3P-pjBI!WKh^F4f} zc&5LfuxX-=02O!j#uy}04J1=ZB1wVaWRanGy}jiTd8|xT8>|sQp;+P8!ae58Wp)n? z;QAWRljLh_!wdu^bMpn}<_qf{={`#v0ue%zv%{R3Rt%m#&8`<;WZrRb{C|h<6KFHc z$kY^LxgswO zez}k&%j7|aGOCp#(SSifh`K=ppsYsx#xF6kFvgF6_zMoa^c>61n5$bYj039e()9#y~+L@iBqb_Qd5V*F}v=QG^}-wM^neHJS9 z%qapX0}#nV$6;=dS`^RRqG{+VQA;AU0@tM?ET&6PP3%XddJ(7%pGZ6`*_<`#?euH8 zyW``==#`77cJ6C@eRq2@6OULjUsXc;s~*)JPG|i%yM@+!sO@Z^I)8BvnIA>f3P`Qd zriM_QE!(ycY}tzy>p&>$34WgBf9*9PwVw*1ds4RFU=?RR9=`Uj@yFl&^6g`>Y+trJ zyQgP!BIT-3)YGfItbVKU=o^i-_D10F|G3{_snLIypU2r(NR-gp^lPQ+Tt1&0nHv7n z?|=1M0SHp5zUQAi_&Q2w_vV*VO)Ue;FwDp7#}`JP>pJ7hHfN8dej_CN2C&tLs7 XN{+GSu*VdP00000NkvXXu0mjf=B-lg literal 6320 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 B(@X#W diff --git a/padbuildimg/assets/lat/vdp.png b/padbuildimg/assets/lat/vdp.png new file mode 100644 index 0000000000000000000000000000000000000000..5c50b17ac9d50fd652e13f0cf71442edca943ea4 GIT binary patch literal 2166 zcmV-+2#NQJP)X1^@s6$6kv|00009a7bBm000XU z000XU0RWnu7ytkWC`m*?RA_u{jhL6}6hp=I@J2ukm5k$q%Og z<*mQtH|2w4EQ&pz`M6Q<5QBZoqvX_a4i(Z2B%SvlWCnB-)4@did* zM~`$8@YWOc))8%KMQy!I+~0z#HiD``Gz17x?4a1OrQ~9AW?&wfhbgwR_P85T_f1|DU}!78Ji!PBx?~}2>V!4$K)itZ+8%A{EXK3x8V$4BP-pC#bUwN z>0{y}6PY@7A$GgHhkP&?q^zury1Gi7?VqDIsx-B#w4{wAXWnvBGV+q7N#6*QOzP-w zBP0)+tP2T~1d1iu{puB8-6!Nbqx|s?8(F`8J!xraXqrY%O$}$xoIzDpCQO(>VPPSG zK!DSyPg7S{M=Ta2H#e7U+qSXcH!HCRE)u$4n{0LZN3noOV)(?T-iXVjI@}Z%7E)VV z%eiys5JE6_?p&Iin_0JR9g3o$>pB3lX3b*ClqnoMc#!JqY8(y+85tQ^WIKj2{K8dl zNFXo(Z5S3yii2Qh7+u$yIB_B+B_&LnG>MXu65MV#TefUr{`~ovOeV6jvZ$)6V$YsE zELgCB-Me>_mzRgf<3Uv;G=2CEXE!b6^3gYl27PFTk#zlTE%7dg$Y2lwItJm*p+>dX zX=?MKX&RCwk)NN>jvYHFE-q&I^5vA4mQqkqfTya8Y~UO)bI~H!tXV^5W+w4?9FNDt zmD*}jJ5Ms*taIveHSLXcWIwSKb4DJBAxi59NVMxIF?2hP`^Rh}&>0~V3f-t`Z*Qlp ztc=N%C)3i>!r{Gp`LzUZ*kP>%$4g6Dv0?@1&!6Y&)vIjZzMb?=KWi18KigrI=s-IC zH<}J_P4?ENS7b0SqJTe9VHot>;E3c6B1PC}i$qaX6^Nnh8e6w+<WorZF=`LM)%?Id2qfye+(^`y)p=zWy_9MUZ#+tRV-e~+E}%BZieBOceWSY&ivM^O~qZZ}0mMZCCd z8P_v2c}9nFA80->e4w|3>Vv%@SffE*RuN-<`Z_7u4`WDjpM6V4>9&UqU^XZABK?h* zfsQ3{+q0W^*T?&sngWOcod81Mdgw{&-rmE31BaMBdpd1xDg_0(xLhu3Yisd(y_A=i zLomo}ARl^oTQ=ZZKoM}r_a6dZR1p168YY`-fEo=g-D$53f>>Po(o)}hiIL_mxj2Ap z47LYPeVfkCr+H;R9P86)-;|2TtEm20yYxK%P8QNfD!4S z)1hj)xZEY50gkhmMk#X649{BkbF^ zk89Vi5%&A}p{DUXklEFy3D^o`06zy(fn4AvKn}IC_x;TnLMCm-l3}-wNFXqY;kQMy z01iL_q|^-D^H=fjwsfMJgkkv5brn@rd0JIjOTrldwZJ-Hws}x;9cNz zM>g(RYY~PG!pYR`CaE?Bt;3HvR^MT~cBqX@A!oAt)GvmSaE@U>BpjjfLK*6(?-BES z1hD|txQ;5jaQ>(mSN05qNSsB)wFrTW@8Pf5j1p`_9WfU7j8(WEcnl#8gV`ZLPKLzO zv(5NEIcj`m9CACFc5?}EDt?GXFsu$Fkbe+ZNQ57TRL$Np*;dbBs0|UKSG02M; z?sSPqAFyH$RPoo}Z#$LN=1M>Mlk)C2SVKvW2Ngw?yD@q}xhPAQRan zuv&&si0-^zZ+vjnwMn|NT#ozON6+Ys%v`}(A+gVf3>~ut4nvi%5xX_ s{@M&yHm0hvP_%RZ)uyJ61OjLO3zS6FGOkF~fdBvi07*qoM6N<$g0f!!Bme*a literal 0 HcmV?d00001 From dda10b279240b940cbd56ff4632941adae507ed3 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Sep 2020 02:48:26 -0700 Subject: [PATCH 0107/1220] link update --- padbuildimg/padbuildimg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 59139e2d..f3e8a832 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -143,7 +143,7 @@ AWK_CIRCLE = 'circle' AWK_STAR = 'star' DELAY_BUFFER = 'delay_buffer' -REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/chasehult/pdchu-cog/master/padbuildimg/assets/' +REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/padbuildimg/assets/' REMOTE_AWK_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/media/awakenings/{0:03d}.png' # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' From ebe5194444c4055a26d2a0d8533a84e6e5b242af Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 10:40:22 -0700 Subject: [PATCH 0108/1220] edit flush --- padboard/padboard.py | 4 +++- padinfo/padinfo.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/padboard/padboard.py b/padboard/padboard.py index b7cb87be..52071e83 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -14,7 +14,8 @@ DATA_DIR = os.path.join('data', 'padboard') -DAWNGLARE_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" +DAWNGLARE_BOARD_TEMPLATE = "https://pad.dawnglare.com/?patt={}" +CNINJA_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" class PadBoard(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -91,6 +92,7 @@ async def dawnglare(self, ctx, user: discord.Member = None): # Convert O (used by padvision code) to X (used by Puzzled for bombs) board_text_nc = board_text_nc.replace('o', 'x') msg = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc) + msg += '\n'+CNINJA_BOARD_TEMPLATE.format(board_text_nc) await ctx.send(msg) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1950cc1f..071e1372 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -44,7 +44,7 @@ INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' -RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png' +RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' From 4a9d06a74f609576982a43bf99a1c551604b464a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 10:53:48 -0700 Subject: [PATCH 0109/1220] tsutils --- README.md | 2 -- azurelane/azurelane.py | 2 +- azurelane/info.json | 4 ++-- chronomagia/chronomagia.py | 7 +++---- chronomagia/info.json | 4 ++-- dadguide/dadguide.py | 31 +++++++++++++++---------------- dadguide/info.json | 4 ++-- padboard/info.json | 4 ++-- padboard/padboard.py | 6 +++--- padbuildimg/info.json | 3 +-- padbuildimg/padbuildimg.py | 7 ++----- padbuilds/info.json | 4 ++-- padbuilds/padbuilds.py | 3 +-- padevents/info.json | 7 ++----- padevents/padevents.py | 2 +- padglobal/info.json | 3 +-- padglobal/padglobal.py | 16 ++++++++-------- padguidedb/info.json | 6 ++---- padguidedb/padguidedb.py | 6 +++--- padinfo/info.json | 3 +-- padinfo/padinfo.py | 6 +++--- padmonitor/info.json | 3 +-- padmonitor/padmonitor.py | 6 +++--- padsearch/info.json | 3 +-- padsearch/padsearch.py | 4 ++-- pricecheck/info.json | 3 +-- pricecheck/pricecheck.py | 2 +- profile/info.json | 4 ++-- profile/profile.py | 2 +- schoolidol/info.json | 4 ++-- schoolidol/schoolidol.py | 2 +- 31 files changed, 72 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 4d2ee06c..65f0cbd4 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,6 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type * Some cogs will have cross dependencies on each other. Check the command prompt that Tsubaki is running from or type `^traceback 1` if you encounter any errors. * Common dependencies include: - * rpadutils * dadguide * padinfo * Note that when you edit cogs with dependencies, you might need to do multiple reloads. For example, if updating dadguide, you will need to reload padinfo as well. @@ -100,7 +99,6 @@ contain utility libraries. | calculator | Replacement for the calculator cog that doesnt suck | | fancysay | Make the bot say special things | | memes | CustomCommands except role-limited | -| rpadutils | Utility library shared by many other libraries | | sqlactivitylog | Archives messages in sqlite, allows for lookup | | timecog | Convert/print time in different timezones | | trutils | Misc utilities intended for my usage only | diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index fafd06bb..35ccf1cf 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -4,11 +4,11 @@ import aiohttp import discord +from tsutils import Menu, EmojiUpdater, char_to_emoji from redbot.core import commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline -from rpadutils import Menu, EmojiUpdater, char_to_emoji BASE_URL = 'https://storage.googleapis.com/mirubot/alimages/raw' DATA_URL = '{}/azure_lane.json'.format(BASE_URL) diff --git a/azurelane/info.json b/azurelane/info.json index de8e6992..8e34b3ce 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, - "requirements": ["aiohttp"], + "require_cogs": {}, + "requirements": ["tsutils", "aiohttp"], "tags": ["azurlane", "azurelane"] } diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index 5a4a2c84..ebab859a 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -7,13 +7,12 @@ from collections import OrderedDict import discord +import tsutils +from tsutils import Menu, EmojiUpdater from redbot.core import commands, data_manager from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline -import rpadutils -from rpadutils import Menu, EmojiUpdater - def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) @@ -78,7 +77,7 @@ async def refresh_data(self): await self.bot.wait_until_ready() standard_expiry_secs = 2 * 60 * 60 - summary_text = await rpadutils.makeAsyncCachedPlainRequest( + summary_text = await tsutils.makeAsyncCachedPlainRequest( _data_file('summary.csv'), SUMMARY_SHEET, standard_expiry_secs) file_reader = csv.reader(summary_text.splitlines(), delimiter=',') next(file_reader, None) # skip header diff --git a/chronomagia/info.json b/chronomagia/info.json index 28eb9f35..0a71ca8e 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, - "requirements": ["aiohttp"], + "require_cogs": {}, + "requirements": ["tsutils", "aiohttp"], "tags": ["chronomagia"] } diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index a7581d29..c5269367 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -23,14 +23,13 @@ import pytz import romkan import discord +import tsutils from redbot.core import checks, data_manager from redbot.core import commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline from redbot.core.utils import AsyncIter -import rpadutils - logger = logging.getLogger('red.tsubaki.dadguide') @@ -195,7 +194,7 @@ async def download_and_refresh_nicknames(self): def write_monster_computed_names(self): results = {} for name, nm in self.index.all_entries.items(): - results[name] = int(rpadutils.get_pdx_id_dadguide(nm)) + results[name] = int(tsutils.get_pdx_id_dadguide(nm)) with open(NAMES_EXPORT_PATH, 'w', encoding='utf-8') as f: json.dump(results, f) @@ -205,7 +204,7 @@ def write_monster_computed_names(self): entry = {'bn': list(nm.group_basenames)} if nm.extra_nicknames: entry['nn'] = list(nm.extra_nicknames) - results[int(rpadutils.get_pdx_id_dadguide(nm))] = entry + results[int(tsutils.get_pdx_id_dadguide(nm))] = entry with open(BASENAMES_EXPORT_PATH, 'w', encoding='utf-8') as f: json.dump(results, f) @@ -231,15 +230,15 @@ def _csv_to_tuples(self, file_path: str, cols: int = 2): async def _download_files(self): one_hour_secs = 1 * 60 * 60 - await rpadutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, one_hour_secs) + await tsutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, one_hour_secs) async def _download_override_files(self): one_hour_secs = 1 * 60 * 60 - await rpadutils.makeAsyncCachedPlainRequest( + await tsutils.makeAsyncCachedPlainRequest( NICKNAME_FILE_PATTERN, NICKNAME_OVERRIDES_SHEET, one_hour_secs) - await rpadutils.makeAsyncCachedPlainRequest( + await tsutils.makeAsyncCachedPlainRequest( BASENAME_FILE_PATTERN, GROUP_BASENAMES_OVERRIDES_SHEET, one_hour_secs) - await rpadutils.makeAsyncCachedPlainRequest( + await tsutils.makeAsyncCachedPlainRequest( PANTHNAME_FILE_PATTERN, PANTHNAME_OVERRIDES_SHEET, one_hour_secs) @commands.group() @@ -257,7 +256,7 @@ async def setdatafile(self, ctx, *, data_file): -class DadguideSettings(rpadutils.CogSettings): +class DadguideSettings(tsutils.CogSettings): def make_default_settings(self): config = { 'data_file': '', @@ -853,7 +852,7 @@ def __init__(self, item, database): self.roma_subname = make_roma_subname(self.name_jp) else: # Remove annoying stuff from NA names, like Jörmungandr - self.name_na = rpadutils.rmdiacritics(self.name_na) + self.name_na = tsutils.rmdiacritics(self.name_na) self.name_na = self.name_na_override or self.name_na @@ -1184,13 +1183,13 @@ def make_roma_subname(name_jp): adjusted_subname = '' for part in subname.split('・'): roma_part = romkan.to_roma(part) - if part != roma_part and not rpadutils.containsJp(roma_part): + if part != roma_part and not tsutils.containsJp(roma_part): adjusted_subname += ' ' + roma_part.strip('-') return adjusted_subname.strip() -class MonsterIndex(rpadutils.aobject): +class MonsterIndex(tsutils.aobject): async def __init__(self, monster_database, nickname_overrides, basename_overrides, panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory @@ -1380,7 +1379,7 @@ def is_pixel(n): return prefixes def find_monster(self, query): - query = rpadutils.rmdiacritics(query).lower().strip() + query = tsutils.rmdiacritics(query).lower().strip() # id search if query.isdigit(): @@ -1397,7 +1396,7 @@ def find_monster(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_jp = rpadutils.containsJp(query) + contains_jp = tsutils.containsJp(query) if len(query) < 2 and contains_jp: return None, 'Japanese queries must be at least 2 characters', None elif len(query) < 4 and not contains_jp: @@ -1484,7 +1483,7 @@ def find_monster2(self, query): Follows a similar logic to the regular id but after each check, will remove any potential match that doesn't contain every single specified prefix. """ - query = rpadutils.rmdiacritics(query).lower().strip() + query = tsutils.rmdiacritics(query).lower().strip() # id search if query.isdigit(): m = self.monster_no_na_to_named_monster.get(int(query)) @@ -1497,7 +1496,7 @@ def find_monster2(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_jp = rpadutils.containsJp(query) + contains_jp = tsutils.containsJp(query) if len(query) < 2 and contains_jp: return None, 'Japanese queries must be at least 2 characters', None elif len(query) < 4 and not contains_jp: diff --git a/dadguide/info.json b/dadguide/info.json index 053d556a..e40915d6 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, - "requirements": ["pytz", "romkan"], + "require_cogs": {}, + "requirements": ["tsutils", "pytz", "romkan"], "tags": ["PAD"] } diff --git a/padboard/info.json b/padboard/info.json index e6036c5d..a6450cea 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {}, - "requirements": [], + "require_cogs": {"padvision": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["tsutils"], "tags": ["PAD"] } diff --git a/padboard/padboard.py b/padboard/padboard.py index 52071e83..f7c75914 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -10,7 +10,7 @@ from redbot.core.utils.chat_formatting import inline from padvision import padvision -from rpadutils import rpadutils +from tsutils import tsutils DATA_DIR = os.path.join('data', 'padboard') @@ -41,7 +41,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): @commands.Cog.listener("on_message") async def log_message(self, message): - url = rpadutils.extract_image_url(message) + url = tsutils.extract_image_url(message) if url: self.logs[message.author.id].append(url) @@ -99,7 +99,7 @@ async def dawnglare(self, ctx, user: discord.Member = None): async def get_recent_image(self, ctx, user: discord.Member = None, message: discord.Message = None): user_id = user.id if user else ctx.author.id - image_url = rpadutils.extract_image_url(message) + image_url = tsutils.extract_image_url(message) if image_url is None: image_url = self.find_image(user_id) diff --git a/padbuildimg/info.json b/padbuildimg/info.json index 1796d4aa..16f6c984 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -10,10 +10,9 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, - "requirements": ["ply", "Pillow", "aiohttp"], + "requirements": ["tsutils", "ply", "Pillow", "aiohttp"], "tags": ["PAD"] } diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index f3e8a832..ed7e32a3 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -13,11 +13,8 @@ from PIL import ImageDraw from PIL import ImageFont from PIL import ImageChops - -from redbot.core import commands - -from rpadutils.rpadutils import CogSettings -from redbot.core import checks +from tsutils import CogSettings +from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline HELP_MSG = """ diff --git a/padbuilds/info.json b/padbuilds/info.json index 7bef9ac1..ae3ed7b6 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, - "requirements": [], + "require_cogs": {}, + "requirements": ["tsutils"], "tags": ["PAD"] } diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index 1d6f3f69..90f5a545 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -1,12 +1,11 @@ import re import discord +from tsutils import CogSettings from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import pagify, box -from rpadutils import CogSettings - class PadBuilds(commands.Cog): """Custom PAD builds diff --git a/padevents/info.json b/padevents/info.json index a98c549b..28a2be3b 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -9,10 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["pytz", "prettytable"], + "require_cogs": {"dadguide": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["tsutils", "pytz", "prettytable"], "tags": ["PAD"] } diff --git a/padevents/padevents.py b/padevents/padevents.py index 88e1f2ec..d3ebde70 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -9,13 +9,13 @@ import prettytable import discord +from tsutils import CogSettings from redbot.core import checks from redbot.core import commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify from dadguide import dadguide -from rpadutils import CogSettings SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] diff --git a/padglobal/info.json b/padglobal/info.json index 25283a59..39f32672 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -10,10 +10,9 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, - "requirements": ["aiohttp", "prettytable"], + "requirements": ["tsutils", "aiohttp", "prettytable"], "tags": ["PAD"] } diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index befb889d..21f83456 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -11,14 +11,14 @@ import aiohttp import discord import prettytable +import tsutils +from tsutils import CogSettings, safe_read_json, replace_emoji_names_with_code,\ + clean_global_mentions, confirm_message from redbot.core import checks, data_manager from redbot.core import commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify -import rpadutils -from rpadutils import CogSettings, safe_read_json, replace_emoji_names_with_code,\ - clean_global_mentions, confirm_message global PADGLOBAL_COG @@ -283,7 +283,7 @@ def list_or_none(l): sent_messages = [] for page in pagify(msg): sent_messages.append(await ctx.send(box(page))) - await rpadutils.await_and_remove(self.bot, sent_messages[-1], ctx.author, + await tsutils.await_and_remove(self.bot, sent_messages[-1], ctx.author, delete_msgs=sent_messages, timeout=30) @commands.command() @@ -799,7 +799,7 @@ def which_to_text(self): for grp in sorted(monsters.keys()): tbl.add_row([grp, ', '.join(sorted(monsters[grp]))]) - tbl_string = rpadutils.strip_right_multiline(tbl.get_string()) + tbl_string = tsutils.strip_right_multiline(tbl.get_string()) return tbl_string async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, timestamp): @@ -871,7 +871,7 @@ async def getwhich(self, ctx): for mon in sorted(sorted(monsters), key=lambda x: x[1]): tbl.add_row(mon) - msg = rpadutils.strip_right_multiline(tbl.get_string()) + msg = tsutils.strip_right_multiline(tbl.get_string()) for page in pagify(msg): await ctx.send(box(page)) @@ -1215,8 +1215,8 @@ def emojify(self, message): if guild.id in emoteservers: continue emojis.extend(guild.emojis) - message = rpadutils.replace_emoji_names_with_code(emojis, message) - return rpadutils.fix_emojis_for_server(emojis, message) + message = tsutils.replace_emoji_names_with_code(emojis, message) + return tsutils.fix_emojis_for_server(emojis, message) def check_simple_tree(monster): attr1 = monster.attr1 diff --git a/padguidedb/info.json b/padguidedb/info.json index 7643e57c..4c36d429 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -9,9 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs" - }, - "requirements": ["pymysql"], + "require_cogs": {}, + "requirements": ["tsutils", "pymysql"], "tags": ["PAD"] } diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index c99f7442..83e91402 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -10,7 +10,7 @@ from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify -from rpadutils import CogSettings, rpadutils +from tsutils import CogSettings, tsutils PADGUIDEDB_COG = None @@ -186,10 +186,10 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): if stderr: print("Dungeon Load Error:\n" + stderr.decode()) - await rpadutils.doubleup(ctx, inline( + await tsutils.doubleup(ctx, inline( 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) else: - await rpadutils.doubleup(ctx, inline( + await tsutils.doubleup(ctx, inline( 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) self.queue_size -= 1 diff --git a/padinfo/info.json b/padinfo/info.json index ae47b7a6..d984d84f 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -10,10 +10,9 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, - "requirements": ["prettytable"], + "requirements": ["tsutils", "prettytable"], "tags": ["PAD"] } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 071e1372..141b359e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -14,8 +14,8 @@ from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box -import rpadutils -from rpadutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics +import tsutils +from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics logger = logging.getLogger('red.tsubaki.padinfo') @@ -63,7 +63,7 @@ class ServerFilter(Enum): def get_pdx_url(m): - return INFO_PDX_TEMPLATE.format(rpadutils.get_pdx_id(m)) + return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) def get_portrait_url(m): diff --git a/padmonitor/info.json b/padmonitor/info.json index 6496394e..b97f09f9 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -10,9 +10,8 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, - "requirements": [], + "requirements": ["tsutils"], "tags": ["PAD"] } diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 6f94fd01..2ce531e9 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -3,7 +3,7 @@ from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline, box, pagify -import rpadutils +import tsutils class PadMonitor(commands.Cog): @@ -60,7 +60,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: msg += '\n\tNo. {} {}'.format(m.monster_no, m.name_na) - if rpadutils.containsJp( + if tsutils.containsJp( m.name_na) and m.name_na_override != m.name_na and m.name_na_override is not None: msg += ' ({})'.format(m.name_na_override) return msg @@ -107,7 +107,7 @@ async def rmnewchannel(self, ctx): await ctx.send(inline('done')) -class PadMonitorSettings(rpadutils.CogSettings): +class PadMonitorSettings(tsutils.CogSettings): def make_default_settings(self): config = { 'jp_seen_ids': [], diff --git a/padsearch/info.json b/padsearch/info.json index 88b04d13..92b73cb8 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -10,9 +10,8 @@ "hidden": false, "disabled": false, "require_cogs": { - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, - "requirements": ["ply"], + "requirements": ["tsutils", "ply"], "tags": ["PAD"] } diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 0ef3120b..ccd21650 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -8,7 +8,7 @@ from redbot.core.bot import Red from redbot.core.utils.chat_formatting import box, pagify -from rpadutils import rpadutils +from tsutils import tsutils HELP_MSG = """ {0.prefix}search @@ -54,7 +54,7 @@ """ -@rpadutils.timeout_after(1) +@tsutils.timeout_after(1) def filt_timeout(filts, ms): for f in filts: ms = [m for m in ms if f(m)] diff --git a/pricecheck/info.json b/pricecheck/info.json index 9490b7d6..6f0e4fd4 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -11,10 +11,9 @@ "disabled": false, "require_cogs": { "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", - "rpadutils": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, - "requirements": [], + "requirements": ["tsutils"], "tags": ["PAD"] } diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index fe3deb23..29dc0e0d 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -4,7 +4,7 @@ from redbot.core import commands, checks, Config from redbot.core.utils.chat_formatting import inline, box, pagify -from rpadutils import auth_check +from tsutils import auth_check PC_TEXT = """{name} - Stamina Cost: {stam_cost} diff --git a/profile/info.json b/profile/info.json index cf5499c9..9d5481f1 100644 --- a/profile/info.json +++ b/profile/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, - "requirements": [], + "require_cogs": {}, + "requirements": ["tsutils"], "tags": ["PAD"] } diff --git a/profile/profile.py b/profile/profile.py index cb0eaede..b6090db7 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -5,7 +5,7 @@ from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify -from rpadutils import CogSettings +from tsutils import CogSettings def normalizeServer(server): diff --git a/schoolidol/info.json b/schoolidol/info.json index 471f0049..f75e8b38 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -9,7 +9,7 @@ "min_python_version" : [3,8,1], "hidden": false, "disabled": false, - "require_cogs": {"rpadutils": "https://github.com/TsubakiBotPad/misc-cogs"}, - "requirements": ["aiohttp"], + "require_cogs": {}, + "requirements": ["tsutils", "aiohttp"], "tags": ["schoolidol"] } diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index 45e13fe2..47c8008d 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -7,7 +7,7 @@ from redbot.core import commands from redbot.core.utils.chat_formatting import inline -from rpadutils import Menu, EmojiUpdater, char_to_emoji +from tsutils import Menu, EmojiUpdater, char_to_emoji FIRST_REQ = 'https://schoolido.lu/api/cards/?page_size=100' From e320b8c1f3519de95122ef2cad6dc584441842ba Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 12:16:51 -0700 Subject: [PATCH 0110/1220] this was way easier than i thought it'd be --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 141b359e..b0f4958e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1071,7 +1071,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): if not len(awakenings_row): awakenings_row = 'No Awakenings' - killers_row = '**Available Killers:** {}'.format(' '.join(m.killers)) + killers_row = '**Available Killers:** [{} slots] {}'.format(m.latent_slots, ' '.join(m.killers)) embed.description = '{}\n{}'.format(awakenings_row, killers_row) From 02cfd4ba589ada9eee6cf529305a9cdc3ee9e79d Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 15:17:03 -0700 Subject: [PATCH 0111/1220] double the prs ;-; --- dadguide/dadguide.py | 2 +- padboard/padboard.py | 2 +- padbuildimg/padbuildimg.py | 2 +- padglobal/padglobal.py | 14 +++++++------- padguidedb/padguidedb.py | 16 ++++++++-------- padinfo/padinfo.py | 6 +++--- padmonitor/padmonitor.py | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index c5269367..ee7379f0 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -251,7 +251,7 @@ async def dadguide(self, ctx): async def setdatafile(self, ctx, *, data_file): """Set a local path to dadguide data instead of downloading it.""" self.settings.set_data_file(data_file) - await ctx.send(inline('Done')) + await ctx.tick() diff --git a/padboard/padboard.py b/padboard/padboard.py index f7c75914..a360a0fb 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -53,7 +53,7 @@ async def padboard(self, ctx): @padboard.command() async def set_tflite_path(self, ctx, *, path): await self.config.tflite_path.set(path) - await ctx.send(inline("Done")) + await ctx.tick() def find_image(self, user_id): diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index a3763498..2bb30ecb 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -819,7 +819,7 @@ async def refreshassets(self, ctx): await ctx.send('Downloading assets to {}'.format(self.settings.buildImgParams().ASSETS_DIR)) awk_ids = self.bot.get_cog('Dadguide').database.get_awoken_skill_ids() await self.settings.downloadAllAssets(awk_ids) - await ctx.send('Done') + await ctx.tick() @commands.command() @commands.guild_only() diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 21f83456..9c3d8845 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -631,7 +631,7 @@ async def rmglossary(self, ctx, *, term): if not await confirm_message(ctx, "Are you sure you want to globally remove the glossary data for {}?".format(term)): return self.settings.rmGlossary(term) - await ctx.send("done") + await ctx.tick() @commands.command() @commands.check(check_enabled) @@ -712,7 +712,7 @@ async def rmboss(self, ctx, *, term): return self.settings.rmBoss(term) - await ctx.send("done") + await ctx.tick() @commands.command() @commands.check(check_enabled) @@ -845,7 +845,7 @@ async def rmwhich(self, ctx, *, monster_id: int): return self.settings.rmWhich(name) - await ctx.send("done") + await ctx.tick() @padglobal.command() async def getwhich(self, ctx): @@ -894,7 +894,7 @@ async def debuglookup(self, ctx, *, term: str): async def addadmin(self, ctx, user: discord.Member): """Adds a user to the pad global admin""" self.settings.addAdmin(user.id) - await ctx.send(inline("Done!")) + await ctx.tick() @padglobal.command() @checks.is_owner() @@ -910,7 +910,7 @@ async def rmadmin(self, ctx, user): except ValueError: await ctx.send(inline("Invalid user id.")) return - await ctx.send(inline("Done")) + await ctx.tick() @padglobal.command() @checks.is_owner() @@ -1172,7 +1172,7 @@ async def rmdungeonguide(self, ctx, term: str): if not await confirm_message(ctx, "Are you sure you want to globally remove the dungeonguide data for {}?".format(term)): return self.settings.rmDungeonGuide(term) - await ctx.send("done") + await ctx.tick() @padglobal.command() async def addleaderguide(self, ctx, monster_id: int, *, definition: str): @@ -1203,7 +1203,7 @@ async def rmleaderguide(self, ctx, monster_id: int): return self.settings.rmLeaderGuide(name) - await ctx.send("done") + await ctx.tick() def emojify(self, message): emojis = list() diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 83e91402..94c6849c 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -74,7 +74,7 @@ async def padguidedb(self, context): async def addadmin(self, ctx, user: discord.Member): """Adds a user to the padguide db admin""" self.settings.addAdmin(user.id) - await ctx.send("done") + await ctx.tick() @padguidedb.command() @checks.is_owner() @@ -90,14 +90,14 @@ async def rmadmin(self, ctx, user): except ValueError: await ctx.send(inline("Invalid user id.")) return - await ctx.send("done") + await ctx.tick() @padguidedb.command() @checks.is_owner() async def setconfigfile(self, ctx, *, config_file): """Set the database config file.""" self.settings.setConfigFile(config_file) - await ctx.send(inline('Done')) + await ctx.tick() @padguidedb.command() @is_padguidedb_admin() @@ -120,28 +120,28 @@ async def searchdungeon(self, ctx, *, search_text): async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): """Set the dungeon script file.""" self.settings.setDungeonScriptFile(dungeon_script_file) - await ctx.send(inline('Done')) + await ctx.tick() @padguidedb.command() @checks.is_owner() async def setfulletlfile(self, ctx, *, full_etl_file): """Set the full ETL file.""" self.settings.setFullETLFile(full_etl_file) - await ctx.send(inline('Done')) + await ctx.tick() @padguidedb.command() @checks.is_owner() async def setimageupdatefile(self, ctx, *, image_update_file): """Set the image update file.""" self.settings.setImageUpdateFile(image_update_file) - await ctx.send(inline('Done')) + await ctx.tick() @padguidedb.command() @checks.is_owner() async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): """Set the dungeon script.""" self.settings.setUserInfo(server, user_uuid, user_intid) - await ctx.send(inline('Done')) + await ctx.tick() @padguidedb.command() @is_padguidedb_admin() @@ -248,7 +248,7 @@ async def cleardungeon(self, ctx, dungeon_id: int): with self.get_cursor() as cursor: sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) - await ctx.send(inline("Done")) + await ctx.tick() # @padguidedb.command() @is_padguidedb_admin() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b0f4958e..0f6cec44 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -598,12 +598,12 @@ async def idmode(self, ctx, id_type): """Switch between number mode and evo mode""" if id_type in ['evo']: if self.settings.setEvoID(ctx.author.id): - await ctx.send(inline("Done")) + await ctx.tick() else: await ctx.send(inline("You're already using evo mode")) elif id_type in ['number', 'default']: if self.settings.rmEvoID(ctx.author.id): - await ctx.send(inline("Done")) + await ctx.tick() else: await ctx.send(inline("You're already using number mode")) else: @@ -628,7 +628,7 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): async def setvoicepath(self, ctx, *, path=''): """Set path to the voice direcory""" self.settings.setVoiceDir(path) - await ctx.send(inline('Done')) + await ctx.tick() @checks.is_owner() @padinfo.command() diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 2ce531e9..a989f01c 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -96,7 +96,7 @@ async def padmonitor(self, ctx): async def addnewchannel(self, ctx): """Sets announcements for the current channel.""" self.settings.add_new_monster_channel(ctx.channel.id) - await ctx.send(inline('done')) + await ctx.tick() @padmonitor.command() @commands.guild_only() @@ -104,7 +104,7 @@ async def addnewchannel(self, ctx): async def rmnewchannel(self, ctx): """Removes announcements for the current channel.""" self.settings.rm_new_monster_channel(ctx.channel.id) - await ctx.send(inline('done')) + await ctx.tick() class PadMonitorSettings(tsutils.CogSettings): From 109fa9b097a9ba7ea705e9394ff409deed578c97 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 15:35:02 -0700 Subject: [PATCH 0112/1220] preparations --- README.md | 26 -------------------------- azurelane/azurelane.py | 1 - padbuildimg/padbuildimg.py | 7 ------- padevents/padevents.py | 14 +++++++------- padguidedb/padguidedb.py | 2 -- padinfo/padinfo.py | 12 ++++++------ padvision/padvision.py | 2 ++ profile/profile.py | 4 ++-- 8 files changed, 17 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 65f0cbd4..2d92715d 100644 --- a/README.md +++ b/README.md @@ -86,29 +86,3 @@ DadGuide mobile app. | padrem | Rare Egg Machine simulation | | padvision | Utilities relating to PAD image scanning | | profile | Global user PAD profile storage and lookup | - - -# Admin/util cogs - -Cogs that make server administration easier, do miscellaneous useful things, or -contain utility libraries. - -| Cog | Purpose | -| --- | --- | -| baduser | Tracks misbehaving users, other misc user tracking | -| calculator | Replacement for the calculator cog that doesnt suck | -| fancysay | Make the bot say special things | -| memes | CustomCommands except role-limited | -| sqlactivitylog | Archives messages in sqlite, allows for lookup | -| timecog | Convert/print time in different timezones | -| trutils | Misc utilities intended for my usage only | -| twitter2 | Mirrors a twitter feed to a channel | - - -# Other/deprecated cogs - -Cogs not intended for normal use, or superceded. - -| Cog | Purpose | -| --- | --- | -| donations | Tracks users who have donated for hosting fees | diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 35ccf1cf..7cb5c275 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -100,7 +100,6 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): def make_card_embed(c, image_idx): cid = c['id'] name = c['name_en'] - print(c) info_url = c['url'] image = c['images'][image_idx] image_title = image['title'] diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 2bb30ecb..470b8a29 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -483,7 +483,6 @@ async def process_card(self, card_str, is_assist=False): card = None repeat = 1 for tok in iter(self.lexer.token, None): - # print('{} - {}'.format(tok.type, tok.value)) if tok.type == 'ASSIST': assist_str = tok.value elif tok.type == 'REPEAT': @@ -755,23 +754,17 @@ async def padbuildimg(self, ctx, *, build_str: str): """Create a build image based on input. Use ^helpbuildimg for more info. """ - # print('BUILD_STR: {}'.format(build_str)) - # start = time.perf_counter() async with ctx.typing(): params = self.settings.buildImgParams() try: pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) - # print('PARSE: {}'.format(time.perf_counter() - start)) await pbg.process_build(build_str) - # start = time.perf_counter() pbg.generate_build_image() - # print('DRAW: {}'.format(time.perf_counter() - start)) except commands.UserFeedbackCheckFailure as ex: await ctx.send(box(str(ex) + '\nSee ^helpbuildimg for syntax')) return -1 - # start = time.perf_counter() if pbg.build_img is not None: with io.BytesIO() as build_io: pbg.build_img.save(build_io, format='PNG') diff --git a/padevents/padevents.py b/padevents/padevents.py index d3ebde70..0afb937b 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -15,8 +15,6 @@ from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify -from dadguide import dadguide - SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] @@ -151,7 +149,9 @@ async def testevent(self, ctx, server, seconds: int = 0): dg_cog = self.bot.get_cog('Dadguide') await dg_cog.wait_until_ready() - te = dadguide.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) + dg_module = __import__(dg_cog.__module__) + + te = dg_module.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) te.server = server te.event_type_id = EventType.Guerrilla.value @@ -392,17 +392,17 @@ def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False @commands.command(aliases=['events']) async def eventsna(self, ctx): - """Print upcoming daily events for NA.""" + """Display upcoming daily events for NA.""" await self.doPartial(ctx, 'NA') @commands.command() async def eventsjp(self, ctx): - """Print upcoming daily events for JP.""" + """Display upcoming daily events for JP.""" await self.doPartial(ctx, 'JP') @commands.command() async def eventskr(self, ctx): - """Print upcoming daily events for KR.""" + """Display upcoming daily events for KR.""" await self.doPartial(ctx, 'KR') async def doPartial(self, ctx, server): @@ -494,7 +494,7 @@ def removeDailyReg(self, channel_id, server): class Event: - def __init__(self, scheduled_event: dadguide.DgScheduledEvent): + def __init__(self, scheduled_event: "DgScheduledEvent"): self.key = scheduled_event.key() self.server = SUPPORTED_SERVERS[scheduled_event.server_id] self.open_datetime = scheduled_event.open_datetime diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 94c6849c..6150e242 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -232,11 +232,9 @@ async def dungeondrops(self, ctx, dungeon_id: int): msg = 'floor_id,count\n' for row in results1: - print(row) msg += ','.join(map(str,row.values()))+"\n" msg += '\n\nfloor_id,monster_id,count\n' for row in results2: - print(row) msg += ','.join(map(str,row.values()))+"\n" for page in pagify(msg): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0f6cec44..5cf35d4c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -804,7 +804,7 @@ def monsterToBaseEmbed(m: "DgMonster"): return embed -def printEvoListFields(list_of_monsters, embed, name): +def addEvoListFields(list_of_monsters, embed, name): if not len(list_of_monsters): return field_name = name.format(len(list_of_monsters)) @@ -822,15 +822,15 @@ def monsterToEvoEmbed(m: "DgMonster"): embed.description = 'No alternate evos or evo gem' return embed - printEvoListFields(m.alt_evos, embed, '{} alternate evo(s)') + addEvoListFields(m.alt_evos, embed, '{} alternate evo(s)') if not m.evo_gem: return embed - printEvoListFields([m.evo_gem], embed, '{} evo gem(s)') + addEvoListFields([m.evo_gem], embed, '{} evo gem(s)') return embed -def printMonsterEvoOfList(monster_list, embed, field_name): +def addMonsterEvoOfList(monster_list, embed, field_name): if not len(monster_list): return field_data = '' @@ -857,10 +857,10 @@ def monsterToEvoMatsEmbed(m: "DgMonster"): field_data = 'None' embed.add_field(name=field_name, value=field_data) - printMonsterEvoOfList(m.material_of, embed, 'Material for') + addMonsterEvoOfList(m.material_of, embed, 'Material for') if not m.evo_gem: return embed - printMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") + addMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") return embed diff --git a/padvision/padvision.py b/padvision/padvision.py index 3ee6e9b9..daae3788 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -1,4 +1,5 @@ import io +import logging import traceback import PIL.Image @@ -6,6 +7,7 @@ ORB_IMG_SIZE = 40 +logger = logging.getLogger('red.padbot-cogs.padvision') class PadVision(commands.Cog): def __init__(self, bot, *args, **kwargs): diff --git a/profile/profile.py b/profile/profile.py index b6090db7..cca3050a 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -69,7 +69,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): @commands.command(name="idme") async def idMe(self, ctx, server=None): - """Prints out your profile to the current channel + """Displays your profile to the current channel If you do not provide a server, your default is used """ @@ -97,7 +97,7 @@ async def idTo(self, ctx, user: discord.Member, server=None): @commands.command(name="idfor") async def idFor(self, ctx, user: discord.Member, server=None): - """Prints out the profile of the specified user + """Displays the profile of the specified user If you do not provide a server, your default is used. """ From 4bf571b2cee0278c13c21c7d6596099c9445fb4b Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 16:19:14 -0700 Subject: [PATCH 0113/1220] Redbot Compliance --- azurelane/azurelane.py | 13 ++++++++----- chronomagia/chronomagia.py | 18 ++++++++--------- dadguide/dadguide.py | 11 ++++------- padbuildimg/padbuildimg.py | 5 ++++- padevents/padevents.py | 30 +++++++++++++--------------- padglobal/padglobal.py | 20 +++++++++---------- padguidedb/padguidedb.py | 18 ++++++++--------- padinfo/padinfo.py | 40 +++++++++++++++++++++++++------------- padmonitor/padmonitor.py | 8 +++++--- padsearch/padsearch.py | 12 +++++++----- padvision/padvision.py | 3 +-- profile/profile.py | 8 ++++---- schoolidol/schoolidol.py | 11 +++++++---- 13 files changed, 107 insertions(+), 90 deletions(-) diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 7cb5c275..7ae3e08b 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -1,14 +1,16 @@ import difflib import json +import logging from collections import OrderedDict import aiohttp import discord from tsutils import Menu, EmojiUpdater, char_to_emoji -from redbot.core import commands -from redbot.core.bot import Red +from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline +logger = logging.getLogger('red.padbot-cogs.azurelane') + BASE_URL = 'https://storage.googleapis.com/mirubot/alimages/raw' DATA_URL = '{}/azure_lane.json'.format(BASE_URL) @@ -17,7 +19,7 @@ class AzureLane(commands.Cog): """AzureLane.""" - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.card_data = [] @@ -42,7 +44,7 @@ async def reload_al(self): async with session.get(DATA_URL) as resp: raw_resp = await resp.text() self.card_data = json.loads(raw_resp)['items'] - print('done retrieving cards: {}'.format(len(self.card_data))) + logger.info('done retrieving cards: {}'.format(len(self.card_data))) self.id_to_card = {c['id']: c for c in self.card_data} name_to_card = {'{}'.format(c['name_en']).lower(): c for c in self.card_data} @@ -54,6 +56,7 @@ async def reload_al(self): } @commands.command() + @checks.bot_has_permissions(embed_links=True) async def alid(self, ctx, *, query: str): """Azure Lane query.""" query = query.lower().strip() @@ -94,7 +97,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): result_embed.set_footer(text=discord.Embed.Empty) await result_msg.edit(embed=result_embed) except Exception as ex: - print('Menu failure', ex) + logger.exception('Menu failure') def make_card_embed(c, image_idx): diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index ebab859a..2a003d20 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -4,15 +4,16 @@ import os import traceback import urllib.parse +import logging from collections import OrderedDict import discord import tsutils from tsutils import Menu, EmojiUpdater -from redbot.core import commands, data_manager -from redbot.core.bot import Red +from redbot.core import commands, data_manager, checks from redbot.core.utils.chat_formatting import inline +logger = logging.getLogger('red.padbot-cogs.chronomagia') def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) @@ -42,7 +43,7 @@ def __init__(self, csv_row): class ChronoMagia(commands.Cog): """ChronoMagia.""" - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.card_data = [] @@ -67,10 +68,9 @@ async def reload_cm_task(self): while self == self.bot.get_cog('ChronoMagia'): try: await self.refresh_data() - print('Done refreshing ChronoMagia') + logger.info('Done refreshing ChronoMagia') except Exception as ex: - print("reload CM loop caught exception " + str(ex)) - traceback.print_exc() + logger.exception("reload CM loop caught exception " + str(ex)) await asyncio.sleep(60 * 60 * 1) async def refresh_data(self): @@ -87,10 +87,11 @@ async def refresh_data(self): # Ignore empty rows continue if len(row) < 11: - print('bad row: ', row) + logger.error('bad row: ', row) self.card_data.append(CmCard(row)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def cmid(self, ctx, *, query: str): """ChronoMagia query.""" query = clean_name_for_query(query) @@ -134,7 +135,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): result_embed.set_footer(text=discord.Embed.Empty) await result_msg.edit(embed=result_embed) except Exception as ex: - print('Menu failure', ex) + logger.exception('Menu failure') def make_base_embed(c: CmCard): @@ -169,7 +170,6 @@ def make_embed(c: CmCard): def make_img_embed(c: CmCard): embed = make_base_embed(c) url = PIC_URL.format(urllib.parse.quote(c.name)) - print(url) embed.set_image(url=url) return embed diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index ee7379f0..5688e5c2 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -26,11 +26,10 @@ import tsutils from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline from redbot.core.utils import AsyncIter -logger = logging.getLogger('red.tsubaki.dadguide') +logger = logging.getLogger('red.padbot-cogs.dadguide') def _data_file(file_name: str) -> str: @@ -57,7 +56,7 @@ def _data_file(file_name: str) -> str: class Dadguide(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self._is_ready = asyncio.Event() @@ -135,15 +134,13 @@ async def reload_data_task(self): self._is_ready.set() except Exception as ex: short_wait = True - logger.info("dadguide data download/refresh failed: %s", ex) - traceback.print_exc() + logger.exception("dadguide data download/refresh failed: %s", ex) try: wait_time = 60 if short_wait else 60 * 60 * 4 await asyncio.sleep(wait_time) except Exception as ex: - logger.info("dadguide data wait loop failed: %s", ex) - traceback.print_exc() + logger.exception("dadguide data wait loop failed: %s", ex) raise ex async def reload_config_files(self): diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 470b8a29..f3a6b71c 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -776,7 +776,10 @@ async def padbuildimg(self, ctx, *, build_str: str): except discord.errors.Forbidden as ex: await ctx.send(inline('Failed to send build to {}'.format(ctx.author))) else: - await ctx.send(file=discord.File(build_io,'pad_build.png')) + try: + await ctx.send(file=discord.File(build_io,'pad_build.png')) + except discord.errors.Forbidden as ex: + await ctx.send(inline("Failed to send build. (Insufficient Permisisons)")) else: await ctx.send(box('Invalid build, see ^helpbuildimg')) return 0 diff --git a/padevents/padevents.py b/padevents/padevents.py index 0afb937b..034464a9 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -3,6 +3,7 @@ import re import datetime import pytz +import logging from collections import defaultdict from datetime import timedelta from enum import Enum @@ -12,14 +13,15 @@ from tsutils import CogSettings from redbot.core import checks from redbot.core import commands -from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify +logger = logging.getLogger('red.padbot-cogs.padevents') + SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] class PadEvents(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -53,10 +55,9 @@ async def reload_padevents(self): while self == self.bot.get_cog('PadEvents'): try: await self.refresh_data() - print('Done refreshing PadEvents') + logger.info('Done refreshing PadEvents') except Exception as ex: - print("reload padevents loop caught exception " + str(ex)) - traceback.print_exc() + logger.exception("reload padevents loop caught exception " + str(ex)) await asyncio.sleep(60 * 60 * 1) @@ -70,7 +71,7 @@ async def refresh_data(self): try: new_events.append(Event(se)) except Exception as ex: - print(ex) + logger.exception("Refresh error:") self.events = new_events self.started_events = {ev.key for ev in new_events if ev.is_started()} @@ -104,10 +105,8 @@ async def check_started(self): await channel.send(message) except Exception as ex: - # deregister gr - traceback.print_exc() # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) - print("caught exception while sending guerrilla msg:\n" + str(ex)) + logger.exception("caught exception while sending guerrilla msg:") else: if not e.dungeon_type in [DungeonType.Normal]: @@ -120,19 +119,17 @@ async def check_started(self): if server == daily_registration['server']: await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), msg, channel_id=daily_registration['channel_id']) - print("daily_reg server") + logger.info("daily_reg server") except Exception as ex: - traceback.print_exc() # self.settings.removeDailyReg( # daily_registration['channel_id'], daily_registration['server']) - print("caught exception while sending daily msg:\n" + str(ex)) + logger.exception("caught exception while sending daily msg:") except Exception as ex: - traceback.print_exc() - print("caught exception while checking guerrillas:\n" + str(ex)) + logger.exception("caught exception while checking guerrillas:") await asyncio.sleep(10) - print("done check_started") + logger.info("done check_started") @commands.group(aliases=['pde']) @checks.mod_or_permissions(manage_guild=True) @@ -324,8 +321,7 @@ async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): else: await self.bot.get_channel(int(channel_id)).send(format_type(page)) except Exception as e: - print("page output failed " + str(e)) - print("tried to print: " + page) + logger.exception("page output failed " + str(e), "tried to output: " + page) def makeActiveOutput(self, table_name, event_list): tbl = prettytable.PrettyTable(["Time", table_name]) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 9c3d8845..8186b3ca 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -6,6 +6,7 @@ import json import os import re +import logging from collections import defaultdict import aiohttp @@ -16,9 +17,9 @@ clean_global_mentions, confirm_message from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify +logger = logging.getLogger('red.padbot-cogs.padglobal') global PADGLOBAL_COG @@ -91,7 +92,7 @@ def monster_id_to_named_monster(monster_id): async def check_enabled(ctx): - """If the server is disabled, print a warning and return False""" + """If the server is disabled, raise a warning and return False""" if PADGLOBAL_COG.settings.checkDisabled(ctx.message): msg = await ctx.send(inline(DISABLED_MSG)) await asyncio.sleep(3) @@ -103,7 +104,7 @@ async def check_enabled(ctx): class PadGlobal(commands.Cog): """Global PAD commands.""" - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) global PADGLOBAL_COG PADGLOBAL_COG = self @@ -168,7 +169,7 @@ class of problems where the bot can read messages but not respond, so this msg = '------------------------\n' msg += '{} shut down the bot because: {}\n'.format(ctx.author.name, reason) msg += '------------------------\n' - print(msg) + logger.critical(msg) try: app_info = await self.bot.application_info() @@ -176,7 +177,7 @@ class of problems where the bot can read messages but not respond, so this await owner.send(msg) await ctx.send("Owner has been notified, shutting down...") except Exception as ex: - print('Failed to notifiy for breakglass: ' + str(ex)) + logger.exception('Failed to notifiy for breakglass: ' + str(ex)) await self.bot.shutdown() @@ -442,24 +443,23 @@ async def pad(self, ctx): """Shows PAD global command list""" configured = self.settings.faq() + self.settings.boards() cmdlist = {k: v for k, v in self.c_commands.items() if k not in configured} - await self.print_cmdlist(ctx, cmdlist) + await self.send_cmdlist(ctx, cmdlist) @commands.command() @commands.check(check_enabled) async def padfaq(self, ctx): """Shows PAD FAQ command list""" cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.faq()} - await self.print_cmdlist(ctx, cmdlist) + await self.send_cmdlist(ctx, cmdlist) @commands.command() @commands.check(check_enabled) async def boards(self, ctx): """Shows PAD Boards command list""" cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} - await self.print_cmdlist(ctx, cmdlist) + await self.send_cmdlist(ctx, cmdlist) - async def print_cmdlist(self, ctx, cmdlist, inline=False): - #TODO: Write a docstring once I figure out what this does + async def send_cmdlist(self, ctx, cmdlist, inline=False): if not cmdlist: await ctx.send("There are no padglobal commands yet") return diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 6150e242..4ee3974a 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -2,22 +2,22 @@ import csv import io import json +import logging import discord import pymysql from redbot.core import checks from redbot.core import commands -from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify from tsutils import CogSettings, tsutils -PADGUIDEDB_COG = None +logger = logging.getLogger('red.padbot-cogs.padguidedb') def is_padguidedb_admin_check(ctx): - is_owner = ctx.author.id in PADGUIDEDB_COG.bot.owner_ids - return is_owner or PADGUIDEDB_COG.settings.checkAdmin(ctx.author.id) + is_owner = ctx.author.id in ctx.bot.owner_ids + return is_owner or ctx.bot.get_cog("PadGuideDb").settings.checkAdmin(ctx.author.id) def is_padguidedb_admin(): @@ -27,7 +27,7 @@ def is_padguidedb_admin(): class PadGuideDb(commands.Cog): """PadGuide Database manipulator""" - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.settings = PadGuideDbSettings("padguidedb") @@ -36,8 +36,6 @@ def __init__(self, bot: Red, *args, **kwargs): self.full_etl_lock = asyncio.Lock() self.extract_images_lock = asyncio.Lock() self.dungeon_load_lock = asyncio.Lock() - global PADGUIDEDB_COG - PADGUIDEDB_COG = self async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -185,7 +183,7 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): stdout, stderr = await process.communicate() if stderr: - print("Dungeon Load Error:\n" + stderr.decode()) + logger.error("Dungeon Load Error:\n" + stderr.decode()) await tsutils.doubleup(ctx, inline( 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) else: @@ -291,7 +289,7 @@ async def fulletl(self, ctx): stdout, stderr = await process.communicate() if stderr: - print("Full ETL Error:\n" + stderr.decode()) + logger.error("Full ETL Error:\n" + stderr.decode()) await ctx.send(inline('Full ETL failed')) return await ctx.send(inline('Full ETL finished')) @@ -316,7 +314,7 @@ async def extractimages(self, ctx): stdout, stderr = await process.communicate() if stderr: - print("Image Extract Error:\n" + stderr.decode()) + logger.error("Image Extract Error:\n" + stderr.decode()) await ctx.send(inline('Image extract failed')) return await ctx.send(inline('Image extract finished')) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5cf35d4c..d7cc3d74 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -9,19 +9,17 @@ import discord import prettytable -from redbot.core import checks, data_manager -from redbot.core import commands -from redbot.core.bot import Red +from redbot.core import checks, data_manager, commands from redbot.core.utils.chat_formatting import inline, box import tsutils from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics -logger = logging.getLogger('red.tsubaki.padinfo') +logger = logging.getLogger('red.padbot-cogs.padinfo') HELP_MSG = """ {0.prefix}helpid : shows this message -{0.prefix}id : look up a monster and print a link to puzzledragonx +{0.prefix}id : look up a monster and show a link to puzzledragonx {0.prefix}pic : Look up a monster and display its image inline Options for @@ -158,7 +156,7 @@ def on_update(self, ctx, selected_emoji): class PadInfo(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -223,7 +221,6 @@ async def reload_nicknames(self): except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) - traceback.print_exc() await asyncio.sleep(wait_time) @@ -231,7 +228,7 @@ async def refresh_index(self): """Refresh the monster indexes.""" dg_cog = self.bot.get_cog('Dadguide') if not dg_cog: - print("Cog 'Dadguide' not loaded") + logger.warning("Cog 'Dadguide' not loaded") return logger.info('Waiting until DG is ready') await dg_cog.wait_until_ready() @@ -254,7 +251,7 @@ def get_monster_by_id(self, monster_id: int): @commands.command() async def jpname(self, ctx, *, query: str): - """Print the Japanese name of a monster""" + """Show the Japanese name of a monster""" m, err, debug_info = await self.findMonster(query) if m is not None: await ctx.send(monsterToHeader(m)) @@ -263,6 +260,7 @@ async def jpname(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command(name="id", aliases="iD Id ID".split()) + @checks.bot_has_permissions(embed_links=True) async def _id(self, ctx, *, query: str): """Monster info (main tab)""" prefix = ctx.prefix + "id" @@ -270,11 +268,13 @@ async def _id(self, ctx, *, query: str): await self._do_id(ctx, query) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def idna(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" await self._do_id(ctx, query, server_filter=ServerFilter.na) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def idjp(self, ctx, *, query: str): """Monster info (limited to JP monsters ONLY)""" await self._do_id(ctx, query, server_filter=ServerFilter.jp) @@ -287,16 +287,19 @@ async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): await ctx.send(self.makeFailureMsg(err)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def id2(self, ctx, *, query: str): """Monster info (main tab)""" await self._do_id2(ctx, query) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def id2na(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" await self._do_id2(ctx, query, server_filter=ServerFilter.na) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def id2jp(self, ctx, *, query: str): """Monster info (limited to JP monsters ONLY)""" await self._do_id2(ctx, query, server_filter=ServerFilter.jp) @@ -309,6 +312,7 @@ async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): await ctx.send(self.makeFailureMsg(err)) @commands.command(name="evos") + @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): """Monster info (evolutions tab)""" m, err, debug_info = await self.findMonster(query) @@ -318,6 +322,7 @@ async def evos(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command(name="mats", aliases=['evomats', 'evomat']) + @checks.bot_has_permissions(embed_links=True) async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" m, err, debug_info = await self.findMonster(query) @@ -327,6 +332,7 @@ async def evomats(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" m, err, debug_info = await self.findMonster(query) @@ -338,6 +344,7 @@ async def pantheon(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def skillups(self, ctx, *, query: str): """Monster info (evolutions tab)""" m, err, debug_info = await self.findMonster(query) @@ -391,7 +398,7 @@ def get_id_emoji_options(self, m=None, scroll=False): # it's impossible for the previous/next ones to be accessed because # IdEmojiUpdater won't allow it, however they have to be defined - # so that the buttons print in the first place + # so that the buttons display in the first place if scroll: emoji_to_embed[self.first_monster_emoji] = None emoji_to_embed[self.previous_monster_emoji] = None @@ -431,10 +438,10 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): result_embed.set_footer(text=discord.Embed.Empty) await result_msg.edit(embed=result_embed) except Exception as ex: - print('Menu failure', ex) - traceback.print_exc() + logger.error('Menu failure', exc_info=1) @commands.command(aliases=['img']) + @checks.bot_has_permissions(embed_links=True) async def pic(self, ctx, *, query: str): """Monster info (full image tab)""" m, err, debug_info = await self.findMonster(query) @@ -444,6 +451,7 @@ async def pic(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def links(self, ctx, *, query: str): """Monster links""" m, err, debug_info = await self.findMonster(query) @@ -461,6 +469,7 @@ async def links(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command(aliases=['stats']) + @checks.bot_has_permissions(embed_links=True) async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" m, err, debug_info = await self.findMonster(query) @@ -470,6 +479,7 @@ async def otherinfo(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" m, err, debug_info = await self.findMonster(query) @@ -480,6 +490,7 @@ async def lookup(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): """Monster info (for all monsters in the evo tree)""" m, err, debug_info = await self.findMonster(query) @@ -489,6 +500,7 @@ async def evolist(self, ctx, *, query): await ctx.send(self.makeFailureMsg(err)) @commands.command() + @checks.bot_has_permissions(embed_links=True) async def collabscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" DGCOG = self.bot.get_cog("Dadguide") @@ -513,6 +525,7 @@ async def collabscroll(self, ctx, *, query: str): @commands.command(aliases=['leaders', 'leaderskills', 'ls']) + @checks.bot_has_permissions(embed_links=True) async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): """Display the multiplier and leaderskills for two monsters @@ -553,11 +566,13 @@ async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, ba await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @commands.command(aliases=['helppic', 'helpimg']) + @checks.bot_has_permissions(embed_links=True) async def helpid(self, ctx): """Whispers you info on how to craft monster queries for [p]id""" await ctx.author.send(box(HELP_MSG.format(ctx))) @commands.command() + @checks.bot_has_permissions(speak=True) async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" voice = ctx.author.voice @@ -653,7 +668,6 @@ async def iddiff(self, ctx): if m1 and m2: ctx.send("Major Discrepency: {} -> {}/{}".format(query, m1.name_na, m2.name_na)) await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) - print(f) def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index a989f01c..7146466b 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,10 +1,12 @@ import asyncio +import logging from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline, box, pagify import tsutils +logger = logging.getLogger('red.padbot-cogs.padmonitor') class PadMonitor(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -30,7 +32,7 @@ async def check_seen_loop(self): try: await self.check_seen() except Exception as ex: - print("check seen loop caught exception " + str(ex)) + logger.exception("check seen loop caught exception " + str(ex)) await asyncio.sleep(60 * 5) @@ -44,7 +46,7 @@ async def check_seen(self): def process(existing, new_map, name): if not existing: - print('preloading', len(new_map)) + logger.info('preloading', len(new_map)) existing.extend(new_map.keys()) self.settings.save_settings() return None @@ -82,7 +84,7 @@ async def announce(self, channel_id, message): for page in pagify(message): await channel.send(box(page)) except Exception as ex: - print('failed to send message to', channel_id, ' : ', ex) + logger.exception('failed to send message to', channel_id, ' : ', ex) @commands.group() @checks.mod_or_permissions(manage_guild=True) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index ccd21650..4818ef86 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -1,15 +1,17 @@ import json import re +import logging from fnmatch import fnmatch from ply import lex from redbot.core import checks from redbot.core import commands -from redbot.core.bot import Red from redbot.core.utils.chat_formatting import box, pagify from tsutils import tsutils +logger = logging.getLogger('red.padbot-cogs.padsearch') + HELP_MSG = """ {0.prefix}search @@ -37,7 +39,7 @@ * attabsorb : Attribute Absorb shield null * absorbnull : Damage Abasorb shield null * combo(n) : Increase combo count by n -* shield(n) : Reduce damage taken by n% +* shield(n) uce damage taken by n% * resolve : Leader skill where you survive when HP reduced to 0 Multiple instance filters @@ -685,7 +687,7 @@ async def check_re_filters(self, ms, ctx): try: return filt_timeout(self.re_filters, ms) except TimeoutError: - print("Timeout with patttern: \"{}\" by user {} ({})".format( + logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( '", "'.join(self.regeces), ctx.author.name, ctx.author.id)) raise commands.UserFeedbackCheckFailure("Regex took too long to compile. Stop trying to break the bot") except re.error as e: @@ -695,7 +697,7 @@ async def check_glob_filters(self, ms, ctx): try: return filt_timeout(self.gl_filters, ms) except TimeoutError: - print("Timeout with patttern: \"{}\" by user {} ({})".format( + logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( '", "'.join(self.globs), ctx.author.name, ctx.author.id)) raise commands.UserFeedbackCheckFailure("Glob took too long to compile. Stop trying to break the bot") @@ -719,7 +721,7 @@ def setIfType(self, expected_type, given_type, current_value, new_value): class PadSearch(commands.Cog): """PAD data searching.""" - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot diff --git a/padvision/padvision.py b/padvision/padvision.py index daae3788..56152801 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -130,8 +130,7 @@ def process(self): try: self._process() except Exception as ex: - print("orb extractor failed " + str(ex)) - traceback.print_exc() + logger.error("orb extractor failed ", exc_info=1) def _process(self): import numpy as np diff --git a/profile/profile.py b/profile/profile.py index cca3050a..ff0db483 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -1,12 +1,13 @@ import re +import logging import discord from redbot.core import commands -from redbot.core.bot import Red from redbot.core.utils.chat_formatting import inline, box, pagify from tsutils import CogSettings +logger = logging.getLogger('red.padbot-cogs.profile') def normalizeServer(server): server = server.upper().strip() @@ -39,7 +40,7 @@ def computeOldGroup(str_id): class Profile(commands.Cog): - def __init__(self, bot: Red, *args, **kwargs): + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.settings = ProfileSettings("profile") @@ -265,8 +266,7 @@ async def pageOutput(self, ctx, msg): try: await ctx.author.send(box(page)) except Exception as e: - print("page output failed " + str(e)) - print("tried to print: " + page) + logger.exception("page output failed " + str(e)) class ProfileSettings(CogSettings): diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index 47c8008d..a7c9688f 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -1,14 +1,17 @@ import difflib import json +import logging from collections import OrderedDict import aiohttp import discord -from redbot.core import commands +from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline from tsutils import Menu, EmojiUpdater, char_to_emoji +logger = logging.getLogger('red.padbot-cogs.schoolidol') + FIRST_REQ = 'https://schoolido.lu/api/cards/?page_size=100' @@ -41,13 +44,12 @@ async def reload_sif(self): next_req = FIRST_REQ async with aiohttp.ClientSession() as session: while next_req: - print(next_req) async with session.get(next_req) as resp: raw_resp = await resp.text() js_resp = json.loads(raw_resp) next_req = js_resp['next'] self.card_data.extend(js_resp['results']) - print('done retrieving cards: {}'.format(len(self.card_data))) + logger.info('done retrieving cards: {}'.format(len(self.card_data))) self.id_to_card = {c['id']: c for c in self.card_data} name_to_card = {'{}'.format(c['idol']['name']).lower(): c for c in self.card_data} @@ -64,6 +66,7 @@ async def reload_sif(self): } @commands.command() + @checks.bot_has_permissions(embed_links=True) async def sifid(self, ctx, *, query: str): """SIF query.""" query = query.lower().strip() @@ -114,7 +117,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): result_embed.set_footer(text=discord.Embed.Empty) await self.bot.edit_message(result_msg, embed=result_embed) except Exception as ex: - print('Menu failure', ex) + logger.exception('Menu failure') def make_card_embed(c, url_field): From 9488adcc6ca394f0462c4cf921926b724a51cd33 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 16:36:25 -0700 Subject: [PATCH 0114/1220] don't set ready early --- dadguide/dadguide.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 5688e5c2..b96ec5aa 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -124,7 +124,6 @@ async def reload_data_task(self): # We already had a copy of the database at startup, signal that we're ready now. if self.database.has_database(): logger.info('Using stored database at load') - self._is_ready.set() while self == self.bot.get_cog('Dadguide'): short_wait = False From 6dc8ed6d0ab3c94f641333a08ee61972eb74f9ac Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 16:58:24 -0700 Subject: [PATCH 0115/1220] update dependancies --- padboard/padboard.py | 6 ++++-- requirements.txt | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/padboard/padboard.py b/padboard/padboard.py index a360a0fb..c6c214c4 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -9,7 +9,6 @@ from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline -from padvision import padvision from tsutils import tsutils DATA_DIR = os.path.join('data', 'padboard') @@ -125,5 +124,8 @@ async def nc_classify(self, image_data): model_path = await self.config.tflite_path() if not model_path: return None - img_extractor = padvision.NeuralClassifierBoardExtractor(model_path, img_np, image_data) + PDV_COG = self.bot.get_cog("PadVision") + raise IOError("PadVision is not loaded") + PDV_MODULE = __import__(PDV_COG.__module__) + img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, img_np, image_data) return img_extractor.get_board() diff --git a/requirements.txt b/requirements.txt index 84ca97ef..4d3df173 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,18 +2,16 @@ setuptools wheel Red-DiscordBot[postgres] -backoff prettytable -git+https://github.com/tejstead/python-romkan +romkan google-api google-api-python-client google-cloud google-cloud-texttospeech pymysql -python-dateutil ply -aioodbc # might require: sudo apt-get install unixodbc-dev python3.8-dev opencv-python Pillow pytz aiohttp +oauth2client From d2a71dc7564c806f07f357a4dffdde894f382083 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 17:02:55 -0700 Subject: [PATCH 0116/1220] accept error --- padboard/padboard.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/padboard/padboard.py b/padboard/padboard.py index c6c214c4..aea3dd11 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -81,7 +81,11 @@ async def dawnglare(self, ctx, user: discord.Member = None): if not image_data: return - img_board_nc = await self.nc_classify(image_data) + try: + img_board_nc = await self.nc_classify(image_data) + except IOError: + await ctx.send("PadVision not loaded.") + return if not img_board_nc: await ctx.send(inline("TFLite path not set.")) @@ -125,7 +129,8 @@ async def nc_classify(self, image_data): if not model_path: return None PDV_COG = self.bot.get_cog("PadVision") - raise IOError("PadVision is not loaded") + if not PDV_COG: + raise IOError("PadVision is not loaded") PDV_MODULE = __import__(PDV_COG.__module__) img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, img_np, image_data) return img_extractor.get_board() From 17c2ccd9036c2ed7f54756f03a56c02d00ca29c2 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 22:31:25 -0700 Subject: [PATCH 0117/1220] diffcheck --- padinfo/padinfo.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d7cc3d74..109683bb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -2,6 +2,7 @@ import json import logging import os +import io import traceback import urllib.parse from collections import OrderedDict @@ -11,6 +12,7 @@ import prettytable from redbot.core import checks, data_manager, commands from redbot.core.utils.chat_formatting import inline, box +from redbot.core.utils import AsyncIter import tsutils from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics @@ -650,12 +652,14 @@ async def setvoicepath(self, ctx, *, path=''): async def iddiff(self, ctx): """Runs the diff checker for id and id2""" await ctx.send("Running diff checker...") - hist_aggreg = list(self.historic_lookups) + list(self.historic_lookups_id2) + hist_aggreg = list(self.historic_lookups) s = 0 f = [] - for query in hist_aggreg: + async for c, query in AsyncIter(enumerate(hist_aggreg)): m1, err1, debug_info1 = await self.findMonster(query) m2, err2, debug_info2 = await self.findMonster2(query) + if c % 50 == 0: + await ctx.send(inline("{}/{} complete.".format(c, len(hist_aggreg)))) if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): s += 1 continue @@ -666,8 +670,11 @@ async def iddiff(self, ctx): [debug_info1, debug_info2] )) if m1 and m2: - ctx.send("Major Discrepency: {} -> {}/{}".format(query, m1.name_na, m2.name_na)) + await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_na, m2.name_na)) await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) + file = discord.File(io.BytesIO(json.dumps(f).encode()), filename="diff.json") + await ctx.send(file=file) + def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] From c06ab2aa1eb624dd1d457303ebbdb8c3e560a27a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 23:57:05 -0700 Subject: [PATCH 0118/1220] the worst part is how calm it is like it doesn't even tell me my code is awful it's like "hey i made 1500 edits for you try better next time please :)" --- azurelane/__init__.py | 1 + azurelane/azurelane.py | 21 +-- azurelane/info.json | 38 ++++-- chronomagia/__init__.py | 1 + dadguide/__init__.py | 1 + dadguide/dadguide.py | 256 ++++++++++++++++++++++++------------- dadguide/info.json | 38 ++++-- damagecalc/__init__.py | 1 + damagecalc/info.json | 36 ++++-- info.json | 8 +- padboard/info.json | 38 ++++-- padboard/padboard.py | 25 ++-- padbuildimg/__init__.py | 1 + padbuildimg/info.json | 46 ++++--- padbuildimg/padbuildimg.py | 234 ++++++++++++++++++++++----------- padbuilds/info.json | 36 ++++-- padevents/__init__.py | 1 + padevents/info.json | 40 ++++-- padevents/padevents.py | 130 ++++++++++++------- padglobal/padglobal.py | 210 ++++++++++++++++++++---------- padguidedb/__init__.py | 1 + padguidedb/info.json | 37 ++++-- padguidedb/padguidedb.py | 68 ++++++---- padinfo/__init__.py | 1 + padinfo/info.json | 43 ++++--- padinfo/padinfo.py | 184 ++++++++++++++++---------- padmonitor/__init__.py | 1 + padsearch/__init__.py | 1 + padsearch/info.json | 41 +++--- padvision/__init__.py | 1 + padvision/info.json | 37 ++++-- padvision/padvision.py | 13 +- pricecheck/__init__.py | 1 + pricecheck/pricecheck.py | 34 +++-- profile/__init__.py | 1 + profile/profile.py | 49 ++++--- schoolidol/__init__.py | 1 + schoolidol/info.json | 37 ++++-- 38 files changed, 1118 insertions(+), 595 deletions(-) diff --git a/azurelane/__init__.py b/azurelane/__init__.py index 00b7bfe4..7b3c1723 100644 --- a/azurelane/__init__.py +++ b/azurelane/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = AzureLane(bot) bot.add_cog(n) diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 7ae3e08b..030e987f 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -1,17 +1,15 @@ +import aiohttp import difflib +import discord import json import logging from collections import OrderedDict - -import aiohttp -import discord -from tsutils import Menu, EmojiUpdater, char_to_emoji from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline +from tsutils import Menu, EmojiUpdater, char_to_emoji logger = logging.getLogger('red.padbot-cogs.azurelane') - BASE_URL = 'https://storage.googleapis.com/mirubot/alimages/raw' DATA_URL = '{}/azure_lane.json'.format(BASE_URL) @@ -47,7 +45,8 @@ async def reload_al(self): logger.info('done retrieving cards: {}'.format(len(self.card_data))) self.id_to_card = {c['id']: c for c in self.card_data} - name_to_card = {'{}'.format(c['name_en']).lower(): c for c in self.card_data} + name_to_card = {'{}'.format(c['name_en']).lower(): c for c in + self.card_data} # collection_name_to_card = {'{} {}'.format( # i['title'], c['name_en']).lower(): c for i in c['images'] for c in self.card_data} self.names_to_card = { @@ -90,7 +89,9 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): try: result_msg, result_embed = await self.menu.custom_menu(ctx, - EmojiUpdater(emoji_to_embed), starting_menu_emoji, + EmojiUpdater( + emoji_to_embed), + starting_menu_emoji, timeout=20) if result_msg and result_embed: # Message is finished but not deleted, clear the footer @@ -111,11 +112,13 @@ def make_card_embed(c, image_idx): file_name = url[url.rfind("/") + 1:] header = 'No. {} {} - {}'.format(cid, name, image_title) - image_url = 'https://storage.googleapis.com/mirubot/alimages/raw/{}'.format(file_name) + image_url = 'https://storage.googleapis.com/mirubot/alimages/raw/{}'.format( + file_name) embed = discord.Embed() embed.title = header embed.url = info_url embed.set_image(url=image_url) - embed.set_footer(text='Requester may click the reactions below to switch tabs') + embed.set_footer( + text='Requester may click the reactions below to switch tabs') return embed diff --git a/azurelane/info.json b/azurelane/info.json index 8e34b3ce..66626808 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -1,15 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Displays data about Azur Lane.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows Azur Lane data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "aiohttp"], - "tags": ["azurlane", "azurelane"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Displays data about Azur Lane.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Azur Lane data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "aiohttp" + ], + "tags": [ + "azurlane", + "azurelane" + ] } diff --git a/chronomagia/__init__.py b/chronomagia/__init__.py index b96f5f30..230cbca9 100644 --- a/chronomagia/__init__.py +++ b/chronomagia/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = ChronoMagia(bot) bot.add_cog(n) diff --git a/dadguide/__init__.py b/dadguide/__init__.py index 5bff88a7..1159ca75 100644 --- a/dadguide/__init__.py +++ b/dadguide/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = Dadguide(bot) bot.add_cog(n) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b96ec5aa..bea40671 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -9,31 +9,31 @@ import asyncio import csv import difflib +import discord import json import logging import os +import pytz import re +import romkan import shutil import sqlite3 as lite import traceback +import tsutils from collections import defaultdict, deque, OrderedDict from datetime import datetime from enum import Enum - -import pytz -import romkan -import discord -import tsutils from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.utils.chat_formatting import inline from redbot.core.utils import AsyncIter +from redbot.core.utils.chat_formatting import inline logger = logging.getLogger('red.padbot-cogs.dadguide') def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), + file_name) CSV_FILE_PATTERN = '{}.csv' @@ -133,7 +133,8 @@ async def reload_data_task(self): self._is_ready.set() except Exception as ex: short_wait = True - logger.exception("dadguide data download/refresh failed: %s", ex) + logger.exception("dadguide data download/refresh failed: %s", + ex) try: wait_time = 60 if short_wait else 60 * 60 * 4 @@ -165,7 +166,8 @@ async def download_and_refresh_nicknames(self): panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) self.nickname_overrides = {x[0].lower(): int(x[1]) - for x in nickname_overrides if x[1].isdigit()} + for x in nickname_overrides if + x[1].isdigit()} self.basename_overrides = defaultdict(set) for x in basename_overrides: @@ -173,14 +175,17 @@ async def download_and_refresh_nicknames(self): if k.isdigit(): self.basename_overrides[int(k)].add(v.lower()) - self.panthname_overrides = {x[0].lower(): x[1].lower() for x in panthname_overrides} - self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) + self.panthname_overrides = {x[0].lower(): x[1].lower() for x in + panthname_overrides} + self.panthname_overrides.update( + {v: v for _, v in self.panthname_overrides.items()}) logger.info('Loading dg database') self.database = load_database(self.database) logger.info('Building dg monster index') self.index = await MonsterIndex(self.database, self.nickname_overrides, - self.basename_overrides, self.panthname_overrides) + self.basename_overrides, + self.panthname_overrides) logger.info('Writing dg monster computed names') self.write_monster_computed_names() @@ -226,14 +231,16 @@ def _csv_to_tuples(self, file_path: str, cols: int = 2): async def _download_files(self): one_hour_secs = 1 * 60 * 60 - await tsutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, one_hour_secs) + await tsutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, + one_hour_secs) async def _download_override_files(self): one_hour_secs = 1 * 60 * 60 await tsutils.makeAsyncCachedPlainRequest( NICKNAME_FILE_PATTERN, NICKNAME_OVERRIDES_SHEET, one_hour_secs) await tsutils.makeAsyncCachedPlainRequest( - BASENAME_FILE_PATTERN, GROUP_BASENAMES_OVERRIDES_SHEET, one_hour_secs) + BASENAME_FILE_PATTERN, GROUP_BASENAMES_OVERRIDES_SHEET, + one_hour_secs) await tsutils.makeAsyncCachedPlainRequest( PANTHNAME_FILE_PATTERN, PANTHNAME_OVERRIDES_SHEET, one_hour_secs) @@ -250,8 +257,6 @@ async def setdatafile(self, ctx, *, data_file): await ctx.tick() - - class DadguideSettings(tsutils.CogSettings): def make_default_settings(self): config = { @@ -345,7 +350,8 @@ def close(self): self._con = None @staticmethod - def _select_builder(tables, key=None, where=None, order=None, distinct=False): + def _select_builder(tables, key=None, where=None, order=None, + distinct=False): if distinct: SELECT_FROM = 'SELECT DISTINCT {fields} FROM {first_table}' else: @@ -365,11 +371,14 @@ def _select_builder(tables, key=None, where=None, order=None, distinct=False): break else: other_tables.append(table) - query = [SELECT_FROM.format(first_table=first_table, fields=', '.join(fields_lst))] + query = [SELECT_FROM.format(first_table=first_table, + fields=', '.join(fields_lst))] prev_table = first_table if key: for k, other in zip(key, other_tables): - query.append(JOIN.format(first_table=prev_table, other_table=other, key=k)) + query.append( + JOIN.format(first_table=prev_table, other_table=other, + key=k)) prev_table = other if where: query.append(WHERE.format(condition=where)) @@ -399,7 +408,8 @@ def _as_generator(self, cursor, d_type): yield d_type(res) res = cursor.fetchone() - def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): + def _query_many(self, query, param, d_type, idx_key=None, + as_generator=False): cursor = self._con.cursor() cursor.execute(query, param) if cursor.rowcount == 0: @@ -410,11 +420,14 @@ def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): if idx_key is None: return [d_type(res, self) for res in cursor.fetchall()] else: - return DictWithAttrAccess({res[idx_key]: d_type(res, self) for res in cursor.fetchall()}) + return DictWithAttrAccess( + {res[idx_key]: d_type(res, self) for res in + cursor.fetchall()}) def _max_id(self): cursor = self._con.cursor() - cursor.execute("SELECT MAX(monster_id) FROM monsters WHERE monster_id < 10000") + cursor.execute( + "SELECT MAX(monster_id) FROM monsters WHERE monster_id < 10000") return cursor.fetchone()['MAX(monster_id)'] def _select_one_entry_by_pk(self, pk, d_type): @@ -427,7 +440,8 @@ def _select_one_entry_by_pk(self, pk, d_type): def _get_table_fields(self, table_name: str): # SQL inject vulnerable :v - table_info = self._query_many('PRAGMA table_info(' + table_name + ')', (), dict) + table_info = self._query_many('PRAGMA table_info(' + table_name + ')', + (), dict) pk = None fields = [] for c in table_info: @@ -450,7 +464,8 @@ def get_awoken_skill(self, awoken_skill_id): def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' return [r.awoken_skill_id for r in - self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] + self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, + as_generator=True)] def get_monsters_by_awakenings(self, awoken_skill_id: int): return self._query_many( @@ -471,7 +486,8 @@ def get_awakenings_by_monster(self, monster_id, is_super=None): where = '{0}.monster_id=?'.format(DgAwakening.TABLE) param = (monster_id,) else: - where = '{0}.monster_id=? AND {0}.is_super=?'.format(DgAwakening.TABLE) + where = '{0}.monster_id=? AND {0}.is_super=?'.format( + DgAwakening.TABLE) param = (monster_id, is_super) return self._query_many( self._select_builder( @@ -527,7 +543,8 @@ def monster_in_mp_shop(self, monster_id): return self._query_one( self._select_builder( tables={DgMonster.TABLE: ('buy_mp',)}, - where='{0}.monster_id=? AND buy_mp IS NOT NULL'.format(DgMonster.TABLE) + where='{0}.monster_id=? AND buy_mp IS NOT NULL'.format( + DgMonster.TABLE) ), (monster_id,), DgDrop) is not None @@ -572,7 +589,9 @@ def get_evolution_by_material(self, monster_id): return self._query_many( self._select_builder( tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where=' OR '.join(['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in range(1, 6)]) + where=' OR '.join( + ['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in + range(1, 6)]) ), (monster_id,) * 5, DgEvolution) @@ -604,7 +623,8 @@ def get_evolution_tree_ids(self, base_monster_id): def monster_id_to_no(self, monster_id, region=Server.JP): res = self._query_one( self._select_builder( - tables={DgMonster.TABLE: ('monster_no_{}'.format(region.name.lower()),)}, + tables={DgMonster.TABLE: ( + 'monster_no_{}'.format(region.name.lower()),)}, where='{}.monster_id=?'.format(DgMonster.TABLE) ), (monster_id,), @@ -625,10 +645,12 @@ def _get_monsters_where(self, where, param): DgMonster) def get_monsters_by_series(self, series_id: int): - return self._get_monsters_where('{}.series_id=?'.format(DgMonster.TABLE), (series_id,)) + return self._get_monsters_where( + '{}.series_id=?'.format(DgMonster.TABLE), (series_id,)) def get_monsters_by_active(self, active_skill_id: int): - return self._get_monsters_where('{}.active_skill_id=?'.format(DgMonster.TABLE), (active_skill_id,)) + return self._get_monsters_where( + '{}.active_skill_id=?'.format(DgMonster.TABLE), (active_skill_id,)) def get_monster_evo_gem(self, name: str, region='jp'): gem_suffix = { @@ -644,37 +666,43 @@ def get_monster_evo_gem(self, name: str, region='jp'): return self._query_one( self._select_builder( tables={DgMonster.TABLE: DgMonster.FIELDS}, - where='{0}.name_{1}=? AND {0}.leader_skill_id=10628'.format(DgMonster.TABLE, region) + where='{0}.name_{1}=? AND {0}.leader_skill_id=10628'.format( + DgMonster.TABLE, region) ), (non_gem_name,), DgMonster) def get_na_only_monsters(self): return self._get_monsters_where( - '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format(DgMonster.TABLE), + '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format( + DgMonster.TABLE), ()) def get_monster(self, monster_id: int): return self._select_one_entry_by_pk(monster_id, DgMonster) def get_all_monster_jp_name(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_jp',)}), (), DictWithAttrAccess, - as_generator=as_generator) + return self._query_many( + self._select_builder(tables={DgMonster.TABLE: ('name_jp',)}), (), + DictWithAttrAccess, + as_generator=as_generator) def get_all_monsters(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, - as_generator=as_generator) - + return self._query_many( + self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), + (), DgMonster, + as_generator=as_generator) def get_all_events(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), DgScheduledEvent, + return self._query_many(self._select_builder( + tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), + DgScheduledEvent, as_generator=as_generator) def get_dungeon_by_id(self, dungeon_id: int): return self._select_one_entry_by_pk(dungeon_id, DgDungeon) - def enum_or_none(enum, value, default=None): if value is not None: return enum(value) @@ -819,7 +847,8 @@ def __init__(self, item, database): @property def open_datetime(self): - return datetime.utcfromtimestamp(self.start_timestamp).replace(tzinfo=pytz.UTC) + return datetime.utcfromtimestamp(self.start_timestamp).replace( + tzinfo=pytz.UTC) @open_datetime.setter def open_datetime(self, value): @@ -827,14 +856,14 @@ def open_datetime(self, value): @property def close_datetime(self): - return datetime.utcfromtimestamp(self.end_timestamp).replace(tzinfo=pytz.UTC) + return datetime.utcfromtimestamp(self.end_timestamp).replace( + tzinfo=pytz.UTC) @close_datetime.setter def close_datetime(self, value): self.end_timestamp = int(value.timestamp()) - class DgMonster(DadguideItem): TABLE = 'monsters' PK = 'monster_id' @@ -863,12 +892,15 @@ def __init__(self, item, database): self.in_pem = bool(self.pal_egg) self.in_rem = bool(self.rem_egg) - self.awakenings = self._database.get_awakenings_by_monster(self.monster_id) - self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) + self.awakenings = self._database.get_awakenings_by_monster( + self.monster_id) + self.superawakening_count = sum( + int(a.is_super) for a in self.awakenings) self.is_inheritable = bool(self.inheritable) - self.evo_from = self._database.get_prev_evolution_by_monster(self.monster_id) + self.evo_from = self._database.get_prev_evolution_by_monster( + self.monster_id) self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) @@ -878,7 +910,8 @@ def __init__(self, item, database): base_id = next_base.from_id next_base = self._database.get_prev_evolution_by_monster(base_id) self._base_monster_id = base_id - self._alt_evo_id_list = self._database.get_evolution_tree_ids(self._base_monster_id) + self._alt_evo_id_list = self._database.get_evolution_tree_ids( + self._base_monster_id) self.search = MonsterSearchHelper(self) @@ -890,7 +923,9 @@ def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): s_min = float(self[key + '_min']) s_max = float(self[key + '_max']) if self.level > 1: - s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** self[key + '_scale'] + s_val = s_min + (s_max - s_min) * ( + (min(lv, self.level) - 1) / (self.level - 1)) ** self[ + key + '_scale'] else: s_val = s_min if lv > 99: @@ -950,7 +985,8 @@ def true_evo_type(self): def mats_for_evo(self): if self.evo_from is None: return [] - return [self._database.get_monster(self.evo_from['mat_{}_id'.format(i)]) for i in range(1, 6) if + return [self._database.get_monster(self.evo_from['mat_{}_id'.format(i)]) + for i in range(1, 6) if self.evo_from['mat_{}_id'.format(i)] is not None] @property @@ -967,7 +1003,8 @@ def _evolutions_to(self): @property def evo_to(self): - return [self._database.get_monster(x.to_id) for x in self._evolutions_to()] + return [self._database.get_monster(x.to_id) for x in + self._evolutions_to()] @property def base_monster(self): @@ -1091,7 +1128,8 @@ def __init__(self, m: DgMonster): self.types = [t.name for t in m.types] def replace_colors(text: str): - return text.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') + return text.replace('red', 'fire').replace('blue', 'water').replace( + 'green', 'wood') self.leader = replace_colors(self.leader) self.active = replace_colors(self.active) @@ -1162,7 +1200,9 @@ def strip_next_clause(txt: str, sep: str): txt = active_desc parts = re.split(r'\Wand\W|;\W', txt) for i in range(0, len(parts)): - parts[i] = strip_prev_clause(parts[i], change_txt) if change_txt in parts[i] else '' + parts[i] = strip_prev_clause(parts[i], + change_txt) if change_txt in parts[ + i] else '' for part in parts: sub_parts = part.split(' to ') @@ -1184,9 +1224,9 @@ def make_roma_subname(name_jp): return adjusted_subname.strip() - class MonsterIndex(tsutils.aobject): - async def __init__(self, monster_database, nickname_overrides, basename_overrides, + async def __init__(self, monster_database, nickname_overrides, + basename_overrides, panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory base_monster_ids = monster_database.get_base_monster_ids() @@ -1228,13 +1268,15 @@ async def __init__(self, monster_database, nickname_overrides, basename_override group_basename_overrides = basename_overrides.get(base_id, []) evolution_tree = [monster_database.get_monster(m) for m in monster_database.get_evolution_tree_ids(base_id)] - named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) + named_mg = NamedMonsterGroup(evolution_tree, + group_basename_overrides) for monster in evolution_tree: if accept_filter and not accept_filter(monster): continue prefixes = self.compute_prefixes(monster, evolution_tree) extra_nicknames = monster_id_to_nicknames[monster.monster_id] - named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames) + named_monster = NamedMonster(monster, named_mg, prefixes, + extra_nicknames) named_monsters.append(named_monster) # Sort the NamedMonsters into the opposite order we want to accept their nicknames in @@ -1276,9 +1318,12 @@ def named_monsters_sort(nm: NamedMonster): self.pantheons[pantheon.lower()].add(nm) self.all_monsters = named_monsters - self.all_na_name_to_monsters = {m.name_na.lower(): m for m in named_monsters} - self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} - self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} + self.all_na_name_to_monsters = {m.name_na.lower(): m for m in + named_monsters} + self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in + named_monsters} + self.monster_id_to_named_monster = {m.monster_id: m for m in + named_monsters} for nickname, monster_id in nickname_overrides.items(): nm = self.monster_id_to_named_monster.get(monster_id) @@ -1315,8 +1360,10 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('chibi') awoken = lower_name.startswith('awoken') or '覚醒' in lower_name - revo = lower_name.startswith('reincarnated') or '転生' in lower_name - srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name + revo = lower_name.startswith( + 'reincarnated') or '転生' in lower_name or m.true_evo_type == InternalEvoType.Reincarnated + srevo = lower_name.startswith( + 'super reincarnated') or '超転生' in lower_name mega = lower_name.startswith('mega awoken') or '極醒' in lower_name awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega @@ -1362,7 +1409,8 @@ def is_pixel(n): for gm in evotree: if is_pixel(gm): - prefixes.update(['pixel'] if is_pixel(m) else ['np', 'nonpixel']) + prefixes.update( + ['pixel'] if is_pixel(m) else ['np', 'nonpixel']) break if m.is_equip: @@ -1404,16 +1452,21 @@ def find_monster(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): if query.endswith("base {}".format(m.monster_id)): - matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) + matches.add(discord.utils.find( + lambda mo: m.base_monster_no == mo.monster_id, + self.all_entries.values())) if len(matches): - return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() + return self.pickBestMonster( + matches), None, "Base ID match, max of 1".format() # prefix search for nicknames, space-preceeded, take max id for nickname, m in self.all_entries.items(): if nickname.startswith(query + ' '): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) + return self.pickBestMonster( + matches), None, "Space nickname prefix, max of {}".format( + len(matches)) # prefix search for nicknames, take max id for nickname, m in self.all_entries.items(): @@ -1421,19 +1474,24 @@ def find_monster(self, query): matches.add(m) if len(matches): all_names = ",".join(map(lambda x: x.name_na, matches)) - return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( + return self.pickBestMonster( + matches), None, "Nickname prefix, max of {}, matches=({})".format( len(matches), all_names) # prefix search for full name, take max id for nickname, m in self.all_entries.items(): - if (m.name_na.lower().startswith(query) or m.name_jp.lower().startswith(query)): + if (m.name_na.lower().startswith( + query) or m.name_jp.lower().startswith(query)): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) + return self.pickBestMonster( + matches), None, "Full name, max of {}".format(len(matches)) # for nicknames with 2 names, prefix search 2nd word, take max id if query in self.two_word_entries: - return self.two_word_entries[query], None, "Second-word nickname prefix, max of {}".format(len(matches)) + return self.two_word_entries[ + query], None, "Second-word nickname prefix, max of {}".format( + len(matches)) # TODO: refactor 2nd search characteristcs for 2nd word @@ -1442,21 +1500,25 @@ def find_monster(self, query): if (query in m.name_na.lower() or query in m.name_jp.lower()): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( + return self.pickBestMonster( + matches), None, 'Nickname contains nickname match ({})'.format( len(matches)) # No decent matches. Try near hits on nickname instead - matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) + matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, + cutoff=.8) if len(matches): match = matches[0] - return self.all_entries[match], None, 'Close nickname match ({})'.format(match) + return self.all_entries[ + match], None, 'Close nickname match ({})'.format(match) # Still no decent matches. Try near hits on full name instead matches = difflib.get_close_matches( query, self.all_na_name_to_monsters.keys(), n=1, cutoff=.9) if len(matches): match = matches[0] - return self.all_na_name_to_monsters[match], None, 'Close name match ({})'.format(match) + return self.all_na_name_to_monsters[ + match], None, 'Close name match ({})'.format(match) # About to give up, try matching all words matches = set() @@ -1465,7 +1527,8 @@ def find_monster(self, query): all(map(lambda x: x in m.name_jp.lower(), query.split()))): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( + return self.pickBestMonster( + matches), None, 'All word match on full name, max of {}'.format( len(matches)) # couldn't find anything @@ -1519,7 +1582,9 @@ def find_monster2(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): if query.endswith("base {}".format(m.monster_id)): - matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) + matches.add(discord.utils.find( + lambda mo: m.base_monster_no == mo.monster_id, + self.all_entries.values())) matches.remove_potential_matches_without_all_prefixes(query_prefixes) # first try to get matches from nicknames @@ -1534,30 +1599,36 @@ def find_monster2(self, query): for nickname, m in self.all_na_name_to_monsters.items(): if new_query in m.name_na.lower() or new_query in m.name_jp.lower(): matches.add(m) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.remove_potential_matches_without_all_prefixes( + query_prefixes) # check for exact match on pantheon name but only if needed if not matches.length(): for pantheon in self.all_pantheon_nicknames: if new_query == pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, + matches.get_monsters_from_potential_pantheon_match(pantheon, + self.pantheon_nick_to_name, self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.remove_potential_matches_without_all_prefixes( + query_prefixes) # check for any match on pantheon name, again but only if needed if not matches.length(): for pantheon in self.all_pantheon_nicknames: if new_query in pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, + matches.get_monsters_from_potential_pantheon_match(pantheon, + self.pantheon_nick_to_name, self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.remove_potential_matches_without_all_prefixes( + query_prefixes) if matches.length(): return matches.pick_best_monster(), None, None return None, "Could not find a match for: " + query, None def pickBestMonster(self, named_monster_list): - return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) + return max(named_monster_list, key=lambda x: ( + not x.is_low_priority, x.rarity, x.monster_no_na)) class PotentialMatches(object): @@ -1582,12 +1653,15 @@ def remove_potential_matches_without_all_prefixes(self, query_prefixes): break self.match_list.difference_update(to_remove) - def get_monsters_from_potential_pantheon_match(self, pantheon, pantheon_nick_to_name, pantheons): + def get_monsters_from_potential_pantheon_match(self, pantheon, + pantheon_nick_to_name, + pantheons): full_name = pantheon_nick_to_name[pantheon] self.update(pantheons[full_name]) def pick_best_monster(self): - return max(self.match_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) + return max(self.match_list, key=lambda x: ( + not x.is_low_priority, x.rarity, x.monster_no_na)) class NamedMonsterGroup(object): @@ -1602,13 +1676,15 @@ def __init__(self, evolution_tree: list, basename_overrides: list): self.base_monster_no_na = base_monster.monster_no_na self.monster_no_to_basename = { - m.monster_id: self._compute_monster_basename(m) for m in evolution_tree + m.monster_id: self._compute_monster_basename(m) for m in + evolution_tree } self.computed_basename = self._compute_group_basename(evolution_tree) self.computed_basenames = set([self.computed_basename]) if '-' in self.computed_basename: - self.computed_basenames.add(self.computed_basename.replace('-', ' ')) + self.computed_basenames.add( + self.computed_basename.replace('-', ' ')) self.basenames = basename_overrides or self.computed_basenames @@ -1652,11 +1728,13 @@ def count_and_id(): entry[0] += 1 entry[1] = max(entry[1], m.monster_id) - entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] + entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in + basename_to_info.items()] return max(entries)[2] def _is_low_priority_monster(self, m: DgMonster): - lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] + lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, + MonsterType.Vendor] lp_substrings = ['tamadra'] lp_min_rarity = 2 name = m.name_na.lower() @@ -1676,7 +1754,8 @@ def _is_low_priority_group(self, mg: list): class NamedMonster(object): - def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set): + def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, + prefixes: set, extra_nicknames: set): # Must not hold onto monster or monster_group! # Hold on to the IDs instead @@ -1705,7 +1784,8 @@ def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixe self.name_jp = monster.name_jp # These are just extra metadata - self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] + self.monster_basename = monster_group.monster_no_to_basename[ + self.monster_id] self.group_computed_basename = monster_group.computed_basename self.extra_nicknames = extra_nicknames diff --git a/dadguide/info.json b/dadguide/info.json index e40915d6..8823f056 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -1,15 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "No commands, but loads all the monster data stored in Tsubaki.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Loads monster data (LOAD THIS SECOND)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "pytz", "romkan"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "No commands, but loads all the monster data stored in Tsubaki.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Loads monster data (LOAD THIS SECOND)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "pytz", + "romkan" + ], + "tags": [ + "PAD" + ] } diff --git a/damagecalc/__init__.py b/damagecalc/__init__.py index 26416e02..177e57ab 100644 --- a/damagecalc/__init__.py +++ b/damagecalc/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = DamageCalc(bot) bot.add_cog(n) diff --git a/damagecalc/info.json b/damagecalc/info.json index e6456626..4594d9f8 100644 --- a/damagecalc/info.json +++ b/damagecalc/info.json @@ -1,15 +1,25 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Calculate Puzzle & Dragons data", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Calculates Puzzle & Dragons data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["ply"], - "tags": ["pad"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Calculate Puzzle & Dragons data", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Calculates Puzzle & Dragons data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "ply" + ], + "tags": [ + "pad" + ] } diff --git a/info.json b/info.json index 90b10de9..a11b82f8 100644 --- a/info.json +++ b/info.json @@ -1,5 +1,7 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "A collection of cogs related to the mobile game Puzzle and Dragons, along with a few unrelated games.", - "short": "The main Tsubotki repo." + "author": [ + "The Tsubaki Bot Team" + ], + "description": "A collection of cogs related to the mobile game Puzzle and Dragons, along with a few unrelated games.", + "short": "The main Tsubotki repo." } diff --git a/padboard/info.json b/padboard/info.json index a6450cea..1adc8002 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -1,15 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Turn a screenshot into a P&D simulation via DawnGlare.", - "end_user_data_statement": "No personal data is stored persistantly.", - "install_msg": "", - "short": "Dawnglare utils", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"padvision": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Turn a screenshot into a P&D simulation via DawnGlare.", + "end_user_data_statement": "No personal data is stored persistantly.", + "install_msg": "", + "short": "Dawnglare utils", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "padvision": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/padboard/padboard.py b/padboard/padboard.py index aea3dd11..9e876599 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -1,14 +1,12 @@ -import os -from collections import defaultdict -from collections import deque - import aiohttp import cv2 import discord import numpy as np +import os +from collections import defaultdict +from collections import deque from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline - from tsutils import tsutils DATA_DIR = os.path.join('data', 'padboard') @@ -16,6 +14,7 @@ DAWNGLARE_BOARD_TEMPLATE = "https://pad.dawnglare.com/?patt={}" CNINJA_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" + class PadBoard(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -54,7 +53,6 @@ async def set_tflite_path(self, ctx, *, path): await self.config.tflite_path.set(path) await ctx.tick() - def find_image(self, user_id): urls = list(self.logs[user_id]) if urls: @@ -95,11 +93,12 @@ async def dawnglare(self, ctx, user: discord.Member = None): # Convert O (used by padvision code) to X (used by Puzzled for bombs) board_text_nc = board_text_nc.replace('o', 'x') msg = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc) - msg += '\n'+CNINJA_BOARD_TEMPLATE.format(board_text_nc) + msg += '\n' + CNINJA_BOARD_TEMPLATE.format(board_text_nc) await ctx.send(msg) - async def get_recent_image(self, ctx, user: discord.Member = None, message: discord.Message = None): + async def get_recent_image(self, ctx, user: discord.Member = None, + message: discord.Message = None): user_id = user.id if user else ctx.author.id image_url = tsutils.extract_image_url(message) @@ -108,10 +107,12 @@ async def get_recent_image(self, ctx, user: discord.Member = None, message: disc if not image_url: if user: - await ctx.send(inline("Couldn't find an image in that user's recent messages.")) + await ctx.send(inline( + "Couldn't find an image in that user's recent messages.")) else: await ctx.send( - inline("Couldn't find an image in your recent messages. Upload or link to one and try again")) + inline( + "Couldn't find an image in your recent messages. Upload or link to one and try again")) return None image_data = await self.download_image(image_url) @@ -132,5 +133,7 @@ async def nc_classify(self, image_data): if not PDV_COG: raise IOError("PadVision is not loaded") PDV_MODULE = __import__(PDV_COG.__module__) - img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, img_np, image_data) + img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, + img_np, + image_data) return img_extractor.get_board() diff --git a/padbuildimg/__init__.py b/padbuildimg/__init__.py index 5a0008d3..e8e6d36f 100644 --- a/padbuildimg/__init__.py +++ b/padbuildimg/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadBuildImage(bot)) diff --git a/padbuildimg/info.json b/padbuildimg/info.json index 16f6c984..a1ef892d 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -1,18 +1,32 @@ { - "author": ["chu2.718281828459045235360287471#0110", "The Tsubaki Bot Team"], - "description": "Build a PAD team image via commands.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "PDChu cog", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "ply", "Pillow", "aiohttp"], - "tags": ["PAD"] + "author": [ + "chu2.718281828459045235360287471#0110", + "The Tsubaki Bot Team" + ], + "description": "Build a PAD team image via commands.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PDChu cog", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "ply", + "Pillow", + "aiohttp" + ], + "tags": [ + "PAD" + ] } diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index f3a6b71c..db01e466 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -1,21 +1,20 @@ -import math +import aiohttp import csv -import os +import discord import io -from shutil import rmtree +import math +import os import re import urllib - -import discord -import aiohttp -from ply import lex from PIL import Image +from PIL import ImageChops from PIL import ImageDraw from PIL import ImageFont -from PIL import ImageChops -from tsutils import CogSettings +from ply import lex from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline +from shutil import rmtree +from tsutils import CogSettings HELP_MSG = """ ^buildimg @@ -55,11 +54,15 @@ Case insensitive, order does not matter """ EXAMPLE_MSG = "Examples:\n1P{}\n2P{}\n3P{}\nLatent Validation{}\nStats Validation{}".format( - box("bj(weld)lv110/baldin[gok *3](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian)"), - box("amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf)"), - box("zela(assist amen) *3/base raizer * 2/zela; zela(assist amen) *4/base valeria/zela; zela * 6"), + box( + "bj(weld)lv110/baldin[gok *3](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian)"), + box( + "amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf)"), + box( + "zela(assist amen) *3/base raizer * 2/zela; zela(assist amen) *4/base valeria/zela; zela * 6"), box("eir[drk,drk,sdr]/eir[bak,bak,sdr]/eir[sdr *4, dek]/eir[sdr *8, dek]"), - box("dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15") + box( + "dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15") ) """ @@ -142,6 +145,8 @@ DELAY_BUFFER = 'delay_buffer' REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/padbuildimg/assets/' REMOTE_AWK_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/media/awakenings/{0:03d}.png' + + # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' class DictWithAttributeAccess(dict): @@ -183,13 +188,15 @@ async def red_delete_data_for_user(self, *, requester, user_id): def buildImgParams(self): if 'build_img_params' not in self.bot_settings: - self.bot_settings['build_img_params'] = self.make_default_build_img_params() + self.bot_settings[ + 'build_img_params'] = self.make_default_build_img_params() self.save_settings() return DictWithAttributeAccess(self.bot_settings['build_img_params']) def setBuildImgParamsByKey(self, key, value): if 'build_img_params' not in self.bot_settings: - self.bot_settings['build_img_params'] = self.make_default_build_img_params() + self.bot_settings[ + 'build_img_params'] = self.make_default_build_img_params() if key in self.bot_settings['build_img_params']: self.bot_settings['build_img_params'][key] = value self.save_settings() @@ -209,14 +216,21 @@ async def downloadAllAssets(self, awk_ids): os.mkdir(params.ASSETS_DIR + 'lat/') os.mkdir(params.ASSETS_DIR + 'awk/') for lat in LATENTS_MAP.values(): - await self.downloadAssets(REMOTE_ASSET_URL + 'lat/' + lat + '.png', params.ASSETS_DIR + 'lat/' + lat + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + 'lat/' + lat + '.png', + params.ASSETS_DIR + 'lat/' + lat + '.png') for awk in awk_ids: - await self.downloadAssets(REMOTE_AWK_URL.format(awk), params.ASSETS_DIR + 'awk/' + str(awk) + '.png') - await self.downloadAssets(REMOTE_ASSET_URL + AWK_CIRCLE + '.png', params.ASSETS_DIR + AWK_CIRCLE + '.png') - await self.downloadAssets(REMOTE_ASSET_URL + AWK_STAR + '.png', params.ASSETS_DIR + AWK_STAR + '.png') - await self.downloadAssets(REMOTE_ASSET_URL + DELAY_BUFFER + '.png', params.ASSETS_DIR + DELAY_BUFFER + '.png') + await self.downloadAssets(REMOTE_AWK_URL.format(awk), + params.ASSETS_DIR + 'awk/' + str( + awk) + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + AWK_CIRCLE + '.png', + params.ASSETS_DIR + AWK_CIRCLE + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + AWK_STAR + '.png', + params.ASSETS_DIR + AWK_STAR + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + DELAY_BUFFER + '.png', + params.ASSETS_DIR + DELAY_BUFFER + '.png') font_name = os.path.basename(params.FONT_NAME) - await self.downloadAssets(REMOTE_ASSET_URL + font_name, params.ASSETS_DIR + font_name) + await self.downloadAssets(REMOTE_ASSET_URL + font_name, + params.ASSETS_DIR + font_name) def dmOnly(self, server_id): if 'dm_only' not in self.bot_settings: @@ -234,11 +248,13 @@ def toggleDmOnly(self, server_id): self.bot_settings['dm_only'].append(server_id) self.save_settings() + def lstripalpha(s): while s and not s[0].isdigit(): - s=s[1:] + s = s[1:] return s + class PaDTeamLexer(object): tokens = [ 'ID', @@ -290,7 +306,8 @@ def t_LATENT(self, t): for i in range(count): t.value.insert(idx, latent) t.value = t.value[0:MAX_LATENTS] - t.value = [REVERSE_LATENTS_MAP[l] for l in t.value if l in REVERSE_LATENTS_MAP] + t.value = [REVERSE_LATENTS_MAP[l] for l in t.value if + l in REVERSE_LATENTS_MAP] return t def t_STATS(self, t): @@ -363,7 +380,9 @@ def t_REPEAT(self, t): t_ignore = '\t\n' def t_error(self, t): - raise commands.UserFeedbackCheckFailure("Parse Error: Unknown text '{}' at position {}".format(t.value, t.lexpos)) + raise commands.UserFeedbackCheckFailure( + "Parse Error: Unknown text '{}' at position {}".format(t.value, + t.lexpos)) def build(self, **kwargs): # pass debug=1 to enable verbose output @@ -380,7 +399,8 @@ def validate_latents(latents, card_types): return latents for idx, l in enumerate(latents): if 200 < l < 209: - if not any([l in TYPE_TO_KILLERS_MAP[t] for t in card_types if t is not None]): + if not any([l in TYPE_TO_KILLERS_MAP[t] for t in card_types if + t is not None]): latents[idx] = None latents = [l for l in latents if l is not None] return latents if len(latents) > 0 else None @@ -388,10 +408,14 @@ def validate_latents(latents, card_types): def outline_text(draw, x, y, font, text_color, text, thickness=1): shadow_color = 'black' - draw.text((x - thickness, y - thickness), text, font=font, fill=shadow_color) - draw.text((x + thickness, y - thickness), text, font=font, fill=shadow_color) - draw.text((x - thickness, y + thickness), text, font=font, fill=shadow_color) - draw.text((x + thickness, y + thickness), text, font=font, fill=shadow_color) + draw.text((x - thickness, y - thickness), text, font=font, + fill=shadow_color) + draw.text((x + thickness, y - thickness), text, font=font, + fill=shadow_color) + draw.text((x - thickness, y + thickness), text, font=font, + fill=shadow_color) + draw.text((x + thickness, y + thickness), text, font=font, + fill=shadow_color) draw.text((x, y), text, font=font, fill=text_color) @@ -425,7 +449,9 @@ def __init__(self, params, padinfo_cog, build_name='pad_build'): self.build_img = None async def process_build(self, input_str): - team_strings = [row for row in csv.reader(re.split('[;\n]', input_str), delimiter='/') if len(row) > 0] + team_strings = [row for row in + csv.reader(re.split('[;\n]', input_str), delimiter='/') + if len(row) > 0] if len(team_strings) > 3: team_strings = team_strings[0:3] for team in team_strings: @@ -492,15 +518,18 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = await self.padinfo_cog.findMonster(tok.value) + card, err, debug_info = await self.padinfo_cog.findMonster( + tok.value) if card is None: - raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) + raise commands.UserFeedbackCheckFailure( + 'Lookup Error: {}'.format(err)) if not card.is_inheritable: if is_assist: return None, None else: result_card['GOLD_STAR'] = False - result_card['MNO'] = card.monster_no_na if card.monster_no_na != card.monster_id else card.monster_no_jp + result_card[ + 'MNO'] = card.monster_no_na if card.monster_no_na != card.monster_id else card.monster_no_jp result_card['ID'] = card.monster_id elif tok.type == 'P_ALL': if tok.value >= 297: @@ -526,19 +555,25 @@ async def process_card(self, card_str, is_assist=False): 110 if card.limit_mult is not None and card.limit_mult > 1 else card.level ) if card.active_skill: - result_card['MAX_SLV'] = card.active_skill.turn_max - card.active_skill.turn_min + 1 + result_card[ + 'MAX_SLV'] = card.active_skill.turn_max - card.active_skill.turn_min + 1 else: result_card['MAX_SLV'] = 0 - result_card['MAX_AWAKE'] = len(card.awakenings) - card.superawakening_count + result_card['MAX_AWAKE'] = len( + card.awakenings) - card.superawakening_count if is_assist: - result_card['MAX_AWAKE'] = result_card['MAX_AWAKE'] if result_card['AWAKE'] > 0 else 0 + result_card['MAX_AWAKE'] = result_card['MAX_AWAKE'] if \ + result_card['AWAKE'] > 0 else 0 result_card['AWAKE'] = result_card['MAX_AWAKE'] result_card['SUPER'] = 0 else: - result_card['SUPER'] = min(result_card['SUPER'], card.superawakening_count) + result_card['SUPER'] = min(result_card['SUPER'], + card.superawakening_count) if result_card['SUPER'] > 0: - super_awakes = [x.awoken_skill_id for x in card.awakenings[-card.superawakening_count:]] - result_card['SUPER'] = super_awakes[result_card['SUPER'] - 1] + super_awakes = [x.awoken_skill_id for x in card.awakenings[ + -card.superawakening_count:]] + result_card['SUPER'] = super_awakes[ + result_card['SUPER'] - 1] result_card['LV'] = max(100, result_card['LV']) card_att = card.attr1 if is_assist: @@ -546,7 +581,8 @@ async def process_card(self, card_str, is_assist=False): else: parsed_cards = [result_card] if isinstance(assist_str, str): - assist_card, assist_att = await self.process_card(assist_str, is_assist=True) + assist_card, assist_att = await self.process_card(assist_str, + is_assist=True) if card_att is not None and assist_att is not None: assist_card['ON_COLOR'] = card_att == assist_att parsed_cards.append(assist_card) @@ -561,7 +597,8 @@ def combine_latents(self, latents): if len(latents) > MAX_LATENTS: latents = latents[0:MAX_LATENTS] latents_bar = Image.new('RGBA', - (self.params.PORTRAIT_WIDTH, self.params.LATENTS_WIDTH * 2), + (self.params.PORTRAIT_WIDTH, + self.params.LATENTS_WIDTH * 2), (255, 255, 255, 0)) x_offset = 0 y_offset = 0 @@ -586,12 +623,14 @@ def combine_latents(self, latents): sorted_latents.extend(one_slot) last_height = 0 for l in sorted_latents: - latent_icon = Image.open(self.params.ASSETS_DIR + 'lat/' + LATENTS_MAP[l] + '.png') + latent_icon = Image.open( + self.params.ASSETS_DIR + 'lat/' + LATENTS_MAP[l] + '.png') if x_offset + latent_icon.size[0] > self.params.PORTRAIT_WIDTH: row_count += 1 x_offset = 0 y_offset += last_height - if row_count >= MAX_LATENTS//4 and x_offset + latent_icon.size[0] >= self.params.LATENTS_WIDTH * (MAX_LATENTS%4): + if row_count >= MAX_LATENTS // 4 and x_offset + latent_icon.size[ + 0] >= self.params.LATENTS_WIDTH * (MAX_LATENTS % 4): break latents_bar.paste(latent_icon, (x_offset, y_offset)) last_height = latent_icon.size[1] @@ -603,9 +642,11 @@ def combine_portrait(self, card, show_stats=True, show_supers=False): if card['ID'] == DELAY_BUFFER: return Image.open(self.params.ASSETS_DIR + DELAY_BUFFER + '.png') if 'http' in self.params.PORTRAIT_DIR: - portrait = Image.open(urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))) + portrait = Image.open(urllib.request.urlopen( + self.params.PORTRAIT_DIR.format(monster_id=card['ID']))) else: - portrait = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])) + portrait = Image.open( + self.params.PORTRAIT_DIR.format(monster_id=card['ID'])) draw = ImageDraw.Draw(portrait) slv_offset = 80 if show_stats: @@ -614,24 +655,32 @@ def combine_portrait(self, card, show_stats=True, show_supers=False): if 0 < sum_plus: if sum_plus < 297: font = ImageFont.truetype(self.params.FONT_NAME, 14) - outline_text(draw, 5, 2, font, 'yellow', '+{:d} HP'.format(card['+HP'])) - outline_text(draw, 5, 14, font, 'yellow', '+{:d} ATK'.format(card['+ATK'])) - outline_text(draw, 5, 26, font, 'yellow', '+{:d} RCV'.format(card['+RCV'])) + outline_text(draw, 5, 2, font, 'yellow', + '+{:d} HP'.format(card['+HP'])) + outline_text(draw, 5, 14, font, 'yellow', + '+{:d} ATK'.format(card['+ATK'])) + outline_text(draw, 5, 26, font, 'yellow', + '+{:d} RCV'.format(card['+RCV'])) else: font = ImageFont.truetype(self.params.FONT_NAME, 18) outline_text(draw, 5, 0, font, 'yellow', '+297') # level if card['LV'] > 0: - outline_text(draw, 5, 75, ImageFont.truetype(self.params.FONT_NAME, 18), + outline_text(draw, 5, 75, + ImageFont.truetype(self.params.FONT_NAME, 18), 'white', 'Lv.{:d}'.format(card['LV'])) slv_offset = 65 # skill level if card['MAX_SLV'] > 0 and card['SLV'] > 0: - slv_txt = 'SLv.max' if card['SLV'] >= card['MAX_SLV'] else 'SLv.{:d}'.format(card['SLV']) + slv_txt = 'SLv.max' if card['SLV'] >= card[ + 'MAX_SLV'] else 'SLv.{:d}'.format(card['SLV']) outline_text(draw, 5, slv_offset, - ImageFont.truetype(self.params.FONT_NAME, 12), 'pink', slv_txt) + ImageFont.truetype(self.params.FONT_NAME, 12), 'pink', + slv_txt) # ID - outline_text(draw, 67, 82, ImageFont.truetype(self.params.FONT_NAME, 12), 'lightblue', str(card['MNO'])) + outline_text(draw, 67, 82, + ImageFont.truetype(self.params.FONT_NAME, 12), 'lightblue', + str(card['MNO'])) del draw if card['MAX_AWAKE'] > 0: # awakening @@ -641,13 +690,17 @@ def combine_portrait(self, card, show_stats=True, show_supers=False): awake = Image.open(self.params.ASSETS_DIR + AWK_CIRCLE + '.png') draw = ImageDraw.Draw(awake) draw.text((8, -2), str(card['AWAKE']), - font=ImageFont.truetype(self.params.FONT_NAME, 18), fill='yellow') + font=ImageFont.truetype(self.params.FONT_NAME, 18), + fill='yellow') del draw - portrait.paste(awake, (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, 5), awake) + portrait.paste(awake, + (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, 5), + awake) awake.close() if show_supers and card['SUPER'] > 0: # SA - awake = Image.open(self.params.ASSETS_DIR + 'awk/' + str(card['SUPER']) + '.png') + awake = Image.open( + self.params.ASSETS_DIR + 'awk/' + str(card['SUPER']) + '.png') portrait.paste(awake, (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, (self.params.PORTRAIT_WIDTH - awake.size[0]) // 2), @@ -661,19 +714,24 @@ def generate_build_image(self, include_instructions=False): team_size = max([len(x) for x in self.build['TEAM']]) p_w = self.params.PORTRAIT_WIDTH * math.ceil(team_size / 2) + \ self.params.PADDING * math.ceil(team_size / 10) - p_h = (self.params.PORTRAIT_WIDTH + self.params.LATENTS_WIDTH + self.params.PADDING) * \ + p_h = ( + self.params.PORTRAIT_WIDTH + self.params.LATENTS_WIDTH + self.params.PADDING) * \ 2 * len(self.build['TEAM']) include_instructions &= self.build['INSTRUCTION'] is not None if include_instructions: - p_h += len(self.build['INSTRUCTION']) * (self.params.PORTRAIT_WIDTH // 2 + self.params.PADDING) + p_h += len(self.build['INSTRUCTION']) * ( + self.params.PORTRAIT_WIDTH // 2 + self.params.PADDING) self.build_img = Image.new('RGBA', (p_w, p_h), (255, 255, 255, 0)) y_offset = 0 for team in self.build['TEAM']: - has_assist = any([card is not None for idx, card in enumerate(team) if idx % 2 == 1]) - has_latents = any([card['LATENT'] is not None for idx, card in enumerate(team) - if idx % 2 == 0 and card is not None]) + has_assist = any( + [card is not None for idx, card in enumerate(team) if + idx % 2 == 1]) + has_latents = any( + [card['LATENT'] is not None for idx, card in enumerate(team) + if idx % 2 == 0 and card is not None]) if has_assist: y_offset += self.params.PORTRAIT_WIDTH for idx, card in enumerate(team): @@ -692,7 +750,8 @@ def generate_build_image(self, include_instructions=False): portrait, (x_offset + x * self.params.PORTRAIT_WIDTH, y_offset + y * self.params.PORTRAIT_WIDTH)) - if has_latents and idx % 2 == 0 and card['LATENT'] is not None: + if has_latents and idx % 2 == 0 and card[ + 'LATENT'] is not None: latents = self.combine_latents(card['LATENT']) self.build_img.paste( latents, @@ -712,7 +771,9 @@ def generate_build_image(self, include_instructions=False): for step in self.build['INSTRUCTION']: x_offset = self.params.PADDING outline_text(draw, x_offset, y_offset + text_padding, - font, 'white', 'F{:d} - P{:d} '.format(step['FLOOR'], step['PLAYER'] + 1)) + font, 'white', + 'F{:d} - P{:d} '.format(step['FLOOR'], + step['PLAYER'] + 1)) x_offset += self.params.PORTRAIT_WIDTH if step['ACTIVE'] is not None: actives_used = [self.build['TEAM'][idx][ids] @@ -720,13 +781,24 @@ def generate_build_image(self, include_instructions=False): for ids in side] for card in actives_used: if 'http' in self.params.PORTRAIT_DIR: - p_small = Image.open(urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) + p_small = Image.open(urllib.request.urlopen( + self.params.PORTRAIT_DIR.format( + monster_id=card['ID']))).resize(( + self.params.PORTRAIT_WIDTH // 2, + self.params.PORTRAIT_WIDTH // 2), + Image.LINEAR) else: - p_small = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) + p_small = Image.open( + self.params.PORTRAIT_DIR.format( + monster_id=card['ID'])).resize(( + self.params.PORTRAIT_WIDTH // 2, + self.params.PORTRAIT_WIDTH // 2), + Image.LINEAR) self.build_img.paste(p_small, (x_offset, y_offset)) x_offset += self.params.PORTRAIT_WIDTH // 2 x_offset += self.params.PADDING - outline_text(draw, x_offset, y_offset + text_padding, font, 'white', step['ACTION']) + outline_text(draw, x_offset, y_offset + text_padding, font, + 'white', step['ACTION']) y_offset += self.params.PORTRAIT_WIDTH // 2 del draw @@ -745,8 +817,9 @@ async def helpbuildimg(self, ctx): """Help info for the buildimage command.""" await ctx.author.send(box(HELP_MSG)) if checks.admin_or_permissions(manage_guild=True): - await ctx.author.send(box('For Server Admins: Output location can be changed between current channel and ' - 'direct messages via ^togglebuildimgoutput')) + await ctx.author.send(box( + 'For Server Admins: Output location can be changed between current channel and ' + 'direct messages via ^togglebuildimgoutput')) await ctx.author.send(EXAMPLE_MSG) @commands.command(aliases=['buildimg', 'pdchu']) @@ -758,7 +831,8 @@ async def padbuildimg(self, ctx, *, build_str: str): async with ctx.typing(): params = self.settings.buildImgParams() try: - pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) + pbg = PadBuildImageGenerator(params, + self.bot.get_cog('PadInfo')) await pbg.process_build(build_str) pbg.generate_build_image() except commands.UserFeedbackCheckFailure as ex: @@ -771,15 +845,20 @@ async def padbuildimg(self, ctx, *, build_str: str): build_io.seek(0) if ctx.guild and self.settings.dmOnly(ctx.guild.id): try: - await ctx.author.send(file=discord.File(build_io,'pad_build.png')) - await ctx.send(inline('Sent build to {}'.format(ctx.author))) + await ctx.author.send( + file=discord.File(build_io, 'pad_build.png')) + await ctx.send( + inline('Sent build to {}'.format(ctx.author))) except discord.errors.Forbidden as ex: - await ctx.send(inline('Failed to send build to {}'.format(ctx.author))) + await ctx.send(inline( + 'Failed to send build to {}'.format(ctx.author))) else: try: - await ctx.send(file=discord.File(build_io,'pad_build.png')) + await ctx.send( + file=discord.File(build_io, 'pad_build.png')) except discord.errors.Forbidden as ex: - await ctx.send(inline("Failed to send build. (Insufficient Permisisons)")) + await ctx.send(inline( + "Failed to send build. (Insufficient Permisisons)")) else: await ctx.send(box('Invalid build, see ^helpbuildimg')) return 0 @@ -796,10 +875,12 @@ async def configbuildimg(self, ctx, param_key: str, param_value: str): LATENTS_WIDTH - width of 1 slot latent, default 25 FONT_NAME - path to font """ - if param_key in ['ASSETS_DIR', 'PORTRAIT_DIR', 'PORTRAIT_WIDTH', 'PADDING', 'LATENTS_WIDTH', 'FONT_NAME']: + if param_key in ['ASSETS_DIR', 'PORTRAIT_DIR', 'PORTRAIT_WIDTH', + 'PADDING', 'LATENTS_WIDTH', 'FONT_NAME']: if param_key in ['PORTRAIT_WIDTH', 'PADDING', 'LATENTS_WIDTH']: param_value = int(param_value) - if param_key in ['ASSETS_DIR'] and param_value[-1] not in ['/', '\\']: + if param_key in ['ASSETS_DIR'] and param_value[-1] not in ['/', + '\\']: param_value += '/' self.settings.setBuildImgParamsByKey(param_key, param_value) await ctx.send(box('Set {} to {}'.format(param_key, param_value))) @@ -812,7 +893,8 @@ async def refreshassets(self, ctx): """ Refresh assets folder """ - await ctx.send('Downloading assets to {}'.format(self.settings.buildImgParams().ASSETS_DIR)) + await ctx.send('Downloading assets to {}'.format( + self.settings.buildImgParams().ASSETS_DIR)) awk_ids = self.bot.get_cog('Dadguide').database.get_awoken_skill_ids() await self.settings.downloadAllAssets(awk_ids) await ctx.tick() diff --git a/padbuilds/info.json b/padbuilds/info.json index ae3ed7b6..76282200 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -1,15 +1,25 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "View user-set builds for various dungeons.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Builds to help you through dungeons", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "View user-set builds for various dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Builds to help you through dungeons", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/padevents/__init__.py b/padevents/__init__.py index e72e9a91..7f7dbc01 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = PadEvents(bot) bot.add_cog(n) diff --git a/padevents/info.json b/padevents/info.json index 28a2be3b..f0144455 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -1,15 +1,29 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Stay up to date on PAD dungeons.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "PAD Event tracker", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"dadguide": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["tsutils", "pytz", "prettytable"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Stay up to date on PAD dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PAD Event tracker", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "pytz", + "prettytable" + ], + "tags": [ + "PAD" + ] } diff --git a/padevents/padevents.py b/padevents/padevents.py index 034464a9..80d81ce4 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,19 +1,18 @@ -import traceback import asyncio -import re import datetime -import pytz +import discord import logging +import prettytable +import pytz +import re +import traceback from collections import defaultdict from datetime import timedelta from enum import Enum - -import prettytable -import discord -from tsutils import CogSettings from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify +from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.padevents') @@ -57,7 +56,8 @@ async def reload_padevents(self): await self.refresh_data() logger.info('Done refreshing PadEvents') except Exception as ex: - logger.exception("reload padevents loop caught exception " + str(ex)) + logger.exception( + "reload padevents loop caught exception " + str(ex)) await asyncio.sleep(60 * 60 * 1) @@ -80,33 +80,42 @@ async def check_started(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadEvents'): try: - events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) + events = filter(lambda + e: e.is_started() and not e.key in self.started_events, + self.events) daily_refresh_servers = set() for e in events: self.started_events.add(e.key) - if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: + if e.event_type in [EventType.Guerrilla, + EventType.GuerrillaNew, + EventType.SpecialWeek]: for gr in list(self.settings.listGuerrillaReg()): if e.server == gr['server']: try: - message = box("Server " + e.server + ", group " + - e.groupLongName() + " : " + e.name_and_modifier) - channel = self.bot.get_channel(int(gr['channel_id'])) + message = box( + "Server " + e.server + ", group " + + e.groupLongName() + " : " + e.name_and_modifier) + channel = self.bot.get_channel( + int(gr['channel_id'])) try: role_name = '{}_group_{}'.format( e.server, e.groupLongName()) - role = get_role(channel.guild.roles, role_name) + role = get_role(channel.guild.roles, + role_name) if role and role.mentionable: message = "{} `: {} is starting`".format( - role.mention, e.name_and_modifier) + role.mention, + e.name_and_modifier) except: pass # do nothing if role is missing await channel.send(message) except Exception as ex: # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) - logger.exception("caught exception while sending guerrilla msg:") + logger.exception( + "caught exception while sending guerrilla msg:") else: if not e.dungeon_type in [DungeonType.Normal]: @@ -114,16 +123,21 @@ async def check_started(self): for server in daily_refresh_servers: msg = self.makeActiveText(server) - for daily_registration in list(self.settings.listDailyReg()): + for daily_registration in list( + self.settings.listDailyReg()): try: if server == daily_registration['server']: - await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), - msg, channel_id=daily_registration['channel_id']) + await self.pageOutput(self.bot.get_channel( + daily_registration['channel_id']), + msg, channel_id= + daily_registration[ + 'channel_id']) logger.info("daily_reg server") except Exception as ex: # self.settings.removeDailyReg( # daily_registration['channel_id'], daily_registration['server']) - logger.exception("caught exception while sending daily msg:") + logger.exception( + "caught exception while sending daily msg:") except Exception as ex: logger.exception("caught exception while checking guerrillas:") @@ -148,7 +162,7 @@ async def testevent(self, ctx, server, seconds: int = 0): await dg_cog.wait_until_ready() dg_module = __import__(dg_cog.__module__) - te = dg_module.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) + te = dg_module.DgScheduledEvent({'dungeon_id': 1}, dg_cog.database) te.server = server te.event_type_id = EventType.Guerrilla.value @@ -157,7 +171,8 @@ async def testevent(self, ctx, server, seconds: int = 0): te.key = lambda: fuid te.group_name = 'Yellow' - te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) + te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta( + seconds=seconds) te.close_datetime = te.open_datetime + timedelta(minutes=1) te.dungeon_name = 'fake_dungeon_name' te.event_modifier = 'fake_event_modifier' @@ -250,7 +265,8 @@ def makeChannelList(self, reg_list): channel = self.bot.get_channel(int(reg_channel_id)) channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' server_name = channel.guild.name if channel else 'Unknown server' - msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' + msg += " " + cr[ + 'server'] + " : " + server_name + '(' + channel_name + ')\n' return msg @padevents.command() @@ -275,7 +291,8 @@ def makeActiveText(self, server): special_events = active_events.withType( EventType.Special).itemsByCloseTime() if len(special_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Special Events', special_events) + msg += "\n\n" + self.makeActiveOutput('Special Events', + special_events) all_etc_events = active_events.withType(EventType.Etc) @@ -285,29 +302,37 @@ def makeActiveText(self, server): msg += "\n\n" + self.makeActiveOutput('Etc Events', etc_events) # Old-style guerrillas - active_guerrilla_events = active_events.withType(EventType.Guerrilla).items() + active_guerrilla_events = active_events.withType( + EventType.Guerrilla).items() if len(active_guerrilla_events) > 0: msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + self.makeActiveGuerrillaOutput('Active Guerrillas', + active_guerrilla_events) guerrilla_events = pending_events.withType(EventType.Guerrilla).items() if len(guerrilla_events) > 0: - msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', guerrilla_events) + msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', + guerrilla_events) # New-style guerrillas - active_guerrilla_events = active_events.withType(EventType.SpecialWeek).items() + active_guerrilla_events = active_events.withType( + EventType.SpecialWeek).items() if len(active_guerrilla_events) > 0: msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + self.makeActiveGuerrillaOutput('Active Guerrillas', + active_guerrilla_events) - guerrilla_events = pending_events.withType(EventType.SpecialWeek).items() + guerrilla_events = pending_events.withType( + EventType.SpecialWeek).items() if len(guerrilla_events) > 0: msg += "\n\n" + \ self.makeFullGuerrillaOutput( - 'Guerrilla Events', guerrilla_events, starter_guerilla=True) + 'Guerrilla Events', guerrilla_events, + starter_guerilla=True) # clean up long headers - msg = msg.replace('-------------------------------------', '-----------------------') + msg = msg.replace('-------------------------------------', + '-----------------------') return msg @@ -319,9 +344,11 @@ async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): if channel_id is None: await ctx.send(format_type(page)) else: - await self.bot.get_channel(int(channel_id)).send(format_type(page)) + await self.bot.get_channel(int(channel_id)).send( + format_type(page)) except Exception as e: - logger.exception("page output failed " + str(e), "tried to output: " + page) + logger.exception("page output failed " + str(e), + "tried to output: " + page) def makeActiveOutput(self, table_name, event_list): tbl = prettytable.PrettyTable(["Time", table_name]) @@ -340,16 +367,19 @@ def makeActiveGuerrillaOutput(self, table_name, event_list): tbl.align[table_name] = "l" tbl.align["Time"] = "r" for e in event_list: - tbl.add_row([e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) + tbl.add_row( + [e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) return tbl.get_string() - def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False): + def makeFullGuerrillaOutput(self, table_name, event_list, + starter_guerilla=False): events_by_name = defaultdict(list) for e in event_list: events_by_name[e.name_and_modifier].append(e) rows = list() - grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] + grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", + "D", "E"] for name, events in events_by_name.items(): events = sorted(events, key=lambda e: e.open_datetime) events_by_group = defaultdict(list) @@ -497,10 +527,11 @@ def __init__(self, scheduled_event: "DgScheduledEvent"): self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' - self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' + self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) - self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') + self.clean_event_name = self.event_name.replace('!', '').replace(' ', + '') self.name_and_modifier = self.clean_dungeon_name if self.clean_event_name != '': @@ -563,11 +594,14 @@ def toGuerrillaStr(self): return fmtTimeShort(self.startPst()) def toDateStr(self): - return self.server + "," + self.group + "," + fmtTime(self.startPst()) + "," + fmtTime( + return self.server + "," + self.group + "," + fmtTime( + self.startPst()) + "," + fmtTime( self.startEst()) + "," + self.startFromNow() def groupShortName(self): - return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') + return self.group.upper().replace('RED', 'R').replace('BLUE', + 'B').replace( + 'GREEN', 'G') def groupLongName(self): return self.group.upper() @@ -577,7 +611,8 @@ def toPartialEvent(self, pe): if self.is_started(): return group + " " + self.endFromNow() + " " + self.name_and_modifier else: - return group + " " + fmtTimeShort(self.startPst()) + " " + fmtTimeShort( + return group + " " + fmtTimeShort( + self.startPst()) + " " + fmtTimeShort( self.startEst()) + " " + self.startFromNow() + " " + self.name_and_modifier @@ -604,7 +639,8 @@ def withDungeonType(self, dungeon_type, exclude=False): return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) def withNameContains(self, name, exclude=False): - return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) + return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), + exclude) def excludeUnwantedEvents(self): return self.withFunc(isEventWanted) @@ -625,10 +661,14 @@ def availableOnly(self): return self.withFunc(lambda e: e.is_available()) def itemsByOpenTime(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) + return list(sorted(self.event_list, + key=(lambda e: (e.open_datetime, e.dungeon_name)), + reverse=reverse)) def itemsByCloseTime(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) + return list(sorted(self.event_list, + key=(lambda e: (e.close_datetime, e.dungeon_name)), + reverse=reverse)) # TIME_FMT = """%a %b %d %H:%M:%S %Y""" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8186b3ca..89d66ef4 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1,23 +1,22 @@ +import aiohttp import asyncio import csv import datetime import difflib +import discord import io import json -import os -import re import logging -from collections import defaultdict - -import aiohttp -import discord +import os import prettytable +import re import tsutils -from tsutils import CogSettings, safe_read_json, replace_emoji_names_with_code,\ - clean_global_mentions, confirm_message +from collections import defaultdict from redbot.core import checks, data_manager from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify +from tsutils import CogSettings, safe_read_json, replace_emoji_names_with_code, \ + clean_global_mentions, confirm_message logger = logging.getLogger('red.padbot-cogs.padglobal') @@ -25,7 +24,8 @@ def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), + file_name) DATA_EXPORT_PATH = _data_file('padglobal_data.json') @@ -47,8 +47,9 @@ def _data_file(file_name: str) -> str: DISABLED_MSG = 'PAD Global info disabled on this server' FARMABLE_MSG = 'This monster is **farmable** so make as many copies of whichever evos you like.' -MP_BUY_MSG = ('This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' - ', check {}mpdra? for specific recommendations.') +MP_BUY_MSG = ( + 'This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' + ', check {}mpdra? for specific recommendations.') SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution.' @@ -58,11 +59,13 @@ def mod_help(self, ctx, help_type): commands.Command.format_help_for_context = lambda s, c: mod_help(s, c, "help") -commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, "short_doc") +commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, + "short_doc") def is_padglobal_admin_check(ctx): - return checks.is_owner() or ctx.bot.get_cog("PadGlobal").settings.check_admin(ctx.author.id) + return checks.is_owner() or ctx.bot.get_cog( + "PadGlobal").settings.check_admin(ctx.author.id) def is_padglobal_admin(): @@ -130,9 +133,12 @@ async def red_delete_data_for_user(self, *, requester, user_id): def _export_data(self): faq_and_boards = self.settings.faq() + self.settings.boards() - general = {k: v for k, v in self.c_commands.items() if k not in faq_and_boards} - faq = {k: v for k, v in self.c_commands.items() if k in self.settings.faq()} - boards = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} + general = {k: v for k, v in self.c_commands.items() if + k not in faq_and_boards} + faq = {k: v for k, v in self.c_commands.items() if + k in self.settings.faq()} + boards = {k: v for k, v in self.c_commands.items() if + k in self.settings.boards()} glossary = self.settings.glossary() bosses = self.settings.boss() which = self.settings.which() @@ -167,7 +173,8 @@ class of problems where the bot can read messages but not respond, so this could handle those cases. """ msg = '------------------------\n' - msg += '{} shut down the bot because: {}\n'.format(ctx.author.name, reason) + msg += '{} shut down the bot because: {}\n'.format(ctx.author.name, + reason) msg += '------------------------\n' logger.critical(msg) @@ -190,8 +197,10 @@ async def togglepadglobal(self, ctx): self.settings.rmDisabledServer(server_id) else: self.settings.addDisabledServer(server_id) - status = 'disabled' if self.settings.checkDisabled(ctx.message) else 'enabled' - await ctx.send(inline('PAD Global commands {} on this server').format(status)) + status = 'disabled' if self.settings.checkDisabled( + ctx.message) else 'enabled' + await ctx.send( + inline('PAD Global commands {} on this server').format(status)) @commands.command() @is_padglobal_admin() @@ -234,7 +243,8 @@ def list_or_none(l): msg += "\n\nNickname original components:" msg += "\n monster_basename: {}".format(m.monster_basename) - msg += "\n group_computed_basename: {}".format(m.group_computed_basename) + msg += "\n group_computed_basename: {}".format( + m.group_computed_basename) msg += "\n extra_nicknames: {}".format(list_or_none(m.extra_nicknames)) msg += "\n\nNickname final components:" @@ -242,10 +252,14 @@ def list_or_none(l): msg += "\n prefixes: {}".format(list_or_none(m.prefixes)) msg += "\n\nAccepted nickname entries:" - accepted_nn = list(filter(lambda nn: m.monster_id == padinfo_cog.index_all.all_entries[nn].monster_id, - m.final_nicknames)) - accepted_twnn = list(filter(lambda nn: m.monster_id == padinfo_cog.index_all.two_word_entries[nn].monster_id, - m.final_two_word_nicknames)) + accepted_nn = list(filter( + lambda nn: m.monster_id == padinfo_cog.index_all.all_entries[ + nn].monster_id, + m.final_nicknames)) + accepted_twnn = list(filter( + lambda nn: m.monster_id == padinfo_cog.index_all.two_word_entries[ + nn].monster_id, + m.final_two_word_nicknames)) msg += "\n nicknames: {}".format(list_or_none(accepted_nn)) msg += "\n two_word_nicknames: {}".format(list_or_none(accepted_twnn)) @@ -260,7 +274,8 @@ def list_or_none(l): replaced_nn_info = map(lambda nn: ( nn, padinfo_cog.index_all.all_entries[nn]), replaced_nn) replaced_twnn_info = map( - lambda nn: (nn, padinfo_cog.index_all.two_word_entries[nn]), replaced_twnn) + lambda nn: (nn, padinfo_cog.index_all.two_word_entries[nn]), + replaced_twnn) replaced_nn_text = list(map(lambda nn_info: '{} : {}. {}'.format( nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_na), @@ -271,7 +286,8 @@ def list_or_none(l): replaced_twnn_info)) msg += "\n nicknames: {}".format(list_or_none(replaced_nn_text)) - msg += "\n two_word_nicknames: {}".format(list_or_none(replaced_twnn_text)) + msg += "\n two_word_nicknames: {}".format( + list_or_none(replaced_twnn_text)) msg += "\n\nNickname entry sort parts:" msg += "\n (is_low_priority, group_size, monster_no_na) : ({}, {}, {})".format( @@ -285,7 +301,7 @@ def list_or_none(l): for page in pagify(msg): sent_messages.append(await ctx.send(box(page))) await tsutils.await_and_remove(self.bot, sent_messages[-1], ctx.author, - delete_msgs=sent_messages, timeout=30) + delete_msgs=sent_messages, timeout=30) @commands.command() @is_padglobal_admin() @@ -298,7 +314,7 @@ async def forceindexreload(self, ctx): await padinfo_cog.refresh_index() await ctx.send('finished reload') - @commands.group(aliases = ['pdg']) + @commands.group(aliases=['pdg']) @is_padglobal_admin() async def padglobal(self, ctx): """PAD global custom commands.""" @@ -319,7 +335,7 @@ async def edit(self, ctx, command: str, *, text: str): """Edit a custom command or alias""" await self._add(ctx, command, text, False) - async def _add(self, ctx, command, text, confirm = True): + async def _add(self, ctx, command, text, confirm=True): command = command.lower() text = clean_global_mentions(text) text = text.replace(u'\u200b', '') @@ -349,8 +365,11 @@ async def _add(self, ctx, command, text, confirm = True): ted = self.c_commands[ted] alias = True if confirm: - conf = await confirm_message(ctx,"Are you sure you want to edit the {}command {}?" \ - .format("alias to " if alias else "", ted)) + conf = await confirm_message(ctx, + "Are you sure you want to edit the {}command {}?" \ + .format( + "alias to " if alias else "", + ted)) if not conf: return else: @@ -435,28 +454,33 @@ async def checktype(self, ctx, command: str): elif command in self.c_commands: await ctx.send('{} is a general padglobal command.'.format(command)) else: - await ctx.send('{} is not a padglobal command. It might be a meme or a custom command.'.format(command)) + await ctx.send( + '{} is not a padglobal command. It might be a meme or a custom command.'.format( + command)) @commands.command() @commands.check(check_enabled) async def pad(self, ctx): """Shows PAD global command list""" configured = self.settings.faq() + self.settings.boards() - cmdlist = {k: v for k, v in self.c_commands.items() if k not in configured} + cmdlist = {k: v for k, v in self.c_commands.items() if + k not in configured} await self.send_cmdlist(ctx, cmdlist) @commands.command() @commands.check(check_enabled) async def padfaq(self, ctx): """Shows PAD FAQ command list""" - cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.faq()} + cmdlist = {k: v for k, v in self.c_commands.items() if + k in self.settings.faq()} await self.send_cmdlist(ctx, cmdlist) @commands.command() @commands.check(check_enabled) async def boards(self, ctx): """Shows PAD Boards command list""" - cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} + cmdlist = {k: v for k, v in self.c_commands.items() if + k in self.settings.boards()} await self.send_cmdlist(ctx, cmdlist) async def send_cmdlist(self, ctx, cmdlist, inline=False): @@ -545,7 +569,8 @@ async def padto(self, ctx, to_user: discord.Member, *, term: str): result = self.c_commands.get(corrected_term, None) await self._do_send_term(ctx, to_user, term, corrected_term, result) - async def _do_send_term(self, ctx, to_user: discord.Member, term, corrected_term, result): + async def _do_send_term(self, ctx, to_user: discord.Member, term, + corrected_term, result): """Does the heavy lifting shared by padto and glossaryto.""" if result: result_output = '**{}** : {}'.format(corrected_term, result) @@ -579,7 +604,8 @@ async def glossary(self, ctx, *, term: str = None): def glossary_to_text(self, ctx): glossary = self.settings.glossary() - msg = '__**PAD Glossary terms (also check out {0}pad / {0}padfaq / {0}boards / {0}which)**__'.format(ctx.prefix) + msg = '__**PAD Glossary terms (also check out {0}pad / {0}padfaq / {0}boards / {0}which)**__'.format( + ctx.prefix) for term in sorted(glossary.keys()): definition = glossary[term] msg += '\n**{}** : {}'.format(term, definition) @@ -596,7 +622,8 @@ def lookup_glossary(self, term): matches = self._get_corrected_cmds(term, glossary.keys()) if not matches: - matches = difflib.get_close_matches(term, glossary.keys(), n=1, cutoff=.8) + matches = difflib.get_close_matches(term, glossary.keys(), n=1, + cutoff=.8) if not matches: return term, None @@ -615,7 +642,8 @@ async def addglossary(self, ctx, term, *, definition): term = term.lower() definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') - definition = replace_emoji_names_with_code(self._get_emojis(), definition) + definition = replace_emoji_names_with_code(self._get_emojis(), + definition) op = 'EDITED' if term in self.settings.glossary() else 'ADDED' self.settings.addGlossary(term, definition) @@ -628,7 +656,9 @@ async def rmglossary(self, ctx, *, term): if term not in self.settings.glossary(): await ctx.send("Glossary item doesn't exist.") return - if not await confirm_message(ctx, "Are you sure you want to globally remove the glossary data for {}?".format(term)): + if not await confirm_message(ctx, + "Are you sure you want to globally remove the glossary data for {}?".format( + term)): return self.settings.rmGlossary(term) await ctx.tick() @@ -641,7 +671,9 @@ async def boss(self, ctx, *, term: str = None): term_new, definition = self.lookup_boss(term) if definition: if term_new != term.lower(): - await ctx.send('No entry for {} found, corrected to {}'.format(term, term_new)) + await ctx.send( + 'No entry for {} found, corrected to {}'.format(term, + term_new)) await ctx.send(self.emojify(definition)) else: await ctx.send(inline('No mechanics found')) @@ -667,7 +699,8 @@ def lookup_boss(self, term): matches = self._get_corrected_cmds(term, bosses.keys()) if not matches: - matches = difflib.get_close_matches(term, bosses.keys(), n=1, cutoff=.8) + matches = difflib.get_close_matches(term, bosses.keys(), n=1, + cutoff=.8) if not matches: return term, None @@ -677,7 +710,8 @@ def lookup_boss(self, term): def boss_to_text(self, ctx): bosses = self.settings.boss() - msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format(ctx.prefix) + msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( + ctx.prefix) for term in sorted(bosses.keys()): definition = bosses[term] msg += '\n**{}**\n{}'.format(term, definition) @@ -685,7 +719,8 @@ def boss_to_text(self, ctx): def boss_to_text_index(self, ctx): bosses = self.settings.boss() - msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format(ctx.prefix) + msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( + ctx.prefix) msg = msg + '\n' + ',\n'.join(sorted(bosses.keys())) return msg @@ -697,7 +732,8 @@ async def addboss(self, ctx, term, *, definition): op = 'EDITED' if term in self.settings.boss() else 'ADDED' definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') - definition = replace_emoji_names_with_code(self._get_emojis(), definition) + definition = replace_emoji_names_with_code(self._get_emojis(), + definition) self.settings.addBoss(term, definition) await ctx.send("PAD boss mechanics successfully {}.".format(op)) @@ -708,7 +744,9 @@ async def rmboss(self, ctx, *, term): if term not in self.settings.boss(): await ctx.send("Boss mechanics item doesn't exist.") return - if not await confirm_message(ctx, "Are you sure you want to globally remove the boss data for {}?".format(term)): + if not await confirm_message(ctx, + "Are you sure you want to globally remove the boss data for {}?".format( + term)): return self.settings.rmBoss(term) @@ -720,19 +758,23 @@ async def which(self, ctx, *, term: str = None): """Shows PAD Which Monster entries""" if term is None: - await ctx.author.send('__**PAD Which Monster**__ *(also check out {0}pad / {0}padfaq / {0}boards / {0}glossary)*'.format(ctx.prefix)) + await ctx.author.send( + '__**PAD Which Monster**__ *(also check out {0}pad / {0}padfaq / {0}boards / {0}glossary)*'.format( + ctx.prefix)) msg = self.which_to_text() for page in pagify(msg): await ctx.author.send(box(page)) return - name, definition, timestamp, success = await self._resolve_which(ctx, term) + name, definition, timestamp, success = await self._resolve_which(ctx, + term) if name is None or definition is None: return if not success: await ctx.send('`Which {}`\n{}'.format(name, definition)) return - await ctx.send(inline('Which {} - Last Updated {}'.format(name, timestamp))) + await ctx.send( + inline('Which {} - Last Updated {}'.format(name, timestamp))) await ctx.send(self.emojify(definition)) async def _resolve_which(self, ctx, term): @@ -762,7 +804,8 @@ async def _resolve_which(self, ctx, term): elif check_simple_tree(monster): return name, SIMPLE_TREE_MSG, None, False else: - await ctx.send(inline('No which info for {} (#{})'.format(name, monster_id))) + await ctx.send( + inline('No which info for {} (#{})'.format(name, monster_id))) return None, None, None, None @commands.command() @@ -772,7 +815,8 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): [p]whichto @{0.author.name} saria """ - name, definition, timestamp, success = await self._resolve_which(ctx, term) + name, definition, timestamp, success = await self._resolve_which(ctx, + term) if name is None or definition is None: return @@ -802,9 +846,12 @@ def which_to_text(self): tbl_string = tsutils.strip_right_multiline(tbl.get_string()) return tbl_string - async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, timestamp): + async def _do_send_which(self, ctx, to_user: discord.Member, name, + definition, timestamp): """Does the heavy lifting for whichto.""" - result_output = '**Which {} - Last Updated {}**\n{}'.format(name, timestamp, definition) + result_output = '**Which {} - Last Updated {}**\n{}'.format(name, + timestamp, + definition) result = "{} asked me to send you this:\n{}".format( ctx.author.name, result_output) @@ -822,7 +869,9 @@ async def addwhich(self, ctx, monster_id: int, *, definition): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send( + "I think you meant {} for {}.".format(m.monster_no_na, + m.name_na)) name = m.monster_id op = 'EDITED' if name in self.settings.which() else 'ADDED' @@ -835,8 +884,12 @@ async def rmwhich(self, ctx, *, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) - if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format(m.name_na)): + await ctx.send( + "I think you meant {} for {}.".format(m.monster_no_na, + m.name_na)) + if not await confirm_message(ctx, + "Are you sure you want to globally remove the which data for {}?".format( + m.name_na)): return name = m.monster_id @@ -886,8 +939,9 @@ async def debuglookup(self, ctx, *, term: str): name = nm.name_na if nm else nm monster_id = nm.monster_id if nm else nm definition = self.settings.which().get(monster_id, None) - await ctx.send('Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' - .format(base, name, monster_id, err, deb)) + await ctx.send( + 'Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' + .format(base, name, monster_id, err, deb)) @padglobal.command() @checks.is_owner() @@ -918,8 +972,9 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): """Set the emoji servers by ID (csv)""" self.settings.emojiServers().clear() if emoji_servers: - self.settings.setEmojiServers(re.findall(r'\d+',emoji_servers)) - await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) + self.settings.setEmojiServers(re.findall(r'\d+', emoji_servers)) + await ctx.send( + inline('Set {} servers'.format(len(self.settings.emojiServers())))) def _get_emojis(self): emojis = list() @@ -957,7 +1012,8 @@ async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): all_emojis = self._get_emojis() source_url = PORTRAIT_TEMPLATE.format(monster_id) - emoji_name = 'pad_' + ('na_' if server == 'na' else '') + str(monster_id) + emoji_name = 'pad_' + ('na_' if server == 'na' else '') + str( + monster_id) for e in all_emojis: if emoji_name == e.name: @@ -973,8 +1029,10 @@ async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): async with aiohttp.ClientSession() as sess: async with sess.get(source_url) as resp: emoji_content = await resp.read() - await emoji_server.create_custom_emoji(name=emoji_name, image=emoji_content) - await ctx.send(inline('Done creating emoji named {}'.format(emoji_name))) + await emoji_server.create_custom_emoji(name=emoji_name, + image=emoji_content) + await ctx.send(inline( + 'Done creating emoji named {}'.format(emoji_name))) except Exception as ex: await ctx.send(box('Error:\n' + str(ex))) @@ -983,7 +1041,8 @@ async def checkCC(self, message): if message.author.id == self.bot.user.id: return - global_ignores = {'blacklist': []} # self.bot.get_cog('Core').global_ignores + global_ignores = { + 'blacklist': []} # self.bot.get_cog('Core').global_ignores if message.author.id in global_ignores["blacklist"]: return False @@ -1005,7 +1064,8 @@ async def checkCC(self, message): return if final_cmd != cmd: - await message.channel.send(inline('Corrected to: {}'.format(final_cmd))) + await message.channel.send( + inline('Corrected to: {}'.format(final_cmd))) result = self.c_commands[final_cmd] while result in self.c_commands: result = self.c_commands[result] @@ -1107,7 +1167,8 @@ async def get_guide_text(self, term: str): return None, None, 'No dungeon or monster matched that query' name = nm.group_computed_basename.title() - definition = self.settings.leaderGuide().get(str(nm.base_monster_no), None) + definition = self.settings.leaderGuide().get(str(nm.base_monster_no), + None) if definition is None: return None, None, 'A monster matched that query but has no guide' @@ -1169,7 +1230,9 @@ async def rmdungeonguide(self, ctx, term: str): if term not in self.settings.dungeonGuide(): await ctx.send("DungeonGuide doesn't exist.") return - if not await confirm_message(ctx, "Are you sure you want to globally remove the dungeonguide data for {}?".format(term)): + if not await confirm_message(ctx, + "Are you sure you want to globally remove the dungeonguide data for {}?".format( + term)): return self.settings.rmDungeonGuide(term) await ctx.tick() @@ -1180,7 +1243,9 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send( + "I think you meant {} for {}.".format(m.monster_no_na, + m.name_na)) name = m.monster_id op = 'EDITED' if name in self.settings.leaderGuide() else 'ADDED' @@ -1193,8 +1258,12 @@ async def rmleaderguide(self, ctx, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) - if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format(m.name_na)): + await ctx.send( + "I think you meant {} for {}.".format(m.monster_no_na, + m.name_na)) + if not await confirm_message(ctx, + "Are you sure you want to globally remove the leaderguide data for {}?".format( + m.name_na)): return name = m.monster_id @@ -1218,6 +1287,7 @@ def emojify(self, message): message = tsutils.replace_emoji_names_with_code(emojis, message) return tsutils.fix_emojis_for_server(emojis, message) + def check_simple_tree(monster): attr1 = monster.attr1 active_skill = monster.active_skill diff --git a/padguidedb/__init__.py b/padguidedb/__init__.py index 4a503383..86d4637e 100644 --- a/padguidedb/__init__.py +++ b/padguidedb/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): pdb = PadGuideDb(bot) bot.add_cog(pdb) diff --git a/padguidedb/info.json b/padguidedb/info.json index 4c36d429..ae9e2e31 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Dungeon scraper and general database manager.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Dungeon data scraper", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "pymysql"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Dungeon scraper and general database manager.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Dungeon data scraper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "pymysql" + ], + "tags": [ + "PAD" + ] } diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 4ee3974a..892489b3 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -1,15 +1,13 @@ import asyncio import csv +import discord import io import json import logging - -import discord import pymysql from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify - from tsutils import CogSettings, tsutils logger = logging.getLogger('red.padbot-cogs.padguidedb') @@ -17,7 +15,8 @@ def is_padguidedb_admin_check(ctx): is_owner = ctx.author.id in ctx.bot.owner_ids - return is_owner or ctx.bot.get_cog("PadGuideDb").settings.checkAdmin(ctx.author.id) + return is_owner or ctx.bot.get_cog("PadGuideDb").settings.checkAdmin( + ctx.author.id) def is_padguidedb_admin(): @@ -108,7 +107,8 @@ async def searchdungeon(self, ctx, *, search_text): ' order by dungeon_id desc limit 20') cursor.execute(sql, [search_text, search_text]) results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + msg = 'Results\n' + json.dumps(results, indent=2, + ensure_ascii=False) await ctx.send(inline(sql)) for page in pagify(msg): await ctx.send(box(page)) @@ -136,34 +136,44 @@ async def setimageupdatefile(self, ctx, *, image_update_file): @padguidedb.command() @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): + async def setuserinfo(self, ctx, server: str, user_uuid: str, + user_intid: str): """Set the dungeon script.""" self.settings.setUserInfo(server, user_uuid, user_intid) await ctx.tick() @padguidedb.command() @is_padguidedb_admin() - async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): + async def loaddungeon(self, ctx, server: str, dungeon_id: int, + dungeon_floor_id: int, queues: int = 1): if queues > 5: await ctx.send("You must send less than 5 queues.") return elif self.queue_size + queues > 60: - await ctx.send("The size of the queue cannot exceed 60. It is currently {}.".format(self.queue_size)) + await ctx.send( + "The size of the queue cannot exceed 60. It is currently {}.".format( + self.queue_size)) return elif not self.settings.hasUserInfo(server): - await ctx.send("There is no account associated with server '{}'.".format(server.upper())) + await ctx.send( + "There is no account associated with server '{}'.".format( + server.upper())) return self.queue_size += queues if queues == 1: - await ctx.send(inline('Queueing load in slot {}'.format(self.queue_size))) + await ctx.send( + inline('Queueing load in slot {}'.format(self.queue_size))) else: await ctx.send( - inline('Queueing loads in slots {}-{}'.format(self.queue_size - queues + 1, self.queue_size))) + inline('Queueing loads in slots {}-{}'.format( + self.queue_size - queues + 1, self.queue_size))) event_loop = asyncio.get_event_loop() for queue in range(queues): - event_loop.create_task(self.do_dungeon_load(ctx, server.upper(), dungeon_id, dungeon_floor_id)) + event_loop.create_task( + self.do_dungeon_load(ctx, server.upper(), dungeon_id, + dungeon_floor_id)) async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): async with self.dungeon_load_lock: @@ -185,26 +195,31 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): if stderr: logger.error("Dungeon Load Error:\n" + stderr.decode()) await tsutils.doubleup(ctx, inline( - 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) + 'Load for {} {} {} failed'.format(server, dungeon_id, + dungeon_floor_id))) else: await tsutils.doubleup(ctx, inline( - 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) + 'Load for {} {} {} finished'.format(server, dungeon_id, + dungeon_floor_id))) self.queue_size -= 1 @padguidedb.command() @is_padguidedb_admin() - async def olddungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): + async def olddungeondrops(self, ctx, dungeon_id: int, + dungeon_floor_id: int): with self.get_cursor() as cursor: sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" " FROM wave_data" " WHERE dungeon_id = {} AND floor_id = {}" " GROUP BY 1, 2" - " ORDER BY 1, 2").format(int(dungeon_id), int(dungeon_floor_id)) + " ORDER BY 1, 2").format(int(dungeon_id), + int(dungeon_floor_id)) cursor.execute(sql) results = list(cursor.fetchall()) msg = 'stage,drop_monster_id,count' for row in results: - msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], row['count']) + msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], + row['count']) for page in pagify(msg): await ctx.send(box(page)) @@ -230,10 +245,10 @@ async def dungeondrops(self, ctx, dungeon_id: int): msg = 'floor_id,count\n' for row in results1: - msg += ','.join(map(str,row.values()))+"\n" + msg += ','.join(map(str, row.values())) + "\n" msg += '\n\nfloor_id,monster_id,count\n' for row in results2: - msg += ','.join(map(str,row.values()))+"\n" + msg += ','.join(map(str, row.values())) + "\n" for page in pagify(msg): await ctx.send(box(page)) @@ -242,7 +257,8 @@ async def dungeondrops(self, ctx, dungeon_id: int): @checks.is_owner() async def cleardungeon(self, ctx, dungeon_id: int): with self.get_cursor() as cursor: - sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) + sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format( + int(dungeon_id)) cursor.execute(sql) await ctx.tick() @@ -250,7 +266,8 @@ async def cleardungeon(self, ctx, dungeon_id: int): @is_padguidedb_admin() async def dungeondata(self, ctx, dungeon_id: int): with self.get_cursor() as cursor: - sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) + sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format( + int(dungeon_id)) cursor.execute(sql) results = list(cursor.fetchall()) order = sorted(results[0]) @@ -260,7 +277,8 @@ async def dungeondata(self, ctx, dungeon_id: int): writer.writerow(order) writer.writerows(rows) fauxfile.seek(0) - m = await ctx.send(file=discord.File(fauxfile, filename="dungeondata.csv")) + m = await ctx.send( + file=discord.File(fauxfile, filename="dungeondata.csv")) await asyncio.sleep(10) await m.delete() @@ -278,7 +296,8 @@ async def fulletl(self, ctx): return async with self.full_etl_lock: - await ctx.send(inline('Running full ETL pipeline: this could take a while')) + await ctx.send( + inline('Running full ETL pipeline: this could take a while')) process = await asyncio.create_subprocess_exec( 'bash', self.settings.fullETLFile(), @@ -303,7 +322,8 @@ async def extractimages(self, ctx): return async with self.extract_images_lock: - await ctx.send(inline('Running image extract pipeline: this could take a while')) + await ctx.send(inline( + 'Running image extract pipeline: this could take a while')) process = await asyncio.create_subprocess_exec( 'bash', self.settings.imageUpdateFile(), diff --git a/padinfo/__init__.py b/padinfo/__init__.py index 13827c7b..0505758e 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = PadInfo(bot) bot.add_cog(n) diff --git a/padinfo/info.json b/padinfo/info.json index d984d84f..40c5aae7 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -1,18 +1,29 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Serves monster data (LOAD THIS THIRD)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "speech": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "prettytable"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Serves monster data (LOAD THIS THIRD)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "speech": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "prettytable" + ], + "tags": [ + "PAD" + ] } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 109683bb..b0a7171a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,21 +1,20 @@ import asyncio +import discord +import io import json import logging import os -import io +import prettytable import traceback +import tsutils import urllib.parse from collections import OrderedDict from enum import Enum - -import discord -import prettytable from redbot.core import checks, data_manager, commands -from redbot.core.utils.chat_formatting import inline, box from redbot.core.utils import AsyncIter - -import tsutils -from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics +from redbot.core.utils.chat_formatting import inline, box +from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, \ + CogSettings, rmdiacritics logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -75,7 +74,8 @@ def get_pic_url(m): def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), + file_name) class IdEmojiUpdater(EmojiUpdater): @@ -91,7 +91,8 @@ def on_update(self, ctx, selected_emoji): if self.pad_info.settings.checkEvoID(ctx.author.id): DGCOG = self.bot.get_cog('Dadguide') evos = list(map(lambda x: [x['from_id'], x['to_id']], - DGCOG.database.get_all_evolutions_by_monster(self.m.monster_id))) + DGCOG.database.get_all_evolutions_by_monster( + self.m.monster_id))) evos = list(set(sum(evos, []))) evos.sort() index = evos.index(self.m.monster_id) @@ -126,7 +127,8 @@ def on_update(self, ctx, selected_emoji): class ScrollEmojiUpdater(EmojiUpdater): def __init__(self, emoji_to_embed, m: "DgMonster" = None, - ms: "List[int]" = None, selected_emoji=None, pad_info=None, bot=None): + ms: "List[int]" = None, selected_emoji=None, pad_info=None, + bot=None): self.emoji_dict = emoji_to_embed self.m = m self.ms = ms @@ -153,7 +155,8 @@ def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=True) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, + scroll=True) return True @@ -191,8 +194,10 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_file_path = _data_file('historic_lookups.json') self.historic_lookups = safe_read_json(self.historic_lookups_file_path) - self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') - self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) + self.historic_lookups_file_path_id2 = _data_file( + 'historic_lookups_id2.json') + self.historic_lookups_id2 = safe_read_json( + self.historic_lookups_file_path_id2) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -222,7 +227,8 @@ async def reload_nicknames(self): await self.refresh_index() except Exception as ex: wait_time = 5 - logger.exception("reload padinfo loop caught exception " + str(ex)) + logger.exception( + "reload padinfo loop caught exception " + str(ex)) await asyncio.sleep(wait_time) @@ -282,7 +288,8 @@ async def idjp(self, ctx, *, query: str): await self._do_id(ctx, query, server_filter=ServerFilter.jp) async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = await self.findMonster(query, server_filter=server_filter) + m, err, debug_info = await self.findMonster(query, + server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -307,7 +314,8 @@ async def id2jp(self, ctx, *, query: str): await self._do_id2(ctx, query, server_filter=ServerFilter.jp) async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = await self.findMonster2(query, server_filter=server_filter) + m, err, debug_info = await self.findMonster2(query, + server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -363,7 +371,8 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): ctx, starting_menu_emoji, IdEmojiUpdater(emoji_to_embed, pad_info=self, - m=m, selected_emoji=starting_menu_emoji, bot=self.bot) + m=m, selected_emoji=starting_menu_emoji, + bot=self.bot) ) async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): @@ -372,7 +381,7 @@ async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): ctx, starting_menu_emoji, ScrollEmojiUpdater(emoji_to_embed, pad_info=self, bot=self.bot, - m=m, ms=ms, selected_emoji=starting_menu_emoji) + m=m, ms=ms, selected_emoji=starting_menu_emoji) ) def get_id_emoji_options(self, m=None, scroll=False): @@ -411,7 +420,6 @@ def get_id_emoji_options(self, m=None, scroll=False): emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message return emoji_to_embed - async def _do_evolistmenu(self, ctx, sm): monsters = sm.alt_evos monsters.sort(key=lambda m: m.monster_id) @@ -423,18 +431,23 @@ async def _do_evolistmenu(self, ctx, sm): if m.monster_id == sm.monster_id: starting_menu_emoji = emoji - return await self._do_menu(ctx, starting_menu_emoji, EmojiUpdater(emoji_to_embed), timeout=60) + return await self._do_menu(ctx, starting_menu_emoji, + EmojiUpdater(emoji_to_embed), timeout=60) - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): + async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, + timeout=30): if starting_menu_emoji not in emoji_to_embed.emoji_dict: # Selected menu wasn't generated for this monster return EMBED_NOT_GENERATED - emoji_to_embed.emoji_dict[self.remove_emoji] = self.menu.reaction_delete_message + emoji_to_embed.emoji_dict[ + self.remove_emoji] = self.menu.reaction_delete_message try: - result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, - starting_menu_emoji, timeout=timeout) + result_msg, result_embed = await self.menu.custom_menu(ctx, + emoji_to_embed, + starting_menu_emoji, + timeout=timeout) if result_msg and result_embed: # Message is finished but not deleted, clear the footer result_embed.set_footer(text=discord.Embed.Empty) @@ -460,10 +473,10 @@ async def links(self, ctx, *, query: str): if m is not None: embed = monsterToBaseEmbed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) embed.set_footer(text='') await ctx.send(embed=embed) @@ -511,8 +524,8 @@ async def collabscroll(self, ctx, *, query: str): ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) ms = [m._alt_evo_id_list for m in ms - if m._base_monster_id == m.monster_id - and m.sell_mp >= 3000] + if m._base_monster_id == m.monster_id + and m.sell_mp >= 3000] ms = [m for ml in ms for m in ml] if m._base_monster_id != m.monster_id: @@ -525,17 +538,18 @@ async def collabscroll(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command(aliases=['leaders', 'leaderskills', 'ls']) @checks.bot_has_permissions(embed_links=True) - async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): + async def leaderskill(self, ctx, left_query: str, right_query: str = None, + *, bad=None): """Display the multiplier and leaderskills for two monsters If either your left or right query contains spaces, wrap in quotes. e.g.: [p]leaderskill "r sonia" "b sonia" """ if bad: - await ctx.send(inline('Too many inputs. Try wrapping your queries in quotes.')) + await ctx.send( + inline('Too many inputs. Try wrapping your queries in quotes.')) return # Handle a very specific failure case, user typing something like "uuvo ragdra" @@ -562,8 +576,10 @@ async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, ba emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis()) - emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis()) + emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, + self.get_emojis()) + emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, + self.get_emojis()) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -579,7 +595,8 @@ async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" voice = ctx.author.voice if not voice: - await ctx.send(inline('You must be in a voice channel to use this command')) + await ctx.send( + inline('You must be in a voice channel to use this command')) return channel = voice.channel @@ -597,10 +614,11 @@ async def padsay(self, ctx, server, *, query: str = None): if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: - await ctx.send(inline("No voice file found for "+m.name)) + await ctx.send(inline("No voice file found for " + m.name)) return base_dir = self.settings.voiceDir() - voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) + voice_file = os.path.join(base_dir, server, + '{0:03d}.wav'.format(voice_id)) header = '{} ({})'.format(monsterToHeader(m), server) if not os.path.exists(voice_file): await ctx.send(inline('Could not find voice for ' + header)) @@ -638,7 +656,8 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): self.settings.emojiServers().clear() if emoji_servers: self.settings.setEmojiServers(emoji_servers.split(',')) - await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) + await ctx.send( + inline('Set {} servers'.format(len(self.settings.emojiServers())))) @padinfo.command() @checks.is_owner() @@ -659,26 +678,33 @@ async def iddiff(self, ctx): m1, err1, debug_info1 = await self.findMonster(query) m2, err2, debug_info2 = await self.findMonster2(query) if c % 50 == 0: - await ctx.send(inline("{}/{} complete.".format(c, len(hist_aggreg)))) + await ctx.send( + inline("{}/{} complete.".format(c, len(hist_aggreg)))) if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): s += 1 continue f.append((query, - [m1.monster_id if m1 else None, m2.monster_id if m2 else None], + [m1.monster_id if m1 else None, + m2.monster_id if m2 else None], [err1, err2], [debug_info1, debug_info2] )) if m1 and m2: - await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_na, m2.name_na)) - await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) - file = discord.File(io.BytesIO(json.dumps(f).encode()), filename="diff.json") + await ctx.send( + "Major Discrepency: `{}` -> {}/{}".format(query, m1.name_na, + m2.name_na)) + await ctx.send("Done running diff checker. {}/{} passed.".format(s, + len( + hist_aggreg))) + file = discord.File(io.BytesIO(json.dumps(f).encode()), + filename="diff.json") await ctx.send(file=file) - def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] - return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] + return [e for g in self.bot.guilds if g.id in server_ids for e in + g.emojis] def makeFailureMsg(self, err): msg = ('Lookup failed: {}.\n' @@ -692,7 +718,8 @@ async def findMonster(self, query, server_filter=ServerFilter.any): monster_no = nm.monster_id if nm else -1 self.historic_lookups[query] = monster_no - json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) + json.dump(self.historic_lookups, + open(self.historic_lookups_file_path, "w+")) m = self.get_monster_by_id(nm.monster_id) if nm else None @@ -709,7 +736,9 @@ async def _findMonster(self, query, server_filter=ServerFilter.any): elif server_filter == ServerFilter.jp: monster_index = self.index_jp else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) + raise ValueError( + "server_filter must be type ServerFilter not " + str( + type(server_filter))) return monster_index.find_monster(query) async def findMonster2(self, query, server_filter=ServerFilter.any): @@ -718,7 +747,8 @@ async def findMonster2(self, query, server_filter=ServerFilter.any): monster_no = nm.monster_id if nm else -1 self.historic_lookups_id2[query] = monster_no - json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) + json.dump(self.historic_lookups_id2, + open(self.historic_lookups_file_path_id2, "w+")) m = self.get_monster_by_id(nm.monster_id) if nm else None @@ -735,7 +765,9 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): elif server_filter == ServerFilter.jp: monster_index = self.index_jp else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) + raise ValueError( + "server_filter must be type ServerFilter not " + str( + type(server_filter))) return monster_index.find_monster2(query) @@ -821,7 +853,8 @@ def monsterToBaseEmbed(m: "DgMonster"): embed.set_thumbnail(url=monsterToThumbnailUrl(m)) embed.title = header embed.url = get_pdx_url(m) - embed.set_footer(text='Requester may click the reactions below to switch tabs') + embed.set_footer( + text='Requester may click the reactions below to switch tabs') return embed @@ -859,7 +892,8 @@ def addMonsterEvoOfList(monster_list, embed, field_name): field_data = '{} monsters'.format(len(monster_list)) else: item_count = min(len(monster_list), 5) - for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: + for ae in sorted(monster_list, key=lambda x: x.monster_no_na, + reverse=True)[:item_count]: field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) embed.add_field(name=field_name, value=field_data) @@ -881,7 +915,8 @@ def monsterToEvoMatsEmbed(m: "DgMonster"): addMonsterEvoOfList(m.material_of, embed, 'Material for') if not m.evo_gem: return embed - addMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") + addMonsterEvoOfList(m.evo_gem.material_of, embed, + "Tree's gem (may not be this evo) is mat for") return embed @@ -938,9 +973,12 @@ def monsterToPicEmbed(m: "DgMonster", animated=False): embed.set_thumbnail(url='') extra_links = [] if animated: - extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), monsterToGifUrl(m))) + extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), + monsterToGifUrl(m))) if m.orb_skin_id is not None: - extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), monsterToOrbSkinCBUrl(m))) + extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), + monsterToOrbSkinCBUrl( + m))) if len(extra_links) > 0: embed.add_field(name='Extra Links', value='\n'.join(extra_links)) @@ -960,7 +998,8 @@ def monsterToOrbSkinUrl(m: "DgMonster", link_text='Regular'): def monsterToOrbSkinCBUrl(m: "DgMonster", link_text='Color Blind'): - return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) + return '[{}]({})'.format(link_text, + ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) def monsterToGifEmbed(m: "DgMonster"): @@ -986,7 +1025,8 @@ def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): else: rhp, ratk, rrcv, rresist = 1, 1, 1, 0 - multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) + multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, + rrcv, rresist) embed = discord.Embed() embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) @@ -1051,7 +1091,9 @@ def monsterToEmbed(m: "DgMonster", emoji_list): os = "" if m.orb_skin_id is None else " (Orb Skin)" - info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format(m.rarity, m.base_monster.rarity, os, m.cost) + info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format(m.rarity, + m.base_monster.rarity, + os, m.cost) if acquire_text: info_row_2 += '\n**{}**'.format(acquire_text) if m.is_inheritable: @@ -1066,7 +1108,8 @@ def monsterToEmbed(m: "DgMonster", emoji_list): hp, atk, rcv, weighted = m.stats() if m.limit_mult > 0: lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, m.limit_mult) + stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, + m.limit_mult) stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( hp, lb_hp, atk, lb_atk, rcv, lb_rcv) else: @@ -1092,7 +1135,9 @@ def monsterToEmbed(m: "DgMonster", emoji_list): if not len(awakenings_row): awakenings_row = 'No Awakenings' - killers_row = '**Available Killers:** [{} slots] {}'.format(m.latent_slots, ' '.join(m.killers)) + killers_row = '**Available Killers:** [{} slots] {}'.format(m.latent_slots, + ' '.join( + m.killers)) embed.description = '{}\n{}'.format(awakenings_row, killers_row) @@ -1142,10 +1187,10 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): body_text += "\n**JP Name**: {}".format(m.name_jp) body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) if m.history_us: body_text += '\n**History:** {}'.format(m.history_us) @@ -1244,14 +1289,17 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): } -def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=None, resist2=None): +def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, + rcv2=None, resist2=None): if all([x is None for x in (hp2, atk2, rcv2, resist2)]): hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 def fmtNum(val): return ('{:.2f}').format(val).strip('0').rstrip('.') - text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) + text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), + fmtNum(rcv1 * rcv2)) if resist1 > 0 or resist2 > 0: - text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) + text += ' Resist {}%'.format( + fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) return text diff --git a/padmonitor/__init__.py b/padmonitor/__init__.py index db55be78..b9489397 100644 --- a/padmonitor/__init__.py +++ b/padmonitor/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = PadMonitor(bot) bot.add_cog(n) diff --git a/padsearch/__init__.py b/padsearch/__init__.py index bbe85cbb..faf836f2 100644 --- a/padsearch/__init__.py +++ b/padsearch/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadSearch(bot)) diff --git a/padsearch/info.json b/padsearch/info.json index 92b73cb8..b9105701 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -1,17 +1,28 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Search for cards using advanced filters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Card search", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "ply"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Search for cards using advanced filters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Card search", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "ply" + ], + "tags": [ + "PAD" + ] } diff --git a/padvision/__init__.py b/padvision/__init__.py index eca6ba01..ca87a397 100644 --- a/padvision/__init__.py +++ b/padvision/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadVision(bot)) diff --git a/padvision/info.json b/padvision/info.json index f2228d47..86713d17 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "A beefy helper cog for neural networks.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Neural net helper", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["Pillow"], - "tags": ["PAD", "sharedlibrary"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "A beefy helper cog for neural networks.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Neural net helper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "Pillow" + ], + "tags": [ + "PAD", + "sharedlibrary" + ] } diff --git a/padvision/padvision.py b/padvision/padvision.py index 56152801..07aaf7e5 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -1,14 +1,14 @@ +import PIL.Image import io import logging import traceback - -import PIL.Image from redbot.core import commands ORB_IMG_SIZE = 40 logger = logging.getLogger('red.padbot-cogs.padvision') + class PadVision(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -91,7 +91,8 @@ def get_orb_vertices(self, x, y): box_yend = box_ystart + self.orb_size box_xstart = x * self.orb_size + self.xstart box_xend = box_xstart + self.orb_size - return int(box_xstart + offset), int(box_ystart + offset), int(box_xend - offset), int(box_yend - offset) + return int(box_xstart + offset), int(box_ystart + offset), int( + box_xend - offset), int(box_yend - offset) def get_orb_coords(self, x, y): box_xstart, box_ystart, box_xend, box_yend = self.get_orb_vertices(x, y) @@ -150,12 +151,14 @@ def _process(self): input_tensor_idx = input_details['index'] output_tensor_idx = output_details['index'] - input_data = np.zeros((1, input_orb_size, input_orb_size, 3), dtype='uint8') + input_data = np.zeros((1, input_orb_size, input_orb_size, 3), + dtype='uint8') for y, x in board_iterator(): box_coords = oe.get_orb_vertices(x, y) orb_img = self.img.crop(box_coords) - orb_img = orb_img.resize((input_orb_size, input_orb_size), PIL.Image.ANTIALIAS) + orb_img = orb_img.resize((input_orb_size, input_orb_size), + PIL.Image.ANTIALIAS) input_data[0] = np.array(orb_img) interpreter.set_tensor(input_tensor_idx, input_data) diff --git a/pricecheck/__init__.py b/pricecheck/__init__.py index 1a4dff78..520f0547 100644 --- a/pricecheck/__init__.py +++ b/pricecheck/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PriceCheck(bot)) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 29dc0e0d..6ed9520d 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -1,9 +1,7 @@ -import re - import discord +import re from redbot.core import commands, checks, Config from redbot.core.utils.chat_formatting import inline, box, pagify - from tsutils import auth_check PC_TEXT = """{name} - Stamina Cost: {stam_cost} @@ -41,7 +39,8 @@ async def pricecheck(self, ctx, *, query): """Displays pricing data for a tradable non-collab gem.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline( + "Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" @@ -54,11 +53,11 @@ async def pricecheck(self, ctx, *, query): await ctx.send("{} does not have PC data.".format(nm.name_na)) return sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name = nm.name_na, - stam_cost = sc, - pp_val = sc*83/50, - points = sc*83/50/297, - foot = foot) + pct = PC_TEXT.format(name=nm.name_na, + stam_cost=sc, + pp_val=sc * 83 / 50, + points=sc * 83 / 50 / 297, + foot=foot) if await self.config.channel(ctx.channel).dm(): for page in pagify(pct): await ctx.author.send(box(page)) @@ -76,7 +75,8 @@ async def set(self, ctx, stam_cost: float, *, query): """Adds stamina cost data to a card.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline( + "Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" @@ -89,14 +89,17 @@ async def set(self, ctx, stam_cost: float, *, query): if str(nm.base_monster_no) in pcs: foot = pcs[str(nm.base_monster_no)][1] pcs[str(nm.base_monster_no)] = (stam_cost, foot) - await ctx.send(box("Set {} ({}) to {}".format(nm.name_na, nm.base_monster_no, stam_cost))) + await ctx.send(box( + "Set {} ({}) to {}".format(nm.name_na, nm.base_monster_no, + stam_cost))) @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) async def setfooter(self, ctx, query, *, footer=""): """Adds notes regarding the stamina cost of a card.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline( + "Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" @@ -109,14 +112,17 @@ async def setfooter(self, ctx, query, *, footer=""): if str(nm.base_monster_no) in pcs: sc = pcs[str(nm.base_monster_no)][0] pcs[str(nm.base_monster_no)] = (sc, footer.strip('`')) - await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_na, nm.base_monster_no, footer))) + await ctx.send(box( + "Set {} ({}) footer to '{}'".format(nm.name_na, nm.base_monster_no, + footer))) @pcadmin.command(aliases=['delete', 'del', 'rm']) async def remove(self, ctx, *, query): """Removes stamina cost data from a card.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline( + "Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" diff --git a/profile/__init__.py b/profile/__init__.py index 244090d8..da213419 100644 --- a/profile/__init__.py +++ b/profile/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "Profile data is stored persistantly." + def setup(bot): bot.add_cog(Profile(bot)) diff --git a/profile/profile.py b/profile/profile.py index ff0db483..ec88376b 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -1,14 +1,13 @@ -import re -import logging - import discord +import logging +import re from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify - from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.profile') + def normalizeServer(server): server = server.upper().strip() return 'NA' if server == 'US' else server @@ -18,7 +17,8 @@ def normalizeServer(server): def validateAndCleanId(id): - id = id.replace('-', '').replace(' ', '').replace(',', '').replace('.', '').strip() + id = id.replace('-', '').replace(' ', '').replace(',', '').replace('.', + '').strip() if re.match(r'^\d{9}$', id): return id else: @@ -27,7 +27,8 @@ def validateAndCleanId(id): def formatNameLine(server, pad_name, pad_id): group = computeOldGroup(pad_id) - return "[{}]: '{}' : {} (Group {})".format(server, pad_name, formatId(pad_id), group) + return "[{}]: '{}' : {} (Group {})".format(server, pad_name, + formatId(pad_id), group) def formatId(id): @@ -51,9 +52,11 @@ async def red_get_data_for_user(self, *, user_id): data = "Stored data for user with ID {}:\n".format(user_id) if udata['servers']: - data += " - You have a profile on the following server(s): {}.\n".format(', '.join(udata['servers'])) + data += " - You have a profile on the following server(s): {}.\n".format( + ', '.join(udata['servers'])) if udata['default_server']: - data += " - Your default server is {}.\n".format(udata['default_server']) + data += " - Your default server is {}.\n".format( + udata['default_server']) if not any(udata.values()): data = "No data is stored for user with ID {}.\n".format(user_id) @@ -90,8 +93,9 @@ async def idTo(self, ctx, user: discord.Member, server=None): if profile_msg is None: return - warning = inline("{} asked me to send you this message. Report any harassment to the mods.".format( - ctx.author.name)) + warning = inline( + "{} asked me to send you this message. Report any harassment to the mods.".format( + ctx.author.name)) msg = warning + "\n" + profile_msg await ctx.send(user, msg) await ctx.author.send(inline("Sent your profile to " + user.name)) @@ -164,11 +168,14 @@ async def setId(self, ctx, server, *id): id = " ".join(id) clean_id = validateAndCleanId(id) if clean_id is None: - await ctx.send(inline('Your ID looks invalid, expected a 9 digit code, got: {}'.format(id))) + await ctx.send(inline( + 'Your ID looks invalid, expected a 9 digit code, got: {}'.format( + id))) return self.settings.setId(ctx.author.id, server, clean_id) - await ctx.send(inline('Set your id for {} to: {}'.format(server, formatId(clean_id)))) + await ctx.send(inline( + 'Set your id for {} to: {}'.format(server, formatId(clean_id)))) @profile.command(name="name") async def setName(self, ctx, server, *name): @@ -179,7 +186,8 @@ async def setName(self, ctx, server, *name): name = " ".join(name) self.settings.setName(ctx.author.id, server, name) - await ctx.send(inline('Set your name for {} to: {}'.format(server, name))) + await ctx.send( + inline('Set your name for {} to: {}'.format(server, name))) @profile.command(name="text") async def setText(self, ctx, server, *text): @@ -198,7 +206,8 @@ async def setText(self, ctx, server, *text): return self.settings.setProfileText(ctx.author.id, server, text) - await ctx.send(inline('Set your profile for ' + server + ' to:\n' + text)) + await ctx.send( + inline('Set your profile for ' + server + ' to:\n' + text)) @profile.command(name="clear") async def clear(self, ctx, server=None): @@ -231,23 +240,27 @@ async def search(self, ctx, server, *search_text): return # Get all profiles for server - profiles = [p[server] for p in self.settings.profiles().values() if server in p] + profiles = [p[server] for p in self.settings.profiles().values() if + server in p] # Eliminate profiles without an ID set profiles = filter(lambda p: 'id' in p, profiles) profiles = list(profiles) # Match the profiles against the search text - matching_profiles = filter(lambda p: search_text in p.get('text', '').lower(), profiles) + matching_profiles = filter( + lambda p: search_text in p.get('text', '').lower(), profiles) matching_profiles = list(matching_profiles) template = 'Found {}/{} matching profiles in {} for : {}' - msg = template.format(len(matching_profiles), len(profiles), server, search_text) + msg = template.format(len(matching_profiles), len(profiles), server, + search_text) await ctx.send(inline(msg)) if len(matching_profiles) == 0: return - msg = 'Displaying {} matches for server {}:\n'.format(len(matching_profiles), server) + msg = 'Displaying {} matches for server {}:\n'.format( + len(matching_profiles), server) for p in matching_profiles: pad_id = formatId(p['id']) pad_name = p.get('name', 'unknown') diff --git a/schoolidol/__init__.py b/schoolidol/__init__.py index 44e09da4..2379fffc 100644 --- a/schoolidol/__init__.py +++ b/schoolidol/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = SchoolIdol(bot) bot.add_cog(n) diff --git a/schoolidol/info.json b/schoolidol/info.json index f75e8b38..8e50f075 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Displays data about School Idol.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows School Idol data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "aiohttp"], - "tags": ["schoolidol"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Displays data about School Idol.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows School Idol data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "aiohttp" + ], + "tags": [ + "schoolidol" + ] } From 533c60416baacd5afd1da0781a68a546fe25783f Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Sep 2020 23:58:13 -0700 Subject: [PATCH 0119/1220] aparently i just made a commit with nothing in it? i'm not sure what's going on --- chronomagia/chronomagia.py | 39 ++++++++++++------- chronomagia/info.json | 37 +++++++++++------- damagecalc/damagecalc.py | 27 ++++++++----- padboard/__init__.py | 1 + padbuilds/__init__.py | 1 + padbuilds/padbuilds.py | 8 ++-- padglobal/__init__.py | 1 + padglobal/info.json | 44 ++++++++++++++-------- padmonitor/info.json | 40 ++++++++++++-------- padmonitor/padmonitor.py | 5 +-- padsearch/padsearch.py | 77 ++++++++++++++++++++++++-------------- pricecheck/info.json | 44 +++++++++++++--------- profile/info.json | 36 +++++++++++------- schoolidol/schoolidol.py | 26 ++++++++----- 14 files changed, 244 insertions(+), 142 deletions(-) diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index 2a003d20..165d75d5 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -1,22 +1,23 @@ import asyncio import csv import difflib +import discord +import logging import os import traceback +import tsutils import urllib.parse -import logging from collections import OrderedDict - -import discord -import tsutils -from tsutils import Menu, EmojiUpdater from redbot.core import commands, data_manager, checks from redbot.core.utils.chat_formatting import inline +from tsutils import Menu, EmojiUpdater logger = logging.getLogger('red.padbot-cogs.chronomagia') + def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), + file_name) SUMMARY_SHEET = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQsO9Xi9cKaUQWPvDjjIKpHotZ036LCTN66PuNoQwvb8qZi4LmEUEOYmHDyqUJUzghI28aPrQHfRSYd/pub?gid=1488138129&single=true&output=csv' @@ -102,11 +103,13 @@ async def cmid(self, ctx, *, query: str): names_to_card = {x.name_clean: x for x in self.card_data} # Check if the card name starts with the query - matches = list(filter(lambda x: x.startswith(query), names_to_card.keys())) + matches = list( + filter(lambda x: x.startswith(query), names_to_card.keys())) # Find a card that closely matches the query if not matches: - matches = difflib.get_close_matches(query, names_to_card.keys(), n=1, cutoff=.6) + matches = difflib.get_close_matches(query, names_to_card.keys(), + n=1, cutoff=.6) # Find a card that contains the query text if not matches: @@ -128,8 +131,11 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): emoji_to_embed[remove_emoji] = self.menu.reaction_delete_message try: - result_msg, result_embed = await self.menu.custom_menu(ctx, EmojiUpdater(emoji_to_embed), - starting_menu_emoji, timeout=20) + result_msg, result_embed = await self.menu.custom_menu(ctx, + EmojiUpdater( + emoji_to_embed), + starting_menu_emoji, + timeout=20) if result_msg and result_embed: # Message is finished but not deleted, clear the footer result_embed.set_footer(text=discord.Embed.Empty) @@ -141,7 +147,8 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): def make_base_embed(c: CmCard): embed = discord.Embed() embed.title = c.name - embed.set_footer(text='Requester may click the reactions below to switch tabs') + embed.set_footer( + text='Requester may click the reactions below to switch tabs') return embed @@ -149,10 +156,14 @@ def make_embed(c: CmCard): embed = make_base_embed(c) embed.add_field( - name=c.monspell, value='{}\nCost {}'.format(c.rarity, c.cost), inline=True) + name=c.monspell, value='{}\nCost {}'.format(c.rarity, c.cost), + inline=True) if c.monspell == 'Monster': - mtype = '\n{}/{} '.format(c.type1, c.type2) if c.type2 else '{} '.format(c.type1) - embed.add_field(name=mtype, value='Atk {}\nDef {}'.format(c.atk, c.defn), inline=True) + mtype = '\n{}/{} '.format(c.type1, + c.type2) if c.type2 else '{} '.format(c.type1) + embed.add_field(name=mtype, + value='Atk {}\nDef {}'.format(c.atk, c.defn), + inline=True) if c.expansion: embed.add_field(name='Expansion', value=c.expansion, inline=False) diff --git a/chronomagia/info.json b/chronomagia/info.json index 0a71ca8e..c136d0cd 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Displays data about Chrono Ma:Gia.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows Chrono Ma:Gia data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "aiohttp"], - "tags": ["chronomagia"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Displays data about Chrono Ma:Gia.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Chrono Ma:Gia data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "aiohttp" + ], + "tags": [ + "chronomagia" + ] } diff --git a/damagecalc/damagecalc.py b/damagecalc/damagecalc.py index c0dd510d..1949ca7a 100644 --- a/damagecalc/damagecalc.py +++ b/damagecalc/damagecalc.py @@ -62,7 +62,8 @@ def t_ROW(self, t): t.value = t.value.strip('row').strip('(').strip(')') t.value = int(t.value) if t.value else 6 if t.value < 6 or t.value > 30: - raise commands.UserFeedbackCheckFailure('row must have 6-30 orbs, got ' + t.value) + raise commands.UserFeedbackCheckFailure( + 'row must have 6-30 orbs, got ' + t.value) return t @@ -76,7 +77,8 @@ def t_ORB(self, t): t.value = t.value.strip('orb').strip('s').strip('(').strip(')') t.value = int(t.value) if t.value else 3 if t.value < 3 or t.value > 30: - raise commands.UserFeedbackCheckFailure('match must have 3-30 orbs, got ' + t.value) + raise commands.UserFeedbackCheckFailure( + 'match must have 3-30 orbs, got ' + t.value) return t def t_COMBO(self, t): @@ -88,7 +90,8 @@ def t_COMBO(self, t): t_ignore = ' \t\n' def t_error(self, t): - raise commands.UserFeedbackCheckFailure("Invalid text: '%s'" % (t.value,)) + raise commands.UserFeedbackCheckFailure( + "Invalid text: '%s'" % (t.value,)) def build(self, **kwargs): # pass debug=1 to enable verbose output @@ -148,14 +151,17 @@ def __init__(self, lexer): if self.combos is None: self.combos = 0 - if (len(self.row_matches) + len(self.tpa_matches) + len(self.orb_matches)) == 0: - raise commands.UserFeedbackCheckFailure('You need to specify at least one attack match (orb, tpa, row)') + if (len(self.row_matches) + len(self.tpa_matches) + len( + self.orb_matches)) == 0: + raise commands.UserFeedbackCheckFailure( + 'You need to specify at least one attack match (orb, tpa, row)') def setIfType(self, expected_type, given_type, current_value, new_value): if expected_type != given_type: return current_value if current_value is not None: - raise commands.UserFeedbackCheckFailure('You set {} more than once'.format(given_type)) + raise commands.UserFeedbackCheckFailure( + 'You set {} more than once'.format(given_type)) return new_value def updateWithMonster(self, monster): @@ -166,7 +172,8 @@ def updateWithMonster(self, monster): def calculateMatchDamage(self, match, all_enhanced): orb_damage = (1 + (match - 3) * .25) - oe_damage = (1 + .06 * match) * (1 + .05 * self.oe) if all_enhanced else 1 + oe_damage = (1 + .06 * match) * ( + 1 + .05 * self.oe) if all_enhanced else 1 tpa_damage = math.pow(1.5, self.tpas) if match == 4 else 1 return self.atk * orb_damage * oe_damage * tpa_damage @@ -175,7 +182,8 @@ def calculate(self, all_enhanced): for match in (self.row_matches + self.tpa_matches + self.orb_matches): base_damage += self.calculateMatchDamage(match, all_enhanced) - combo_count = len(self.row_matches) + len(self.tpa_matches) + len(self.orb_matches) + self.combos + combo_count = len(self.row_matches) + len(self.tpa_matches) + len( + self.orb_matches) + self.combos combo_mult = 1 + (combo_count - 1) * .25 row_mult = 1 + self.rows / 10 * len(self.row_matches) @@ -266,6 +274,7 @@ async def damage(self, ctx, *, damage_spec): damage = config.calculate(all_enhanced=False) enhanced_damage = config.calculate(all_enhanced=True) await ctx.send( - "```Damage (no enhanced) : {}\nDamage (all enhanced) : {}```".format(damage, enhanced_damage)) + "```Damage (no enhanced) : {}\nDamage (all enhanced) : {}```".format( + damage, enhanced_damage)) except commands.UserFeedbackCheckFailure as e: await ctx.send(e.message) diff --git a/padboard/__init__.py b/padboard/__init__.py index 97b63a4e..ddbada96 100644 --- a/padboard/__init__.py +++ b/padboard/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored persistantly." + def setup(bot): bot.add_cog(PadBoard(bot)) diff --git a/padbuilds/__init__.py b/padbuilds/__init__.py index d3532a22..655f0aca 100644 --- a/padbuilds/__init__.py +++ b/padbuilds/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadBuilds(bot)) diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index 90f5a545..023b3e3c 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -1,10 +1,9 @@ -import re - import discord -from tsutils import CogSettings +import re from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import pagify, box +from tsutils import CogSettings class PadBuilds(commands.Cog): @@ -137,7 +136,8 @@ async def list(self, ctx): @commands.Cog.listener("on_message") async def on_message(self, message): - if len(message.content) < 2 or isinstance(message.channel, discord.abc.PrivateChannel): + if len(message.content) < 2 or isinstance(message.channel, + discord.abc.PrivateChannel): return server = message.guild diff --git a/padglobal/__init__.py b/padglobal/__init__.py index e722b093..04aa3de7 100644 --- a/padglobal/__init__.py +++ b/padglobal/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadGlobal(bot)) diff --git a/padglobal/info.json b/padglobal/info.json index 39f32672..df8836e6 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -1,18 +1,30 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Tsubaki guides and tips written by P&D professionals", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "aiohttp", "prettytable"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Tsubaki guides and tips written by P&D professionals", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "aiohttp", + "prettytable" + ], + "tags": [ + "PAD" + ] } diff --git a/padmonitor/info.json b/padmonitor/info.json index b97f09f9..ad76ff92 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -1,17 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Checks for and announces new monsters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Announce new monsters", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Checks for and announces new monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Announce new monsters", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 7146466b..96a71db3 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,13 +1,12 @@ import asyncio import logging - +import tsutils from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline, box, pagify -import tsutils - logger = logging.getLogger('red.padbot-cogs.padmonitor') + class PadMonitor(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 4818ef86..39aa84e4 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -1,13 +1,11 @@ import json -import re import logging +import re from fnmatch import fnmatch - from ply import lex from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify - from tsutils import tsutils logger = logging.getLogger('red.padbot-cogs.padsearch') @@ -406,7 +404,8 @@ def t_WEIGHTED(self, t): t_ignore = ' \t\n' def t_error(self, t): - raise commands.UserFeedbackCheckFailure("Unknown text '%s'" % (t.value,)) + raise commands.UserFeedbackCheckFailure( + "Unknown text '%s'" % (t.value,)) def build(self, **kwargs): # pass debug=1 to enable verbose output @@ -455,21 +454,26 @@ def __init__(self, lexer): value = tok.value self.all |= type == 'ALL' self.cd = self.setIfType('CD', type, self.cd, value) - self.farmable = self.setIfType('FARMABLE', type, self.farmable, value) + self.farmable = self.setIfType('FARMABLE', type, self.farmable, + value) self.haste = self.setIfType('HASTE', type, self.haste, value) - self.inheritable = self.setIfType('INHERITABLE', type, self.inheritable, value) + self.inheritable = self.setIfType('INHERITABLE', type, + self.inheritable, value) self.shuffle = self.setIfType('SHUFFLE', type, self.shuffle, value) self.unlock = self.setIfType('UNLOCK', type, self.unlock, value) self.resolve = self.setIfType('RESOLVE', type, self.resolve, value) self.delay = self.setIfType('DELAY', type, self.delay, value) self.combo = self.setIfType('COMBO', type, self.combo, value) - self.absorbnull = self.setIfType('ABSORBNULL', type, self.absorbnull, value) - self.attabsorb = self.setIfType('ATTABSORB', type, self.attabsorb, value) + self.absorbnull = self.setIfType('ABSORBNULL', type, + self.absorbnull, value) + self.attabsorb = self.setIfType('ATTABSORB', type, self.attabsorb, + value) self.shield = self.setIfType('SHIELD', type, self.shield, value) self.atk = self.setIfType('ATK', type, self.atk, value) self.hp = self.setIfType('HP', type, self.hp, value) self.rcv = self.setIfType('RCV', type, self.rcv, value) - self.weighted = self.setIfType('WEIGHTED', type, self.weighted, value) + self.weighted = self.setIfType('WEIGHTED', type, self.weighted, + value) if type == 'ACTIVE': self.active.append(value) @@ -498,7 +502,8 @@ def __init__(self, lexer): if type == 'TYPE': if value not in TYPES: raise commands.UserFeedbackCheckFailure( - 'Unexpected type {}, expected one of {}'.format(value, TYPES)) + 'Unexpected type {}, expected one of {}'.format(value, + TYPES)) self.types.append(value) if type == 'REMOVE': self.remove.append(value) @@ -513,7 +518,8 @@ def __init__(self, lexer): # Single if self.cd: - self.filters.append(lambda m: m.search.active_min and m.search.active_min <= self.cd) + self.filters.append(lambda + m: m.search.active_min and m.search.active_min <= self.cd) if self.farmable: self.filters.append(lambda m: m.farmable_evo) @@ -551,10 +557,12 @@ def __init__(self, lexer): self.filters.append(lambda m, tt=text_to, tf=text_from: - [tt] in m.search.orb_convert.values() if text_from == 'any' else - (tf in m.search.orb_convert.keys() if text_to == 'any' else - (tf in m.search.orb_convert.keys() and - tt in m.search.orb_convert[tf]))) + [ + tt] in m.search.orb_convert.values() if text_from == 'any' else + ( + tf in m.search.orb_convert.keys() if text_to == 'any' else + (tf in m.search.orb_convert.keys() and + tt in m.search.orb_convert[tf]))) if self.absorbnull: text = 'damage absorb shield' @@ -569,17 +577,21 @@ def __init__(self, lexer): self.filters.append(lambda m, t=text: t in m.search.active_desc) if self.atk: - self.filters.append(lambda m: m.search.atk and m.search.atk >= self.atk) + self.filters.append( + lambda m: m.search.atk and m.search.atk >= self.atk) if self.hp: - self.filters.append(lambda m: m.search.hp and m.search.hp >= self.hp) + self.filters.append( + lambda m: m.search.hp and m.search.hp >= self.hp) if self.rcv: - self.filters.append(lambda m: m.search.rcv and m.search.rcv >= self.rcv) + self.filters.append( + lambda m: m.search.rcv and m.search.rcv >= self.rcv) if self.weighted: self.filters.append( - lambda m: m.search.weighted_stats and m.search.weighted_stats >= self.weighted) + lambda + m: m.search.weighted_stats and m.search.weighted_stats >= self.weighted) # Multiple if self.reactive: @@ -618,7 +630,8 @@ def __init__(self, lexer): if text == 'any': filters.append(lambda m: m.search.column_convert) else: - filters.append(lambda m, t=text: t in m.search.column_convert) + filters.append( + lambda m, t=text: t in m.search.column_convert) self.filters.append(self.or_filters(filters)) if self.hascolor: @@ -675,7 +688,8 @@ def __init__(self, lexer): filters.append(lambda m, t=text: t not in m.search.name) self.filters.append(self.or_filters(filters)) if not self.filters and not self.re_filters and not self.gl_filters: - raise commands.UserFeedbackCheckFailure('You need to specify at least one filter') + raise commands.UserFeedbackCheckFailure( + 'You need to specify at least one filter') def check_filters(self, m): for f in self.filters: @@ -689,9 +703,11 @@ async def check_re_filters(self, ms, ctx): except TimeoutError: logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( '", "'.join(self.regeces), ctx.author.name, ctx.author.id)) - raise commands.UserFeedbackCheckFailure("Regex took too long to compile. Stop trying to break the bot") + raise commands.UserFeedbackCheckFailure( + "Regex took too long to compile. Stop trying to break the bot") except re.error as e: - raise commands.UserFeedbackCheckFailure("Regex search threw error '{}'".format(e.msg)) + raise commands.UserFeedbackCheckFailure( + "Regex search threw error '{}'".format(e.msg)) async def check_glob_filters(self, ms, ctx): try: @@ -699,7 +715,8 @@ async def check_glob_filters(self, ms, ctx): except TimeoutError: logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( '", "'.join(self.globs), ctx.author.name, ctx.author.id)) - raise commands.UserFeedbackCheckFailure("Glob took too long to compile. Stop trying to break the bot") + raise commands.UserFeedbackCheckFailure( + "Glob took too long to compile. Stop trying to break the bot") def or_filters(self, filters): def fn(m, filters=filters): @@ -714,7 +731,8 @@ def setIfType(self, expected_type, given_type, current_value, new_value): if expected_type != given_type: return current_value if current_value is not None: - raise commands.UserFeedbackCheckFailure('You set {} more than once'.format(given_type)) + raise commands.UserFeedbackCheckFailure( + 'You set {} more than once'.format(given_type)) return new_value @@ -768,7 +786,8 @@ async def search(self, ctx, *, filter_spec: str): matched_monsters = [m for m in monsters if config.check_filters(m)] # Running regeces/globs on a timer matched_monsters = await config.check_re_filters(matched_monsters, ctx) - matched_monsters = await config.check_glob_filters(matched_monsters, ctx) + matched_monsters = await config.check_glob_filters(matched_monsters, + ctx) # Removing entry with names that have gems in it @@ -777,7 +796,8 @@ async def search(self, ctx, *, filter_spec: str): msg = 'Matched {} monsters'.format(len(matched_monsters)) dm_required = False if len(matched_monsters) == len(monsters): - await ctx.send("All monsters were matched. Try with a more specific query.") + await ctx.send( + "All monsters were matched. Try with a more specific query.") return if len(matched_monsters) > 10: if not config.all: @@ -817,4 +837,5 @@ async def debugsearch(self, ctx, *, query): await ctx.send(box('No match: ' + err)) return - await ctx.send(box(json.dumps(m.search, indent=2, default=lambda o: o.__dict__))) + await ctx.send( + box(json.dumps(m.search, indent=2, default=lambda o: o.__dict__))) diff --git a/pricecheck/info.json b/pricecheck/info.json index 6f0e4fd4..879a94df 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -1,19 +1,29 @@ { - "author": ["Aradia Megido#2552"], - "description": "A cog that enables the display of pricing information for evolution material gems.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Price Check", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "Aradia Megido#2552" + ], + "description": "A cog that enables the display of pricing information for evolution material gems.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Price Check", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/profile/info.json b/profile/info.json index 9d5481f1..88dbb00e 100644 --- a/profile/info.json +++ b/profile/info.json @@ -1,15 +1,25 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Holds all of your Puzzle & Dragons accounts in one place", - "end_user_data_statement": "Profile data is stored persistantly.", - "install_msg": "", - "short": "P&D Phonebook", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Holds all of your Puzzle & Dragons accounts in one place", + "end_user_data_statement": "Profile data is stored persistantly.", + "install_msg": "", + "short": "P&D Phonebook", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index a7c9688f..33da79ba 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -1,13 +1,11 @@ +import aiohttp import difflib +import discord import json import logging from collections import OrderedDict - -import aiohttp -import discord from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline - from tsutils import Menu, EmojiUpdater, char_to_emoji logger = logging.getLogger('red.padbot-cogs.schoolidol') @@ -52,12 +50,17 @@ async def reload_sif(self): logger.info('done retrieving cards: {}'.format(len(self.card_data))) self.id_to_card = {c['id']: c for c in self.card_data} - name_to_card = {'{}'.format(c['idol']['name']).lower(): c for c in self.card_data} - firstname_to_card = {c['idol']['name'].lower().split(' ')[-1]: c for c in self.card_data} + name_to_card = {'{}'.format(c['idol']['name']).lower(): c for c in + self.card_data} + firstname_to_card = {c['idol']['name'].lower().split(' ')[-1]: c for c + in self.card_data} collection_name_to_card = {'{} {}'.format( - c['translated_collection'], c['idol']['name']).lower(): c for c in self.card_data} + c['translated_collection'], c['idol']['name']).lower(): c for c in + self.card_data} collection_firstname_to_card = {'{} {}'.format( - c['translated_collection'], c['idol']['name'].split(' ')[-1]).lower(): c for c in self.card_data} + c['translated_collection'], + c['idol']['name'].split(' ')[-1]).lower(): c for c in + self.card_data} self.names_to_card = { **name_to_card, **firstname_to_card, @@ -110,7 +113,9 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): try: result_msg, result_embed = await self.menu.custom_menu(ctx, - EmojiUpdater(emoji_to_embed), starting_menu_emoji, + EmojiUpdater( + emoji_to_embed), + starting_menu_emoji, timeout=20) if result_msg and result_embed: # Message is finished but not deleted, clear the footer @@ -132,7 +137,8 @@ def make_card_embed(c, url_field): embed.title = toHeader(c) embed.url = get_info_url(c) embed.set_image(url=base_url.format(cid, url_field)) - embed.set_footer(text='Requester may click the reactions below to switch tabs') + embed.set_footer( + text='Requester may click the reactions below to switch tabs') return embed From 882be85c68a32579c5e9426a97358354eee110a1 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 15 Sep 2020 00:20:35 -0700 Subject: [PATCH 0120/1220] Revert "aparently i just made a commit with nothing in it? i'm not sure what's going on" This reverts commit 533c60416baacd5afd1da0781a68a546fe25783f. --- chronomagia/chronomagia.py | 39 +++++++------------ chronomagia/info.json | 37 +++++++----------- damagecalc/damagecalc.py | 27 +++++-------- padboard/__init__.py | 1 - padbuilds/__init__.py | 1 - padbuilds/padbuilds.py | 8 ++-- padglobal/__init__.py | 1 - padglobal/info.json | 44 ++++++++-------------- padmonitor/info.json | 40 ++++++++------------ padmonitor/padmonitor.py | 5 ++- padsearch/padsearch.py | 77 ++++++++++++++------------------------ pricecheck/info.json | 44 +++++++++------------- profile/info.json | 36 +++++++----------- schoolidol/schoolidol.py | 26 +++++-------- 14 files changed, 142 insertions(+), 244 deletions(-) diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index 165d75d5..2a003d20 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -1,23 +1,22 @@ import asyncio import csv import difflib -import discord -import logging import os import traceback -import tsutils import urllib.parse +import logging from collections import OrderedDict + +import discord +import tsutils +from tsutils import Menu, EmojiUpdater from redbot.core import commands, data_manager, checks from redbot.core.utils.chat_formatting import inline -from tsutils import Menu, EmojiUpdater logger = logging.getLogger('red.padbot-cogs.chronomagia') - def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), - file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) SUMMARY_SHEET = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQsO9Xi9cKaUQWPvDjjIKpHotZ036LCTN66PuNoQwvb8qZi4LmEUEOYmHDyqUJUzghI28aPrQHfRSYd/pub?gid=1488138129&single=true&output=csv' @@ -103,13 +102,11 @@ async def cmid(self, ctx, *, query: str): names_to_card = {x.name_clean: x for x in self.card_data} # Check if the card name starts with the query - matches = list( - filter(lambda x: x.startswith(query), names_to_card.keys())) + matches = list(filter(lambda x: x.startswith(query), names_to_card.keys())) # Find a card that closely matches the query if not matches: - matches = difflib.get_close_matches(query, names_to_card.keys(), - n=1, cutoff=.6) + matches = difflib.get_close_matches(query, names_to_card.keys(), n=1, cutoff=.6) # Find a card that contains the query text if not matches: @@ -131,11 +128,8 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): emoji_to_embed[remove_emoji] = self.menu.reaction_delete_message try: - result_msg, result_embed = await self.menu.custom_menu(ctx, - EmojiUpdater( - emoji_to_embed), - starting_menu_emoji, - timeout=20) + result_msg, result_embed = await self.menu.custom_menu(ctx, EmojiUpdater(emoji_to_embed), + starting_menu_emoji, timeout=20) if result_msg and result_embed: # Message is finished but not deleted, clear the footer result_embed.set_footer(text=discord.Embed.Empty) @@ -147,8 +141,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): def make_base_embed(c: CmCard): embed = discord.Embed() embed.title = c.name - embed.set_footer( - text='Requester may click the reactions below to switch tabs') + embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed @@ -156,14 +149,10 @@ def make_embed(c: CmCard): embed = make_base_embed(c) embed.add_field( - name=c.monspell, value='{}\nCost {}'.format(c.rarity, c.cost), - inline=True) + name=c.monspell, value='{}\nCost {}'.format(c.rarity, c.cost), inline=True) if c.monspell == 'Monster': - mtype = '\n{}/{} '.format(c.type1, - c.type2) if c.type2 else '{} '.format(c.type1) - embed.add_field(name=mtype, - value='Atk {}\nDef {}'.format(c.atk, c.defn), - inline=True) + mtype = '\n{}/{} '.format(c.type1, c.type2) if c.type2 else '{} '.format(c.type1) + embed.add_field(name=mtype, value='Atk {}\nDef {}'.format(c.atk, c.defn), inline=True) if c.expansion: embed.add_field(name='Expansion', value=c.expansion, inline=False) diff --git a/chronomagia/info.json b/chronomagia/info.json index c136d0cd..0a71ca8e 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -1,26 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Displays data about Chrono Ma:Gia.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows Chrono Ma:Gia data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "tsutils", - "aiohttp" - ], - "tags": [ - "chronomagia" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Displays data about Chrono Ma:Gia.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Chrono Ma:Gia data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["tsutils", "aiohttp"], + "tags": ["chronomagia"] } diff --git a/damagecalc/damagecalc.py b/damagecalc/damagecalc.py index 1949ca7a..c0dd510d 100644 --- a/damagecalc/damagecalc.py +++ b/damagecalc/damagecalc.py @@ -62,8 +62,7 @@ def t_ROW(self, t): t.value = t.value.strip('row').strip('(').strip(')') t.value = int(t.value) if t.value else 6 if t.value < 6 or t.value > 30: - raise commands.UserFeedbackCheckFailure( - 'row must have 6-30 orbs, got ' + t.value) + raise commands.UserFeedbackCheckFailure('row must have 6-30 orbs, got ' + t.value) return t @@ -77,8 +76,7 @@ def t_ORB(self, t): t.value = t.value.strip('orb').strip('s').strip('(').strip(')') t.value = int(t.value) if t.value else 3 if t.value < 3 or t.value > 30: - raise commands.UserFeedbackCheckFailure( - 'match must have 3-30 orbs, got ' + t.value) + raise commands.UserFeedbackCheckFailure('match must have 3-30 orbs, got ' + t.value) return t def t_COMBO(self, t): @@ -90,8 +88,7 @@ def t_COMBO(self, t): t_ignore = ' \t\n' def t_error(self, t): - raise commands.UserFeedbackCheckFailure( - "Invalid text: '%s'" % (t.value,)) + raise commands.UserFeedbackCheckFailure("Invalid text: '%s'" % (t.value,)) def build(self, **kwargs): # pass debug=1 to enable verbose output @@ -151,17 +148,14 @@ def __init__(self, lexer): if self.combos is None: self.combos = 0 - if (len(self.row_matches) + len(self.tpa_matches) + len( - self.orb_matches)) == 0: - raise commands.UserFeedbackCheckFailure( - 'You need to specify at least one attack match (orb, tpa, row)') + if (len(self.row_matches) + len(self.tpa_matches) + len(self.orb_matches)) == 0: + raise commands.UserFeedbackCheckFailure('You need to specify at least one attack match (orb, tpa, row)') def setIfType(self, expected_type, given_type, current_value, new_value): if expected_type != given_type: return current_value if current_value is not None: - raise commands.UserFeedbackCheckFailure( - 'You set {} more than once'.format(given_type)) + raise commands.UserFeedbackCheckFailure('You set {} more than once'.format(given_type)) return new_value def updateWithMonster(self, monster): @@ -172,8 +166,7 @@ def updateWithMonster(self, monster): def calculateMatchDamage(self, match, all_enhanced): orb_damage = (1 + (match - 3) * .25) - oe_damage = (1 + .06 * match) * ( - 1 + .05 * self.oe) if all_enhanced else 1 + oe_damage = (1 + .06 * match) * (1 + .05 * self.oe) if all_enhanced else 1 tpa_damage = math.pow(1.5, self.tpas) if match == 4 else 1 return self.atk * orb_damage * oe_damage * tpa_damage @@ -182,8 +175,7 @@ def calculate(self, all_enhanced): for match in (self.row_matches + self.tpa_matches + self.orb_matches): base_damage += self.calculateMatchDamage(match, all_enhanced) - combo_count = len(self.row_matches) + len(self.tpa_matches) + len( - self.orb_matches) + self.combos + combo_count = len(self.row_matches) + len(self.tpa_matches) + len(self.orb_matches) + self.combos combo_mult = 1 + (combo_count - 1) * .25 row_mult = 1 + self.rows / 10 * len(self.row_matches) @@ -274,7 +266,6 @@ async def damage(self, ctx, *, damage_spec): damage = config.calculate(all_enhanced=False) enhanced_damage = config.calculate(all_enhanced=True) await ctx.send( - "```Damage (no enhanced) : {}\nDamage (all enhanced) : {}```".format( - damage, enhanced_damage)) + "```Damage (no enhanced) : {}\nDamage (all enhanced) : {}```".format(damage, enhanced_damage)) except commands.UserFeedbackCheckFailure as e: await ctx.send(e.message) diff --git a/padboard/__init__.py b/padboard/__init__.py index ddbada96..97b63a4e 100644 --- a/padboard/__init__.py +++ b/padboard/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "No personal data is stored persistantly." - def setup(bot): bot.add_cog(PadBoard(bot)) diff --git a/padbuilds/__init__.py b/padbuilds/__init__.py index 655f0aca..d3532a22 100644 --- a/padbuilds/__init__.py +++ b/padbuilds/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): bot.add_cog(PadBuilds(bot)) diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index 023b3e3c..90f5a545 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -1,9 +1,10 @@ -import discord import re + +import discord +from tsutils import CogSettings from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import pagify, box -from tsutils import CogSettings class PadBuilds(commands.Cog): @@ -136,8 +137,7 @@ async def list(self, ctx): @commands.Cog.listener("on_message") async def on_message(self, message): - if len(message.content) < 2 or isinstance(message.channel, - discord.abc.PrivateChannel): + if len(message.content) < 2 or isinstance(message.channel, discord.abc.PrivateChannel): return server = message.guild diff --git a/padglobal/__init__.py b/padglobal/__init__.py index 04aa3de7..e722b093 100644 --- a/padglobal/__init__.py +++ b/padglobal/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): bot.add_cog(PadGlobal(bot)) diff --git a/padglobal/info.json b/padglobal/info.json index df8836e6..39f32672 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -1,30 +1,18 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Tsubaki guides and tips written by P&D professionals", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils", - "aiohttp", - "prettytable" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Tsubaki guides and tips written by P&D professionals", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["tsutils", "aiohttp", "prettytable"], + "tags": ["PAD"] } diff --git a/padmonitor/info.json b/padmonitor/info.json index ad76ff92..b97f09f9 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -1,27 +1,17 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Checks for and announces new monsters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Announce new monsters", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Checks for and announces new monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Announce new monsters", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["tsutils"], + "tags": ["PAD"] } diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 96a71db3..7146466b 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,11 +1,12 @@ import asyncio import logging -import tsutils + from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline, box, pagify -logger = logging.getLogger('red.padbot-cogs.padmonitor') +import tsutils +logger = logging.getLogger('red.padbot-cogs.padmonitor') class PadMonitor(commands.Cog): def __init__(self, bot, *args, **kwargs): diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 39aa84e4..4818ef86 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -1,11 +1,13 @@ import json -import logging import re +import logging from fnmatch import fnmatch + from ply import lex from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify + from tsutils import tsutils logger = logging.getLogger('red.padbot-cogs.padsearch') @@ -404,8 +406,7 @@ def t_WEIGHTED(self, t): t_ignore = ' \t\n' def t_error(self, t): - raise commands.UserFeedbackCheckFailure( - "Unknown text '%s'" % (t.value,)) + raise commands.UserFeedbackCheckFailure("Unknown text '%s'" % (t.value,)) def build(self, **kwargs): # pass debug=1 to enable verbose output @@ -454,26 +455,21 @@ def __init__(self, lexer): value = tok.value self.all |= type == 'ALL' self.cd = self.setIfType('CD', type, self.cd, value) - self.farmable = self.setIfType('FARMABLE', type, self.farmable, - value) + self.farmable = self.setIfType('FARMABLE', type, self.farmable, value) self.haste = self.setIfType('HASTE', type, self.haste, value) - self.inheritable = self.setIfType('INHERITABLE', type, - self.inheritable, value) + self.inheritable = self.setIfType('INHERITABLE', type, self.inheritable, value) self.shuffle = self.setIfType('SHUFFLE', type, self.shuffle, value) self.unlock = self.setIfType('UNLOCK', type, self.unlock, value) self.resolve = self.setIfType('RESOLVE', type, self.resolve, value) self.delay = self.setIfType('DELAY', type, self.delay, value) self.combo = self.setIfType('COMBO', type, self.combo, value) - self.absorbnull = self.setIfType('ABSORBNULL', type, - self.absorbnull, value) - self.attabsorb = self.setIfType('ATTABSORB', type, self.attabsorb, - value) + self.absorbnull = self.setIfType('ABSORBNULL', type, self.absorbnull, value) + self.attabsorb = self.setIfType('ATTABSORB', type, self.attabsorb, value) self.shield = self.setIfType('SHIELD', type, self.shield, value) self.atk = self.setIfType('ATK', type, self.atk, value) self.hp = self.setIfType('HP', type, self.hp, value) self.rcv = self.setIfType('RCV', type, self.rcv, value) - self.weighted = self.setIfType('WEIGHTED', type, self.weighted, - value) + self.weighted = self.setIfType('WEIGHTED', type, self.weighted, value) if type == 'ACTIVE': self.active.append(value) @@ -502,8 +498,7 @@ def __init__(self, lexer): if type == 'TYPE': if value not in TYPES: raise commands.UserFeedbackCheckFailure( - 'Unexpected type {}, expected one of {}'.format(value, - TYPES)) + 'Unexpected type {}, expected one of {}'.format(value, TYPES)) self.types.append(value) if type == 'REMOVE': self.remove.append(value) @@ -518,8 +513,7 @@ def __init__(self, lexer): # Single if self.cd: - self.filters.append(lambda - m: m.search.active_min and m.search.active_min <= self.cd) + self.filters.append(lambda m: m.search.active_min and m.search.active_min <= self.cd) if self.farmable: self.filters.append(lambda m: m.farmable_evo) @@ -557,12 +551,10 @@ def __init__(self, lexer): self.filters.append(lambda m, tt=text_to, tf=text_from: - [ - tt] in m.search.orb_convert.values() if text_from == 'any' else - ( - tf in m.search.orb_convert.keys() if text_to == 'any' else - (tf in m.search.orb_convert.keys() and - tt in m.search.orb_convert[tf]))) + [tt] in m.search.orb_convert.values() if text_from == 'any' else + (tf in m.search.orb_convert.keys() if text_to == 'any' else + (tf in m.search.orb_convert.keys() and + tt in m.search.orb_convert[tf]))) if self.absorbnull: text = 'damage absorb shield' @@ -577,21 +569,17 @@ def __init__(self, lexer): self.filters.append(lambda m, t=text: t in m.search.active_desc) if self.atk: - self.filters.append( - lambda m: m.search.atk and m.search.atk >= self.atk) + self.filters.append(lambda m: m.search.atk and m.search.atk >= self.atk) if self.hp: - self.filters.append( - lambda m: m.search.hp and m.search.hp >= self.hp) + self.filters.append(lambda m: m.search.hp and m.search.hp >= self.hp) if self.rcv: - self.filters.append( - lambda m: m.search.rcv and m.search.rcv >= self.rcv) + self.filters.append(lambda m: m.search.rcv and m.search.rcv >= self.rcv) if self.weighted: self.filters.append( - lambda - m: m.search.weighted_stats and m.search.weighted_stats >= self.weighted) + lambda m: m.search.weighted_stats and m.search.weighted_stats >= self.weighted) # Multiple if self.reactive: @@ -630,8 +618,7 @@ def __init__(self, lexer): if text == 'any': filters.append(lambda m: m.search.column_convert) else: - filters.append( - lambda m, t=text: t in m.search.column_convert) + filters.append(lambda m, t=text: t in m.search.column_convert) self.filters.append(self.or_filters(filters)) if self.hascolor: @@ -688,8 +675,7 @@ def __init__(self, lexer): filters.append(lambda m, t=text: t not in m.search.name) self.filters.append(self.or_filters(filters)) if not self.filters and not self.re_filters and not self.gl_filters: - raise commands.UserFeedbackCheckFailure( - 'You need to specify at least one filter') + raise commands.UserFeedbackCheckFailure('You need to specify at least one filter') def check_filters(self, m): for f in self.filters: @@ -703,11 +689,9 @@ async def check_re_filters(self, ms, ctx): except TimeoutError: logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( '", "'.join(self.regeces), ctx.author.name, ctx.author.id)) - raise commands.UserFeedbackCheckFailure( - "Regex took too long to compile. Stop trying to break the bot") + raise commands.UserFeedbackCheckFailure("Regex took too long to compile. Stop trying to break the bot") except re.error as e: - raise commands.UserFeedbackCheckFailure( - "Regex search threw error '{}'".format(e.msg)) + raise commands.UserFeedbackCheckFailure("Regex search threw error '{}'".format(e.msg)) async def check_glob_filters(self, ms, ctx): try: @@ -715,8 +699,7 @@ async def check_glob_filters(self, ms, ctx): except TimeoutError: logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( '", "'.join(self.globs), ctx.author.name, ctx.author.id)) - raise commands.UserFeedbackCheckFailure( - "Glob took too long to compile. Stop trying to break the bot") + raise commands.UserFeedbackCheckFailure("Glob took too long to compile. Stop trying to break the bot") def or_filters(self, filters): def fn(m, filters=filters): @@ -731,8 +714,7 @@ def setIfType(self, expected_type, given_type, current_value, new_value): if expected_type != given_type: return current_value if current_value is not None: - raise commands.UserFeedbackCheckFailure( - 'You set {} more than once'.format(given_type)) + raise commands.UserFeedbackCheckFailure('You set {} more than once'.format(given_type)) return new_value @@ -786,8 +768,7 @@ async def search(self, ctx, *, filter_spec: str): matched_monsters = [m for m in monsters if config.check_filters(m)] # Running regeces/globs on a timer matched_monsters = await config.check_re_filters(matched_monsters, ctx) - matched_monsters = await config.check_glob_filters(matched_monsters, - ctx) + matched_monsters = await config.check_glob_filters(matched_monsters, ctx) # Removing entry with names that have gems in it @@ -796,8 +777,7 @@ async def search(self, ctx, *, filter_spec: str): msg = 'Matched {} monsters'.format(len(matched_monsters)) dm_required = False if len(matched_monsters) == len(monsters): - await ctx.send( - "All monsters were matched. Try with a more specific query.") + await ctx.send("All monsters were matched. Try with a more specific query.") return if len(matched_monsters) > 10: if not config.all: @@ -837,5 +817,4 @@ async def debugsearch(self, ctx, *, query): await ctx.send(box('No match: ' + err)) return - await ctx.send( - box(json.dumps(m.search, indent=2, default=lambda o: o.__dict__))) + await ctx.send(box(json.dumps(m.search, indent=2, default=lambda o: o.__dict__))) diff --git a/pricecheck/info.json b/pricecheck/info.json index 879a94df..6f0e4fd4 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -1,29 +1,19 @@ { - "author": [ - "Aradia Megido#2552" - ], - "description": "A cog that enables the display of pricing information for evolution material gems.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Price Check", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils" - ], - "tags": [ - "PAD" - ] + "author": ["Aradia Megido#2552"], + "description": "A cog that enables the display of pricing information for evolution material gems.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Price Check", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["tsutils"], + "tags": ["PAD"] } diff --git a/profile/info.json b/profile/info.json index 88dbb00e..9d5481f1 100644 --- a/profile/info.json +++ b/profile/info.json @@ -1,25 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Holds all of your Puzzle & Dragons accounts in one place", - "end_user_data_statement": "Profile data is stored persistantly.", - "install_msg": "", - "short": "P&D Phonebook", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "tsutils" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Holds all of your Puzzle & Dragons accounts in one place", + "end_user_data_statement": "Profile data is stored persistantly.", + "install_msg": "", + "short": "P&D Phonebook", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["tsutils"], + "tags": ["PAD"] } diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index 33da79ba..a7c9688f 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -1,11 +1,13 @@ -import aiohttp import difflib -import discord import json import logging from collections import OrderedDict + +import aiohttp +import discord from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline + from tsutils import Menu, EmojiUpdater, char_to_emoji logger = logging.getLogger('red.padbot-cogs.schoolidol') @@ -50,17 +52,12 @@ async def reload_sif(self): logger.info('done retrieving cards: {}'.format(len(self.card_data))) self.id_to_card = {c['id']: c for c in self.card_data} - name_to_card = {'{}'.format(c['idol']['name']).lower(): c for c in - self.card_data} - firstname_to_card = {c['idol']['name'].lower().split(' ')[-1]: c for c - in self.card_data} + name_to_card = {'{}'.format(c['idol']['name']).lower(): c for c in self.card_data} + firstname_to_card = {c['idol']['name'].lower().split(' ')[-1]: c for c in self.card_data} collection_name_to_card = {'{} {}'.format( - c['translated_collection'], c['idol']['name']).lower(): c for c in - self.card_data} + c['translated_collection'], c['idol']['name']).lower(): c for c in self.card_data} collection_firstname_to_card = {'{} {}'.format( - c['translated_collection'], - c['idol']['name'].split(' ')[-1]).lower(): c for c in - self.card_data} + c['translated_collection'], c['idol']['name'].split(' ')[-1]).lower(): c for c in self.card_data} self.names_to_card = { **name_to_card, **firstname_to_card, @@ -113,9 +110,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): try: result_msg, result_embed = await self.menu.custom_menu(ctx, - EmojiUpdater( - emoji_to_embed), - starting_menu_emoji, + EmojiUpdater(emoji_to_embed), starting_menu_emoji, timeout=20) if result_msg and result_embed: # Message is finished but not deleted, clear the footer @@ -137,8 +132,7 @@ def make_card_embed(c, url_field): embed.title = toHeader(c) embed.url = get_info_url(c) embed.set_image(url=base_url.format(cid, url_field)) - embed.set_footer( - text='Requester may click the reactions below to switch tabs') + embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed From f74ab7d4380603bf10ed2a6005fc1b33ea9e78be Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 15 Sep 2020 00:20:39 -0700 Subject: [PATCH 0121/1220] Revert "the worst part is how calm it is like it doesn't even tell me my code is awful it's like "hey i made 1500 edits for you try better next time please :)"" This reverts commit c06ab2aa1eb624dd1d457303ebbdb8c3e560a27a. --- azurelane/__init__.py | 1 - azurelane/azurelane.py | 21 ++- azurelane/info.json | 38 ++---- chronomagia/__init__.py | 1 - dadguide/__init__.py | 1 - dadguide/dadguide.py | 256 +++++++++++++------------------------ dadguide/info.json | 38 ++---- damagecalc/__init__.py | 1 - damagecalc/info.json | 36 ++---- info.json | 8 +- padboard/info.json | 38 ++---- padboard/padboard.py | 25 ++-- padbuildimg/__init__.py | 1 - padbuildimg/info.json | 46 +++---- padbuildimg/padbuildimg.py | 234 +++++++++++---------------------- padbuilds/info.json | 36 ++---- padevents/__init__.py | 1 - padevents/info.json | 40 ++---- padevents/padevents.py | 130 +++++++------------ padglobal/padglobal.py | 210 ++++++++++-------------------- padguidedb/__init__.py | 1 - padguidedb/info.json | 37 ++---- padguidedb/padguidedb.py | 68 ++++------ padinfo/__init__.py | 1 - padinfo/info.json | 43 +++---- padinfo/padinfo.py | 184 ++++++++++---------------- padmonitor/__init__.py | 1 - padsearch/__init__.py | 1 - padsearch/info.json | 41 +++--- padvision/__init__.py | 1 - padvision/info.json | 37 ++---- padvision/padvision.py | 13 +- pricecheck/__init__.py | 1 - pricecheck/pricecheck.py | 34 ++--- profile/__init__.py | 1 - profile/profile.py | 49 +++---- schoolidol/__init__.py | 1 - schoolidol/info.json | 37 ++---- 38 files changed, 595 insertions(+), 1118 deletions(-) diff --git a/azurelane/__init__.py b/azurelane/__init__.py index 7b3c1723..00b7bfe4 100644 --- a/azurelane/__init__.py +++ b/azurelane/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = AzureLane(bot) bot.add_cog(n) diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 030e987f..7ae3e08b 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -1,15 +1,17 @@ -import aiohttp import difflib -import discord import json import logging from collections import OrderedDict + +import aiohttp +import discord +from tsutils import Menu, EmojiUpdater, char_to_emoji from redbot.core import commands, checks from redbot.core.utils.chat_formatting import inline -from tsutils import Menu, EmojiUpdater, char_to_emoji logger = logging.getLogger('red.padbot-cogs.azurelane') + BASE_URL = 'https://storage.googleapis.com/mirubot/alimages/raw' DATA_URL = '{}/azure_lane.json'.format(BASE_URL) @@ -45,8 +47,7 @@ async def reload_al(self): logger.info('done retrieving cards: {}'.format(len(self.card_data))) self.id_to_card = {c['id']: c for c in self.card_data} - name_to_card = {'{}'.format(c['name_en']).lower(): c for c in - self.card_data} + name_to_card = {'{}'.format(c['name_en']).lower(): c for c in self.card_data} # collection_name_to_card = {'{} {}'.format( # i['title'], c['name_en']).lower(): c for i in c['images'] for c in self.card_data} self.names_to_card = { @@ -89,9 +90,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): try: result_msg, result_embed = await self.menu.custom_menu(ctx, - EmojiUpdater( - emoji_to_embed), - starting_menu_emoji, + EmojiUpdater(emoji_to_embed), starting_menu_emoji, timeout=20) if result_msg and result_embed: # Message is finished but not deleted, clear the footer @@ -112,13 +111,11 @@ def make_card_embed(c, image_idx): file_name = url[url.rfind("/") + 1:] header = 'No. {} {} - {}'.format(cid, name, image_title) - image_url = 'https://storage.googleapis.com/mirubot/alimages/raw/{}'.format( - file_name) + image_url = 'https://storage.googleapis.com/mirubot/alimages/raw/{}'.format(file_name) embed = discord.Embed() embed.title = header embed.url = info_url embed.set_image(url=image_url) - embed.set_footer( - text='Requester may click the reactions below to switch tabs') + embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed diff --git a/azurelane/info.json b/azurelane/info.json index 66626808..8e34b3ce 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -1,27 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Displays data about Azur Lane.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows Azur Lane data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "tsutils", - "aiohttp" - ], - "tags": [ - "azurlane", - "azurelane" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Displays data about Azur Lane.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Azur Lane data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["tsutils", "aiohttp"], + "tags": ["azurlane", "azurelane"] } diff --git a/chronomagia/__init__.py b/chronomagia/__init__.py index 230cbca9..b96f5f30 100644 --- a/chronomagia/__init__.py +++ b/chronomagia/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = ChronoMagia(bot) bot.add_cog(n) diff --git a/dadguide/__init__.py b/dadguide/__init__.py index 1159ca75..5bff88a7 100644 --- a/dadguide/__init__.py +++ b/dadguide/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = Dadguide(bot) bot.add_cog(n) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index bea40671..b96ec5aa 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -9,31 +9,31 @@ import asyncio import csv import difflib -import discord import json import logging import os -import pytz import re -import romkan import shutil import sqlite3 as lite import traceback -import tsutils from collections import defaultdict, deque, OrderedDict from datetime import datetime from enum import Enum + +import pytz +import romkan +import discord +import tsutils from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import inline +from redbot.core.utils import AsyncIter logger = logging.getLogger('red.padbot-cogs.dadguide') def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), - file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) CSV_FILE_PATTERN = '{}.csv' @@ -133,8 +133,7 @@ async def reload_data_task(self): self._is_ready.set() except Exception as ex: short_wait = True - logger.exception("dadguide data download/refresh failed: %s", - ex) + logger.exception("dadguide data download/refresh failed: %s", ex) try: wait_time = 60 if short_wait else 60 * 60 * 4 @@ -166,8 +165,7 @@ async def download_and_refresh_nicknames(self): panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) self.nickname_overrides = {x[0].lower(): int(x[1]) - for x in nickname_overrides if - x[1].isdigit()} + for x in nickname_overrides if x[1].isdigit()} self.basename_overrides = defaultdict(set) for x in basename_overrides: @@ -175,17 +173,14 @@ async def download_and_refresh_nicknames(self): if k.isdigit(): self.basename_overrides[int(k)].add(v.lower()) - self.panthname_overrides = {x[0].lower(): x[1].lower() for x in - panthname_overrides} - self.panthname_overrides.update( - {v: v for _, v in self.panthname_overrides.items()}) + self.panthname_overrides = {x[0].lower(): x[1].lower() for x in panthname_overrides} + self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) logger.info('Loading dg database') self.database = load_database(self.database) logger.info('Building dg monster index') self.index = await MonsterIndex(self.database, self.nickname_overrides, - self.basename_overrides, - self.panthname_overrides) + self.basename_overrides, self.panthname_overrides) logger.info('Writing dg monster computed names') self.write_monster_computed_names() @@ -231,16 +226,14 @@ def _csv_to_tuples(self, file_path: str, cols: int = 2): async def _download_files(self): one_hour_secs = 1 * 60 * 60 - await tsutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, - one_hour_secs) + await tsutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, one_hour_secs) async def _download_override_files(self): one_hour_secs = 1 * 60 * 60 await tsutils.makeAsyncCachedPlainRequest( NICKNAME_FILE_PATTERN, NICKNAME_OVERRIDES_SHEET, one_hour_secs) await tsutils.makeAsyncCachedPlainRequest( - BASENAME_FILE_PATTERN, GROUP_BASENAMES_OVERRIDES_SHEET, - one_hour_secs) + BASENAME_FILE_PATTERN, GROUP_BASENAMES_OVERRIDES_SHEET, one_hour_secs) await tsutils.makeAsyncCachedPlainRequest( PANTHNAME_FILE_PATTERN, PANTHNAME_OVERRIDES_SHEET, one_hour_secs) @@ -257,6 +250,8 @@ async def setdatafile(self, ctx, *, data_file): await ctx.tick() + + class DadguideSettings(tsutils.CogSettings): def make_default_settings(self): config = { @@ -350,8 +345,7 @@ def close(self): self._con = None @staticmethod - def _select_builder(tables, key=None, where=None, order=None, - distinct=False): + def _select_builder(tables, key=None, where=None, order=None, distinct=False): if distinct: SELECT_FROM = 'SELECT DISTINCT {fields} FROM {first_table}' else: @@ -371,14 +365,11 @@ def _select_builder(tables, key=None, where=None, order=None, break else: other_tables.append(table) - query = [SELECT_FROM.format(first_table=first_table, - fields=', '.join(fields_lst))] + query = [SELECT_FROM.format(first_table=first_table, fields=', '.join(fields_lst))] prev_table = first_table if key: for k, other in zip(key, other_tables): - query.append( - JOIN.format(first_table=prev_table, other_table=other, - key=k)) + query.append(JOIN.format(first_table=prev_table, other_table=other, key=k)) prev_table = other if where: query.append(WHERE.format(condition=where)) @@ -408,8 +399,7 @@ def _as_generator(self, cursor, d_type): yield d_type(res) res = cursor.fetchone() - def _query_many(self, query, param, d_type, idx_key=None, - as_generator=False): + def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): cursor = self._con.cursor() cursor.execute(query, param) if cursor.rowcount == 0: @@ -420,14 +410,11 @@ def _query_many(self, query, param, d_type, idx_key=None, if idx_key is None: return [d_type(res, self) for res in cursor.fetchall()] else: - return DictWithAttrAccess( - {res[idx_key]: d_type(res, self) for res in - cursor.fetchall()}) + return DictWithAttrAccess({res[idx_key]: d_type(res, self) for res in cursor.fetchall()}) def _max_id(self): cursor = self._con.cursor() - cursor.execute( - "SELECT MAX(monster_id) FROM monsters WHERE monster_id < 10000") + cursor.execute("SELECT MAX(monster_id) FROM monsters WHERE monster_id < 10000") return cursor.fetchone()['MAX(monster_id)'] def _select_one_entry_by_pk(self, pk, d_type): @@ -440,8 +427,7 @@ def _select_one_entry_by_pk(self, pk, d_type): def _get_table_fields(self, table_name: str): # SQL inject vulnerable :v - table_info = self._query_many('PRAGMA table_info(' + table_name + ')', - (), dict) + table_info = self._query_many('PRAGMA table_info(' + table_name + ')', (), dict) pk = None fields = [] for c in table_info: @@ -464,8 +450,7 @@ def get_awoken_skill(self, awoken_skill_id): def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' return [r.awoken_skill_id for r in - self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, - as_generator=True)] + self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] def get_monsters_by_awakenings(self, awoken_skill_id: int): return self._query_many( @@ -486,8 +471,7 @@ def get_awakenings_by_monster(self, monster_id, is_super=None): where = '{0}.monster_id=?'.format(DgAwakening.TABLE) param = (monster_id,) else: - where = '{0}.monster_id=? AND {0}.is_super=?'.format( - DgAwakening.TABLE) + where = '{0}.monster_id=? AND {0}.is_super=?'.format(DgAwakening.TABLE) param = (monster_id, is_super) return self._query_many( self._select_builder( @@ -543,8 +527,7 @@ def monster_in_mp_shop(self, monster_id): return self._query_one( self._select_builder( tables={DgMonster.TABLE: ('buy_mp',)}, - where='{0}.monster_id=? AND buy_mp IS NOT NULL'.format( - DgMonster.TABLE) + where='{0}.monster_id=? AND buy_mp IS NOT NULL'.format(DgMonster.TABLE) ), (monster_id,), DgDrop) is not None @@ -589,9 +572,7 @@ def get_evolution_by_material(self, monster_id): return self._query_many( self._select_builder( tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where=' OR '.join( - ['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in - range(1, 6)]) + where=' OR '.join(['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in range(1, 6)]) ), (monster_id,) * 5, DgEvolution) @@ -623,8 +604,7 @@ def get_evolution_tree_ids(self, base_monster_id): def monster_id_to_no(self, monster_id, region=Server.JP): res = self._query_one( self._select_builder( - tables={DgMonster.TABLE: ( - 'monster_no_{}'.format(region.name.lower()),)}, + tables={DgMonster.TABLE: ('monster_no_{}'.format(region.name.lower()),)}, where='{}.monster_id=?'.format(DgMonster.TABLE) ), (monster_id,), @@ -645,12 +625,10 @@ def _get_monsters_where(self, where, param): DgMonster) def get_monsters_by_series(self, series_id: int): - return self._get_monsters_where( - '{}.series_id=?'.format(DgMonster.TABLE), (series_id,)) + return self._get_monsters_where('{}.series_id=?'.format(DgMonster.TABLE), (series_id,)) def get_monsters_by_active(self, active_skill_id: int): - return self._get_monsters_where( - '{}.active_skill_id=?'.format(DgMonster.TABLE), (active_skill_id,)) + return self._get_monsters_where('{}.active_skill_id=?'.format(DgMonster.TABLE), (active_skill_id,)) def get_monster_evo_gem(self, name: str, region='jp'): gem_suffix = { @@ -666,43 +644,37 @@ def get_monster_evo_gem(self, name: str, region='jp'): return self._query_one( self._select_builder( tables={DgMonster.TABLE: DgMonster.FIELDS}, - where='{0}.name_{1}=? AND {0}.leader_skill_id=10628'.format( - DgMonster.TABLE, region) + where='{0}.name_{1}=? AND {0}.leader_skill_id=10628'.format(DgMonster.TABLE, region) ), (non_gem_name,), DgMonster) def get_na_only_monsters(self): return self._get_monsters_where( - '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format( - DgMonster.TABLE), + '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format(DgMonster.TABLE), ()) def get_monster(self, monster_id: int): return self._select_one_entry_by_pk(monster_id, DgMonster) def get_all_monster_jp_name(self, as_generator=True): - return self._query_many( - self._select_builder(tables={DgMonster.TABLE: ('name_jp',)}), (), - DictWithAttrAccess, - as_generator=as_generator) + return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_jp',)}), (), DictWithAttrAccess, + as_generator=as_generator) def get_all_monsters(self, as_generator=True): - return self._query_many( - self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), - (), DgMonster, - as_generator=as_generator) + return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, + as_generator=as_generator) + def get_all_events(self, as_generator=True): - return self._query_many(self._select_builder( - tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), - DgScheduledEvent, + return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), DgScheduledEvent, as_generator=as_generator) def get_dungeon_by_id(self, dungeon_id: int): return self._select_one_entry_by_pk(dungeon_id, DgDungeon) + def enum_or_none(enum, value, default=None): if value is not None: return enum(value) @@ -847,8 +819,7 @@ def __init__(self, item, database): @property def open_datetime(self): - return datetime.utcfromtimestamp(self.start_timestamp).replace( - tzinfo=pytz.UTC) + return datetime.utcfromtimestamp(self.start_timestamp).replace(tzinfo=pytz.UTC) @open_datetime.setter def open_datetime(self, value): @@ -856,14 +827,14 @@ def open_datetime(self, value): @property def close_datetime(self): - return datetime.utcfromtimestamp(self.end_timestamp).replace( - tzinfo=pytz.UTC) + return datetime.utcfromtimestamp(self.end_timestamp).replace(tzinfo=pytz.UTC) @close_datetime.setter def close_datetime(self, value): self.end_timestamp = int(value.timestamp()) + class DgMonster(DadguideItem): TABLE = 'monsters' PK = 'monster_id' @@ -892,15 +863,12 @@ def __init__(self, item, database): self.in_pem = bool(self.pal_egg) self.in_rem = bool(self.rem_egg) - self.awakenings = self._database.get_awakenings_by_monster( - self.monster_id) - self.superawakening_count = sum( - int(a.is_super) for a in self.awakenings) + self.awakenings = self._database.get_awakenings_by_monster(self.monster_id) + self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) self.is_inheritable = bool(self.inheritable) - self.evo_from = self._database.get_prev_evolution_by_monster( - self.monster_id) + self.evo_from = self._database.get_prev_evolution_by_monster(self.monster_id) self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) @@ -910,8 +878,7 @@ def __init__(self, item, database): base_id = next_base.from_id next_base = self._database.get_prev_evolution_by_monster(base_id) self._base_monster_id = base_id - self._alt_evo_id_list = self._database.get_evolution_tree_ids( - self._base_monster_id) + self._alt_evo_id_list = self._database.get_evolution_tree_ids(self._base_monster_id) self.search = MonsterSearchHelper(self) @@ -923,9 +890,7 @@ def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): s_min = float(self[key + '_min']) s_max = float(self[key + '_max']) if self.level > 1: - s_val = s_min + (s_max - s_min) * ( - (min(lv, self.level) - 1) / (self.level - 1)) ** self[ - key + '_scale'] + s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** self[key + '_scale'] else: s_val = s_min if lv > 99: @@ -985,8 +950,7 @@ def true_evo_type(self): def mats_for_evo(self): if self.evo_from is None: return [] - return [self._database.get_monster(self.evo_from['mat_{}_id'.format(i)]) - for i in range(1, 6) if + return [self._database.get_monster(self.evo_from['mat_{}_id'.format(i)]) for i in range(1, 6) if self.evo_from['mat_{}_id'.format(i)] is not None] @property @@ -1003,8 +967,7 @@ def _evolutions_to(self): @property def evo_to(self): - return [self._database.get_monster(x.to_id) for x in - self._evolutions_to()] + return [self._database.get_monster(x.to_id) for x in self._evolutions_to()] @property def base_monster(self): @@ -1128,8 +1091,7 @@ def __init__(self, m: DgMonster): self.types = [t.name for t in m.types] def replace_colors(text: str): - return text.replace('red', 'fire').replace('blue', 'water').replace( - 'green', 'wood') + return text.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') self.leader = replace_colors(self.leader) self.active = replace_colors(self.active) @@ -1200,9 +1162,7 @@ def strip_next_clause(txt: str, sep: str): txt = active_desc parts = re.split(r'\Wand\W|;\W', txt) for i in range(0, len(parts)): - parts[i] = strip_prev_clause(parts[i], - change_txt) if change_txt in parts[ - i] else '' + parts[i] = strip_prev_clause(parts[i], change_txt) if change_txt in parts[i] else '' for part in parts: sub_parts = part.split(' to ') @@ -1224,9 +1184,9 @@ def make_roma_subname(name_jp): return adjusted_subname.strip() + class MonsterIndex(tsutils.aobject): - async def __init__(self, monster_database, nickname_overrides, - basename_overrides, + async def __init__(self, monster_database, nickname_overrides, basename_overrides, panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory base_monster_ids = monster_database.get_base_monster_ids() @@ -1268,15 +1228,13 @@ async def __init__(self, monster_database, nickname_overrides, group_basename_overrides = basename_overrides.get(base_id, []) evolution_tree = [monster_database.get_monster(m) for m in monster_database.get_evolution_tree_ids(base_id)] - named_mg = NamedMonsterGroup(evolution_tree, - group_basename_overrides) + named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) for monster in evolution_tree: if accept_filter and not accept_filter(monster): continue prefixes = self.compute_prefixes(monster, evolution_tree) extra_nicknames = monster_id_to_nicknames[monster.monster_id] - named_monster = NamedMonster(monster, named_mg, prefixes, - extra_nicknames) + named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames) named_monsters.append(named_monster) # Sort the NamedMonsters into the opposite order we want to accept their nicknames in @@ -1318,12 +1276,9 @@ def named_monsters_sort(nm: NamedMonster): self.pantheons[pantheon.lower()].add(nm) self.all_monsters = named_monsters - self.all_na_name_to_monsters = {m.name_na.lower(): m for m in - named_monsters} - self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in - named_monsters} - self.monster_id_to_named_monster = {m.monster_id: m for m in - named_monsters} + self.all_na_name_to_monsters = {m.name_na.lower(): m for m in named_monsters} + self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} + self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} for nickname, monster_id in nickname_overrides.items(): nm = self.monster_id_to_named_monster.get(monster_id) @@ -1360,10 +1315,8 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('chibi') awoken = lower_name.startswith('awoken') or '覚醒' in lower_name - revo = lower_name.startswith( - 'reincarnated') or '転生' in lower_name or m.true_evo_type == InternalEvoType.Reincarnated - srevo = lower_name.startswith( - 'super reincarnated') or '超転生' in lower_name + revo = lower_name.startswith('reincarnated') or '転生' in lower_name + srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name mega = lower_name.startswith('mega awoken') or '極醒' in lower_name awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega @@ -1409,8 +1362,7 @@ def is_pixel(n): for gm in evotree: if is_pixel(gm): - prefixes.update( - ['pixel'] if is_pixel(m) else ['np', 'nonpixel']) + prefixes.update(['pixel'] if is_pixel(m) else ['np', 'nonpixel']) break if m.is_equip: @@ -1452,21 +1404,16 @@ def find_monster(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): if query.endswith("base {}".format(m.monster_id)): - matches.add(discord.utils.find( - lambda mo: m.base_monster_no == mo.monster_id, - self.all_entries.values())) + matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) if len(matches): - return self.pickBestMonster( - matches), None, "Base ID match, max of 1".format() + return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() # prefix search for nicknames, space-preceeded, take max id for nickname, m in self.all_entries.items(): if nickname.startswith(query + ' '): matches.add(m) if len(matches): - return self.pickBestMonster( - matches), None, "Space nickname prefix, max of {}".format( - len(matches)) + return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) # prefix search for nicknames, take max id for nickname, m in self.all_entries.items(): @@ -1474,24 +1421,19 @@ def find_monster(self, query): matches.add(m) if len(matches): all_names = ",".join(map(lambda x: x.name_na, matches)) - return self.pickBestMonster( - matches), None, "Nickname prefix, max of {}, matches=({})".format( + return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( len(matches), all_names) # prefix search for full name, take max id for nickname, m in self.all_entries.items(): - if (m.name_na.lower().startswith( - query) or m.name_jp.lower().startswith(query)): + if (m.name_na.lower().startswith(query) or m.name_jp.lower().startswith(query)): matches.add(m) if len(matches): - return self.pickBestMonster( - matches), None, "Full name, max of {}".format(len(matches)) + return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) # for nicknames with 2 names, prefix search 2nd word, take max id if query in self.two_word_entries: - return self.two_word_entries[ - query], None, "Second-word nickname prefix, max of {}".format( - len(matches)) + return self.two_word_entries[query], None, "Second-word nickname prefix, max of {}".format(len(matches)) # TODO: refactor 2nd search characteristcs for 2nd word @@ -1500,25 +1442,21 @@ def find_monster(self, query): if (query in m.name_na.lower() or query in m.name_jp.lower()): matches.add(m) if len(matches): - return self.pickBestMonster( - matches), None, 'Nickname contains nickname match ({})'.format( + return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( len(matches)) # No decent matches. Try near hits on nickname instead - matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, - cutoff=.8) + matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) if len(matches): match = matches[0] - return self.all_entries[ - match], None, 'Close nickname match ({})'.format(match) + return self.all_entries[match], None, 'Close nickname match ({})'.format(match) # Still no decent matches. Try near hits on full name instead matches = difflib.get_close_matches( query, self.all_na_name_to_monsters.keys(), n=1, cutoff=.9) if len(matches): match = matches[0] - return self.all_na_name_to_monsters[ - match], None, 'Close name match ({})'.format(match) + return self.all_na_name_to_monsters[match], None, 'Close name match ({})'.format(match) # About to give up, try matching all words matches = set() @@ -1527,8 +1465,7 @@ def find_monster(self, query): all(map(lambda x: x in m.name_jp.lower(), query.split()))): matches.add(m) if len(matches): - return self.pickBestMonster( - matches), None, 'All word match on full name, max of {}'.format( + return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( len(matches)) # couldn't find anything @@ -1582,9 +1519,7 @@ def find_monster2(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): if query.endswith("base {}".format(m.monster_id)): - matches.add(discord.utils.find( - lambda mo: m.base_monster_no == mo.monster_id, - self.all_entries.values())) + matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) matches.remove_potential_matches_without_all_prefixes(query_prefixes) # first try to get matches from nicknames @@ -1599,36 +1534,30 @@ def find_monster2(self, query): for nickname, m in self.all_na_name_to_monsters.items(): if new_query in m.name_na.lower() or new_query in m.name_jp.lower(): matches.add(m) - matches.remove_potential_matches_without_all_prefixes( - query_prefixes) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) # check for exact match on pantheon name but only if needed if not matches.length(): for pantheon in self.all_pantheon_nicknames: if new_query == pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, - self.pantheon_nick_to_name, + matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, self.pantheons) - matches.remove_potential_matches_without_all_prefixes( - query_prefixes) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) # check for any match on pantheon name, again but only if needed if not matches.length(): for pantheon in self.all_pantheon_nicknames: if new_query in pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, - self.pantheon_nick_to_name, + matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, self.pantheons) - matches.remove_potential_matches_without_all_prefixes( - query_prefixes) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) if matches.length(): return matches.pick_best_monster(), None, None return None, "Could not find a match for: " + query, None def pickBestMonster(self, named_monster_list): - return max(named_monster_list, key=lambda x: ( - not x.is_low_priority, x.rarity, x.monster_no_na)) + return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) class PotentialMatches(object): @@ -1653,15 +1582,12 @@ def remove_potential_matches_without_all_prefixes(self, query_prefixes): break self.match_list.difference_update(to_remove) - def get_monsters_from_potential_pantheon_match(self, pantheon, - pantheon_nick_to_name, - pantheons): + def get_monsters_from_potential_pantheon_match(self, pantheon, pantheon_nick_to_name, pantheons): full_name = pantheon_nick_to_name[pantheon] self.update(pantheons[full_name]) def pick_best_monster(self): - return max(self.match_list, key=lambda x: ( - not x.is_low_priority, x.rarity, x.monster_no_na)) + return max(self.match_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) class NamedMonsterGroup(object): @@ -1676,15 +1602,13 @@ def __init__(self, evolution_tree: list, basename_overrides: list): self.base_monster_no_na = base_monster.monster_no_na self.monster_no_to_basename = { - m.monster_id: self._compute_monster_basename(m) for m in - evolution_tree + m.monster_id: self._compute_monster_basename(m) for m in evolution_tree } self.computed_basename = self._compute_group_basename(evolution_tree) self.computed_basenames = set([self.computed_basename]) if '-' in self.computed_basename: - self.computed_basenames.add( - self.computed_basename.replace('-', ' ')) + self.computed_basenames.add(self.computed_basename.replace('-', ' ')) self.basenames = basename_overrides or self.computed_basenames @@ -1728,13 +1652,11 @@ def count_and_id(): entry[0] += 1 entry[1] = max(entry[1], m.monster_id) - entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in - basename_to_info.items()] + entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] return max(entries)[2] def _is_low_priority_monster(self, m: DgMonster): - lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, - MonsterType.Vendor] + lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] lp_substrings = ['tamadra'] lp_min_rarity = 2 name = m.name_na.lower() @@ -1754,8 +1676,7 @@ def _is_low_priority_group(self, mg: list): class NamedMonster(object): - def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, - prefixes: set, extra_nicknames: set): + def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set): # Must not hold onto monster or monster_group! # Hold on to the IDs instead @@ -1784,8 +1705,7 @@ def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, self.name_jp = monster.name_jp # These are just extra metadata - self.monster_basename = monster_group.monster_no_to_basename[ - self.monster_id] + self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] self.group_computed_basename = monster_group.computed_basename self.extra_nicknames = extra_nicknames diff --git a/dadguide/info.json b/dadguide/info.json index 8823f056..e40915d6 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -1,27 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "No commands, but loads all the monster data stored in Tsubaki.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Loads monster data (LOAD THIS SECOND)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "tsutils", - "pytz", - "romkan" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "No commands, but loads all the monster data stored in Tsubaki.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Loads monster data (LOAD THIS SECOND)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["tsutils", "pytz", "romkan"], + "tags": ["PAD"] } diff --git a/damagecalc/__init__.py b/damagecalc/__init__.py index 177e57ab..26416e02 100644 --- a/damagecalc/__init__.py +++ b/damagecalc/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = DamageCalc(bot) bot.add_cog(n) diff --git a/damagecalc/info.json b/damagecalc/info.json index 4594d9f8..e6456626 100644 --- a/damagecalc/info.json +++ b/damagecalc/info.json @@ -1,25 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Calculate Puzzle & Dragons data", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Calculates Puzzle & Dragons data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "ply" - ], - "tags": [ - "pad" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Calculate Puzzle & Dragons data", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Calculates Puzzle & Dragons data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["ply"], + "tags": ["pad"] } diff --git a/info.json b/info.json index a11b82f8..90b10de9 100644 --- a/info.json +++ b/info.json @@ -1,7 +1,5 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "A collection of cogs related to the mobile game Puzzle and Dragons, along with a few unrelated games.", - "short": "The main Tsubotki repo." + "author": ["The Tsubaki Bot Team"], + "description": "A collection of cogs related to the mobile game Puzzle and Dragons, along with a few unrelated games.", + "short": "The main Tsubotki repo." } diff --git a/padboard/info.json b/padboard/info.json index 1adc8002..a6450cea 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -1,27 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Turn a screenshot into a P&D simulation via DawnGlare.", - "end_user_data_statement": "No personal data is stored persistantly.", - "install_msg": "", - "short": "Dawnglare utils", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "padvision": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Turn a screenshot into a P&D simulation via DawnGlare.", + "end_user_data_statement": "No personal data is stored persistantly.", + "install_msg": "", + "short": "Dawnglare utils", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"padvision": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["tsutils"], + "tags": ["PAD"] } diff --git a/padboard/padboard.py b/padboard/padboard.py index 9e876599..aea3dd11 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -1,12 +1,14 @@ +import os +from collections import defaultdict +from collections import deque + import aiohttp import cv2 import discord import numpy as np -import os -from collections import defaultdict -from collections import deque from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline + from tsutils import tsutils DATA_DIR = os.path.join('data', 'padboard') @@ -14,7 +16,6 @@ DAWNGLARE_BOARD_TEMPLATE = "https://pad.dawnglare.com/?patt={}" CNINJA_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" - class PadBoard(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -53,6 +54,7 @@ async def set_tflite_path(self, ctx, *, path): await self.config.tflite_path.set(path) await ctx.tick() + def find_image(self, user_id): urls = list(self.logs[user_id]) if urls: @@ -93,12 +95,11 @@ async def dawnglare(self, ctx, user: discord.Member = None): # Convert O (used by padvision code) to X (used by Puzzled for bombs) board_text_nc = board_text_nc.replace('o', 'x') msg = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc) - msg += '\n' + CNINJA_BOARD_TEMPLATE.format(board_text_nc) + msg += '\n'+CNINJA_BOARD_TEMPLATE.format(board_text_nc) await ctx.send(msg) - async def get_recent_image(self, ctx, user: discord.Member = None, - message: discord.Message = None): + async def get_recent_image(self, ctx, user: discord.Member = None, message: discord.Message = None): user_id = user.id if user else ctx.author.id image_url = tsutils.extract_image_url(message) @@ -107,12 +108,10 @@ async def get_recent_image(self, ctx, user: discord.Member = None, if not image_url: if user: - await ctx.send(inline( - "Couldn't find an image in that user's recent messages.")) + await ctx.send(inline("Couldn't find an image in that user's recent messages.")) else: await ctx.send( - inline( - "Couldn't find an image in your recent messages. Upload or link to one and try again")) + inline("Couldn't find an image in your recent messages. Upload or link to one and try again")) return None image_data = await self.download_image(image_url) @@ -133,7 +132,5 @@ async def nc_classify(self, image_data): if not PDV_COG: raise IOError("PadVision is not loaded") PDV_MODULE = __import__(PDV_COG.__module__) - img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, - img_np, - image_data) + img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, img_np, image_data) return img_extractor.get_board() diff --git a/padbuildimg/__init__.py b/padbuildimg/__init__.py index e8e6d36f..5a0008d3 100644 --- a/padbuildimg/__init__.py +++ b/padbuildimg/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): bot.add_cog(PadBuildImage(bot)) diff --git a/padbuildimg/info.json b/padbuildimg/info.json index a1ef892d..16f6c984 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -1,32 +1,18 @@ { - "author": [ - "chu2.718281828459045235360287471#0110", - "The Tsubaki Bot Team" - ], - "description": "Build a PAD team image via commands.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "PDChu cog", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils", - "ply", - "Pillow", - "aiohttp" - ], - "tags": [ - "PAD" - ] + "author": ["chu2.718281828459045235360287471#0110", "The Tsubaki Bot Team"], + "description": "Build a PAD team image via commands.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PDChu cog", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["tsutils", "ply", "Pillow", "aiohttp"], + "tags": ["PAD"] } diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index db01e466..f3a6b71c 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -1,20 +1,21 @@ -import aiohttp -import csv -import discord -import io import math +import csv import os +import io +from shutil import rmtree import re import urllib + +import discord +import aiohttp +from ply import lex from PIL import Image -from PIL import ImageChops from PIL import ImageDraw from PIL import ImageFont -from ply import lex +from PIL import ImageChops +from tsutils import CogSettings from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline -from shutil import rmtree -from tsutils import CogSettings HELP_MSG = """ ^buildimg @@ -54,15 +55,11 @@ Case insensitive, order does not matter """ EXAMPLE_MSG = "Examples:\n1P{}\n2P{}\n3P{}\nLatent Validation{}\nStats Validation{}".format( - box( - "bj(weld)lv110/baldin[gok *3](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian)"), - box( - "amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf)"), - box( - "zela(assist amen) *3/base raizer * 2/zela; zela(assist amen) *4/base valeria/zela; zela * 6"), + box("bj(weld)lv110/baldin[gok *3](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian)"), + box("amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf)"), + box("zela(assist amen) *3/base raizer * 2/zela; zela(assist amen) *4/base valeria/zela; zela * 6"), box("eir[drk,drk,sdr]/eir[bak,bak,sdr]/eir[sdr *4, dek]/eir[sdr *8, dek]"), - box( - "dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15") + box("dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15") ) """ @@ -145,8 +142,6 @@ DELAY_BUFFER = 'delay_buffer' REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/padbuildimg/assets/' REMOTE_AWK_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/media/awakenings/{0:03d}.png' - - # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' class DictWithAttributeAccess(dict): @@ -188,15 +183,13 @@ async def red_delete_data_for_user(self, *, requester, user_id): def buildImgParams(self): if 'build_img_params' not in self.bot_settings: - self.bot_settings[ - 'build_img_params'] = self.make_default_build_img_params() + self.bot_settings['build_img_params'] = self.make_default_build_img_params() self.save_settings() return DictWithAttributeAccess(self.bot_settings['build_img_params']) def setBuildImgParamsByKey(self, key, value): if 'build_img_params' not in self.bot_settings: - self.bot_settings[ - 'build_img_params'] = self.make_default_build_img_params() + self.bot_settings['build_img_params'] = self.make_default_build_img_params() if key in self.bot_settings['build_img_params']: self.bot_settings['build_img_params'][key] = value self.save_settings() @@ -216,21 +209,14 @@ async def downloadAllAssets(self, awk_ids): os.mkdir(params.ASSETS_DIR + 'lat/') os.mkdir(params.ASSETS_DIR + 'awk/') for lat in LATENTS_MAP.values(): - await self.downloadAssets(REMOTE_ASSET_URL + 'lat/' + lat + '.png', - params.ASSETS_DIR + 'lat/' + lat + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + 'lat/' + lat + '.png', params.ASSETS_DIR + 'lat/' + lat + '.png') for awk in awk_ids: - await self.downloadAssets(REMOTE_AWK_URL.format(awk), - params.ASSETS_DIR + 'awk/' + str( - awk) + '.png') - await self.downloadAssets(REMOTE_ASSET_URL + AWK_CIRCLE + '.png', - params.ASSETS_DIR + AWK_CIRCLE + '.png') - await self.downloadAssets(REMOTE_ASSET_URL + AWK_STAR + '.png', - params.ASSETS_DIR + AWK_STAR + '.png') - await self.downloadAssets(REMOTE_ASSET_URL + DELAY_BUFFER + '.png', - params.ASSETS_DIR + DELAY_BUFFER + '.png') + await self.downloadAssets(REMOTE_AWK_URL.format(awk), params.ASSETS_DIR + 'awk/' + str(awk) + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + AWK_CIRCLE + '.png', params.ASSETS_DIR + AWK_CIRCLE + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + AWK_STAR + '.png', params.ASSETS_DIR + AWK_STAR + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + DELAY_BUFFER + '.png', params.ASSETS_DIR + DELAY_BUFFER + '.png') font_name = os.path.basename(params.FONT_NAME) - await self.downloadAssets(REMOTE_ASSET_URL + font_name, - params.ASSETS_DIR + font_name) + await self.downloadAssets(REMOTE_ASSET_URL + font_name, params.ASSETS_DIR + font_name) def dmOnly(self, server_id): if 'dm_only' not in self.bot_settings: @@ -248,13 +234,11 @@ def toggleDmOnly(self, server_id): self.bot_settings['dm_only'].append(server_id) self.save_settings() - def lstripalpha(s): while s and not s[0].isdigit(): - s = s[1:] + s=s[1:] return s - class PaDTeamLexer(object): tokens = [ 'ID', @@ -306,8 +290,7 @@ def t_LATENT(self, t): for i in range(count): t.value.insert(idx, latent) t.value = t.value[0:MAX_LATENTS] - t.value = [REVERSE_LATENTS_MAP[l] for l in t.value if - l in REVERSE_LATENTS_MAP] + t.value = [REVERSE_LATENTS_MAP[l] for l in t.value if l in REVERSE_LATENTS_MAP] return t def t_STATS(self, t): @@ -380,9 +363,7 @@ def t_REPEAT(self, t): t_ignore = '\t\n' def t_error(self, t): - raise commands.UserFeedbackCheckFailure( - "Parse Error: Unknown text '{}' at position {}".format(t.value, - t.lexpos)) + raise commands.UserFeedbackCheckFailure("Parse Error: Unknown text '{}' at position {}".format(t.value, t.lexpos)) def build(self, **kwargs): # pass debug=1 to enable verbose output @@ -399,8 +380,7 @@ def validate_latents(latents, card_types): return latents for idx, l in enumerate(latents): if 200 < l < 209: - if not any([l in TYPE_TO_KILLERS_MAP[t] for t in card_types if - t is not None]): + if not any([l in TYPE_TO_KILLERS_MAP[t] for t in card_types if t is not None]): latents[idx] = None latents = [l for l in latents if l is not None] return latents if len(latents) > 0 else None @@ -408,14 +388,10 @@ def validate_latents(latents, card_types): def outline_text(draw, x, y, font, text_color, text, thickness=1): shadow_color = 'black' - draw.text((x - thickness, y - thickness), text, font=font, - fill=shadow_color) - draw.text((x + thickness, y - thickness), text, font=font, - fill=shadow_color) - draw.text((x - thickness, y + thickness), text, font=font, - fill=shadow_color) - draw.text((x + thickness, y + thickness), text, font=font, - fill=shadow_color) + draw.text((x - thickness, y - thickness), text, font=font, fill=shadow_color) + draw.text((x + thickness, y - thickness), text, font=font, fill=shadow_color) + draw.text((x - thickness, y + thickness), text, font=font, fill=shadow_color) + draw.text((x + thickness, y + thickness), text, font=font, fill=shadow_color) draw.text((x, y), text, font=font, fill=text_color) @@ -449,9 +425,7 @@ def __init__(self, params, padinfo_cog, build_name='pad_build'): self.build_img = None async def process_build(self, input_str): - team_strings = [row for row in - csv.reader(re.split('[;\n]', input_str), delimiter='/') - if len(row) > 0] + team_strings = [row for row in csv.reader(re.split('[;\n]', input_str), delimiter='/') if len(row) > 0] if len(team_strings) > 3: team_strings = team_strings[0:3] for team in team_strings: @@ -518,18 +492,15 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = await self.padinfo_cog.findMonster( - tok.value) + card, err, debug_info = await self.padinfo_cog.findMonster(tok.value) if card is None: - raise commands.UserFeedbackCheckFailure( - 'Lookup Error: {}'.format(err)) + raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) if not card.is_inheritable: if is_assist: return None, None else: result_card['GOLD_STAR'] = False - result_card[ - 'MNO'] = card.monster_no_na if card.monster_no_na != card.monster_id else card.monster_no_jp + result_card['MNO'] = card.monster_no_na if card.monster_no_na != card.monster_id else card.monster_no_jp result_card['ID'] = card.monster_id elif tok.type == 'P_ALL': if tok.value >= 297: @@ -555,25 +526,19 @@ async def process_card(self, card_str, is_assist=False): 110 if card.limit_mult is not None and card.limit_mult > 1 else card.level ) if card.active_skill: - result_card[ - 'MAX_SLV'] = card.active_skill.turn_max - card.active_skill.turn_min + 1 + result_card['MAX_SLV'] = card.active_skill.turn_max - card.active_skill.turn_min + 1 else: result_card['MAX_SLV'] = 0 - result_card['MAX_AWAKE'] = len( - card.awakenings) - card.superawakening_count + result_card['MAX_AWAKE'] = len(card.awakenings) - card.superawakening_count if is_assist: - result_card['MAX_AWAKE'] = result_card['MAX_AWAKE'] if \ - result_card['AWAKE'] > 0 else 0 + result_card['MAX_AWAKE'] = result_card['MAX_AWAKE'] if result_card['AWAKE'] > 0 else 0 result_card['AWAKE'] = result_card['MAX_AWAKE'] result_card['SUPER'] = 0 else: - result_card['SUPER'] = min(result_card['SUPER'], - card.superawakening_count) + result_card['SUPER'] = min(result_card['SUPER'], card.superawakening_count) if result_card['SUPER'] > 0: - super_awakes = [x.awoken_skill_id for x in card.awakenings[ - -card.superawakening_count:]] - result_card['SUPER'] = super_awakes[ - result_card['SUPER'] - 1] + super_awakes = [x.awoken_skill_id for x in card.awakenings[-card.superawakening_count:]] + result_card['SUPER'] = super_awakes[result_card['SUPER'] - 1] result_card['LV'] = max(100, result_card['LV']) card_att = card.attr1 if is_assist: @@ -581,8 +546,7 @@ async def process_card(self, card_str, is_assist=False): else: parsed_cards = [result_card] if isinstance(assist_str, str): - assist_card, assist_att = await self.process_card(assist_str, - is_assist=True) + assist_card, assist_att = await self.process_card(assist_str, is_assist=True) if card_att is not None and assist_att is not None: assist_card['ON_COLOR'] = card_att == assist_att parsed_cards.append(assist_card) @@ -597,8 +561,7 @@ def combine_latents(self, latents): if len(latents) > MAX_LATENTS: latents = latents[0:MAX_LATENTS] latents_bar = Image.new('RGBA', - (self.params.PORTRAIT_WIDTH, - self.params.LATENTS_WIDTH * 2), + (self.params.PORTRAIT_WIDTH, self.params.LATENTS_WIDTH * 2), (255, 255, 255, 0)) x_offset = 0 y_offset = 0 @@ -623,14 +586,12 @@ def combine_latents(self, latents): sorted_latents.extend(one_slot) last_height = 0 for l in sorted_latents: - latent_icon = Image.open( - self.params.ASSETS_DIR + 'lat/' + LATENTS_MAP[l] + '.png') + latent_icon = Image.open(self.params.ASSETS_DIR + 'lat/' + LATENTS_MAP[l] + '.png') if x_offset + latent_icon.size[0] > self.params.PORTRAIT_WIDTH: row_count += 1 x_offset = 0 y_offset += last_height - if row_count >= MAX_LATENTS // 4 and x_offset + latent_icon.size[ - 0] >= self.params.LATENTS_WIDTH * (MAX_LATENTS % 4): + if row_count >= MAX_LATENTS//4 and x_offset + latent_icon.size[0] >= self.params.LATENTS_WIDTH * (MAX_LATENTS%4): break latents_bar.paste(latent_icon, (x_offset, y_offset)) last_height = latent_icon.size[1] @@ -642,11 +603,9 @@ def combine_portrait(self, card, show_stats=True, show_supers=False): if card['ID'] == DELAY_BUFFER: return Image.open(self.params.ASSETS_DIR + DELAY_BUFFER + '.png') if 'http' in self.params.PORTRAIT_DIR: - portrait = Image.open(urllib.request.urlopen( - self.params.PORTRAIT_DIR.format(monster_id=card['ID']))) + portrait = Image.open(urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))) else: - portrait = Image.open( - self.params.PORTRAIT_DIR.format(monster_id=card['ID'])) + portrait = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])) draw = ImageDraw.Draw(portrait) slv_offset = 80 if show_stats: @@ -655,32 +614,24 @@ def combine_portrait(self, card, show_stats=True, show_supers=False): if 0 < sum_plus: if sum_plus < 297: font = ImageFont.truetype(self.params.FONT_NAME, 14) - outline_text(draw, 5, 2, font, 'yellow', - '+{:d} HP'.format(card['+HP'])) - outline_text(draw, 5, 14, font, 'yellow', - '+{:d} ATK'.format(card['+ATK'])) - outline_text(draw, 5, 26, font, 'yellow', - '+{:d} RCV'.format(card['+RCV'])) + outline_text(draw, 5, 2, font, 'yellow', '+{:d} HP'.format(card['+HP'])) + outline_text(draw, 5, 14, font, 'yellow', '+{:d} ATK'.format(card['+ATK'])) + outline_text(draw, 5, 26, font, 'yellow', '+{:d} RCV'.format(card['+RCV'])) else: font = ImageFont.truetype(self.params.FONT_NAME, 18) outline_text(draw, 5, 0, font, 'yellow', '+297') # level if card['LV'] > 0: - outline_text(draw, 5, 75, - ImageFont.truetype(self.params.FONT_NAME, 18), + outline_text(draw, 5, 75, ImageFont.truetype(self.params.FONT_NAME, 18), 'white', 'Lv.{:d}'.format(card['LV'])) slv_offset = 65 # skill level if card['MAX_SLV'] > 0 and card['SLV'] > 0: - slv_txt = 'SLv.max' if card['SLV'] >= card[ - 'MAX_SLV'] else 'SLv.{:d}'.format(card['SLV']) + slv_txt = 'SLv.max' if card['SLV'] >= card['MAX_SLV'] else 'SLv.{:d}'.format(card['SLV']) outline_text(draw, 5, slv_offset, - ImageFont.truetype(self.params.FONT_NAME, 12), 'pink', - slv_txt) + ImageFont.truetype(self.params.FONT_NAME, 12), 'pink', slv_txt) # ID - outline_text(draw, 67, 82, - ImageFont.truetype(self.params.FONT_NAME, 12), 'lightblue', - str(card['MNO'])) + outline_text(draw, 67, 82, ImageFont.truetype(self.params.FONT_NAME, 12), 'lightblue', str(card['MNO'])) del draw if card['MAX_AWAKE'] > 0: # awakening @@ -690,17 +641,13 @@ def combine_portrait(self, card, show_stats=True, show_supers=False): awake = Image.open(self.params.ASSETS_DIR + AWK_CIRCLE + '.png') draw = ImageDraw.Draw(awake) draw.text((8, -2), str(card['AWAKE']), - font=ImageFont.truetype(self.params.FONT_NAME, 18), - fill='yellow') + font=ImageFont.truetype(self.params.FONT_NAME, 18), fill='yellow') del draw - portrait.paste(awake, - (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, 5), - awake) + portrait.paste(awake, (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, 5), awake) awake.close() if show_supers and card['SUPER'] > 0: # SA - awake = Image.open( - self.params.ASSETS_DIR + 'awk/' + str(card['SUPER']) + '.png') + awake = Image.open(self.params.ASSETS_DIR + 'awk/' + str(card['SUPER']) + '.png') portrait.paste(awake, (self.params.PORTRAIT_WIDTH - awake.size[0] - 5, (self.params.PORTRAIT_WIDTH - awake.size[0]) // 2), @@ -714,24 +661,19 @@ def generate_build_image(self, include_instructions=False): team_size = max([len(x) for x in self.build['TEAM']]) p_w = self.params.PORTRAIT_WIDTH * math.ceil(team_size / 2) + \ self.params.PADDING * math.ceil(team_size / 10) - p_h = ( - self.params.PORTRAIT_WIDTH + self.params.LATENTS_WIDTH + self.params.PADDING) * \ + p_h = (self.params.PORTRAIT_WIDTH + self.params.LATENTS_WIDTH + self.params.PADDING) * \ 2 * len(self.build['TEAM']) include_instructions &= self.build['INSTRUCTION'] is not None if include_instructions: - p_h += len(self.build['INSTRUCTION']) * ( - self.params.PORTRAIT_WIDTH // 2 + self.params.PADDING) + p_h += len(self.build['INSTRUCTION']) * (self.params.PORTRAIT_WIDTH // 2 + self.params.PADDING) self.build_img = Image.new('RGBA', (p_w, p_h), (255, 255, 255, 0)) y_offset = 0 for team in self.build['TEAM']: - has_assist = any( - [card is not None for idx, card in enumerate(team) if - idx % 2 == 1]) - has_latents = any( - [card['LATENT'] is not None for idx, card in enumerate(team) - if idx % 2 == 0 and card is not None]) + has_assist = any([card is not None for idx, card in enumerate(team) if idx % 2 == 1]) + has_latents = any([card['LATENT'] is not None for idx, card in enumerate(team) + if idx % 2 == 0 and card is not None]) if has_assist: y_offset += self.params.PORTRAIT_WIDTH for idx, card in enumerate(team): @@ -750,8 +692,7 @@ def generate_build_image(self, include_instructions=False): portrait, (x_offset + x * self.params.PORTRAIT_WIDTH, y_offset + y * self.params.PORTRAIT_WIDTH)) - if has_latents and idx % 2 == 0 and card[ - 'LATENT'] is not None: + if has_latents and idx % 2 == 0 and card['LATENT'] is not None: latents = self.combine_latents(card['LATENT']) self.build_img.paste( latents, @@ -771,9 +712,7 @@ def generate_build_image(self, include_instructions=False): for step in self.build['INSTRUCTION']: x_offset = self.params.PADDING outline_text(draw, x_offset, y_offset + text_padding, - font, 'white', - 'F{:d} - P{:d} '.format(step['FLOOR'], - step['PLAYER'] + 1)) + font, 'white', 'F{:d} - P{:d} '.format(step['FLOOR'], step['PLAYER'] + 1)) x_offset += self.params.PORTRAIT_WIDTH if step['ACTIVE'] is not None: actives_used = [self.build['TEAM'][idx][ids] @@ -781,24 +720,13 @@ def generate_build_image(self, include_instructions=False): for ids in side] for card in actives_used: if 'http' in self.params.PORTRAIT_DIR: - p_small = Image.open(urllib.request.urlopen( - self.params.PORTRAIT_DIR.format( - monster_id=card['ID']))).resize(( - self.params.PORTRAIT_WIDTH // 2, - self.params.PORTRAIT_WIDTH // 2), - Image.LINEAR) + p_small = Image.open(urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) else: - p_small = Image.open( - self.params.PORTRAIT_DIR.format( - monster_id=card['ID'])).resize(( - self.params.PORTRAIT_WIDTH // 2, - self.params.PORTRAIT_WIDTH // 2), - Image.LINEAR) + p_small = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) self.build_img.paste(p_small, (x_offset, y_offset)) x_offset += self.params.PORTRAIT_WIDTH // 2 x_offset += self.params.PADDING - outline_text(draw, x_offset, y_offset + text_padding, font, - 'white', step['ACTION']) + outline_text(draw, x_offset, y_offset + text_padding, font, 'white', step['ACTION']) y_offset += self.params.PORTRAIT_WIDTH // 2 del draw @@ -817,9 +745,8 @@ async def helpbuildimg(self, ctx): """Help info for the buildimage command.""" await ctx.author.send(box(HELP_MSG)) if checks.admin_or_permissions(manage_guild=True): - await ctx.author.send(box( - 'For Server Admins: Output location can be changed between current channel and ' - 'direct messages via ^togglebuildimgoutput')) + await ctx.author.send(box('For Server Admins: Output location can be changed between current channel and ' + 'direct messages via ^togglebuildimgoutput')) await ctx.author.send(EXAMPLE_MSG) @commands.command(aliases=['buildimg', 'pdchu']) @@ -831,8 +758,7 @@ async def padbuildimg(self, ctx, *, build_str: str): async with ctx.typing(): params = self.settings.buildImgParams() try: - pbg = PadBuildImageGenerator(params, - self.bot.get_cog('PadInfo')) + pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) await pbg.process_build(build_str) pbg.generate_build_image() except commands.UserFeedbackCheckFailure as ex: @@ -845,20 +771,15 @@ async def padbuildimg(self, ctx, *, build_str: str): build_io.seek(0) if ctx.guild and self.settings.dmOnly(ctx.guild.id): try: - await ctx.author.send( - file=discord.File(build_io, 'pad_build.png')) - await ctx.send( - inline('Sent build to {}'.format(ctx.author))) + await ctx.author.send(file=discord.File(build_io,'pad_build.png')) + await ctx.send(inline('Sent build to {}'.format(ctx.author))) except discord.errors.Forbidden as ex: - await ctx.send(inline( - 'Failed to send build to {}'.format(ctx.author))) + await ctx.send(inline('Failed to send build to {}'.format(ctx.author))) else: try: - await ctx.send( - file=discord.File(build_io, 'pad_build.png')) + await ctx.send(file=discord.File(build_io,'pad_build.png')) except discord.errors.Forbidden as ex: - await ctx.send(inline( - "Failed to send build. (Insufficient Permisisons)")) + await ctx.send(inline("Failed to send build. (Insufficient Permisisons)")) else: await ctx.send(box('Invalid build, see ^helpbuildimg')) return 0 @@ -875,12 +796,10 @@ async def configbuildimg(self, ctx, param_key: str, param_value: str): LATENTS_WIDTH - width of 1 slot latent, default 25 FONT_NAME - path to font """ - if param_key in ['ASSETS_DIR', 'PORTRAIT_DIR', 'PORTRAIT_WIDTH', - 'PADDING', 'LATENTS_WIDTH', 'FONT_NAME']: + if param_key in ['ASSETS_DIR', 'PORTRAIT_DIR', 'PORTRAIT_WIDTH', 'PADDING', 'LATENTS_WIDTH', 'FONT_NAME']: if param_key in ['PORTRAIT_WIDTH', 'PADDING', 'LATENTS_WIDTH']: param_value = int(param_value) - if param_key in ['ASSETS_DIR'] and param_value[-1] not in ['/', - '\\']: + if param_key in ['ASSETS_DIR'] and param_value[-1] not in ['/', '\\']: param_value += '/' self.settings.setBuildImgParamsByKey(param_key, param_value) await ctx.send(box('Set {} to {}'.format(param_key, param_value))) @@ -893,8 +812,7 @@ async def refreshassets(self, ctx): """ Refresh assets folder """ - await ctx.send('Downloading assets to {}'.format( - self.settings.buildImgParams().ASSETS_DIR)) + await ctx.send('Downloading assets to {}'.format(self.settings.buildImgParams().ASSETS_DIR)) awk_ids = self.bot.get_cog('Dadguide').database.get_awoken_skill_ids() await self.settings.downloadAllAssets(awk_ids) await ctx.tick() diff --git a/padbuilds/info.json b/padbuilds/info.json index 76282200..ae3ed7b6 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -1,25 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "View user-set builds for various dungeons.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Builds to help you through dungeons", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "tsutils" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "View user-set builds for various dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Builds to help you through dungeons", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["tsutils"], + "tags": ["PAD"] } diff --git a/padevents/__init__.py b/padevents/__init__.py index 7f7dbc01..e72e9a91 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = PadEvents(bot) bot.add_cog(n) diff --git a/padevents/info.json b/padevents/info.json index f0144455..28a2be3b 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -1,29 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Stay up to date on PAD dungeons.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "PAD Event tracker", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils", - "pytz", - "prettytable" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Stay up to date on PAD dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PAD Event tracker", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {"dadguide": "https://github.com/TsubakiBotPad/padbot-cogs"}, + "requirements": ["tsutils", "pytz", "prettytable"], + "tags": ["PAD"] } diff --git a/padevents/padevents.py b/padevents/padevents.py index 80d81ce4..034464a9 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,18 +1,19 @@ +import traceback import asyncio +import re import datetime -import discord -import logging -import prettytable import pytz -import re -import traceback +import logging from collections import defaultdict from datetime import timedelta from enum import Enum + +import prettytable +import discord +from tsutils import CogSettings from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify -from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.padevents') @@ -56,8 +57,7 @@ async def reload_padevents(self): await self.refresh_data() logger.info('Done refreshing PadEvents') except Exception as ex: - logger.exception( - "reload padevents loop caught exception " + str(ex)) + logger.exception("reload padevents loop caught exception " + str(ex)) await asyncio.sleep(60 * 60 * 1) @@ -80,42 +80,33 @@ async def check_started(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadEvents'): try: - events = filter(lambda - e: e.is_started() and not e.key in self.started_events, - self.events) + events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) daily_refresh_servers = set() for e in events: self.started_events.add(e.key) - if e.event_type in [EventType.Guerrilla, - EventType.GuerrillaNew, - EventType.SpecialWeek]: + if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: for gr in list(self.settings.listGuerrillaReg()): if e.server == gr['server']: try: - message = box( - "Server " + e.server + ", group " + - e.groupLongName() + " : " + e.name_and_modifier) - channel = self.bot.get_channel( - int(gr['channel_id'])) + message = box("Server " + e.server + ", group " + + e.groupLongName() + " : " + e.name_and_modifier) + channel = self.bot.get_channel(int(gr['channel_id'])) try: role_name = '{}_group_{}'.format( e.server, e.groupLongName()) - role = get_role(channel.guild.roles, - role_name) + role = get_role(channel.guild.roles, role_name) if role and role.mentionable: message = "{} `: {} is starting`".format( - role.mention, - e.name_and_modifier) + role.mention, e.name_and_modifier) except: pass # do nothing if role is missing await channel.send(message) except Exception as ex: # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) - logger.exception( - "caught exception while sending guerrilla msg:") + logger.exception("caught exception while sending guerrilla msg:") else: if not e.dungeon_type in [DungeonType.Normal]: @@ -123,21 +114,16 @@ async def check_started(self): for server in daily_refresh_servers: msg = self.makeActiveText(server) - for daily_registration in list( - self.settings.listDailyReg()): + for daily_registration in list(self.settings.listDailyReg()): try: if server == daily_registration['server']: - await self.pageOutput(self.bot.get_channel( - daily_registration['channel_id']), - msg, channel_id= - daily_registration[ - 'channel_id']) + await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), + msg, channel_id=daily_registration['channel_id']) logger.info("daily_reg server") except Exception as ex: # self.settings.removeDailyReg( # daily_registration['channel_id'], daily_registration['server']) - logger.exception( - "caught exception while sending daily msg:") + logger.exception("caught exception while sending daily msg:") except Exception as ex: logger.exception("caught exception while checking guerrillas:") @@ -162,7 +148,7 @@ async def testevent(self, ctx, server, seconds: int = 0): await dg_cog.wait_until_ready() dg_module = __import__(dg_cog.__module__) - te = dg_module.DgScheduledEvent({'dungeon_id': 1}, dg_cog.database) + te = dg_module.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) te.server = server te.event_type_id = EventType.Guerrilla.value @@ -171,8 +157,7 @@ async def testevent(self, ctx, server, seconds: int = 0): te.key = lambda: fuid te.group_name = 'Yellow' - te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta( - seconds=seconds) + te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) te.close_datetime = te.open_datetime + timedelta(minutes=1) te.dungeon_name = 'fake_dungeon_name' te.event_modifier = 'fake_event_modifier' @@ -265,8 +250,7 @@ def makeChannelList(self, reg_list): channel = self.bot.get_channel(int(reg_channel_id)) channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' server_name = channel.guild.name if channel else 'Unknown server' - msg += " " + cr[ - 'server'] + " : " + server_name + '(' + channel_name + ')\n' + msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' return msg @padevents.command() @@ -291,8 +275,7 @@ def makeActiveText(self, server): special_events = active_events.withType( EventType.Special).itemsByCloseTime() if len(special_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Special Events', - special_events) + msg += "\n\n" + self.makeActiveOutput('Special Events', special_events) all_etc_events = active_events.withType(EventType.Etc) @@ -302,37 +285,29 @@ def makeActiveText(self, server): msg += "\n\n" + self.makeActiveOutput('Etc Events', etc_events) # Old-style guerrillas - active_guerrilla_events = active_events.withType( - EventType.Guerrilla).items() + active_guerrilla_events = active_events.withType(EventType.Guerrilla).items() if len(active_guerrilla_events) > 0: msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', - active_guerrilla_events) + self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) guerrilla_events = pending_events.withType(EventType.Guerrilla).items() if len(guerrilla_events) > 0: - msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', - guerrilla_events) + msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', guerrilla_events) # New-style guerrillas - active_guerrilla_events = active_events.withType( - EventType.SpecialWeek).items() + active_guerrilla_events = active_events.withType(EventType.SpecialWeek).items() if len(active_guerrilla_events) > 0: msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', - active_guerrilla_events) + self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) - guerrilla_events = pending_events.withType( - EventType.SpecialWeek).items() + guerrilla_events = pending_events.withType(EventType.SpecialWeek).items() if len(guerrilla_events) > 0: msg += "\n\n" + \ self.makeFullGuerrillaOutput( - 'Guerrilla Events', guerrilla_events, - starter_guerilla=True) + 'Guerrilla Events', guerrilla_events, starter_guerilla=True) # clean up long headers - msg = msg.replace('-------------------------------------', - '-----------------------') + msg = msg.replace('-------------------------------------', '-----------------------') return msg @@ -344,11 +319,9 @@ async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): if channel_id is None: await ctx.send(format_type(page)) else: - await self.bot.get_channel(int(channel_id)).send( - format_type(page)) + await self.bot.get_channel(int(channel_id)).send(format_type(page)) except Exception as e: - logger.exception("page output failed " + str(e), - "tried to output: " + page) + logger.exception("page output failed " + str(e), "tried to output: " + page) def makeActiveOutput(self, table_name, event_list): tbl = prettytable.PrettyTable(["Time", table_name]) @@ -367,19 +340,16 @@ def makeActiveGuerrillaOutput(self, table_name, event_list): tbl.align[table_name] = "l" tbl.align["Time"] = "r" for e in event_list: - tbl.add_row( - [e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) + tbl.add_row([e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) return tbl.get_string() - def makeFullGuerrillaOutput(self, table_name, event_list, - starter_guerilla=False): + def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False): events_by_name = defaultdict(list) for e in event_list: events_by_name[e.name_and_modifier].append(e) rows = list() - grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", - "D", "E"] + grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] for name, events in events_by_name.items(): events = sorted(events, key=lambda e: e.open_datetime) events_by_group = defaultdict(list) @@ -527,11 +497,10 @@ def __init__(self, scheduled_event: "DgScheduledEvent"): self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' - self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' + self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) - self.clean_event_name = self.event_name.replace('!', '').replace(' ', - '') + self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') self.name_and_modifier = self.clean_dungeon_name if self.clean_event_name != '': @@ -594,14 +563,11 @@ def toGuerrillaStr(self): return fmtTimeShort(self.startPst()) def toDateStr(self): - return self.server + "," + self.group + "," + fmtTime( - self.startPst()) + "," + fmtTime( + return self.server + "," + self.group + "," + fmtTime(self.startPst()) + "," + fmtTime( self.startEst()) + "," + self.startFromNow() def groupShortName(self): - return self.group.upper().replace('RED', 'R').replace('BLUE', - 'B').replace( - 'GREEN', 'G') + return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') def groupLongName(self): return self.group.upper() @@ -611,8 +577,7 @@ def toPartialEvent(self, pe): if self.is_started(): return group + " " + self.endFromNow() + " " + self.name_and_modifier else: - return group + " " + fmtTimeShort( - self.startPst()) + " " + fmtTimeShort( + return group + " " + fmtTimeShort(self.startPst()) + " " + fmtTimeShort( self.startEst()) + " " + self.startFromNow() + " " + self.name_and_modifier @@ -639,8 +604,7 @@ def withDungeonType(self, dungeon_type, exclude=False): return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) def withNameContains(self, name, exclude=False): - return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), - exclude) + return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) def excludeUnwantedEvents(self): return self.withFunc(isEventWanted) @@ -661,14 +625,10 @@ def availableOnly(self): return self.withFunc(lambda e: e.is_available()) def itemsByOpenTime(self, reverse=False): - return list(sorted(self.event_list, - key=(lambda e: (e.open_datetime, e.dungeon_name)), - reverse=reverse)) + return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) def itemsByCloseTime(self, reverse=False): - return list(sorted(self.event_list, - key=(lambda e: (e.close_datetime, e.dungeon_name)), - reverse=reverse)) + return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) # TIME_FMT = """%a %b %d %H:%M:%S %Y""" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 89d66ef4..8186b3ca 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1,22 +1,23 @@ -import aiohttp import asyncio import csv import datetime import difflib -import discord import io import json -import logging import os -import prettytable import re -import tsutils +import logging from collections import defaultdict + +import aiohttp +import discord +import prettytable +import tsutils +from tsutils import CogSettings, safe_read_json, replace_emoji_names_with_code,\ + clean_global_mentions, confirm_message from redbot.core import checks, data_manager from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify -from tsutils import CogSettings, safe_read_json, replace_emoji_names_with_code, \ - clean_global_mentions, confirm_message logger = logging.getLogger('red.padbot-cogs.padglobal') @@ -24,8 +25,7 @@ def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), - file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) DATA_EXPORT_PATH = _data_file('padglobal_data.json') @@ -47,9 +47,8 @@ def _data_file(file_name: str) -> str: DISABLED_MSG = 'PAD Global info disabled on this server' FARMABLE_MSG = 'This monster is **farmable** so make as many copies of whichever evos you like.' -MP_BUY_MSG = ( - 'This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' - ', check {}mpdra? for specific recommendations.') +MP_BUY_MSG = ('This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' + ', check {}mpdra? for specific recommendations.') SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution.' @@ -59,13 +58,11 @@ def mod_help(self, ctx, help_type): commands.Command.format_help_for_context = lambda s, c: mod_help(s, c, "help") -commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, - "short_doc") +commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, "short_doc") def is_padglobal_admin_check(ctx): - return checks.is_owner() or ctx.bot.get_cog( - "PadGlobal").settings.check_admin(ctx.author.id) + return checks.is_owner() or ctx.bot.get_cog("PadGlobal").settings.check_admin(ctx.author.id) def is_padglobal_admin(): @@ -133,12 +130,9 @@ async def red_delete_data_for_user(self, *, requester, user_id): def _export_data(self): faq_and_boards = self.settings.faq() + self.settings.boards() - general = {k: v for k, v in self.c_commands.items() if - k not in faq_and_boards} - faq = {k: v for k, v in self.c_commands.items() if - k in self.settings.faq()} - boards = {k: v for k, v in self.c_commands.items() if - k in self.settings.boards()} + general = {k: v for k, v in self.c_commands.items() if k not in faq_and_boards} + faq = {k: v for k, v in self.c_commands.items() if k in self.settings.faq()} + boards = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} glossary = self.settings.glossary() bosses = self.settings.boss() which = self.settings.which() @@ -173,8 +167,7 @@ class of problems where the bot can read messages but not respond, so this could handle those cases. """ msg = '------------------------\n' - msg += '{} shut down the bot because: {}\n'.format(ctx.author.name, - reason) + msg += '{} shut down the bot because: {}\n'.format(ctx.author.name, reason) msg += '------------------------\n' logger.critical(msg) @@ -197,10 +190,8 @@ async def togglepadglobal(self, ctx): self.settings.rmDisabledServer(server_id) else: self.settings.addDisabledServer(server_id) - status = 'disabled' if self.settings.checkDisabled( - ctx.message) else 'enabled' - await ctx.send( - inline('PAD Global commands {} on this server').format(status)) + status = 'disabled' if self.settings.checkDisabled(ctx.message) else 'enabled' + await ctx.send(inline('PAD Global commands {} on this server').format(status)) @commands.command() @is_padglobal_admin() @@ -243,8 +234,7 @@ def list_or_none(l): msg += "\n\nNickname original components:" msg += "\n monster_basename: {}".format(m.monster_basename) - msg += "\n group_computed_basename: {}".format( - m.group_computed_basename) + msg += "\n group_computed_basename: {}".format(m.group_computed_basename) msg += "\n extra_nicknames: {}".format(list_or_none(m.extra_nicknames)) msg += "\n\nNickname final components:" @@ -252,14 +242,10 @@ def list_or_none(l): msg += "\n prefixes: {}".format(list_or_none(m.prefixes)) msg += "\n\nAccepted nickname entries:" - accepted_nn = list(filter( - lambda nn: m.monster_id == padinfo_cog.index_all.all_entries[ - nn].monster_id, - m.final_nicknames)) - accepted_twnn = list(filter( - lambda nn: m.monster_id == padinfo_cog.index_all.two_word_entries[ - nn].monster_id, - m.final_two_word_nicknames)) + accepted_nn = list(filter(lambda nn: m.monster_id == padinfo_cog.index_all.all_entries[nn].monster_id, + m.final_nicknames)) + accepted_twnn = list(filter(lambda nn: m.monster_id == padinfo_cog.index_all.two_word_entries[nn].monster_id, + m.final_two_word_nicknames)) msg += "\n nicknames: {}".format(list_or_none(accepted_nn)) msg += "\n two_word_nicknames: {}".format(list_or_none(accepted_twnn)) @@ -274,8 +260,7 @@ def list_or_none(l): replaced_nn_info = map(lambda nn: ( nn, padinfo_cog.index_all.all_entries[nn]), replaced_nn) replaced_twnn_info = map( - lambda nn: (nn, padinfo_cog.index_all.two_word_entries[nn]), - replaced_twnn) + lambda nn: (nn, padinfo_cog.index_all.two_word_entries[nn]), replaced_twnn) replaced_nn_text = list(map(lambda nn_info: '{} : {}. {}'.format( nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_na), @@ -286,8 +271,7 @@ def list_or_none(l): replaced_twnn_info)) msg += "\n nicknames: {}".format(list_or_none(replaced_nn_text)) - msg += "\n two_word_nicknames: {}".format( - list_or_none(replaced_twnn_text)) + msg += "\n two_word_nicknames: {}".format(list_or_none(replaced_twnn_text)) msg += "\n\nNickname entry sort parts:" msg += "\n (is_low_priority, group_size, monster_no_na) : ({}, {}, {})".format( @@ -301,7 +285,7 @@ def list_or_none(l): for page in pagify(msg): sent_messages.append(await ctx.send(box(page))) await tsutils.await_and_remove(self.bot, sent_messages[-1], ctx.author, - delete_msgs=sent_messages, timeout=30) + delete_msgs=sent_messages, timeout=30) @commands.command() @is_padglobal_admin() @@ -314,7 +298,7 @@ async def forceindexreload(self, ctx): await padinfo_cog.refresh_index() await ctx.send('finished reload') - @commands.group(aliases=['pdg']) + @commands.group(aliases = ['pdg']) @is_padglobal_admin() async def padglobal(self, ctx): """PAD global custom commands.""" @@ -335,7 +319,7 @@ async def edit(self, ctx, command: str, *, text: str): """Edit a custom command or alias""" await self._add(ctx, command, text, False) - async def _add(self, ctx, command, text, confirm=True): + async def _add(self, ctx, command, text, confirm = True): command = command.lower() text = clean_global_mentions(text) text = text.replace(u'\u200b', '') @@ -365,11 +349,8 @@ async def _add(self, ctx, command, text, confirm=True): ted = self.c_commands[ted] alias = True if confirm: - conf = await confirm_message(ctx, - "Are you sure you want to edit the {}command {}?" \ - .format( - "alias to " if alias else "", - ted)) + conf = await confirm_message(ctx,"Are you sure you want to edit the {}command {}?" \ + .format("alias to " if alias else "", ted)) if not conf: return else: @@ -454,33 +435,28 @@ async def checktype(self, ctx, command: str): elif command in self.c_commands: await ctx.send('{} is a general padglobal command.'.format(command)) else: - await ctx.send( - '{} is not a padglobal command. It might be a meme or a custom command.'.format( - command)) + await ctx.send('{} is not a padglobal command. It might be a meme or a custom command.'.format(command)) @commands.command() @commands.check(check_enabled) async def pad(self, ctx): """Shows PAD global command list""" configured = self.settings.faq() + self.settings.boards() - cmdlist = {k: v for k, v in self.c_commands.items() if - k not in configured} + cmdlist = {k: v for k, v in self.c_commands.items() if k not in configured} await self.send_cmdlist(ctx, cmdlist) @commands.command() @commands.check(check_enabled) async def padfaq(self, ctx): """Shows PAD FAQ command list""" - cmdlist = {k: v for k, v in self.c_commands.items() if - k in self.settings.faq()} + cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.faq()} await self.send_cmdlist(ctx, cmdlist) @commands.command() @commands.check(check_enabled) async def boards(self, ctx): """Shows PAD Boards command list""" - cmdlist = {k: v for k, v in self.c_commands.items() if - k in self.settings.boards()} + cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} await self.send_cmdlist(ctx, cmdlist) async def send_cmdlist(self, ctx, cmdlist, inline=False): @@ -569,8 +545,7 @@ async def padto(self, ctx, to_user: discord.Member, *, term: str): result = self.c_commands.get(corrected_term, None) await self._do_send_term(ctx, to_user, term, corrected_term, result) - async def _do_send_term(self, ctx, to_user: discord.Member, term, - corrected_term, result): + async def _do_send_term(self, ctx, to_user: discord.Member, term, corrected_term, result): """Does the heavy lifting shared by padto and glossaryto.""" if result: result_output = '**{}** : {}'.format(corrected_term, result) @@ -604,8 +579,7 @@ async def glossary(self, ctx, *, term: str = None): def glossary_to_text(self, ctx): glossary = self.settings.glossary() - msg = '__**PAD Glossary terms (also check out {0}pad / {0}padfaq / {0}boards / {0}which)**__'.format( - ctx.prefix) + msg = '__**PAD Glossary terms (also check out {0}pad / {0}padfaq / {0}boards / {0}which)**__'.format(ctx.prefix) for term in sorted(glossary.keys()): definition = glossary[term] msg += '\n**{}** : {}'.format(term, definition) @@ -622,8 +596,7 @@ def lookup_glossary(self, term): matches = self._get_corrected_cmds(term, glossary.keys()) if not matches: - matches = difflib.get_close_matches(term, glossary.keys(), n=1, - cutoff=.8) + matches = difflib.get_close_matches(term, glossary.keys(), n=1, cutoff=.8) if not matches: return term, None @@ -642,8 +615,7 @@ async def addglossary(self, ctx, term, *, definition): term = term.lower() definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') - definition = replace_emoji_names_with_code(self._get_emojis(), - definition) + definition = replace_emoji_names_with_code(self._get_emojis(), definition) op = 'EDITED' if term in self.settings.glossary() else 'ADDED' self.settings.addGlossary(term, definition) @@ -656,9 +628,7 @@ async def rmglossary(self, ctx, *, term): if term not in self.settings.glossary(): await ctx.send("Glossary item doesn't exist.") return - if not await confirm_message(ctx, - "Are you sure you want to globally remove the glossary data for {}?".format( - term)): + if not await confirm_message(ctx, "Are you sure you want to globally remove the glossary data for {}?".format(term)): return self.settings.rmGlossary(term) await ctx.tick() @@ -671,9 +641,7 @@ async def boss(self, ctx, *, term: str = None): term_new, definition = self.lookup_boss(term) if definition: if term_new != term.lower(): - await ctx.send( - 'No entry for {} found, corrected to {}'.format(term, - term_new)) + await ctx.send('No entry for {} found, corrected to {}'.format(term, term_new)) await ctx.send(self.emojify(definition)) else: await ctx.send(inline('No mechanics found')) @@ -699,8 +667,7 @@ def lookup_boss(self, term): matches = self._get_corrected_cmds(term, bosses.keys()) if not matches: - matches = difflib.get_close_matches(term, bosses.keys(), n=1, - cutoff=.8) + matches = difflib.get_close_matches(term, bosses.keys(), n=1, cutoff=.8) if not matches: return term, None @@ -710,8 +677,7 @@ def lookup_boss(self, term): def boss_to_text(self, ctx): bosses = self.settings.boss() - msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( - ctx.prefix) + msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format(ctx.prefix) for term in sorted(bosses.keys()): definition = bosses[term] msg += '\n**{}**\n{}'.format(term, definition) @@ -719,8 +685,7 @@ def boss_to_text(self, ctx): def boss_to_text_index(self, ctx): bosses = self.settings.boss() - msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( - ctx.prefix) + msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format(ctx.prefix) msg = msg + '\n' + ',\n'.join(sorted(bosses.keys())) return msg @@ -732,8 +697,7 @@ async def addboss(self, ctx, term, *, definition): op = 'EDITED' if term in self.settings.boss() else 'ADDED' definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') - definition = replace_emoji_names_with_code(self._get_emojis(), - definition) + definition = replace_emoji_names_with_code(self._get_emojis(), definition) self.settings.addBoss(term, definition) await ctx.send("PAD boss mechanics successfully {}.".format(op)) @@ -744,9 +708,7 @@ async def rmboss(self, ctx, *, term): if term not in self.settings.boss(): await ctx.send("Boss mechanics item doesn't exist.") return - if not await confirm_message(ctx, - "Are you sure you want to globally remove the boss data for {}?".format( - term)): + if not await confirm_message(ctx, "Are you sure you want to globally remove the boss data for {}?".format(term)): return self.settings.rmBoss(term) @@ -758,23 +720,19 @@ async def which(self, ctx, *, term: str = None): """Shows PAD Which Monster entries""" if term is None: - await ctx.author.send( - '__**PAD Which Monster**__ *(also check out {0}pad / {0}padfaq / {0}boards / {0}glossary)*'.format( - ctx.prefix)) + await ctx.author.send('__**PAD Which Monster**__ *(also check out {0}pad / {0}padfaq / {0}boards / {0}glossary)*'.format(ctx.prefix)) msg = self.which_to_text() for page in pagify(msg): await ctx.author.send(box(page)) return - name, definition, timestamp, success = await self._resolve_which(ctx, - term) + name, definition, timestamp, success = await self._resolve_which(ctx, term) if name is None or definition is None: return if not success: await ctx.send('`Which {}`\n{}'.format(name, definition)) return - await ctx.send( - inline('Which {} - Last Updated {}'.format(name, timestamp))) + await ctx.send(inline('Which {} - Last Updated {}'.format(name, timestamp))) await ctx.send(self.emojify(definition)) async def _resolve_which(self, ctx, term): @@ -804,8 +762,7 @@ async def _resolve_which(self, ctx, term): elif check_simple_tree(monster): return name, SIMPLE_TREE_MSG, None, False else: - await ctx.send( - inline('No which info for {} (#{})'.format(name, monster_id))) + await ctx.send(inline('No which info for {} (#{})'.format(name, monster_id))) return None, None, None, None @commands.command() @@ -815,8 +772,7 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): [p]whichto @{0.author.name} saria """ - name, definition, timestamp, success = await self._resolve_which(ctx, - term) + name, definition, timestamp, success = await self._resolve_which(ctx, term) if name is None or definition is None: return @@ -846,12 +802,9 @@ def which_to_text(self): tbl_string = tsutils.strip_right_multiline(tbl.get_string()) return tbl_string - async def _do_send_which(self, ctx, to_user: discord.Member, name, - definition, timestamp): + async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, timestamp): """Does the heavy lifting for whichto.""" - result_output = '**Which {} - Last Updated {}**\n{}'.format(name, - timestamp, - definition) + result_output = '**Which {} - Last Updated {}**\n{}'.format(name, timestamp, definition) result = "{} asked me to send you this:\n{}".format( ctx.author.name, result_output) @@ -869,9 +822,7 @@ async def addwhich(self, ctx, monster_id: int, *, definition): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send( - "I think you meant {} for {}.".format(m.monster_no_na, - m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) name = m.monster_id op = 'EDITED' if name in self.settings.which() else 'ADDED' @@ -884,12 +835,8 @@ async def rmwhich(self, ctx, *, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send( - "I think you meant {} for {}.".format(m.monster_no_na, - m.name_na)) - if not await confirm_message(ctx, - "Are you sure you want to globally remove the which data for {}?".format( - m.name_na)): + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format(m.name_na)): return name = m.monster_id @@ -939,9 +886,8 @@ async def debuglookup(self, ctx, *, term: str): name = nm.name_na if nm else nm monster_id = nm.monster_id if nm else nm definition = self.settings.which().get(monster_id, None) - await ctx.send( - 'Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' - .format(base, name, monster_id, err, deb)) + await ctx.send('Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' + .format(base, name, monster_id, err, deb)) @padglobal.command() @checks.is_owner() @@ -972,9 +918,8 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): """Set the emoji servers by ID (csv)""" self.settings.emojiServers().clear() if emoji_servers: - self.settings.setEmojiServers(re.findall(r'\d+', emoji_servers)) - await ctx.send( - inline('Set {} servers'.format(len(self.settings.emojiServers())))) + self.settings.setEmojiServers(re.findall(r'\d+',emoji_servers)) + await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) def _get_emojis(self): emojis = list() @@ -1012,8 +957,7 @@ async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): all_emojis = self._get_emojis() source_url = PORTRAIT_TEMPLATE.format(monster_id) - emoji_name = 'pad_' + ('na_' if server == 'na' else '') + str( - monster_id) + emoji_name = 'pad_' + ('na_' if server == 'na' else '') + str(monster_id) for e in all_emojis: if emoji_name == e.name: @@ -1029,10 +973,8 @@ async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): async with aiohttp.ClientSession() as sess: async with sess.get(source_url) as resp: emoji_content = await resp.read() - await emoji_server.create_custom_emoji(name=emoji_name, - image=emoji_content) - await ctx.send(inline( - 'Done creating emoji named {}'.format(emoji_name))) + await emoji_server.create_custom_emoji(name=emoji_name, image=emoji_content) + await ctx.send(inline('Done creating emoji named {}'.format(emoji_name))) except Exception as ex: await ctx.send(box('Error:\n' + str(ex))) @@ -1041,8 +983,7 @@ async def checkCC(self, message): if message.author.id == self.bot.user.id: return - global_ignores = { - 'blacklist': []} # self.bot.get_cog('Core').global_ignores + global_ignores = {'blacklist': []} # self.bot.get_cog('Core').global_ignores if message.author.id in global_ignores["blacklist"]: return False @@ -1064,8 +1005,7 @@ async def checkCC(self, message): return if final_cmd != cmd: - await message.channel.send( - inline('Corrected to: {}'.format(final_cmd))) + await message.channel.send(inline('Corrected to: {}'.format(final_cmd))) result = self.c_commands[final_cmd] while result in self.c_commands: result = self.c_commands[result] @@ -1167,8 +1107,7 @@ async def get_guide_text(self, term: str): return None, None, 'No dungeon or monster matched that query' name = nm.group_computed_basename.title() - definition = self.settings.leaderGuide().get(str(nm.base_monster_no), - None) + definition = self.settings.leaderGuide().get(str(nm.base_monster_no), None) if definition is None: return None, None, 'A monster matched that query but has no guide' @@ -1230,9 +1169,7 @@ async def rmdungeonguide(self, ctx, term: str): if term not in self.settings.dungeonGuide(): await ctx.send("DungeonGuide doesn't exist.") return - if not await confirm_message(ctx, - "Are you sure you want to globally remove the dungeonguide data for {}?".format( - term)): + if not await confirm_message(ctx, "Are you sure you want to globally remove the dungeonguide data for {}?".format(term)): return self.settings.rmDungeonGuide(term) await ctx.tick() @@ -1243,9 +1180,7 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send( - "I think you meant {} for {}.".format(m.monster_no_na, - m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) name = m.monster_id op = 'EDITED' if name in self.settings.leaderGuide() else 'ADDED' @@ -1258,12 +1193,8 @@ async def rmleaderguide(self, ctx, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send( - "I think you meant {} for {}.".format(m.monster_no_na, - m.name_na)) - if not await confirm_message(ctx, - "Are you sure you want to globally remove the leaderguide data for {}?".format( - m.name_na)): + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format(m.name_na)): return name = m.monster_id @@ -1287,7 +1218,6 @@ def emojify(self, message): message = tsutils.replace_emoji_names_with_code(emojis, message) return tsutils.fix_emojis_for_server(emojis, message) - def check_simple_tree(monster): attr1 = monster.attr1 active_skill = monster.active_skill diff --git a/padguidedb/__init__.py b/padguidedb/__init__.py index 86d4637e..4a503383 100644 --- a/padguidedb/__init__.py +++ b/padguidedb/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): pdb = PadGuideDb(bot) bot.add_cog(pdb) diff --git a/padguidedb/info.json b/padguidedb/info.json index ae9e2e31..4c36d429 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -1,26 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Dungeon scraper and general database manager.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Dungeon data scraper", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "tsutils", - "pymysql" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Dungeon scraper and general database manager.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Dungeon data scraper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["tsutils", "pymysql"], + "tags": ["PAD"] } diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 892489b3..4ee3974a 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -1,13 +1,15 @@ import asyncio import csv -import discord import io import json import logging + +import discord import pymysql from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify + from tsutils import CogSettings, tsutils logger = logging.getLogger('red.padbot-cogs.padguidedb') @@ -15,8 +17,7 @@ def is_padguidedb_admin_check(ctx): is_owner = ctx.author.id in ctx.bot.owner_ids - return is_owner or ctx.bot.get_cog("PadGuideDb").settings.checkAdmin( - ctx.author.id) + return is_owner or ctx.bot.get_cog("PadGuideDb").settings.checkAdmin(ctx.author.id) def is_padguidedb_admin(): @@ -107,8 +108,7 @@ async def searchdungeon(self, ctx, *, search_text): ' order by dungeon_id desc limit 20') cursor.execute(sql, [search_text, search_text]) results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, - ensure_ascii=False) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) await ctx.send(inline(sql)) for page in pagify(msg): await ctx.send(box(page)) @@ -136,44 +136,34 @@ async def setimageupdatefile(self, ctx, *, image_update_file): @padguidedb.command() @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, - user_intid: str): + async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): """Set the dungeon script.""" self.settings.setUserInfo(server, user_uuid, user_intid) await ctx.tick() @padguidedb.command() @is_padguidedb_admin() - async def loaddungeon(self, ctx, server: str, dungeon_id: int, - dungeon_floor_id: int, queues: int = 1): + async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): if queues > 5: await ctx.send("You must send less than 5 queues.") return elif self.queue_size + queues > 60: - await ctx.send( - "The size of the queue cannot exceed 60. It is currently {}.".format( - self.queue_size)) + await ctx.send("The size of the queue cannot exceed 60. It is currently {}.".format(self.queue_size)) return elif not self.settings.hasUserInfo(server): - await ctx.send( - "There is no account associated with server '{}'.".format( - server.upper())) + await ctx.send("There is no account associated with server '{}'.".format(server.upper())) return self.queue_size += queues if queues == 1: - await ctx.send( - inline('Queueing load in slot {}'.format(self.queue_size))) + await ctx.send(inline('Queueing load in slot {}'.format(self.queue_size))) else: await ctx.send( - inline('Queueing loads in slots {}-{}'.format( - self.queue_size - queues + 1, self.queue_size))) + inline('Queueing loads in slots {}-{}'.format(self.queue_size - queues + 1, self.queue_size))) event_loop = asyncio.get_event_loop() for queue in range(queues): - event_loop.create_task( - self.do_dungeon_load(ctx, server.upper(), dungeon_id, - dungeon_floor_id)) + event_loop.create_task(self.do_dungeon_load(ctx, server.upper(), dungeon_id, dungeon_floor_id)) async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): async with self.dungeon_load_lock: @@ -195,31 +185,26 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): if stderr: logger.error("Dungeon Load Error:\n" + stderr.decode()) await tsutils.doubleup(ctx, inline( - 'Load for {} {} {} failed'.format(server, dungeon_id, - dungeon_floor_id))) + 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) else: await tsutils.doubleup(ctx, inline( - 'Load for {} {} {} finished'.format(server, dungeon_id, - dungeon_floor_id))) + 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) self.queue_size -= 1 @padguidedb.command() @is_padguidedb_admin() - async def olddungeondrops(self, ctx, dungeon_id: int, - dungeon_floor_id: int): + async def olddungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): with self.get_cursor() as cursor: sql = ("SELECT stage, drop_monster_id, COUNT(*) AS count" " FROM wave_data" " WHERE dungeon_id = {} AND floor_id = {}" " GROUP BY 1, 2" - " ORDER BY 1, 2").format(int(dungeon_id), - int(dungeon_floor_id)) + " ORDER BY 1, 2").format(int(dungeon_id), int(dungeon_floor_id)) cursor.execute(sql) results = list(cursor.fetchall()) msg = 'stage,drop_monster_id,count' for row in results: - msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], - row['count']) + msg += '\n{},{},{}'.format(row['stage'], row['drop_monster_id'], row['count']) for page in pagify(msg): await ctx.send(box(page)) @@ -245,10 +230,10 @@ async def dungeondrops(self, ctx, dungeon_id: int): msg = 'floor_id,count\n' for row in results1: - msg += ','.join(map(str, row.values())) + "\n" + msg += ','.join(map(str,row.values()))+"\n" msg += '\n\nfloor_id,monster_id,count\n' for row in results2: - msg += ','.join(map(str, row.values())) + "\n" + msg += ','.join(map(str,row.values()))+"\n" for page in pagify(msg): await ctx.send(box(page)) @@ -257,8 +242,7 @@ async def dungeondrops(self, ctx, dungeon_id: int): @checks.is_owner() async def cleardungeon(self, ctx, dungeon_id: int): with self.get_cursor() as cursor: - sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format( - int(dungeon_id)) + sql = "DELETE FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) await ctx.tick() @@ -266,8 +250,7 @@ async def cleardungeon(self, ctx, dungeon_id: int): @is_padguidedb_admin() async def dungeondata(self, ctx, dungeon_id: int): with self.get_cursor() as cursor: - sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format( - int(dungeon_id)) + sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) cursor.execute(sql) results = list(cursor.fetchall()) order = sorted(results[0]) @@ -277,8 +260,7 @@ async def dungeondata(self, ctx, dungeon_id: int): writer.writerow(order) writer.writerows(rows) fauxfile.seek(0) - m = await ctx.send( - file=discord.File(fauxfile, filename="dungeondata.csv")) + m = await ctx.send(file=discord.File(fauxfile, filename="dungeondata.csv")) await asyncio.sleep(10) await m.delete() @@ -296,8 +278,7 @@ async def fulletl(self, ctx): return async with self.full_etl_lock: - await ctx.send( - inline('Running full ETL pipeline: this could take a while')) + await ctx.send(inline('Running full ETL pipeline: this could take a while')) process = await asyncio.create_subprocess_exec( 'bash', self.settings.fullETLFile(), @@ -322,8 +303,7 @@ async def extractimages(self, ctx): return async with self.extract_images_lock: - await ctx.send(inline( - 'Running image extract pipeline: this could take a while')) + await ctx.send(inline('Running image extract pipeline: this could take a while')) process = await asyncio.create_subprocess_exec( 'bash', self.settings.imageUpdateFile(), diff --git a/padinfo/__init__.py b/padinfo/__init__.py index 0505758e..13827c7b 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = PadInfo(bot) bot.add_cog(n) diff --git a/padinfo/info.json b/padinfo/info.json index 40c5aae7..d984d84f 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -1,29 +1,18 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Serves monster data (LOAD THIS THIRD)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "speech": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils", - "prettytable" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Serves monster data (LOAD THIS THIRD)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "speech": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["tsutils", "prettytable"], + "tags": ["PAD"] } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b0a7171a..109683bb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,20 +1,21 @@ import asyncio -import discord -import io import json import logging import os -import prettytable +import io import traceback -import tsutils import urllib.parse from collections import OrderedDict from enum import Enum + +import discord +import prettytable from redbot.core import checks, data_manager, commands -from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import inline, box -from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, \ - CogSettings, rmdiacritics +from redbot.core.utils import AsyncIter + +import tsutils +from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -74,8 +75,7 @@ def get_pic_url(m): def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), - file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) class IdEmojiUpdater(EmojiUpdater): @@ -91,8 +91,7 @@ def on_update(self, ctx, selected_emoji): if self.pad_info.settings.checkEvoID(ctx.author.id): DGCOG = self.bot.get_cog('Dadguide') evos = list(map(lambda x: [x['from_id'], x['to_id']], - DGCOG.database.get_all_evolutions_by_monster( - self.m.monster_id))) + DGCOG.database.get_all_evolutions_by_monster(self.m.monster_id))) evos = list(set(sum(evos, []))) evos.sort() index = evos.index(self.m.monster_id) @@ -127,8 +126,7 @@ def on_update(self, ctx, selected_emoji): class ScrollEmojiUpdater(EmojiUpdater): def __init__(self, emoji_to_embed, m: "DgMonster" = None, - ms: "List[int]" = None, selected_emoji=None, pad_info=None, - bot=None): + ms: "List[int]" = None, selected_emoji=None, pad_info=None, bot=None): self.emoji_dict = emoji_to_embed self.m = m self.ms = ms @@ -155,8 +153,7 @@ def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, - scroll=True) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=True) return True @@ -194,10 +191,8 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_file_path = _data_file('historic_lookups.json') self.historic_lookups = safe_read_json(self.historic_lookups_file_path) - self.historic_lookups_file_path_id2 = _data_file( - 'historic_lookups_id2.json') - self.historic_lookups_id2 = safe_read_json( - self.historic_lookups_file_path_id2) + self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') + self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -227,8 +222,7 @@ async def reload_nicknames(self): await self.refresh_index() except Exception as ex: wait_time = 5 - logger.exception( - "reload padinfo loop caught exception " + str(ex)) + logger.exception("reload padinfo loop caught exception " + str(ex)) await asyncio.sleep(wait_time) @@ -288,8 +282,7 @@ async def idjp(self, ctx, *, query: str): await self._do_id(ctx, query, server_filter=ServerFilter.jp) async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = await self.findMonster(query, - server_filter=server_filter) + m, err, debug_info = await self.findMonster(query, server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -314,8 +307,7 @@ async def id2jp(self, ctx, *, query: str): await self._do_id2(ctx, query, server_filter=ServerFilter.jp) async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = await self.findMonster2(query, - server_filter=server_filter) + m, err, debug_info = await self.findMonster2(query, server_filter=server_filter) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -371,8 +363,7 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): ctx, starting_menu_emoji, IdEmojiUpdater(emoji_to_embed, pad_info=self, - m=m, selected_emoji=starting_menu_emoji, - bot=self.bot) + m=m, selected_emoji=starting_menu_emoji, bot=self.bot) ) async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): @@ -381,7 +372,7 @@ async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): ctx, starting_menu_emoji, ScrollEmojiUpdater(emoji_to_embed, pad_info=self, bot=self.bot, - m=m, ms=ms, selected_emoji=starting_menu_emoji) + m=m, ms=ms, selected_emoji=starting_menu_emoji) ) def get_id_emoji_options(self, m=None, scroll=False): @@ -420,6 +411,7 @@ def get_id_emoji_options(self, m=None, scroll=False): emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message return emoji_to_embed + async def _do_evolistmenu(self, ctx, sm): monsters = sm.alt_evos monsters.sort(key=lambda m: m.monster_id) @@ -431,23 +423,18 @@ async def _do_evolistmenu(self, ctx, sm): if m.monster_id == sm.monster_id: starting_menu_emoji = emoji - return await self._do_menu(ctx, starting_menu_emoji, - EmojiUpdater(emoji_to_embed), timeout=60) + return await self._do_menu(ctx, starting_menu_emoji, EmojiUpdater(emoji_to_embed), timeout=60) - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, - timeout=30): + async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): if starting_menu_emoji not in emoji_to_embed.emoji_dict: # Selected menu wasn't generated for this monster return EMBED_NOT_GENERATED - emoji_to_embed.emoji_dict[ - self.remove_emoji] = self.menu.reaction_delete_message + emoji_to_embed.emoji_dict[self.remove_emoji] = self.menu.reaction_delete_message try: - result_msg, result_embed = await self.menu.custom_menu(ctx, - emoji_to_embed, - starting_menu_emoji, - timeout=timeout) + result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, + starting_menu_emoji, timeout=timeout) if result_msg and result_embed: # Message is finished but not deleted, clear the footer result_embed.set_footer(text=discord.Embed.Empty) @@ -473,10 +460,10 @@ async def links(self, ctx, *, query: str): if m is not None: embed = monsterToBaseEmbed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) embed.set_footer(text='') await ctx.send(embed=embed) @@ -524,8 +511,8 @@ async def collabscroll(self, ctx, *, query: str): ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) ms = [m._alt_evo_id_list for m in ms - if m._base_monster_id == m.monster_id - and m.sell_mp >= 3000] + if m._base_monster_id == m.monster_id + and m.sell_mp >= 3000] ms = [m for ml in ms for m in ml] if m._base_monster_id != m.monster_id: @@ -538,18 +525,17 @@ async def collabscroll(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) + @commands.command(aliases=['leaders', 'leaderskills', 'ls']) @checks.bot_has_permissions(embed_links=True) - async def leaderskill(self, ctx, left_query: str, right_query: str = None, - *, bad=None): + async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): """Display the multiplier and leaderskills for two monsters If either your left or right query contains spaces, wrap in quotes. e.g.: [p]leaderskill "r sonia" "b sonia" """ if bad: - await ctx.send( - inline('Too many inputs. Try wrapping your queries in quotes.')) + await ctx.send(inline('Too many inputs. Try wrapping your queries in quotes.')) return # Handle a very specific failure case, user typing something like "uuvo ragdra" @@ -576,10 +562,8 @@ async def leaderskill(self, ctx, left_query: str, right_query: str = None, emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, - self.get_emojis()) - emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, - self.get_emojis()) + emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis()) + emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis()) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -595,8 +579,7 @@ async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" voice = ctx.author.voice if not voice: - await ctx.send( - inline('You must be in a voice channel to use this command')) + await ctx.send(inline('You must be in a voice channel to use this command')) return channel = voice.channel @@ -614,11 +597,10 @@ async def padsay(self, ctx, server, *, query: str = None): if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: - await ctx.send(inline("No voice file found for " + m.name)) + await ctx.send(inline("No voice file found for "+m.name)) return base_dir = self.settings.voiceDir() - voice_file = os.path.join(base_dir, server, - '{0:03d}.wav'.format(voice_id)) + voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) header = '{} ({})'.format(monsterToHeader(m), server) if not os.path.exists(voice_file): await ctx.send(inline('Could not find voice for ' + header)) @@ -656,8 +638,7 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): self.settings.emojiServers().clear() if emoji_servers: self.settings.setEmojiServers(emoji_servers.split(',')) - await ctx.send( - inline('Set {} servers'.format(len(self.settings.emojiServers())))) + await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) @padinfo.command() @checks.is_owner() @@ -678,33 +659,26 @@ async def iddiff(self, ctx): m1, err1, debug_info1 = await self.findMonster(query) m2, err2, debug_info2 = await self.findMonster2(query) if c % 50 == 0: - await ctx.send( - inline("{}/{} complete.".format(c, len(hist_aggreg)))) + await ctx.send(inline("{}/{} complete.".format(c, len(hist_aggreg)))) if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): s += 1 continue f.append((query, - [m1.monster_id if m1 else None, - m2.monster_id if m2 else None], + [m1.monster_id if m1 else None, m2.monster_id if m2 else None], [err1, err2], [debug_info1, debug_info2] )) if m1 and m2: - await ctx.send( - "Major Discrepency: `{}` -> {}/{}".format(query, m1.name_na, - m2.name_na)) - await ctx.send("Done running diff checker. {}/{} passed.".format(s, - len( - hist_aggreg))) - file = discord.File(io.BytesIO(json.dumps(f).encode()), - filename="diff.json") + await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_na, m2.name_na)) + await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) + file = discord.File(io.BytesIO(json.dumps(f).encode()), filename="diff.json") await ctx.send(file=file) + def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] - return [e for g in self.bot.guilds if g.id in server_ids for e in - g.emojis] + return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] def makeFailureMsg(self, err): msg = ('Lookup failed: {}.\n' @@ -718,8 +692,7 @@ async def findMonster(self, query, server_filter=ServerFilter.any): monster_no = nm.monster_id if nm else -1 self.historic_lookups[query] = monster_no - json.dump(self.historic_lookups, - open(self.historic_lookups_file_path, "w+")) + json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) m = self.get_monster_by_id(nm.monster_id) if nm else None @@ -736,9 +709,7 @@ async def _findMonster(self, query, server_filter=ServerFilter.any): elif server_filter == ServerFilter.jp: monster_index = self.index_jp else: - raise ValueError( - "server_filter must be type ServerFilter not " + str( - type(server_filter))) + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster(query) async def findMonster2(self, query, server_filter=ServerFilter.any): @@ -747,8 +718,7 @@ async def findMonster2(self, query, server_filter=ServerFilter.any): monster_no = nm.monster_id if nm else -1 self.historic_lookups_id2[query] = monster_no - json.dump(self.historic_lookups_id2, - open(self.historic_lookups_file_path_id2, "w+")) + json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) m = self.get_monster_by_id(nm.monster_id) if nm else None @@ -765,9 +735,7 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): elif server_filter == ServerFilter.jp: monster_index = self.index_jp else: - raise ValueError( - "server_filter must be type ServerFilter not " + str( - type(server_filter))) + raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster2(query) @@ -853,8 +821,7 @@ def monsterToBaseEmbed(m: "DgMonster"): embed.set_thumbnail(url=monsterToThumbnailUrl(m)) embed.title = header embed.url = get_pdx_url(m) - embed.set_footer( - text='Requester may click the reactions below to switch tabs') + embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed @@ -892,8 +859,7 @@ def addMonsterEvoOfList(monster_list, embed, field_name): field_data = '{} monsters'.format(len(monster_list)) else: item_count = min(len(monster_list), 5) - for ae in sorted(monster_list, key=lambda x: x.monster_no_na, - reverse=True)[:item_count]: + for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) embed.add_field(name=field_name, value=field_data) @@ -915,8 +881,7 @@ def monsterToEvoMatsEmbed(m: "DgMonster"): addMonsterEvoOfList(m.material_of, embed, 'Material for') if not m.evo_gem: return embed - addMonsterEvoOfList(m.evo_gem.material_of, embed, - "Tree's gem (may not be this evo) is mat for") + addMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") return embed @@ -973,12 +938,9 @@ def monsterToPicEmbed(m: "DgMonster", animated=False): embed.set_thumbnail(url='') extra_links = [] if animated: - extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), - monsterToGifUrl(m))) + extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), monsterToGifUrl(m))) if m.orb_skin_id is not None: - extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), - monsterToOrbSkinCBUrl( - m))) + extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), monsterToOrbSkinCBUrl(m))) if len(extra_links) > 0: embed.add_field(name='Extra Links', value='\n'.join(extra_links)) @@ -998,8 +960,7 @@ def monsterToOrbSkinUrl(m: "DgMonster", link_text='Regular'): def monsterToOrbSkinCBUrl(m: "DgMonster", link_text='Color Blind'): - return '[{}]({})'.format(link_text, - ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) + return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) def monsterToGifEmbed(m: "DgMonster"): @@ -1025,8 +986,7 @@ def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): else: rhp, ratk, rrcv, rresist = 1, 1, 1, 0 - multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, - rrcv, rresist) + multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) embed = discord.Embed() embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) @@ -1091,9 +1051,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): os = "" if m.orb_skin_id is None else " (Orb Skin)" - info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format(m.rarity, - m.base_monster.rarity, - os, m.cost) + info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format(m.rarity, m.base_monster.rarity, os, m.cost) if acquire_text: info_row_2 += '\n**{}**'.format(acquire_text) if m.is_inheritable: @@ -1108,8 +1066,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): hp, atk, rcv, weighted = m.stats() if m.limit_mult > 0: lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, - m.limit_mult) + stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, m.limit_mult) stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( hp, lb_hp, atk, lb_atk, rcv, lb_rcv) else: @@ -1135,9 +1092,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): if not len(awakenings_row): awakenings_row = 'No Awakenings' - killers_row = '**Available Killers:** [{} slots] {}'.format(m.latent_slots, - ' '.join( - m.killers)) + killers_row = '**Available Killers:** [{} slots] {}'.format(m.latent_slots, ' '.join(m.killers)) embed.description = '{}\n{}'.format(awakenings_row, killers_row) @@ -1187,10 +1142,10 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): body_text += "\n**JP Name**: {}".format(m.name_jp) body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) if m.history_us: body_text += '\n**History:** {}'.format(m.history_us) @@ -1289,17 +1244,14 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): } -def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, - rcv2=None, resist2=None): +def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=None, resist2=None): if all([x is None for x in (hp2, atk2, rcv2, resist2)]): hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 def fmtNum(val): return ('{:.2f}').format(val).strip('0').rstrip('.') - text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), - fmtNum(rcv1 * rcv2)) + text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) if resist1 > 0 or resist2 > 0: - text += ' Resist {}%'.format( - fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) + text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) return text diff --git a/padmonitor/__init__.py b/padmonitor/__init__.py index b9489397..db55be78 100644 --- a/padmonitor/__init__.py +++ b/padmonitor/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = PadMonitor(bot) bot.add_cog(n) diff --git a/padsearch/__init__.py b/padsearch/__init__.py index faf836f2..bbe85cbb 100644 --- a/padsearch/__init__.py +++ b/padsearch/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): bot.add_cog(PadSearch(bot)) diff --git a/padsearch/info.json b/padsearch/info.json index b9105701..92b73cb8 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -1,28 +1,17 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Search for cards using advanced filters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Card search", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": [ - "tsutils", - "ply" - ], - "tags": [ - "PAD" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Search for cards using advanced filters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Card search", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": ["tsutils", "ply"], + "tags": ["PAD"] } diff --git a/padvision/__init__.py b/padvision/__init__.py index ca87a397..eca6ba01 100644 --- a/padvision/__init__.py +++ b/padvision/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): bot.add_cog(PadVision(bot)) diff --git a/padvision/info.json b/padvision/info.json index 86713d17..f2228d47 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -1,26 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "A beefy helper cog for neural networks.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Neural net helper", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "Pillow" - ], - "tags": [ - "PAD", - "sharedlibrary" - ] + "author": ["The Tsubaki Bot Team"], + "description": "A beefy helper cog for neural networks.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Neural net helper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["Pillow"], + "tags": ["PAD", "sharedlibrary"] } diff --git a/padvision/padvision.py b/padvision/padvision.py index 07aaf7e5..56152801 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -1,14 +1,14 @@ -import PIL.Image import io import logging import traceback + +import PIL.Image from redbot.core import commands ORB_IMG_SIZE = 40 logger = logging.getLogger('red.padbot-cogs.padvision') - class PadVision(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -91,8 +91,7 @@ def get_orb_vertices(self, x, y): box_yend = box_ystart + self.orb_size box_xstart = x * self.orb_size + self.xstart box_xend = box_xstart + self.orb_size - return int(box_xstart + offset), int(box_ystart + offset), int( - box_xend - offset), int(box_yend - offset) + return int(box_xstart + offset), int(box_ystart + offset), int(box_xend - offset), int(box_yend - offset) def get_orb_coords(self, x, y): box_xstart, box_ystart, box_xend, box_yend = self.get_orb_vertices(x, y) @@ -151,14 +150,12 @@ def _process(self): input_tensor_idx = input_details['index'] output_tensor_idx = output_details['index'] - input_data = np.zeros((1, input_orb_size, input_orb_size, 3), - dtype='uint8') + input_data = np.zeros((1, input_orb_size, input_orb_size, 3), dtype='uint8') for y, x in board_iterator(): box_coords = oe.get_orb_vertices(x, y) orb_img = self.img.crop(box_coords) - orb_img = orb_img.resize((input_orb_size, input_orb_size), - PIL.Image.ANTIALIAS) + orb_img = orb_img.resize((input_orb_size, input_orb_size), PIL.Image.ANTIALIAS) input_data[0] = np.array(orb_img) interpreter.set_tensor(input_tensor_idx, input_data) diff --git a/pricecheck/__init__.py b/pricecheck/__init__.py index 520f0547..1a4dff78 100644 --- a/pricecheck/__init__.py +++ b/pricecheck/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): bot.add_cog(PriceCheck(bot)) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 6ed9520d..29dc0e0d 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -1,7 +1,9 @@ -import discord import re + +import discord from redbot.core import commands, checks, Config from redbot.core.utils.chat_formatting import inline, box, pagify + from tsutils import auth_check PC_TEXT = """{name} - Stamina Cost: {stam_cost} @@ -39,8 +41,7 @@ async def pricecheck(self, ctx, *, query): """Displays pricing data for a tradable non-collab gem.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline( - "Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" @@ -53,11 +54,11 @@ async def pricecheck(self, ctx, *, query): await ctx.send("{} does not have PC data.".format(nm.name_na)) return sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name=nm.name_na, - stam_cost=sc, - pp_val=sc * 83 / 50, - points=sc * 83 / 50 / 297, - foot=foot) + pct = PC_TEXT.format(name = nm.name_na, + stam_cost = sc, + pp_val = sc*83/50, + points = sc*83/50/297, + foot = foot) if await self.config.channel(ctx.channel).dm(): for page in pagify(pct): await ctx.author.send(box(page)) @@ -75,8 +76,7 @@ async def set(self, ctx, stam_cost: float, *, query): """Adds stamina cost data to a card.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline( - "Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" @@ -89,17 +89,14 @@ async def set(self, ctx, stam_cost: float, *, query): if str(nm.base_monster_no) in pcs: foot = pcs[str(nm.base_monster_no)][1] pcs[str(nm.base_monster_no)] = (stam_cost, foot) - await ctx.send(box( - "Set {} ({}) to {}".format(nm.name_na, nm.base_monster_no, - stam_cost))) + await ctx.send(box("Set {} ({}) to {}".format(nm.name_na, nm.base_monster_no, stam_cost))) @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) async def setfooter(self, ctx, query, *, footer=""): """Adds notes regarding the stamina cost of a card.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline( - "Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" @@ -112,17 +109,14 @@ async def setfooter(self, ctx, query, *, footer=""): if str(nm.base_monster_no) in pcs: sc = pcs[str(nm.base_monster_no)][0] pcs[str(nm.base_monster_no)] = (sc, footer.strip('`')) - await ctx.send(box( - "Set {} ({}) footer to '{}'".format(nm.name_na, nm.base_monster_no, - footer))) + await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_na, nm.base_monster_no, footer))) @pcadmin.command(aliases=['delete', 'del', 'rm']) async def remove(self, ctx, *, query): """Removes stamina cost data from a card.""" padinfo_cog = self.bot.get_cog('PadInfo') if padinfo_cog is None: - await ctx.send(inline( - "Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" diff --git a/profile/__init__.py b/profile/__init__.py index da213419..244090d8 100644 --- a/profile/__init__.py +++ b/profile/__init__.py @@ -2,6 +2,5 @@ __red_end_user_data_statement__ = "Profile data is stored persistantly." - def setup(bot): bot.add_cog(Profile(bot)) diff --git a/profile/profile.py b/profile/profile.py index ec88376b..ff0db483 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -1,13 +1,14 @@ -import discord -import logging import re +import logging + +import discord from redbot.core import commands from redbot.core.utils.chat_formatting import inline, box, pagify + from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.profile') - def normalizeServer(server): server = server.upper().strip() return 'NA' if server == 'US' else server @@ -17,8 +18,7 @@ def normalizeServer(server): def validateAndCleanId(id): - id = id.replace('-', '').replace(' ', '').replace(',', '').replace('.', - '').strip() + id = id.replace('-', '').replace(' ', '').replace(',', '').replace('.', '').strip() if re.match(r'^\d{9}$', id): return id else: @@ -27,8 +27,7 @@ def validateAndCleanId(id): def formatNameLine(server, pad_name, pad_id): group = computeOldGroup(pad_id) - return "[{}]: '{}' : {} (Group {})".format(server, pad_name, - formatId(pad_id), group) + return "[{}]: '{}' : {} (Group {})".format(server, pad_name, formatId(pad_id), group) def formatId(id): @@ -52,11 +51,9 @@ async def red_get_data_for_user(self, *, user_id): data = "Stored data for user with ID {}:\n".format(user_id) if udata['servers']: - data += " - You have a profile on the following server(s): {}.\n".format( - ', '.join(udata['servers'])) + data += " - You have a profile on the following server(s): {}.\n".format(', '.join(udata['servers'])) if udata['default_server']: - data += " - Your default server is {}.\n".format( - udata['default_server']) + data += " - Your default server is {}.\n".format(udata['default_server']) if not any(udata.values()): data = "No data is stored for user with ID {}.\n".format(user_id) @@ -93,9 +90,8 @@ async def idTo(self, ctx, user: discord.Member, server=None): if profile_msg is None: return - warning = inline( - "{} asked me to send you this message. Report any harassment to the mods.".format( - ctx.author.name)) + warning = inline("{} asked me to send you this message. Report any harassment to the mods.".format( + ctx.author.name)) msg = warning + "\n" + profile_msg await ctx.send(user, msg) await ctx.author.send(inline("Sent your profile to " + user.name)) @@ -168,14 +164,11 @@ async def setId(self, ctx, server, *id): id = " ".join(id) clean_id = validateAndCleanId(id) if clean_id is None: - await ctx.send(inline( - 'Your ID looks invalid, expected a 9 digit code, got: {}'.format( - id))) + await ctx.send(inline('Your ID looks invalid, expected a 9 digit code, got: {}'.format(id))) return self.settings.setId(ctx.author.id, server, clean_id) - await ctx.send(inline( - 'Set your id for {} to: {}'.format(server, formatId(clean_id)))) + await ctx.send(inline('Set your id for {} to: {}'.format(server, formatId(clean_id)))) @profile.command(name="name") async def setName(self, ctx, server, *name): @@ -186,8 +179,7 @@ async def setName(self, ctx, server, *name): name = " ".join(name) self.settings.setName(ctx.author.id, server, name) - await ctx.send( - inline('Set your name for {} to: {}'.format(server, name))) + await ctx.send(inline('Set your name for {} to: {}'.format(server, name))) @profile.command(name="text") async def setText(self, ctx, server, *text): @@ -206,8 +198,7 @@ async def setText(self, ctx, server, *text): return self.settings.setProfileText(ctx.author.id, server, text) - await ctx.send( - inline('Set your profile for ' + server + ' to:\n' + text)) + await ctx.send(inline('Set your profile for ' + server + ' to:\n' + text)) @profile.command(name="clear") async def clear(self, ctx, server=None): @@ -240,27 +231,23 @@ async def search(self, ctx, server, *search_text): return # Get all profiles for server - profiles = [p[server] for p in self.settings.profiles().values() if - server in p] + profiles = [p[server] for p in self.settings.profiles().values() if server in p] # Eliminate profiles without an ID set profiles = filter(lambda p: 'id' in p, profiles) profiles = list(profiles) # Match the profiles against the search text - matching_profiles = filter( - lambda p: search_text in p.get('text', '').lower(), profiles) + matching_profiles = filter(lambda p: search_text in p.get('text', '').lower(), profiles) matching_profiles = list(matching_profiles) template = 'Found {}/{} matching profiles in {} for : {}' - msg = template.format(len(matching_profiles), len(profiles), server, - search_text) + msg = template.format(len(matching_profiles), len(profiles), server, search_text) await ctx.send(inline(msg)) if len(matching_profiles) == 0: return - msg = 'Displaying {} matches for server {}:\n'.format( - len(matching_profiles), server) + msg = 'Displaying {} matches for server {}:\n'.format(len(matching_profiles), server) for p in matching_profiles: pad_id = formatId(p['id']) pad_name = p.get('name', 'unknown') diff --git a/schoolidol/__init__.py b/schoolidol/__init__.py index 2379fffc..44e09da4 100644 --- a/schoolidol/__init__.py +++ b/schoolidol/__init__.py @@ -2,7 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." - def setup(bot): n = SchoolIdol(bot) bot.add_cog(n) diff --git a/schoolidol/info.json b/schoolidol/info.json index 8e50f075..f75e8b38 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -1,26 +1,15 @@ { - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Displays data about School Idol.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows School Idol data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": [ - "tsutils", - "aiohttp" - ], - "tags": [ - "schoolidol" - ] + "author": ["The Tsubaki Bot Team"], + "description": "Displays data about School Idol.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows School Idol data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version" : [3,8,1], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": ["tsutils", "aiohttp"], + "tags": ["schoolidol"] } From 52b277fb4acad6d2a42672780e391f390b4d3551 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 15 Sep 2020 00:28:12 -0700 Subject: [PATCH 0122/1220] reformat3 --- azurelane/__init__.py | 1 + azurelane/azurelane.py | 10 +++---- azurelane/info.json | 38 ++++++++++++++++--------- chronomagia/__init__.py | 1 + chronomagia/chronomagia.py | 12 ++++---- chronomagia/info.json | 37 +++++++++++++++--------- dadguide/__init__.py | 1 + dadguide/dadguide.py | 28 ++++++++---------- dadguide/info.json | 38 ++++++++++++++++--------- damagecalc/__init__.py | 1 + damagecalc/info.json | 36 ++++++++++++++--------- info.json | 8 ++++-- padboard/__init__.py | 1 + padboard/info.json | 38 ++++++++++++++++--------- padboard/padboard.py | 14 ++++----- padbuildimg/__init__.py | 1 + padbuildimg/info.json | 46 +++++++++++++++++++----------- padbuildimg/padbuildimg.py | 48 ++++++++++++++++++------------- padbuilds/__init__.py | 1 + padbuilds/info.json | 36 ++++++++++++++--------- padbuilds/padbuilds.py | 7 ++--- padevents/__init__.py | 1 + padevents/info.json | 40 +++++++++++++++++--------- padevents/padevents.py | 19 ++++++------- padglobal/__init__.py | 1 + padglobal/info.json | 44 ++++++++++++++++++----------- padglobal/padglobal.py | 58 ++++++++++++++++++++++---------------- padguidedb/__init__.py | 1 + padguidedb/info.json | 37 +++++++++++++++--------- padguidedb/padguidedb.py | 10 +++---- padinfo/__init__.py | 1 + padinfo/info.json | 43 +++++++++++++++++----------- padinfo/padinfo.py | 43 +++++++++++++--------------- padmonitor/__init__.py | 1 + padmonitor/info.json | 40 ++++++++++++++++---------- padmonitor/padmonitor.py | 7 ++--- padsearch/__init__.py | 1 + padsearch/info.json | 41 +++++++++++++++++---------- padsearch/padsearch.py | 4 +-- padvision/__init__.py | 1 + padvision/info.json | 37 +++++++++++++++--------- padvision/padvision.py | 4 +-- pricecheck/__init__.py | 1 + pricecheck/info.json | 44 ++++++++++++++++++----------- pricecheck/pricecheck.py | 18 ++++++------ profile/__init__.py | 1 + profile/info.json | 36 ++++++++++++++--------- profile/profile.py | 9 +++--- schoolidol/__init__.py | 1 + schoolidol/info.json | 37 +++++++++++++++--------- schoolidol/schoolidol.py | 10 +++---- 51 files changed, 600 insertions(+), 394 deletions(-) diff --git a/azurelane/__init__.py b/azurelane/__init__.py index 00b7bfe4..7b3c1723 100644 --- a/azurelane/__init__.py +++ b/azurelane/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = AzureLane(bot) bot.add_cog(n) diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 7ae3e08b..53e88318 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -1,17 +1,15 @@ +import aiohttp import difflib +import discord import json import logging from collections import OrderedDict - -import aiohttp -import discord -from tsutils import Menu, EmojiUpdater, char_to_emoji -from redbot.core import commands, checks +from redbot.core import checks, commands from redbot.core.utils.chat_formatting import inline +from tsutils import EmojiUpdater, Menu, char_to_emoji logger = logging.getLogger('red.padbot-cogs.azurelane') - BASE_URL = 'https://storage.googleapis.com/mirubot/alimages/raw' DATA_URL = '{}/azure_lane.json'.format(BASE_URL) diff --git a/azurelane/info.json b/azurelane/info.json index 8e34b3ce..66626808 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -1,15 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Displays data about Azur Lane.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows Azur Lane data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "aiohttp"], - "tags": ["azurlane", "azurelane"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Displays data about Azur Lane.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Azur Lane data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "aiohttp" + ], + "tags": [ + "azurlane", + "azurelane" + ] } diff --git a/chronomagia/__init__.py b/chronomagia/__init__.py index b96f5f30..230cbca9 100644 --- a/chronomagia/__init__.py +++ b/chronomagia/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = ChronoMagia(bot) bot.add_cog(n) diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index 2a003d20..d274ff96 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -1,20 +1,20 @@ import asyncio import csv import difflib +import discord +import logging import os import traceback +import tsutils import urllib.parse -import logging from collections import OrderedDict - -import discord -import tsutils -from tsutils import Menu, EmojiUpdater -from redbot.core import commands, data_manager, checks +from redbot.core import checks, commands, data_manager from redbot.core.utils.chat_formatting import inline +from tsutils import EmojiUpdater, Menu logger = logging.getLogger('red.padbot-cogs.chronomagia') + def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) diff --git a/chronomagia/info.json b/chronomagia/info.json index 0a71ca8e..c136d0cd 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Displays data about Chrono Ma:Gia.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows Chrono Ma:Gia data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "aiohttp"], - "tags": ["chronomagia"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Displays data about Chrono Ma:Gia.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows Chrono Ma:Gia data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "aiohttp" + ], + "tags": [ + "chronomagia" + ] } diff --git a/dadguide/__init__.py b/dadguide/__init__.py index 5bff88a7..1159ca75 100644 --- a/dadguide/__init__.py +++ b/dadguide/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = Dadguide(bot) bot.add_cog(n) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b96ec5aa..b844ab2c 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -9,25 +9,24 @@ import asyncio import csv import difflib +import discord import json import logging import os +import pytz import re +import romkan import shutil import sqlite3 as lite import traceback -from collections import defaultdict, deque, OrderedDict +import tsutils +from collections import OrderedDict, defaultdict, deque from datetime import datetime from enum import Enum - -import pytz -import romkan -import discord -import tsutils from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.utils.chat_formatting import inline from redbot.core.utils import AsyncIter +from redbot.core.utils.chat_formatting import inline logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -250,8 +249,6 @@ async def setdatafile(self, ctx, *, data_file): await ctx.tick() - - class DadguideSettings(tsutils.CogSettings): def make_default_settings(self): config = { @@ -665,16 +662,15 @@ def get_all_monsters(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, as_generator=as_generator) - def get_all_events(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), DgScheduledEvent, + return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), + DgScheduledEvent, as_generator=as_generator) def get_dungeon_by_id(self, dungeon_id: int): return self._select_one_entry_by_pk(dungeon_id, DgDungeon) - def enum_or_none(enum, value, default=None): if value is not None: return enum(value) @@ -834,7 +830,6 @@ def close_datetime(self, value): self.end_timestamp = int(value.timestamp()) - class DgMonster(DadguideItem): TABLE = 'monsters' PK = 'monster_id' @@ -1184,7 +1179,6 @@ def make_roma_subname(name_jp): return adjusted_subname.strip() - class MonsterIndex(tsutils.aobject): async def __init__(self, monster_database, nickname_overrides, basename_overrides, panthname_overrides, accept_filter=None): @@ -1404,7 +1398,8 @@ def find_monster(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): if query.endswith("base {}".format(m.monster_id)): - matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) + matches.add( + discord.utils.find(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) if len(matches): return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() @@ -1519,7 +1514,8 @@ def find_monster2(self, query): # prefix search for ids, take max id for nickname, m in self.all_entries.items(): if query.endswith("base {}".format(m.monster_id)): - matches.add(discord.utils.find(lambda mo: m.base_monster_no==mo.monster_id, self.all_entries.values())) + matches.add( + discord.utils.find(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) matches.remove_potential_matches_without_all_prefixes(query_prefixes) # first try to get matches from nicknames diff --git a/dadguide/info.json b/dadguide/info.json index e40915d6..8823f056 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -1,15 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "No commands, but loads all the monster data stored in Tsubaki.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Loads monster data (LOAD THIS SECOND)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "pytz", "romkan"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "No commands, but loads all the monster data stored in Tsubaki.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Loads monster data (LOAD THIS SECOND)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "pytz", + "romkan" + ], + "tags": [ + "PAD" + ] } diff --git a/damagecalc/__init__.py b/damagecalc/__init__.py index 26416e02..177e57ab 100644 --- a/damagecalc/__init__.py +++ b/damagecalc/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = DamageCalc(bot) bot.add_cog(n) diff --git a/damagecalc/info.json b/damagecalc/info.json index e6456626..4594d9f8 100644 --- a/damagecalc/info.json +++ b/damagecalc/info.json @@ -1,15 +1,25 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Calculate Puzzle & Dragons data", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Calculates Puzzle & Dragons data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["ply"], - "tags": ["pad"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Calculate Puzzle & Dragons data", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Calculates Puzzle & Dragons data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "ply" + ], + "tags": [ + "pad" + ] } diff --git a/info.json b/info.json index 90b10de9..a11b82f8 100644 --- a/info.json +++ b/info.json @@ -1,5 +1,7 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "A collection of cogs related to the mobile game Puzzle and Dragons, along with a few unrelated games.", - "short": "The main Tsubotki repo." + "author": [ + "The Tsubaki Bot Team" + ], + "description": "A collection of cogs related to the mobile game Puzzle and Dragons, along with a few unrelated games.", + "short": "The main Tsubotki repo." } diff --git a/padboard/__init__.py b/padboard/__init__.py index 97b63a4e..ddbada96 100644 --- a/padboard/__init__.py +++ b/padboard/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored persistantly." + def setup(bot): bot.add_cog(PadBoard(bot)) diff --git a/padboard/info.json b/padboard/info.json index a6450cea..1adc8002 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -1,15 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Turn a screenshot into a P&D simulation via DawnGlare.", - "end_user_data_statement": "No personal data is stored persistantly.", - "install_msg": "", - "short": "Dawnglare utils", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"padvision": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Turn a screenshot into a P&D simulation via DawnGlare.", + "end_user_data_statement": "No personal data is stored persistantly.", + "install_msg": "", + "short": "Dawnglare utils", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "padvision": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/padboard/padboard.py b/padboard/padboard.py index aea3dd11..6a293ded 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -1,14 +1,12 @@ -import os -from collections import defaultdict -from collections import deque - import aiohttp import cv2 import discord import numpy as np -from redbot.core import checks, commands, Config +import os +from collections import defaultdict +from collections import deque +from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import inline - from tsutils import tsutils DATA_DIR = os.path.join('data', 'padboard') @@ -16,6 +14,7 @@ DAWNGLARE_BOARD_TEMPLATE = "https://pad.dawnglare.com/?patt={}" CNINJA_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" + class PadBoard(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -54,7 +53,6 @@ async def set_tflite_path(self, ctx, *, path): await self.config.tflite_path.set(path) await ctx.tick() - def find_image(self, user_id): urls = list(self.logs[user_id]) if urls: @@ -95,7 +93,7 @@ async def dawnglare(self, ctx, user: discord.Member = None): # Convert O (used by padvision code) to X (used by Puzzled for bombs) board_text_nc = board_text_nc.replace('o', 'x') msg = DAWNGLARE_BOARD_TEMPLATE.format(board_text_nc) - msg += '\n'+CNINJA_BOARD_TEMPLATE.format(board_text_nc) + msg += '\n' + CNINJA_BOARD_TEMPLATE.format(board_text_nc) await ctx.send(msg) diff --git a/padbuildimg/__init__.py b/padbuildimg/__init__.py index 5a0008d3..e8e6d36f 100644 --- a/padbuildimg/__init__.py +++ b/padbuildimg/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadBuildImage(bot)) diff --git a/padbuildimg/info.json b/padbuildimg/info.json index 16f6c984..a1ef892d 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -1,18 +1,32 @@ { - "author": ["chu2.718281828459045235360287471#0110", "The Tsubaki Bot Team"], - "description": "Build a PAD team image via commands.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "PDChu cog", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "ply", "Pillow", "aiohttp"], - "tags": ["PAD"] + "author": [ + "chu2.718281828459045235360287471#0110", + "The Tsubaki Bot Team" + ], + "description": "Build a PAD team image via commands.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PDChu cog", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "ply", + "Pillow", + "aiohttp" + ], + "tags": [ + "PAD" + ] } diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index f3a6b71c..76cff6df 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -1,21 +1,20 @@ -import math +import aiohttp import csv -import os +import discord import io -from shutil import rmtree +import math +import os import re import urllib - -import discord -import aiohttp -from ply import lex from PIL import Image +from PIL import ImageChops from PIL import ImageDraw from PIL import ImageFont -from PIL import ImageChops -from tsutils import CogSettings +from ply import lex from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline +from shutil import rmtree +from tsutils import CogSettings HELP_MSG = """ ^buildimg @@ -142,6 +141,8 @@ DELAY_BUFFER = 'delay_buffer' REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/padbuildimg/assets/' REMOTE_AWK_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/media/awakenings/{0:03d}.png' + + # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' class DictWithAttributeAccess(dict): @@ -209,7 +210,8 @@ async def downloadAllAssets(self, awk_ids): os.mkdir(params.ASSETS_DIR + 'lat/') os.mkdir(params.ASSETS_DIR + 'awk/') for lat in LATENTS_MAP.values(): - await self.downloadAssets(REMOTE_ASSET_URL + 'lat/' + lat + '.png', params.ASSETS_DIR + 'lat/' + lat + '.png') + await self.downloadAssets(REMOTE_ASSET_URL + 'lat/' + lat + '.png', + params.ASSETS_DIR + 'lat/' + lat + '.png') for awk in awk_ids: await self.downloadAssets(REMOTE_AWK_URL.format(awk), params.ASSETS_DIR + 'awk/' + str(awk) + '.png') await self.downloadAssets(REMOTE_ASSET_URL + AWK_CIRCLE + '.png', params.ASSETS_DIR + AWK_CIRCLE + '.png') @@ -234,11 +236,13 @@ def toggleDmOnly(self, server_id): self.bot_settings['dm_only'].append(server_id) self.save_settings() + def lstripalpha(s): while s and not s[0].isdigit(): - s=s[1:] + s = s[1:] return s + class PaDTeamLexer(object): tokens = [ 'ID', @@ -363,7 +367,8 @@ def t_REPEAT(self, t): t_ignore = '\t\n' def t_error(self, t): - raise commands.UserFeedbackCheckFailure("Parse Error: Unknown text '{}' at position {}".format(t.value, t.lexpos)) + raise commands.UserFeedbackCheckFailure( + "Parse Error: Unknown text '{}' at position {}".format(t.value, t.lexpos)) def build(self, **kwargs): # pass debug=1 to enable verbose output @@ -500,7 +505,8 @@ async def process_card(self, card_str, is_assist=False): return None, None else: result_card['GOLD_STAR'] = False - result_card['MNO'] = card.monster_no_na if card.monster_no_na != card.monster_id else card.monster_no_jp + result_card[ + 'MNO'] = card.monster_no_na if card.monster_no_na != card.monster_id else card.monster_no_jp result_card['ID'] = card.monster_id elif tok.type == 'P_ALL': if tok.value >= 297: @@ -591,7 +597,8 @@ def combine_latents(self, latents): row_count += 1 x_offset = 0 y_offset += last_height - if row_count >= MAX_LATENTS//4 and x_offset + latent_icon.size[0] >= self.params.LATENTS_WIDTH * (MAX_LATENTS%4): + if row_count >= MAX_LATENTS // 4 and x_offset + latent_icon.size[0] >= self.params.LATENTS_WIDTH * ( + MAX_LATENTS % 4): break latents_bar.paste(latent_icon, (x_offset, y_offset)) last_height = latent_icon.size[1] @@ -720,9 +727,12 @@ def generate_build_image(self, include_instructions=False): for ids in side] for card in actives_used: if 'http' in self.params.PORTRAIT_DIR: - p_small = Image.open(urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) + p_small = Image.open( + urllib.request.urlopen(self.params.PORTRAIT_DIR.format(monster_id=card['ID']))).resize( + (self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) else: - p_small = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])).resize((self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) + p_small = Image.open(self.params.PORTRAIT_DIR.format(monster_id=card['ID'])).resize( + (self.params.PORTRAIT_WIDTH // 2, self.params.PORTRAIT_WIDTH // 2), Image.LINEAR) self.build_img.paste(p_small, (x_offset, y_offset)) x_offset += self.params.PORTRAIT_WIDTH // 2 x_offset += self.params.PADDING @@ -746,7 +756,7 @@ async def helpbuildimg(self, ctx): await ctx.author.send(box(HELP_MSG)) if checks.admin_or_permissions(manage_guild=True): await ctx.author.send(box('For Server Admins: Output location can be changed between current channel and ' - 'direct messages via ^togglebuildimgoutput')) + 'direct messages via ^togglebuildimgoutput')) await ctx.author.send(EXAMPLE_MSG) @commands.command(aliases=['buildimg', 'pdchu']) @@ -771,13 +781,13 @@ async def padbuildimg(self, ctx, *, build_str: str): build_io.seek(0) if ctx.guild and self.settings.dmOnly(ctx.guild.id): try: - await ctx.author.send(file=discord.File(build_io,'pad_build.png')) + await ctx.author.send(file=discord.File(build_io, 'pad_build.png')) await ctx.send(inline('Sent build to {}'.format(ctx.author))) except discord.errors.Forbidden as ex: await ctx.send(inline('Failed to send build to {}'.format(ctx.author))) else: try: - await ctx.send(file=discord.File(build_io,'pad_build.png')) + await ctx.send(file=discord.File(build_io, 'pad_build.png')) except discord.errors.Forbidden as ex: await ctx.send(inline("Failed to send build. (Insufficient Permisisons)")) else: diff --git a/padbuilds/__init__.py b/padbuilds/__init__.py index d3532a22..655f0aca 100644 --- a/padbuilds/__init__.py +++ b/padbuilds/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadBuilds(bot)) diff --git a/padbuilds/info.json b/padbuilds/info.json index ae3ed7b6..76282200 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -1,15 +1,25 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "View user-set builds for various dungeons.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Builds to help you through dungeons", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "View user-set builds for various dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Builds to help you through dungeons", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index 90f5a545..a5a31c2a 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -1,10 +1,9 @@ -import re - import discord -from tsutils import CogSettings +import re from redbot.core import checks from redbot.core import commands -from redbot.core.utils.chat_formatting import pagify, box +from redbot.core.utils.chat_formatting import box, pagify +from tsutils import CogSettings class PadBuilds(commands.Cog): diff --git a/padevents/__init__.py b/padevents/__init__.py index e72e9a91..7f7dbc01 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = PadEvents(bot) bot.add_cog(n) diff --git a/padevents/info.json b/padevents/info.json index 28a2be3b..f0144455 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -1,15 +1,29 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Stay up to date on PAD dungeons.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "PAD Event tracker", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {"dadguide": "https://github.com/TsubakiBotPad/padbot-cogs"}, - "requirements": ["tsutils", "pytz", "prettytable"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Stay up to date on PAD dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "PAD Event tracker", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "pytz", + "prettytable" + ], + "tags": [ + "PAD" + ] } diff --git a/padevents/padevents.py b/padevents/padevents.py index 034464a9..9434e855 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,19 +1,18 @@ -import traceback import asyncio -import re import datetime -import pytz +import discord import logging +import prettytable +import pytz +import re +import traceback from collections import defaultdict from datetime import timedelta from enum import Enum - -import prettytable -import discord -from tsutils import CogSettings from redbot.core import checks from redbot.core import commands -from redbot.core.utils.chat_formatting import inline, box, pagify +from redbot.core.utils.chat_formatting import box, inline, pagify +from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.padevents') @@ -148,7 +147,7 @@ async def testevent(self, ctx, server, seconds: int = 0): await dg_cog.wait_until_ready() dg_module = __import__(dg_cog.__module__) - te = dg_module.DgScheduledEvent({'dungeon_id':1}, dg_cog.database) + te = dg_module.DgScheduledEvent({'dungeon_id': 1}, dg_cog.database) te.server = server te.event_type_id = EventType.Guerrilla.value @@ -497,7 +496,7 @@ def __init__(self, scheduled_event: "DgScheduledEvent"): self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' - self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' + self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') diff --git a/padglobal/__init__.py b/padglobal/__init__.py index e722b093..04aa3de7 100644 --- a/padglobal/__init__.py +++ b/padglobal/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadGlobal(bot)) diff --git a/padglobal/info.json b/padglobal/info.json index 39f32672..df8836e6 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -1,18 +1,30 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Tsubaki guides and tips written by P&D professionals", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "aiohttp", "prettytable"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Tsubaki guides and tips written by P&D professionals", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "aiohttp", + "prettytable" + ], + "tags": [ + "PAD" + ] } diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8186b3ca..8c86e73c 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1,23 +1,21 @@ +import aiohttp import asyncio import csv import datetime import difflib +import discord import io import json -import os -import re import logging -from collections import defaultdict - -import aiohttp -import discord +import os import prettytable +import re import tsutils -from tsutils import CogSettings, safe_read_json, replace_emoji_names_with_code,\ - clean_global_mentions, confirm_message +from collections import defaultdict from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.utils.chat_formatting import inline, box, pagify +from redbot.core.utils.chat_formatting import box, inline, pagify +from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json logger = logging.getLogger('red.padbot-cogs.padglobal') @@ -285,7 +283,7 @@ def list_or_none(l): for page in pagify(msg): sent_messages.append(await ctx.send(box(page))) await tsutils.await_and_remove(self.bot, sent_messages[-1], ctx.author, - delete_msgs=sent_messages, timeout=30) + delete_msgs=sent_messages, timeout=30) @commands.command() @is_padglobal_admin() @@ -298,7 +296,7 @@ async def forceindexreload(self, ctx): await padinfo_cog.refresh_index() await ctx.send('finished reload') - @commands.group(aliases = ['pdg']) + @commands.group(aliases=['pdg']) @is_padglobal_admin() async def padglobal(self, ctx): """PAD global custom commands.""" @@ -319,7 +317,7 @@ async def edit(self, ctx, command: str, *, text: str): """Edit a custom command or alias""" await self._add(ctx, command, text, False) - async def _add(self, ctx, command, text, confirm = True): + async def _add(self, ctx, command, text, confirm=True): command = command.lower() text = clean_global_mentions(text) text = text.replace(u'\u200b', '') @@ -349,8 +347,8 @@ async def _add(self, ctx, command, text, confirm = True): ted = self.c_commands[ted] alias = True if confirm: - conf = await confirm_message(ctx,"Are you sure you want to edit the {}command {}?" \ - .format("alias to " if alias else "", ted)) + conf = await confirm_message(ctx, "Are you sure you want to edit the {}command {}?" \ + .format("alias to " if alias else "", ted)) if not conf: return else: @@ -628,7 +626,8 @@ async def rmglossary(self, ctx, *, term): if term not in self.settings.glossary(): await ctx.send("Glossary item doesn't exist.") return - if not await confirm_message(ctx, "Are you sure you want to globally remove the glossary data for {}?".format(term)): + if not await confirm_message(ctx, + "Are you sure you want to globally remove the glossary data for {}?".format(term)): return self.settings.rmGlossary(term) await ctx.tick() @@ -677,7 +676,8 @@ def lookup_boss(self, term): def boss_to_text(self, ctx): bosses = self.settings.boss() - msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format(ctx.prefix) + msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( + ctx.prefix) for term in sorted(bosses.keys()): definition = bosses[term] msg += '\n**{}**\n{}'.format(term, definition) @@ -685,7 +685,8 @@ def boss_to_text(self, ctx): def boss_to_text_index(self, ctx): bosses = self.settings.boss() - msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format(ctx.prefix) + msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( + ctx.prefix) msg = msg + '\n' + ',\n'.join(sorted(bosses.keys())) return msg @@ -708,7 +709,8 @@ async def rmboss(self, ctx, *, term): if term not in self.settings.boss(): await ctx.send("Boss mechanics item doesn't exist.") return - if not await confirm_message(ctx, "Are you sure you want to globally remove the boss data for {}?".format(term)): + if not await confirm_message(ctx, + "Are you sure you want to globally remove the boss data for {}?".format(term)): return self.settings.rmBoss(term) @@ -720,7 +722,9 @@ async def which(self, ctx, *, term: str = None): """Shows PAD Which Monster entries""" if term is None: - await ctx.author.send('__**PAD Which Monster**__ *(also check out {0}pad / {0}padfaq / {0}boards / {0}glossary)*'.format(ctx.prefix)) + await ctx.author.send( + '__**PAD Which Monster**__ *(also check out {0}pad / {0}padfaq / {0}boards / {0}glossary)*'.format( + ctx.prefix)) msg = self.which_to_text() for page in pagify(msg): await ctx.author.send(box(page)) @@ -836,7 +840,8 @@ async def rmwhich(self, ctx, *, monster_id: int): if m != m.base_monster: m = m.base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) - if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format(m.name_na)): + if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format( + m.name_na)): return name = m.monster_id @@ -887,7 +892,7 @@ async def debuglookup(self, ctx, *, term: str): monster_id = nm.monster_id if nm else nm definition = self.settings.which().get(monster_id, None) await ctx.send('Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' - .format(base, name, monster_id, err, deb)) + .format(base, name, monster_id, err, deb)) @padglobal.command() @checks.is_owner() @@ -918,7 +923,7 @@ async def setemojiservers(self, ctx, *, emoji_servers=''): """Set the emoji servers by ID (csv)""" self.settings.emojiServers().clear() if emoji_servers: - self.settings.setEmojiServers(re.findall(r'\d+',emoji_servers)) + self.settings.setEmojiServers(re.findall(r'\d+', emoji_servers)) await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) def _get_emojis(self): @@ -1169,7 +1174,9 @@ async def rmdungeonguide(self, ctx, term: str): if term not in self.settings.dungeonGuide(): await ctx.send("DungeonGuide doesn't exist.") return - if not await confirm_message(ctx, "Are you sure you want to globally remove the dungeonguide data for {}?".format(term)): + if not await confirm_message(ctx, + "Are you sure you want to globally remove the dungeonguide data for {}?".format( + term)): return self.settings.rmDungeonGuide(term) await ctx.tick() @@ -1194,7 +1201,9 @@ async def rmleaderguide(self, ctx, monster_id: int): if m != m.base_monster: m = m.base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) - if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format(m.name_na)): + if not await confirm_message(ctx, + "Are you sure you want to globally remove the leaderguide data for {}?".format( + m.name_na)): return name = m.monster_id @@ -1218,6 +1227,7 @@ def emojify(self, message): message = tsutils.replace_emoji_names_with_code(emojis, message) return tsutils.fix_emojis_for_server(emojis, message) + def check_simple_tree(monster): attr1 = monster.attr1 active_skill = monster.active_skill diff --git a/padguidedb/__init__.py b/padguidedb/__init__.py index 4a503383..86d4637e 100644 --- a/padguidedb/__init__.py +++ b/padguidedb/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): pdb = PadGuideDb(bot) bot.add_cog(pdb) diff --git a/padguidedb/info.json b/padguidedb/info.json index 4c36d429..ae9e2e31 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Dungeon scraper and general database manager.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Dungeon data scraper", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "pymysql"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Dungeon scraper and general database manager.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Dungeon data scraper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "pymysql" + ], + "tags": [ + "PAD" + ] } diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 4ee3974a..ee9f84db 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -1,15 +1,13 @@ import asyncio import csv +import discord import io import json import logging - -import discord import pymysql from redbot.core import checks from redbot.core import commands -from redbot.core.utils.chat_formatting import inline, box, pagify - +from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings, tsutils logger = logging.getLogger('red.padbot-cogs.padguidedb') @@ -230,10 +228,10 @@ async def dungeondrops(self, ctx, dungeon_id: int): msg = 'floor_id,count\n' for row in results1: - msg += ','.join(map(str,row.values()))+"\n" + msg += ','.join(map(str, row.values())) + "\n" msg += '\n\nfloor_id,monster_id,count\n' for row in results2: - msg += ','.join(map(str,row.values()))+"\n" + msg += ','.join(map(str, row.values())) + "\n" for page in pagify(msg): await ctx.send(box(page)) diff --git a/padinfo/__init__.py b/padinfo/__init__.py index 13827c7b..0505758e 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = PadInfo(bot) bot.add_cog(n) diff --git a/padinfo/info.json b/padinfo/info.json index d984d84f..40c5aae7 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -1,18 +1,29 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Serves monster data (LOAD THIS THIRD)", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "speech": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "prettytable"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Serves monster data (LOAD THIS THIRD)", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "speech": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "prettytable" + ], + "tags": [ + "PAD" + ] } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 109683bb..be83bb5d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,21 +1,19 @@ import asyncio +import discord +import io import json import logging import os -import io +import prettytable import traceback +import tsutils import urllib.parse from collections import OrderedDict from enum import Enum - -import discord -import prettytable -from redbot.core import checks, data_manager, commands -from redbot.core.utils.chat_formatting import inline, box +from redbot.core import checks, commands, data_manager from redbot.core.utils import AsyncIter - -import tsutils -from tsutils import char_to_emoji, Menu, EmojiUpdater, safe_read_json, CogSettings, rmdiacritics +from redbot.core.utils.chat_formatting import box, inline +from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -372,7 +370,7 @@ async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): ctx, starting_menu_emoji, ScrollEmojiUpdater(emoji_to_embed, pad_info=self, bot=self.bot, - m=m, ms=ms, selected_emoji=starting_menu_emoji) + m=m, ms=ms, selected_emoji=starting_menu_emoji) ) def get_id_emoji_options(self, m=None, scroll=False): @@ -411,7 +409,6 @@ def get_id_emoji_options(self, m=None, scroll=False): emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message return emoji_to_embed - async def _do_evolistmenu(self, ctx, sm): monsters = sm.alt_evos monsters.sort(key=lambda m: m.monster_id) @@ -460,10 +457,10 @@ async def links(self, ctx, *, query: str): if m is not None: embed = monsterToBaseEmbed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) embed.set_footer(text='') await ctx.send(embed=embed) @@ -511,8 +508,8 @@ async def collabscroll(self, ctx, *, query: str): ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) ms = [m._alt_evo_id_list for m in ms - if m._base_monster_id == m.monster_id - and m.sell_mp >= 3000] + if m._base_monster_id == m.monster_id + and m.sell_mp >= 3000] ms = [m for ml in ms for m in ml] if m._base_monster_id != m.monster_id: @@ -525,7 +522,6 @@ async def collabscroll(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command(aliases=['leaders', 'leaderskills', 'ls']) @checks.bot_has_permissions(embed_links=True) async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): @@ -597,7 +593,7 @@ async def padsay(self, ctx, server, *, query: str = None): if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: - await ctx.send(inline("No voice file found for "+m.name)) + await ctx.send(inline("No voice file found for " + m.name)) return base_dir = self.settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) @@ -675,7 +671,6 @@ async def iddiff(self, ctx): file = discord.File(io.BytesIO(json.dumps(f).encode()), filename="diff.json") await ctx.send(file=file) - def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] @@ -1142,10 +1137,10 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): body_text += "\n**JP Name**: {}".format(m.name_jp) body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) if m.history_us: body_text += '\n**History:** {}'.format(m.history_us) diff --git a/padmonitor/__init__.py b/padmonitor/__init__.py index db55be78..b9489397 100644 --- a/padmonitor/__init__.py +++ b/padmonitor/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = PadMonitor(bot) bot.add_cog(n) diff --git a/padmonitor/info.json b/padmonitor/info.json index b97f09f9..ad76ff92 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -1,17 +1,27 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Checks for and announces new monsters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Announce new monsters", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Checks for and announces new monsters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Announce new monsters", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 7146466b..21e363d5 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,13 +1,12 @@ import asyncio import logging - -from redbot.core import commands, checks -from redbot.core.utils.chat_formatting import inline, box, pagify - import tsutils +from redbot.core import checks, commands +from redbot.core.utils.chat_formatting import box, inline, pagify logger = logging.getLogger('red.padbot-cogs.padmonitor') + class PadMonitor(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/padsearch/__init__.py b/padsearch/__init__.py index bbe85cbb..faf836f2 100644 --- a/padsearch/__init__.py +++ b/padsearch/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadSearch(bot)) diff --git a/padsearch/info.json b/padsearch/info.json index 92b73cb8..b9105701 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -1,17 +1,28 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Search for cards using advanced filters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Card search", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils", "ply"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Search for cards using advanced filters.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Card search", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils", + "ply" + ], + "tags": [ + "PAD" + ] } diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 4818ef86..bc7e2a2d 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -1,13 +1,11 @@ import json -import re import logging +import re from fnmatch import fnmatch - from ply import lex from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify - from tsutils import tsutils logger = logging.getLogger('red.padbot-cogs.padsearch') diff --git a/padvision/__init__.py b/padvision/__init__.py index eca6ba01..ca87a397 100644 --- a/padvision/__init__.py +++ b/padvision/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PadVision(bot)) diff --git a/padvision/info.json b/padvision/info.json index f2228d47..86713d17 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "A beefy helper cog for neural networks.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Neural net helper", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["Pillow"], - "tags": ["PAD", "sharedlibrary"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "A beefy helper cog for neural networks.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Neural net helper", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "Pillow" + ], + "tags": [ + "PAD", + "sharedlibrary" + ] } diff --git a/padvision/padvision.py b/padvision/padvision.py index 56152801..d2138fcf 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -1,14 +1,14 @@ +import PIL.Image import io import logging import traceback - -import PIL.Image from redbot.core import commands ORB_IMG_SIZE = 40 logger = logging.getLogger('red.padbot-cogs.padvision') + class PadVision(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pricecheck/__init__.py b/pricecheck/__init__.py index 1a4dff78..520f0547 100644 --- a/pricecheck/__init__.py +++ b/pricecheck/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): bot.add_cog(PriceCheck(bot)) diff --git a/pricecheck/info.json b/pricecheck/info.json index 6f0e4fd4..879a94df 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -1,19 +1,29 @@ { - "author": ["Aradia Megido#2552"], - "description": "A cog that enables the display of pricing information for evolution material gems.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Price Check", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": { - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" - }, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "Aradia Megido#2552" + ], + "description": "A cog that enables the display of pricing information for evolution material gems.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Price Check", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": { + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + }, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 29dc0e0d..23fcbd0d 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -1,9 +1,7 @@ -import re - import discord -from redbot.core import commands, checks, Config -from redbot.core.utils.chat_formatting import inline, box, pagify - +import re +from redbot.core import Config, checks, commands +from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check PC_TEXT = """{name} - Stamina Cost: {stam_cost} @@ -54,11 +52,11 @@ async def pricecheck(self, ctx, *, query): await ctx.send("{} does not have PC data.".format(nm.name_na)) return sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name = nm.name_na, - stam_cost = sc, - pp_val = sc*83/50, - points = sc*83/50/297, - foot = foot) + pct = PC_TEXT.format(name=nm.name_na, + stam_cost=sc, + pp_val=sc * 83 / 50, + points=sc * 83 / 50 / 297, + foot=foot) if await self.config.channel(ctx.channel).dm(): for page in pagify(pct): await ctx.author.send(box(page)) diff --git a/profile/__init__.py b/profile/__init__.py index 244090d8..da213419 100644 --- a/profile/__init__.py +++ b/profile/__init__.py @@ -2,5 +2,6 @@ __red_end_user_data_statement__ = "Profile data is stored persistantly." + def setup(bot): bot.add_cog(Profile(bot)) diff --git a/profile/info.json b/profile/info.json index 9d5481f1..88dbb00e 100644 --- a/profile/info.json +++ b/profile/info.json @@ -1,15 +1,25 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Holds all of your Puzzle & Dragons accounts in one place", - "end_user_data_statement": "Profile data is stored persistantly.", - "install_msg": "", - "short": "P&D Phonebook", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils"], - "tags": ["PAD"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Holds all of your Puzzle & Dragons accounts in one place", + "end_user_data_statement": "Profile data is stored persistantly.", + "install_msg": "", + "short": "P&D Phonebook", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils" + ], + "tags": [ + "PAD" + ] } diff --git a/profile/profile.py b/profile/profile.py index ff0db483..d37d8dd2 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -1,14 +1,13 @@ -import re -import logging - import discord +import logging +import re from redbot.core import commands -from redbot.core.utils.chat_formatting import inline, box, pagify - +from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.profile') + def normalizeServer(server): server = server.upper().strip() return 'NA' if server == 'US' else server diff --git a/schoolidol/__init__.py b/schoolidol/__init__.py index 44e09da4..2379fffc 100644 --- a/schoolidol/__init__.py +++ b/schoolidol/__init__.py @@ -2,6 +2,7 @@ __red_end_user_data_statement__ = "No personal data is stored." + def setup(bot): n = SchoolIdol(bot) bot.add_cog(n) diff --git a/schoolidol/info.json b/schoolidol/info.json index f75e8b38..8e50f075 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -1,15 +1,26 @@ { - "author": ["The Tsubaki Bot Team"], - "description": "Displays data about School Idol.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Shows School Idol data", - "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", - "min_python_version" : [3,8,1], - "hidden": false, - "disabled": false, - "require_cogs": {}, - "requirements": ["tsutils", "aiohttp"], - "tags": ["schoolidol"] + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Displays data about School Idol.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Shows School Idol data", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "aiohttp" + ], + "tags": [ + "schoolidol" + ] } diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index a7c9688f..17fb5def 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -1,14 +1,12 @@ +import aiohttp import difflib +import discord import json import logging from collections import OrderedDict - -import aiohttp -import discord -from redbot.core import commands, checks +from redbot.core import checks, commands from redbot.core.utils.chat_formatting import inline - -from tsutils import Menu, EmojiUpdater, char_to_emoji +from tsutils import EmojiUpdater, Menu, char_to_emoji logger = logging.getLogger('red.padbot-cogs.schoolidol') From a10f153f459d8b6c071a41e19d379050ff4acdc6 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 15 Sep 2020 02:32:27 -0700 Subject: [PATCH 0123/1220] langauge code --- dadguide/dadguide.py | 86 ++++++++++++++++++++-------------------- padevents/padevents.py | 2 +- padglobal/padglobal.py | 24 +++++------ padguidedb/padguidedb.py | 4 +- padinfo/padinfo.py | 16 ++++---- padmonitor/padmonitor.py | 8 ++-- padsearch/padsearch.py | 2 +- pricecheck/pricecheck.py | 12 +++--- 8 files changed, 77 insertions(+), 77 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b844ab2c..0d3e7a7b 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -654,8 +654,8 @@ def get_na_only_monsters(self): def get_monster(self, monster_id: int): return self._select_one_entry_by_pk(monster_id, DgMonster) - def get_all_monster_jp_name(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_jp',)}), (), DictWithAttrAccess, + def get_all_monster_ja_name(self, as_generator=True): + return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, as_generator=as_generator) def get_all_monsters(self, as_generator=True): @@ -718,11 +718,11 @@ def skillups(self): @property def desc(self): - return self.desc_na or self.desc_jp + return self.desc_en or self.desc_ja @property def name(self): - return self.name_na or self.name_jp + return self.name_en or self.name_ja class DgLeaderSkill(DadguideItem): @@ -735,11 +735,11 @@ def data(self): @property def desc(self): - return self.desc_na or self.desc_jp + return self.desc_en or self.desc_ja @property def name(self): - return self.name_na or self.name_jp + return self.name_en or self.name_ja class DgAwakening(DadguideItem): @@ -756,7 +756,7 @@ def skill(self): @property def name(self): - return self.skill.name_na if self.skill.name_na is not None else self.skill.name_jp + return self.skill.name_en if self.skill.name_en is not None else self.skill.name_ja class DgAwokenSkill(DadguideItem): @@ -787,7 +787,7 @@ def monsters(self): @property def name(self): - return self.name_na if self.name_na is not None else self.name_jp + return self.name_en if self.name_en is not None else self.name_ja class DgDungeon(DadguideItem): @@ -839,13 +839,13 @@ def __init__(self, item, database): super(DgMonster, self).__init__(item, database) self.roma_subname = None - if self.name_na == self.name_jp: - self.roma_subname = make_roma_subname(self.name_jp) + if self.name_en == self.name_ja: + self.roma_subname = make_roma_subname(self.name_ja) else: # Remove annoying stuff from NA names, like Jörmungandr - self.name_na = tsutils.rmdiacritics(self.name_na) + self.name_en = tsutils.rmdiacritics(self.name_en) - self.name_na = self.name_na_override or self.name_na + self.name_en = self.name_en_override or self.name_en self.attr1 = enum_or_none(Attribute, self.attribute_1_id) self.attr2 = enum_or_none(Attribute, self.attribute_2_id) @@ -950,7 +950,7 @@ def mats_for_evo(self): @property def evo_gem(self): - return self._database.get_monster_evo_gem(self.name_jp) + return self._database.get_monster_evo_gem(self.name_ja) @property def material_of(self): @@ -1068,7 +1068,7 @@ def prev_monster(self): class MonsterSearchHelper(object): def __init__(self, m: DgMonster): - self.name = '{} {}'.format(m.name_na, m.name_jp).lower() + self.name = '{} {}'.format(m.name_en, m.name_ja).lower() leader_skill = m.leader_skill self.leader = leader_skill.desc.lower() if leader_skill else '' active_skill = m.active_skill @@ -1169,12 +1169,12 @@ def strip_next_clause(txt: str, sep: str): self.orb_convert[so].append(do) -def make_roma_subname(name_jp): - subname = name_jp.replace('=', '') +def make_roma_subname(name_ja): + subname = name_ja.replace('=', '') adjusted_subname = '' for part in subname.split('・'): roma_part = romkan.to_roma(part) - if part != roma_part and not tsutils.containsJp(roma_part): + if part != roma_part and not tsutils.containsJa(roma_part): adjusted_subname += ' ' + roma_part.strip('-') return adjusted_subname.strip() @@ -1270,7 +1270,7 @@ def named_monsters_sort(nm: NamedMonster): self.pantheons[pantheon.lower()].add(nm) self.all_monsters = named_monsters - self.all_na_name_to_monsters = {m.name_na.lower(): m for m in named_monsters} + self.all_en_name_to_monsters = {m.name_en.lower(): m for m in named_monsters} self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} @@ -1300,11 +1300,11 @@ def compute_prefixes(self, m: DgMonster, evotree: list): # TODO: add prefixes based on type # Chibi monsters have the same NA name, except lowercased - lower_name = m.name_na.lower() - if m.name_na != m.name_jp: - if lower_name == m.name_na: + lower_name = m.name_en.lower() + if m.name_en != m.name_ja: + if lower_name == m.name_en: prefixes.add('chibi') - elif 'ミニ' in m.name_jp: + elif 'ミニ' in m.name_ja: # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) prefixes.add('chibi') @@ -1351,7 +1351,7 @@ def compute_prefixes(self, m: DgMonster, evotree: list): # If any monster in the group is a pixel, add 'nonpixel' to all the versions # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. def is_pixel(n): - n = n.name_na.lower() + n = n.name_en.lower() return n.startswith('pixel') or n.startswith('ドット') for gm in evotree: @@ -1386,10 +1386,10 @@ def find_monster(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_jp = tsutils.containsJp(query) - if len(query) < 2 and contains_jp: + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_jp: + elif len(query) < 4 and not contains_ja: return None, 'Your query must be at least 4 letters', None # TODO: this should be a length-limited priority queue @@ -1415,13 +1415,13 @@ def find_monster(self, query): if nickname.startswith(query): matches.add(m) if len(matches): - all_names = ",".join(map(lambda x: x.name_na, matches)) + all_names = ",".join(map(lambda x: x.name_en, matches)) return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( len(matches), all_names) # prefix search for full name, take max id for nickname, m in self.all_entries.items(): - if (m.name_na.lower().startswith(query) or m.name_jp.lower().startswith(query)): + if (m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query)): matches.add(m) if len(matches): return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) @@ -1434,7 +1434,7 @@ def find_monster(self, query): # full name contains on nickname, take max id for nickname, m in self.all_entries.items(): - if (query in m.name_na.lower() or query in m.name_jp.lower()): + if (query in m.name_en.lower() or query in m.name_ja.lower()): matches.add(m) if len(matches): return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( @@ -1448,16 +1448,16 @@ def find_monster(self, query): # Still no decent matches. Try near hits on full name instead matches = difflib.get_close_matches( - query, self.all_na_name_to_monsters.keys(), n=1, cutoff=.9) + query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) if len(matches): match = matches[0] - return self.all_na_name_to_monsters[match], None, 'Close name match ({})'.format(match) + return self.all_en_name_to_monsters[match], None, 'Close name match ({})'.format(match) # About to give up, try matching all words matches = set() for nickname, m in self.all_entries.items(): - if (all(map(lambda x: x in m.name_na.lower(), query.split())) or - all(map(lambda x: x in m.name_jp.lower(), query.split()))): + if (all(map(lambda x: x in m.name_en.lower(), query.split())) or + all(map(lambda x: x in m.name_ja.lower(), query.split()))): matches.add(m) if len(matches): return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( @@ -1487,10 +1487,10 @@ def find_monster2(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_jp = tsutils.containsJp(query) - if len(query) < 2 and contains_jp: + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_jp: + elif len(query) < 4 and not contains_ja: return None, 'Your query must be at least 4 letters', None # we want to look up only the main part of the query, and then verify that each result has the prefixes @@ -1527,8 +1527,8 @@ def find_monster2(self, query): # if we don't have any candidates yet, pick a new method if not matches.length(): # try matching on exact names next - for nickname, m in self.all_na_name_to_monsters.items(): - if new_query in m.name_na.lower() or new_query in m.name_jp.lower(): + for nickname, m in self.all_en_name_to_monsters.items(): + if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): matches.add(m) matches.remove_potential_matches_without_all_prefixes(query_prefixes) @@ -1609,7 +1609,7 @@ def __init__(self, evolution_tree: list, basename_overrides: list): self.basenames = basename_overrides or self.computed_basenames def _compute_monster_basename(self, m: DgMonster): - basename = m.name_na.lower() + basename = m.name_en.lower() if ',' in basename: name_parts = basename.split(',') if name_parts[1].strip().startswith('the '): @@ -1655,12 +1655,12 @@ def _is_low_priority_monster(self, m: DgMonster): lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] lp_substrings = ['tamadra'] lp_min_rarity = 2 - name = m.name_na.lower() + name = m.name_en.lower() failed_type = m.type1 in lp_types failed_ss = any([x in name for x in lp_substrings]) failed_rarity = m.rarity < lp_min_rarity - failed_chibi = name == m.name_na and m.name_na != m.name_jp + failed_chibi = name == m.name_en and m.name_en != m.name_ja failed_equip = m.is_equip return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip @@ -1697,8 +1697,8 @@ def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixe self.rarity = monster.rarity # Used in fallback searches - self.name_na = monster.name_na - self.name_jp = monster.name_jp + self.name_en = monster.name_en + self.name_ja = monster.name_ja # These are just extra metadata self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] diff --git a/padevents/padevents.py b/padevents/padevents.py index 9434e855..20cdd34f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -495,7 +495,7 @@ def __init__(self, scheduled_event: "DgScheduledEvent"): self.open_datetime = scheduled_event.open_datetime self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name - self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' + self.dungeon_name = scheduled_event.dungeon.name_en if scheduled_event.dungeon else 'unknown_dungeon' self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8c86e73c..4a8604a0 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -201,7 +201,7 @@ async def write_send(nn_map, file_name): data_holder = io.StringIO() writer = csv.writer(data_holder) for nn, nm in nn_map.items(): - writer.writerow([nn, nm.monster_no_na, nm.name_na]) + writer.writerow([nn, nm.monster_no_na, nm.name_en]) bytes_data = io.BytesIO(data_holder.getvalue().encode()) await ctx.channel.send(file=discord.File(bytes_data, file_name)) @@ -219,7 +219,7 @@ async def debugid(self, ctx, *, query): await ctx.send(box('No match: ' + err)) return - msg = "{}. {}".format(m.monster_no_na, m.name_na) + msg = "{}. {}".format(m.monster_no_na, m.name_en) msg += "\nLookup type: {}".format(debug_info) def list_or_none(l): @@ -261,11 +261,11 @@ def list_or_none(l): lambda nn: (nn, padinfo_cog.index_all.two_word_entries[nn]), replaced_twnn) replaced_nn_text = list(map(lambda nn_info: '{} : {}. {}'.format( - nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_na), + nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_en), replaced_nn_info)) replaced_twnn_text = list(map(lambda nn_info: '{} : {}. {}'.format( - nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_na), + nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_en), replaced_twnn_info)) msg += "\n nicknames: {}".format(list_or_none(replaced_nn_text)) @@ -826,7 +826,7 @@ async def addwhich(self, ctx, monster_id: int, *, definition): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id op = 'EDITED' if name in self.settings.which() else 'ADDED' @@ -839,9 +839,9 @@ async def rmwhich(self, ctx, *, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format( - m.name_na)): + m.name_en)): return name = m.monster_id @@ -888,7 +888,7 @@ async def debuglookup(self, ctx, *, term: str): term = term.lower().replace('?', '') nm, err, deb = await lookup_named_monster(term) base = nm.group_computed_basename.title() if nm else nm - name = nm.name_na if nm else nm + name = nm.name_en if nm else nm monster_id = nm.monster_id if nm else nm definition = self.settings.which().get(monster_id, None) await ctx.send('Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' @@ -1187,7 +1187,7 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id op = 'EDITED' if name in self.settings.leaderGuide() else 'ADDED' @@ -1200,10 +1200,10 @@ async def rmleaderguide(self, ctx, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format( - m.name_na)): + m.name_en)): return name = m.monster_id @@ -1236,7 +1236,7 @@ def check_simple_tree(monster): return False if m.is_equip: return False - if 'awoken' in m.name_na.lower(): + if 'awoken' in m.name_en.lower(): return False return True diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index ee9f84db..4d63be4b 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -101,8 +101,8 @@ async def searchdungeon(self, ctx, *, search_text): """Search""" search_text = '%{}%'.format(search_text) with self.get_cursor() as cursor: - sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' - ' where lower(name_na) like %s or lower(name_jp) like %s' + sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' + ' where lower(name_en) like %s or lower(name_ja) like %s' ' order by dungeon_id desc limit 20') cursor.execute(sql, [search_text, search_text]) results = list(cursor.fetchall()) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index be83bb5d..9144c251 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -255,7 +255,7 @@ async def jpname(self, ctx, *, query: str): m, err, debug_info = await self.findMonster(query) if m is not None: await ctx.send(monsterToHeader(m)) - await ctx.send(box(m.name_jp)) + await ctx.send(box(m.name_ja)) else: await ctx.send(self.makeFailureMsg(err)) @@ -457,7 +457,7 @@ async def links(self, ctx, *, query: str): if m is not None: embed = monsterToBaseEmbed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), SKYOZORA_TEMPLATE.format(m.monster_no_jp), INFO_PDX_TEMPLATE.format(m.monster_no_jp), ILMINA_TEMPLATE.format(m.monster_no_jp)) @@ -666,7 +666,7 @@ async def iddiff(self, ctx): [debug_info1, debug_info2] )) if m1 and m2: - await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_na, m2.name_na)) + await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_en, m2.name_en)) await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) file = discord.File(io.BytesIO(json.dumps(f).encode()), filename="diff.json") await ctx.send(file=file) @@ -781,11 +781,11 @@ def voiceDir(self): def monsterToHeader(m: "DgMonster", link=False): - msg = 'No. {} {}'.format(m.monster_no_na, m.name_na) + msg = 'No. {} {}'.format(m.monster_no_na, m.name_en) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg -def monsterToJpSuffix(m: "DgMonster"): +def monsterToJaSuffix(m: "DgMonster"): suffix = "" if m.roma_subname: suffix += ' [{}]'.format(m.roma_subname) @@ -795,7 +795,7 @@ def monsterToJpSuffix(m: "DgMonster"): def monsterToLongHeader(m: "DgMonster", link=False): - msg = monsterToHeader(m) + monsterToJpSuffix(m) + msg = monsterToHeader(m) + monsterToJaSuffix(m) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg @@ -1135,9 +1135,9 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): tbl.add_row([row_name.format(plus), hp, atk, rcv]) body_text += box(tbl.get_string()) - body_text += "\n**JP Name**: {}".format(m.name_jp) + body_text += "\n**JP Name**: {}".format(m.name_ja) body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), SKYOZORA_TEMPLATE.format(m.monster_no_jp), INFO_PDX_TEMPLATE.format(m.monster_no_jp), ILMINA_TEMPLATE.format(m.monster_no_jp)) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 21e363d5..c4ee83e3 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -60,10 +60,10 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: - msg += '\n\tNo. {} {}'.format(m.monster_no, m.name_na) - if tsutils.containsJp( - m.name_na) and m.name_na_override != m.name_na and m.name_na_override is not None: - msg += ' ({})'.format(m.name_na_override) + msg += '\n\tNo. {} {}'.format(m.monster_no, m.name_en) + if tsutils.containsJa( + m.name_en) and m.name_en_override != m.name_en and m.name_en_override is not None: + msg += ' ({})'.format(m.name_en_override) return msg else: return None diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index bc7e2a2d..f41452b0 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -790,7 +790,7 @@ async def search(self, ctx, *, filter_spec: str): matched_monsters = matched_monsters[0:200] for m in matched_monsters: - msg += '\n\tNo. {} {}'.format(m.monster_no_na, m.name_na) + msg += '\n\tNo. {} {}'.format(m.monster_no_na, m.name_en) if dm_required: header += '\nList too long to display; sent via DM' diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 23fcbd0d..c9d6507f 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -49,10 +49,10 @@ async def pricecheck(self, ctx, *, query): return async with self.config.pcs() as pcs: if str(nm.base_monster_no) not in pcs: - await ctx.send("{} does not have PC data.".format(nm.name_na)) + await ctx.send("{} does not have PC data.".format(nm.name_en)) return sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name=nm.name_na, + pct = PC_TEXT.format(name=nm.name_en, stam_cost=sc, pp_val=sc * 83 / 50, points=sc * 83 / 50 / 297, @@ -87,7 +87,7 @@ async def set(self, ctx, stam_cost: float, *, query): if str(nm.base_monster_no) in pcs: foot = pcs[str(nm.base_monster_no)][1] pcs[str(nm.base_monster_no)] = (stam_cost, foot) - await ctx.send(box("Set {} ({}) to {}".format(nm.name_na, nm.base_monster_no, stam_cost))) + await ctx.send(box("Set {} ({}) to {}".format(nm.name_en, nm.base_monster_no, stam_cost))) @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) async def setfooter(self, ctx, query, *, footer=""): @@ -107,7 +107,7 @@ async def setfooter(self, ctx, query, *, footer=""): if str(nm.base_monster_no) in pcs: sc = pcs[str(nm.base_monster_no)][0] pcs[str(nm.base_monster_no)] = (sc, footer.strip('`')) - await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_na, nm.base_monster_no, footer))) + await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_en, nm.base_monster_no, footer))) @pcadmin.command(aliases=['delete', 'del', 'rm']) async def remove(self, ctx, *, query): @@ -124,10 +124,10 @@ async def remove(self, ctx, *, query): return async with self.config.pcs() as pcs: if str(nm.base_monster_no) not in pcs: - await ctx.send("{} does not have PC data.".format(nm.name_na)) + await ctx.send("{} does not have PC data.".format(nm.name_en)) return del pcs[str(nm.base_monster_no)] - await ctx.send(inline("Removed PC data from {}.".format(nm.name_na))) + await ctx.send(inline("Removed PC data from {}.".format(nm.name_en))) @pcadmin.command(aliases=['set-demon-ly']) async def setdmonly(self, ctx, value: bool = True): From d70b29212b03a6c61b51a7910778ca9f6273e865 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 15 Sep 2020 23:44:14 -0700 Subject: [PATCH 0124/1220] pcfix --- pricecheck/pricecheck.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 23fcbd0d..230c1208 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -4,14 +4,20 @@ from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check -PC_TEXT = """{name} - Stamina Cost: {stam_cost} +DISCLAIMER = ("DISCLAIMER: This calculation is made by Lumon#6561 and is NOT" + " endorsed by the Tsubaki Bot Team. Please remember that this" + " value is meant to be a baseline and does not reflect the current" + " market value of the gem.") -{name} - Plus Point Value: {pp_val} -({points} 297 Plus Points) +PC_TEXT = """{name} + - Stamina Cost: {stam_cost} + - Plus Point Value: {pp_val} ({points} 𝟤𝟫𝟩 Plus Points) {foot} """ +def rint(x, p): + return str(round(x, p)).rstrip('0').rstrip('.') class PriceCheck(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -53,16 +59,18 @@ async def pricecheck(self, ctx, *, query): return sc, foot = pcs[str(nm.base_monster_no)] pct = PC_TEXT.format(name=nm.name_na, - stam_cost=sc, - pp_val=sc * 83 / 50, - points=sc * 83 / 50 / 297, + stam_cost=rint(sc, 2), + pp_val=rint(sc * 83 / 50, 2), + points=rint(sc * 83 / 50 / 297, 2), foot=foot) if await self.config.channel(ctx.channel).dm(): + await ctx.send(inline(DISCLAIMER)) for page in pagify(pct): - await ctx.author.send(box(page)) + await ctx.author.send(box(page.replace("'","ʼ"), lang='py')) else: + await ctx.send(inline(DISCLAIMER)) for page in pagify(pct): - await ctx.send(box(page)) + await ctx.send(box(page.replace("'","ʼ"), lang='py')) @commands.group() @auth_check('pcadmin') From d278d9b28462188c802ee4b949959bceca2baf29 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 16 Sep 2020 05:15:49 -0700 Subject: [PATCH 0125/1220] feedback --- feedback/__init__.py | 7 ++ feedback/feedback.py | 155 +++++++++++++++++++++++++++++++++++++++ feedback/info.json | 26 +++++++ pricecheck/pricecheck.py | 4 +- 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 feedback/__init__.py create mode 100644 feedback/feedback.py create mode 100644 feedback/info.json diff --git a/feedback/__init__.py b/feedback/__init__.py new file mode 100644 index 00000000..975a3c07 --- /dev/null +++ b/feedback/__init__.py @@ -0,0 +1,7 @@ +from .feedback import * + +__red_end_user_data_statement__ = "No personal data is stored." + + +def setup(bot): + bot.add_cog(Feedback(bot)) diff --git a/feedback/feedback.py b/feedback/feedback.py new file mode 100644 index 00000000..20c55fe3 --- /dev/null +++ b/feedback/feedback.py @@ -0,0 +1,155 @@ +import discord +from redbot.core import checks, commands, modlog +from redbot.core.utils.chat_formatting import box, inline, pagify +from tsutils import CogSettings + + +class Feedback(commands.Cog): + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.settings = FeedbackSettings("feedback") + + self.sessions = set() + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + + async def _send_feedback(self, ctx, message: str, feedback_channel, success_message: str): + if feedback_channel is None: + raise commands.UserFeedbackCheckFailure("Feedback channel not set") + + guild = ctx.guild + author = ctx.author + footer = "User ID: " + str(author.id) + + if guild: + source = "from {}".format(guild) + footer += " | Guild ID: " + str(guild.id) + else: + source = "through DM" + + description = "Sent by {} {}".format(author, source) + + e = discord.Embed(description=message) + if author.avatar_url: + e.set_author(name=description, icon_url=author.avatar_url) + else: + e.set_author(name=description) + e.set_footer(text=footer) + + try: + await feedback_channel.send(embed=e) + except: + await ctx.send(inline("I'm unable to deliver your message. Sorry.")) + else: + await ctx.send(inline("Your message has been sent." + " Abusing this feature will result in a blacklist." + + success_message)) + + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def credits(self, ctx): + """Shows info about this bot""" + author_repo = "https://github.com/Twentysix26" + red_repo = author_repo + "/Red-DiscordBot" + rpad_invite = "https://discord.gg/pad" + + about = ( + "This is an instance of [the Red Discord bot]({}), " + "use the '{}info' command for more info. " + "The various PAD related cogs were created by Aradia Megido and tactical_retreat. " + "Massive overhaul of the bot to Red v3 by Aradia Megido. " + "This bot was created for the [PAD Community Server Discord]({}) but " + "is available for other servers on request." + "".format(red_repo, ctx.prefix, rpad_invite)) + + avatar = ( + "Bot avatars supplied by:\n" + "\t[Tsubaki]({}): {}").format("https://twitter.com/_violebot", "Violebot") + + using = ( + "You can use `{0.prefix}help` to get a full list of commands.\n" + "Use `{0.prefix}userhelp` to get a summary of useful user features.\n" + "Use `{0.prefix}modhelp` to get info on moderator-only features." + ) + + embed = discord.Embed() + embed.add_field(name="Instance owned by", value='The Tsubaki Team') + embed.add_field(name="About the bot", value=about, inline=False) + embed.add_field(name="Using the bot", value=using.format(ctx), inline=False) + embed.add_field(name="Avatar credits", value=avatar, inline=False) + embed.set_thumbnail(url=self.bot.user.avatar_url) + + await ctx.send(embed=embed) + + @commands.command() + @commands.cooldown(1, 60, commands.BucketType.user) + async def feedback(self, ctx, *, message: str): + """Provide feedback on the bot. + + Use this command to provide public feedback on the bot. + """ + feedback_channel = self.bot.get_channel(int(self.settings.get_feedback_channel())) + await self._send_feedback(ctx, message, feedback_channel, + " Join the Tsubaki Server to see any responses ({0.prefix}tsubakiserver).".format( + ctx)) + + @commands.command() + @commands.guild_only() + @checks.is_owner() + async def setfeedbackchannel(self, ctx, channel: discord.TextChannel): + """Set the feedback destination channel.""" + self.settings.set_feedback_channel(channel.id) + await ctx.tick() + + @commands.command(aliases=['mamafeedback', 'renifeedback']) + @commands.cooldown(1, 60, commands.BucketType.user) + async def blogfeedback(self, ctx, *, message: str): + """Provide feedback on Reni's blog or translations. + + Use this command to submit feedback on https://pad.protic.site or the JP translations. + """ + feedback_channel = self.bot.get_channel(int(self.settings.get_blog_feedback_channel())) + await self._send_feedback(ctx, message, feedback_channel, + " Join the PDX Server to see any responses ({0.prefix}pdx).".format(ctx)) + + @commands.command() + @commands.guild_only() + @checks.is_owner() + async def setblogfeedbackchannel(self, ctx, channel: discord.TextChannel): + """Set the blog feedback destination channel.""" + self.settings.set_blog_feedback_channel(channel.id) + await ctx.tick() + + +class FeedbackSettings(CogSettings): + def make_default_settings(self): + config = { + 'feedback_channel': None, + 'blog_feedback_channel': None, + } + return config + + def get_feedback_channel(self): + return self.bot_settings.get('feedback_channel') + + def set_feedback_channel(self, channel_id: int): + self.bot_settings['feedback_channel'] = str(channel_id) + self.save_settings() + + def get_blog_feedback_channel(self): + return self.bot_settings.get('blog_feedback_channel') + + def set_blog_feedback_channel(self, channel_id: int): + self.bot_settings['blog_feedback_channel'] = str(channel_id) + self.save_settings() diff --git a/feedback/info.json b/feedback/info.json new file mode 100644 index 00000000..f34d128a --- /dev/null +++ b/feedback/info.json @@ -0,0 +1,26 @@ +{ + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Tsubaki feedback and credits", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "P&D Phonebook", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils" + ], + "tags": [ + "Tsubaki", + "feedback" + ] +} diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 230c1208..07d0754a 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -26,7 +26,9 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9213337337) self.config.register_global(pcs={}) self.config.register_channel(dm=False) - self.bot.get_cog("GlobalAdmin").register_perm("pcadmin") + GADMIN_COG = self.bot.get_cog("GlobalAdmin") + if GADMIN_COG: + GADMIN_COG.register_perm("pcadmin") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" From b68123443595a5faa36ecc68eeaf8303092c0389 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 16 Sep 2020 19:15:30 -0700 Subject: [PATCH 0126/1220] disclaimer change --- pricecheck/pricecheck.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 07d0754a..3ae38ce8 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -4,10 +4,7 @@ from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check -DISCLAIMER = ("DISCLAIMER: This calculation is made by Lumon#6561 and is NOT" - " endorsed by the Tsubaki Bot Team. Please remember that this" - " value is meant to be a baseline and does not reflect the current" - " market value of the gem.") +DISCLAIMER = "**Disclaimer**: This is Lumon's data. Use at your own discretion." PC_TEXT = """{name} - Stamina Cost: {stam_cost} @@ -66,11 +63,11 @@ async def pricecheck(self, ctx, *, query): points=rint(sc * 83 / 50 / 297, 2), foot=foot) if await self.config.channel(ctx.channel).dm(): - await ctx.send(inline(DISCLAIMER)) + await ctx.send(DISCLAIMER) for page in pagify(pct): await ctx.author.send(box(page.replace("'","ʼ"), lang='py')) else: - await ctx.send(inline(DISCLAIMER)) + await ctx.send(DISCLAIMER) for page in pagify(pct): await ctx.send(box(page.replace("'","ʼ"), lang='py')) From 791b6d675ceebbba7db6ba680ced9f5045908405 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 17 Sep 2020 00:34:08 -0700 Subject: [PATCH 0127/1220] small fixes --- padinfo/padinfo.py | 4 ++-- padmonitor/padmonitor.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index be83bb5d..e6ef391b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -46,8 +46,8 @@ RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}cb.png' +ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' +ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 21e363d5..134144c0 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -83,7 +83,7 @@ async def announce(self, channel_id, message): for page in pagify(message): await channel.send(box(page)) except Exception as ex: - logger.exception('failed to send message to', channel_id, ' : ', ex) + logger.exception('failed to send message to {}:'.format(channel_id)) @commands.group() @checks.mod_or_permissions(manage_guild=True) From 2e911c8263312f6176bb85bd0ad4215a951fc0e8 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 17 Sep 2020 04:43:39 -0700 Subject: [PATCH 0128/1220] pdg and pdv --- padglobal/padglobal.py | 61 +++++++++++------------------------------- padvision/padvision.py | 2 +- 2 files changed, 16 insertions(+), 47 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8c86e73c..c4fa69df 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -462,62 +462,31 @@ async def send_cmdlist(self, ctx, cmdlist, inline=False): await ctx.send("There are no padglobal commands yet") return - prefix = ctx.prefix - prefixes = defaultdict(int) + prefixes = defaultdict(list) for c in cmdlist: - m = re.match(r'^([a-zA-Z]+)\d+$', c) + m = re.match(r'^([a-zA-Z]+)(\d+)$', c) if m: - grp = m.group(1) - prefixes[grp] = prefixes[grp] + 1 + prefixes[m.group(1)].append(m.group(2)) - good_prefixes = [cmd for cmd, cnt in prefixes.items() if cnt > 1] - prefix_to_suffix = defaultdict(list) - prefix_to_other = defaultdict(list) + prefix_to_suffix = {cmd: cnt for cmd, cnt in prefixes.items() if len(cnt) > 1} - i = 0 msg = PAD_CMD_HEADER.format(ctx.prefix) + "\n" - if inline: - for cmd in sorted([cmd for cmd in cmdlist.keys()]): - msg += " {} : {}\n".format(cmd, cmdlist[cmd]) - for page in pagify(msg): - await ctx.author.send(box(page)) - return + done_prefixes = [] - for cmd in sorted([cmd for cmd in cmdlist.keys()]): - m = re.match(r'^([a-zA-Z]+)(\d+)$', cmd) + for cmd in sorted(cmdlist): + m = re.match(r'^([a-zA-Z]+)\d+$', cmd) if m: prefix = m.group(1) - if prefix in good_prefixes: - suffix = m.group(2) - prefix_to_suffix[prefix].append(suffix) - continue - - should_skip = False - for good_prefix in good_prefixes: - if cmd.startswith(good_prefix): - prefix_to_other[prefix].append(cmd) - should_skip = True - break - if should_skip: - continue - - msg += " {}{}\n".format(ctx.prefix, cmd) - - if prefix_to_suffix: - msg += "\nThe following commands are indexed:\n" - for prefix in sorted(prefix_to_suffix.keys()): - msg += " {}{}[n]:\n ".format(ctx.prefix, prefix) - - for suffix in sorted(map(int, prefix_to_suffix[prefix])): - msg += " {}{}".format(prefix, suffix) - - if len(prefix_to_other[prefix]): - msg += "\n" - for cmd in sorted(prefix_to_other[prefix]): - msg += " {}{}".format(ctx.prefix, cmd) - msg += "\n\n" + if prefix in prefix_to_suffix and prefix not in done_prefixes: + msg += " {}{}[n]:\n ".format(ctx.prefix, prefix) + for suffix in sorted(map(int, prefix_to_suffix[prefix])): + msg += " {}{}".format(prefix, suffix) + msg += '\n' + done_prefixes.append(prefix) + else: + msg += " {}{}\n".format(ctx.prefix, cmd) for page in pagify(msg): await ctx.author.send(box(page)) diff --git a/padvision/padvision.py b/padvision/padvision.py index d2138fcf..eb8429ad 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -122,7 +122,7 @@ class NeuralClassifierBoardExtractor(object): def __init__(self, model_path, np_img, img_bytes): self.model_path = model_path self.np_img = np_img - self.img = PIL.Image.open(io.BytesIO(img_bytes)) + self.img = PIL.Image.open(io.BytesIO(img_bytes)).convert('RGB') self.processed = False self.results = [['u' for x in range(6)] for y in range(5)] From 3b7c4b1a521fdf5d6bb29eab14741a5766ae798d Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 20 Sep 2020 14:10:25 -0700 Subject: [PATCH 0129/1220] pdchu fix --- padbuildimg/padbuildimg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 76cff6df..ab20726e 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -130,7 +130,7 @@ 'God': [207], # devil 'Devil': [205], # god 'Machine': [205, 201], # god balanced - 'Dragon': [208, 320], # machine healer + 'Dragon': [208, 203], # machine healer 'Physical': [208, 203], # machine healer 'Attacker': [207, 202], # devil physical 'Healer': [204, 206], # dragon attacker From 9d44b4ee0930563456667b33e654cbd5a778dc45 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 21 Sep 2020 10:45:32 -0700 Subject: [PATCH 0130/1220] name_na -> en --- pricecheck/pricecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 89a66bd8..43773e19 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -57,7 +57,7 @@ async def pricecheck(self, ctx, *, query): await ctx.send("{} does not have PC data.".format(nm.name_en)) return sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name=nm.name_na, + pct = PC_TEXT.format(name=nm.name_en, stam_cost=rint(sc, 2), pp_val=rint(sc * 83 / 50, 2), points=rint(sc * 83 / 50 / 297, 2), From e2e736208eeed44b8fce7340a3d871d7a782f2d4 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Sep 2020 00:51:24 -0700 Subject: [PATCH 0131/1220] pdb fix (i think) --- padguidedb/padguidedb.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 4d63be4b..906586e1 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -132,6 +132,13 @@ async def setimageupdatefile(self, ctx, *, image_update_file): self.settings.setImageUpdateFile(image_update_file) await ctx.tick() + @padguidedb.command() + @checks.is_owner() + async def setimageupdatefile(self, ctx, *, python_executable): + """Set the python executable file.""" + self.settings.setPythonExecutable(python_executable) + await ctx.tick() + @padguidedb.command() @checks.is_owner() async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): @@ -166,7 +173,7 @@ async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): async with self.dungeon_load_lock: process = await asyncio.create_subprocess_exec( - '/usr/bin/python3', + self.settings.pythonExecutable(), self.settings.dungeonScriptFile(), '--db_config={}'.format(self.settings.configFile()), '--server={}'.format(server), @@ -326,6 +333,7 @@ def make_default_settings(self): 'dungeon_script_file': '', 'full_etl_file': '', 'update_image_file': '', + 'python_executable': '/usr/bin/python3', 'users': {}, } return config @@ -377,6 +385,13 @@ def setUpdateImageFile(self, update_image_file): self.bot_settings['update_image_file'] = update_image_file self.save_settings() + def pythonExecutable(self): + return self.bot_settings.get('python_executable', '/usr/bin/python3') + + def setPythonExecutable(self, python_executable): + self.bot_settings['python_executable'] = python_executable + self.save_settings() + def setUserInfo(self, server, user_uuid, user_intid): self.bot_settings['users'][server.upper()] = [user_uuid, user_intid] self.save_settings() From 3a328b6a2bd2bc8deb725d83a4d29ed261252038 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Sep 2020 00:54:59 -0700 Subject: [PATCH 0132/1220] shoot sorry --- padguidedb/padguidedb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 906586e1..2389d4a8 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -134,7 +134,7 @@ async def setimageupdatefile(self, ctx, *, image_update_file): @padguidedb.command() @checks.is_owner() - async def setimageupdatefile(self, ctx, *, python_executable): + async def setpythonexecutable(self, ctx, *, python_executable): """Set the python executable file.""" self.settings.setPythonExecutable(python_executable) await ctx.tick() From 372fa97ae2783c55a2333f6e0a5b87f1b04d81da Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Sep 2020 01:52:36 -0700 Subject: [PATCH 0133/1220] aaaaaaaaaa --- dadguide/dadguide.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 0d3e7a7b..a7f39209 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -629,9 +629,9 @@ def get_monsters_by_active(self, active_skill_id: int): def get_monster_evo_gem(self, name: str, region='jp'): gem_suffix = { - 'jp': 'の希石', - 'na': '\'s Gem', - 'kr': ' 의 휘석' + 'ja': 'の希石', + 'en': '\'s Gem', + 'ko': ' 의 휘석' } if region not in gem_suffix: return None From 62b4527b906b54e998e519b189976c9e460f82b8 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Sep 2020 10:08:34 -0700 Subject: [PATCH 0134/1220] pdb fix --- padguidedb/padguidedb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 2389d4a8..5cc16409 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -188,7 +188,7 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): stdout, stderr = await process.communicate() if stderr: - logger.error("Dungeon Load Error:\n" + stderr.decode()) + logger.error("Dungeon Load Error:\n{}\n\n{}".format(stdout.decode(), stderr.decode())) await tsutils.doubleup(ctx, inline( 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) else: From cbc2d70b6d9355ce983a47d27c2b9540437be6db Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 29 Sep 2020 04:56:27 -0700 Subject: [PATCH 0135/1220] Profile Fix --- profile/profile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/profile/profile.py b/profile/profile.py index d37d8dd2..76c7ea2f 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -121,7 +121,7 @@ async def getIdMsg(self, ctx, user, server=None, for_other=True): if server is None: return - pad_id = self.settings.getId(user.id, server) + pad_id = str(self.settings.getId(user.id, server)) pad_name = self.settings.getName(user.id, server) profile_text = self.settings.getProfileText(user.id, server) @@ -299,11 +299,11 @@ def getProfile(self, user, server): return profile[server] def setId(self, user, server, id): - self.getProfile(user, server)['id'] = id + self.getProfile(user, server)['id'] = int(id) self.save_settings() def getId(self, user, server): - return self.getProfile(user, server).get('id', '000000000') + return self.getProfile(user, server).get('id', 000000000) def setName(self, user, server, name): self.getProfile(user, server)['name'] = name From ca9e90ce2f1de51c60573771127784d78bab63da Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 29 Sep 2020 08:50:32 -0700 Subject: [PATCH 0136/1220] Fix PadguideDB naming --- padguidedb/padguidedb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 5cc16409..c4025ec5 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -378,10 +378,10 @@ def setFullETLFile(self, full_etl_file): self.bot_settings['full_etl_file'] = full_etl_file self.save_settings() - def updateImageFile(self): + def updateImageUpdateFile(self): return self.bot_settings.get('dungeon_script_file', '') - def setUpdateImageFile(self, update_image_file): + def setUpdateImageUpdateFile(self, update_image_file): self.bot_settings['update_image_file'] = update_image_file self.save_settings() From 96b16b8c6a08ea4b9840094ac534a09e62e2c97c Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 29 Sep 2020 08:54:12 -0700 Subject: [PATCH 0137/1220] Wait shoot I put it in the wrong place --- padguidedb/padguidedb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index c4025ec5..84fcd88f 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -378,10 +378,10 @@ def setFullETLFile(self, full_etl_file): self.bot_settings['full_etl_file'] = full_etl_file self.save_settings() - def updateImageUpdateFile(self): + def imageUpdateFile(self): return self.bot_settings.get('dungeon_script_file', '') - def setUpdateImageUpdateFile(self, update_image_file): + def setImageUpdateFile(self, update_image_file): self.bot_settings['update_image_file'] = update_image_file self.save_settings() From 9ab6b72e221ffca695b23e7f4bd4d687c605b469 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 29 Sep 2020 12:58:36 -0700 Subject: [PATCH 0138/1220] Added null to relevant cogs --- dadguide/dadguide.py | 6 ++++++ padsearch/padsearch.py | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index a7f39209..449a6fb6 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -271,6 +271,8 @@ class Attribute(Enum): Wood = 2 Light = 3 Dark = 4 + Unused = 5 + Null = 6 class MonsterType(Enum): @@ -1191,6 +1193,8 @@ async def __init__(self, monster_database, nickname_overrides, basename_override Attribute.Wood: ['g'], Attribute.Light: ['l'], Attribute.Dark: ['d'], + Attribute.Unused: ['h'], + Attribute.Null: ['x'], } self.attr_long_prefix_map = { Attribute.Fire: ['red', 'fire'], @@ -1198,6 +1202,8 @@ async def __init__(self, monster_database, nickname_overrides, basename_override Attribute.Wood: ['green', 'wood'], Attribute.Light: ['light'], Attribute.Dark: ['dark'], + Attribute.Unused: ['unused'], + Attribute.Null: ['null'], } self.series_to_prefix_map = { diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index f41452b0..bd4ecb1d 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -16,7 +16,7 @@ To get more than 10 results, include the word 'all' in your search. Colors can be any of: - fire water wood light dark + fire water wood light dark null Additionally, orb colors can be any of: heart jammer poison mortal and bomb @@ -67,6 +67,7 @@ def filt_timeout(filts, ms): 'wood', 'light', 'dark', + 'null', ] TYPES = [ @@ -133,6 +134,7 @@ def split_csv_orbcolors(value): 'g': 'wood', 'l': 'light', 'd': 'dark', + 'x': 'null', 'heart': 'heal', 'h': 'heal', 'p': 'poison', From 8dd08f9d6674b46bcb41e5b068efb46ee8755055 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 30 Sep 2020 00:30:11 -0700 Subject: [PATCH 0139/1220] Null -> Nil --- dadguide/dadguide.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 449a6fb6..99b9e0a7 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -271,8 +271,8 @@ class Attribute(Enum): Wood = 2 Light = 3 Dark = 4 - Unused = 5 - Null = 6 + Unknown = 5 + Nil = 6 class MonsterType(Enum): @@ -1193,8 +1193,8 @@ async def __init__(self, monster_database, nickname_overrides, basename_override Attribute.Wood: ['g'], Attribute.Light: ['l'], Attribute.Dark: ['d'], - Attribute.Unused: ['h'], - Attribute.Null: ['x'], + Attribute.Unknown: ['h'], + Attribute.Nil: ['x'], } self.attr_long_prefix_map = { Attribute.Fire: ['red', 'fire'], @@ -1202,8 +1202,8 @@ async def __init__(self, monster_database, nickname_overrides, basename_override Attribute.Wood: ['green', 'wood'], Attribute.Light: ['light'], Attribute.Dark: ['dark'], - Attribute.Unused: ['unused'], - Attribute.Null: ['null'], + Attribute.Unknown: ['unknown'], + Attribute.Nil: ['null', 'none'], } self.series_to_prefix_map = { From 33a2ea197170727e0715de4d65f1a67db268368a Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 1 Oct 2020 00:19:00 -0700 Subject: [PATCH 0140/1220] Fix PadguideDb --- padguidedb/padguidedb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 84fcd88f..c1c8ecb2 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -372,14 +372,14 @@ def setDungeonScriptFile(self, dungeon_script_file): self.save_settings() def fullETLFile(self): - return self.bot_settings.get('dungeon_script_file', '') + return self.bot_settings.get('full_etl_file', '') def setFullETLFile(self, full_etl_file): self.bot_settings['full_etl_file'] = full_etl_file self.save_settings() def imageUpdateFile(self): - return self.bot_settings.get('dungeon_script_file', '') + return self.bot_settings.get('update_image_file', '') def setImageUpdateFile(self, update_image_file): self.bot_settings['update_image_file'] = update_image_file From f2eeaef0e199a7a504fe088556f18d97dc3dfe56 Mon Sep 17 00:00:00 2001 From: muffin-rice Date: Sun, 4 Oct 2020 12:45:19 -0700 Subject: [PATCH 0141/1220] reset soft idk --- dadguide/dadguide.py | 98 ++++++++++++----------- feedback/__init__.py | 7 ++ feedback/feedback.py | 155 +++++++++++++++++++++++++++++++++++++ feedback/info.json | 26 +++++++ padbuildimg/padbuildimg.py | 2 +- padevents/padevents.py | 2 +- padglobal/padglobal.py | 85 +++++++------------- padguidedb/padguidedb.py | 31 ++++++-- padinfo/padinfo.py | 50 ++++++------ padmonitor/padmonitor.py | 10 +-- padsearch/padsearch.py | 6 +- padvision/padvision.py | 2 +- pricecheck/pricecheck.py | 37 +++++---- profile/profile.py | 6 +- 14 files changed, 354 insertions(+), 163 deletions(-) create mode 100644 feedback/__init__.py create mode 100644 feedback/feedback.py create mode 100644 feedback/info.json diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b844ab2c..99b9e0a7 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -271,6 +271,8 @@ class Attribute(Enum): Wood = 2 Light = 3 Dark = 4 + Unknown = 5 + Nil = 6 class MonsterType(Enum): @@ -629,9 +631,9 @@ def get_monsters_by_active(self, active_skill_id: int): def get_monster_evo_gem(self, name: str, region='jp'): gem_suffix = { - 'jp': 'の希石', - 'na': '\'s Gem', - 'kr': ' 의 휘석' + 'ja': 'の希石', + 'en': '\'s Gem', + 'ko': ' 의 휘석' } if region not in gem_suffix: return None @@ -654,8 +656,8 @@ def get_na_only_monsters(self): def get_monster(self, monster_id: int): return self._select_one_entry_by_pk(monster_id, DgMonster) - def get_all_monster_jp_name(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_jp',)}), (), DictWithAttrAccess, + def get_all_monster_ja_name(self, as_generator=True): + return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, as_generator=as_generator) def get_all_monsters(self, as_generator=True): @@ -718,11 +720,11 @@ def skillups(self): @property def desc(self): - return self.desc_na or self.desc_jp + return self.desc_en or self.desc_ja @property def name(self): - return self.name_na or self.name_jp + return self.name_en or self.name_ja class DgLeaderSkill(DadguideItem): @@ -735,11 +737,11 @@ def data(self): @property def desc(self): - return self.desc_na or self.desc_jp + return self.desc_en or self.desc_ja @property def name(self): - return self.name_na or self.name_jp + return self.name_en or self.name_ja class DgAwakening(DadguideItem): @@ -756,7 +758,7 @@ def skill(self): @property def name(self): - return self.skill.name_na if self.skill.name_na is not None else self.skill.name_jp + return self.skill.name_en if self.skill.name_en is not None else self.skill.name_ja class DgAwokenSkill(DadguideItem): @@ -787,7 +789,7 @@ def monsters(self): @property def name(self): - return self.name_na if self.name_na is not None else self.name_jp + return self.name_en if self.name_en is not None else self.name_ja class DgDungeon(DadguideItem): @@ -839,13 +841,13 @@ def __init__(self, item, database): super(DgMonster, self).__init__(item, database) self.roma_subname = None - if self.name_na == self.name_jp: - self.roma_subname = make_roma_subname(self.name_jp) + if self.name_en == self.name_ja: + self.roma_subname = make_roma_subname(self.name_ja) else: # Remove annoying stuff from NA names, like Jörmungandr - self.name_na = tsutils.rmdiacritics(self.name_na) + self.name_en = tsutils.rmdiacritics(self.name_en) - self.name_na = self.name_na_override or self.name_na + self.name_en = self.name_en_override or self.name_en self.attr1 = enum_or_none(Attribute, self.attribute_1_id) self.attr2 = enum_or_none(Attribute, self.attribute_2_id) @@ -950,7 +952,7 @@ def mats_for_evo(self): @property def evo_gem(self): - return self._database.get_monster_evo_gem(self.name_jp) + return self._database.get_monster_evo_gem(self.name_ja) @property def material_of(self): @@ -1068,7 +1070,7 @@ def prev_monster(self): class MonsterSearchHelper(object): def __init__(self, m: DgMonster): - self.name = '{} {}'.format(m.name_na, m.name_jp).lower() + self.name = '{} {}'.format(m.name_en, m.name_ja).lower() leader_skill = m.leader_skill self.leader = leader_skill.desc.lower() if leader_skill else '' active_skill = m.active_skill @@ -1169,12 +1171,12 @@ def strip_next_clause(txt: str, sep: str): self.orb_convert[so].append(do) -def make_roma_subname(name_jp): - subname = name_jp.replace('=', '') +def make_roma_subname(name_ja): + subname = name_ja.replace('=', '') adjusted_subname = '' for part in subname.split('・'): roma_part = romkan.to_roma(part) - if part != roma_part and not tsutils.containsJp(roma_part): + if part != roma_part and not tsutils.containsJa(roma_part): adjusted_subname += ' ' + roma_part.strip('-') return adjusted_subname.strip() @@ -1191,6 +1193,8 @@ async def __init__(self, monster_database, nickname_overrides, basename_override Attribute.Wood: ['g'], Attribute.Light: ['l'], Attribute.Dark: ['d'], + Attribute.Unknown: ['h'], + Attribute.Nil: ['x'], } self.attr_long_prefix_map = { Attribute.Fire: ['red', 'fire'], @@ -1198,6 +1202,8 @@ async def __init__(self, monster_database, nickname_overrides, basename_override Attribute.Wood: ['green', 'wood'], Attribute.Light: ['light'], Attribute.Dark: ['dark'], + Attribute.Unknown: ['unknown'], + Attribute.Nil: ['null', 'none'], } self.series_to_prefix_map = { @@ -1270,7 +1276,7 @@ def named_monsters_sort(nm: NamedMonster): self.pantheons[pantheon.lower()].add(nm) self.all_monsters = named_monsters - self.all_na_name_to_monsters = {m.name_na.lower(): m for m in named_monsters} + self.all_en_name_to_monsters = {m.name_en.lower(): m for m in named_monsters} self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} @@ -1300,11 +1306,11 @@ def compute_prefixes(self, m: DgMonster, evotree: list): # TODO: add prefixes based on type # Chibi monsters have the same NA name, except lowercased - lower_name = m.name_na.lower() - if m.name_na != m.name_jp: - if lower_name == m.name_na: + lower_name = m.name_en.lower() + if m.name_en != m.name_ja: + if lower_name == m.name_en: prefixes.add('chibi') - elif 'ミニ' in m.name_jp: + elif 'ミニ' in m.name_ja: # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) prefixes.add('chibi') @@ -1351,7 +1357,7 @@ def compute_prefixes(self, m: DgMonster, evotree: list): # If any monster in the group is a pixel, add 'nonpixel' to all the versions # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. def is_pixel(n): - n = n.name_na.lower() + n = n.name_en.lower() return n.startswith('pixel') or n.startswith('ドット') for gm in evotree: @@ -1386,10 +1392,10 @@ def find_monster(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_jp = tsutils.containsJp(query) - if len(query) < 2 and contains_jp: + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_jp: + elif len(query) < 4 and not contains_ja: return None, 'Your query must be at least 4 letters', None # TODO: this should be a length-limited priority queue @@ -1415,13 +1421,13 @@ def find_monster(self, query): if nickname.startswith(query): matches.add(m) if len(matches): - all_names = ",".join(map(lambda x: x.name_na, matches)) + all_names = ",".join(map(lambda x: x.name_en, matches)) return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( len(matches), all_names) # prefix search for full name, take max id for nickname, m in self.all_entries.items(): - if (m.name_na.lower().startswith(query) or m.name_jp.lower().startswith(query)): + if (m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query)): matches.add(m) if len(matches): return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) @@ -1434,7 +1440,7 @@ def find_monster(self, query): # full name contains on nickname, take max id for nickname, m in self.all_entries.items(): - if (query in m.name_na.lower() or query in m.name_jp.lower()): + if (query in m.name_en.lower() or query in m.name_ja.lower()): matches.add(m) if len(matches): return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( @@ -1448,16 +1454,16 @@ def find_monster(self, query): # Still no decent matches. Try near hits on full name instead matches = difflib.get_close_matches( - query, self.all_na_name_to_monsters.keys(), n=1, cutoff=.9) + query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) if len(matches): match = matches[0] - return self.all_na_name_to_monsters[match], None, 'Close name match ({})'.format(match) + return self.all_en_name_to_monsters[match], None, 'Close name match ({})'.format(match) # About to give up, try matching all words matches = set() for nickname, m in self.all_entries.items(): - if (all(map(lambda x: x in m.name_na.lower(), query.split())) or - all(map(lambda x: x in m.name_jp.lower(), query.split()))): + if (all(map(lambda x: x in m.name_en.lower(), query.split())) or + all(map(lambda x: x in m.name_ja.lower(), query.split()))): matches.add(m) if len(matches): return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( @@ -1487,10 +1493,10 @@ def find_monster2(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_jp = tsutils.containsJp(query) - if len(query) < 2 and contains_jp: + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_jp: + elif len(query) < 4 and not contains_ja: return None, 'Your query must be at least 4 letters', None # we want to look up only the main part of the query, and then verify that each result has the prefixes @@ -1527,8 +1533,8 @@ def find_monster2(self, query): # if we don't have any candidates yet, pick a new method if not matches.length(): # try matching on exact names next - for nickname, m in self.all_na_name_to_monsters.items(): - if new_query in m.name_na.lower() or new_query in m.name_jp.lower(): + for nickname, m in self.all_en_name_to_monsters.items(): + if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): matches.add(m) matches.remove_potential_matches_without_all_prefixes(query_prefixes) @@ -1609,7 +1615,7 @@ def __init__(self, evolution_tree: list, basename_overrides: list): self.basenames = basename_overrides or self.computed_basenames def _compute_monster_basename(self, m: DgMonster): - basename = m.name_na.lower() + basename = m.name_en.lower() if ',' in basename: name_parts = basename.split(',') if name_parts[1].strip().startswith('the '): @@ -1655,12 +1661,12 @@ def _is_low_priority_monster(self, m: DgMonster): lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] lp_substrings = ['tamadra'] lp_min_rarity = 2 - name = m.name_na.lower() + name = m.name_en.lower() failed_type = m.type1 in lp_types failed_ss = any([x in name for x in lp_substrings]) failed_rarity = m.rarity < lp_min_rarity - failed_chibi = name == m.name_na and m.name_na != m.name_jp + failed_chibi = name == m.name_en and m.name_en != m.name_ja failed_equip = m.is_equip return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip @@ -1697,8 +1703,8 @@ def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixe self.rarity = monster.rarity # Used in fallback searches - self.name_na = monster.name_na - self.name_jp = monster.name_jp + self.name_en = monster.name_en + self.name_ja = monster.name_ja # These are just extra metadata self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] diff --git a/feedback/__init__.py b/feedback/__init__.py new file mode 100644 index 00000000..975a3c07 --- /dev/null +++ b/feedback/__init__.py @@ -0,0 +1,7 @@ +from .feedback import * + +__red_end_user_data_statement__ = "No personal data is stored." + + +def setup(bot): + bot.add_cog(Feedback(bot)) diff --git a/feedback/feedback.py b/feedback/feedback.py new file mode 100644 index 00000000..20c55fe3 --- /dev/null +++ b/feedback/feedback.py @@ -0,0 +1,155 @@ +import discord +from redbot.core import checks, commands, modlog +from redbot.core.utils.chat_formatting import box, inline, pagify +from tsutils import CogSettings + + +class Feedback(commands.Cog): + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.settings = FeedbackSettings("feedback") + + self.sessions = set() + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + + async def _send_feedback(self, ctx, message: str, feedback_channel, success_message: str): + if feedback_channel is None: + raise commands.UserFeedbackCheckFailure("Feedback channel not set") + + guild = ctx.guild + author = ctx.author + footer = "User ID: " + str(author.id) + + if guild: + source = "from {}".format(guild) + footer += " | Guild ID: " + str(guild.id) + else: + source = "through DM" + + description = "Sent by {} {}".format(author, source) + + e = discord.Embed(description=message) + if author.avatar_url: + e.set_author(name=description, icon_url=author.avatar_url) + else: + e.set_author(name=description) + e.set_footer(text=footer) + + try: + await feedback_channel.send(embed=e) + except: + await ctx.send(inline("I'm unable to deliver your message. Sorry.")) + else: + await ctx.send(inline("Your message has been sent." + " Abusing this feature will result in a blacklist." + + success_message)) + + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def credits(self, ctx): + """Shows info about this bot""" + author_repo = "https://github.com/Twentysix26" + red_repo = author_repo + "/Red-DiscordBot" + rpad_invite = "https://discord.gg/pad" + + about = ( + "This is an instance of [the Red Discord bot]({}), " + "use the '{}info' command for more info. " + "The various PAD related cogs were created by Aradia Megido and tactical_retreat. " + "Massive overhaul of the bot to Red v3 by Aradia Megido. " + "This bot was created for the [PAD Community Server Discord]({}) but " + "is available for other servers on request." + "".format(red_repo, ctx.prefix, rpad_invite)) + + avatar = ( + "Bot avatars supplied by:\n" + "\t[Tsubaki]({}): {}").format("https://twitter.com/_violebot", "Violebot") + + using = ( + "You can use `{0.prefix}help` to get a full list of commands.\n" + "Use `{0.prefix}userhelp` to get a summary of useful user features.\n" + "Use `{0.prefix}modhelp` to get info on moderator-only features." + ) + + embed = discord.Embed() + embed.add_field(name="Instance owned by", value='The Tsubaki Team') + embed.add_field(name="About the bot", value=about, inline=False) + embed.add_field(name="Using the bot", value=using.format(ctx), inline=False) + embed.add_field(name="Avatar credits", value=avatar, inline=False) + embed.set_thumbnail(url=self.bot.user.avatar_url) + + await ctx.send(embed=embed) + + @commands.command() + @commands.cooldown(1, 60, commands.BucketType.user) + async def feedback(self, ctx, *, message: str): + """Provide feedback on the bot. + + Use this command to provide public feedback on the bot. + """ + feedback_channel = self.bot.get_channel(int(self.settings.get_feedback_channel())) + await self._send_feedback(ctx, message, feedback_channel, + " Join the Tsubaki Server to see any responses ({0.prefix}tsubakiserver).".format( + ctx)) + + @commands.command() + @commands.guild_only() + @checks.is_owner() + async def setfeedbackchannel(self, ctx, channel: discord.TextChannel): + """Set the feedback destination channel.""" + self.settings.set_feedback_channel(channel.id) + await ctx.tick() + + @commands.command(aliases=['mamafeedback', 'renifeedback']) + @commands.cooldown(1, 60, commands.BucketType.user) + async def blogfeedback(self, ctx, *, message: str): + """Provide feedback on Reni's blog or translations. + + Use this command to submit feedback on https://pad.protic.site or the JP translations. + """ + feedback_channel = self.bot.get_channel(int(self.settings.get_blog_feedback_channel())) + await self._send_feedback(ctx, message, feedback_channel, + " Join the PDX Server to see any responses ({0.prefix}pdx).".format(ctx)) + + @commands.command() + @commands.guild_only() + @checks.is_owner() + async def setblogfeedbackchannel(self, ctx, channel: discord.TextChannel): + """Set the blog feedback destination channel.""" + self.settings.set_blog_feedback_channel(channel.id) + await ctx.tick() + + +class FeedbackSettings(CogSettings): + def make_default_settings(self): + config = { + 'feedback_channel': None, + 'blog_feedback_channel': None, + } + return config + + def get_feedback_channel(self): + return self.bot_settings.get('feedback_channel') + + def set_feedback_channel(self, channel_id: int): + self.bot_settings['feedback_channel'] = str(channel_id) + self.save_settings() + + def get_blog_feedback_channel(self): + return self.bot_settings.get('blog_feedback_channel') + + def set_blog_feedback_channel(self, channel_id: int): + self.bot_settings['blog_feedback_channel'] = str(channel_id) + self.save_settings() diff --git a/feedback/info.json b/feedback/info.json new file mode 100644 index 00000000..f34d128a --- /dev/null +++ b/feedback/info.json @@ -0,0 +1,26 @@ +{ + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Tsubaki feedback and credits", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "P&D Phonebook", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils" + ], + "tags": [ + "Tsubaki", + "feedback" + ] +} diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 76cff6df..ab20726e 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -130,7 +130,7 @@ 'God': [207], # devil 'Devil': [205], # god 'Machine': [205, 201], # god balanced - 'Dragon': [208, 320], # machine healer + 'Dragon': [208, 203], # machine healer 'Physical': [208, 203], # machine healer 'Attacker': [207, 202], # devil physical 'Healer': [204, 206], # dragon attacker diff --git a/padevents/padevents.py b/padevents/padevents.py index 9434e855..20cdd34f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -495,7 +495,7 @@ def __init__(self, scheduled_event: "DgScheduledEvent"): self.open_datetime = scheduled_event.open_datetime self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name - self.dungeon_name = scheduled_event.dungeon.name_na if scheduled_event.dungeon else 'unknown_dungeon' + self.dungeon_name = scheduled_event.dungeon.name_en if scheduled_event.dungeon else 'unknown_dungeon' self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8c86e73c..64e9e776 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -201,7 +201,7 @@ async def write_send(nn_map, file_name): data_holder = io.StringIO() writer = csv.writer(data_holder) for nn, nm in nn_map.items(): - writer.writerow([nn, nm.monster_no_na, nm.name_na]) + writer.writerow([nn, nm.monster_no_na, nm.name_en]) bytes_data = io.BytesIO(data_holder.getvalue().encode()) await ctx.channel.send(file=discord.File(bytes_data, file_name)) @@ -219,7 +219,7 @@ async def debugid(self, ctx, *, query): await ctx.send(box('No match: ' + err)) return - msg = "{}. {}".format(m.monster_no_na, m.name_na) + msg = "{}. {}".format(m.monster_no_na, m.name_en) msg += "\nLookup type: {}".format(debug_info) def list_or_none(l): @@ -261,11 +261,11 @@ def list_or_none(l): lambda nn: (nn, padinfo_cog.index_all.two_word_entries[nn]), replaced_twnn) replaced_nn_text = list(map(lambda nn_info: '{} : {}. {}'.format( - nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_na), + nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_en), replaced_nn_info)) replaced_twnn_text = list(map(lambda nn_info: '{} : {}. {}'.format( - nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_na), + nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_en), replaced_twnn_info)) msg += "\n nicknames: {}".format(list_or_none(replaced_nn_text)) @@ -462,62 +462,31 @@ async def send_cmdlist(self, ctx, cmdlist, inline=False): await ctx.send("There are no padglobal commands yet") return - prefix = ctx.prefix - prefixes = defaultdict(int) + prefixes = defaultdict(list) for c in cmdlist: - m = re.match(r'^([a-zA-Z]+)\d+$', c) + m = re.match(r'^([a-zA-Z]+)(\d+)$', c) if m: - grp = m.group(1) - prefixes[grp] = prefixes[grp] + 1 + prefixes[m.group(1)].append(m.group(2)) - good_prefixes = [cmd for cmd, cnt in prefixes.items() if cnt > 1] - prefix_to_suffix = defaultdict(list) - prefix_to_other = defaultdict(list) + prefix_to_suffix = {cmd: cnt for cmd, cnt in prefixes.items() if len(cnt) > 1} - i = 0 msg = PAD_CMD_HEADER.format(ctx.prefix) + "\n" - if inline: - for cmd in sorted([cmd for cmd in cmdlist.keys()]): - msg += " {} : {}\n".format(cmd, cmdlist[cmd]) - for page in pagify(msg): - await ctx.author.send(box(page)) - return + done_prefixes = [] - for cmd in sorted([cmd for cmd in cmdlist.keys()]): - m = re.match(r'^([a-zA-Z]+)(\d+)$', cmd) + for cmd in sorted(cmdlist): + m = re.match(r'^([a-zA-Z]+)\d+$', cmd) if m: prefix = m.group(1) - if prefix in good_prefixes: - suffix = m.group(2) - prefix_to_suffix[prefix].append(suffix) - continue - - should_skip = False - for good_prefix in good_prefixes: - if cmd.startswith(good_prefix): - prefix_to_other[prefix].append(cmd) - should_skip = True - break - if should_skip: - continue - - msg += " {}{}\n".format(ctx.prefix, cmd) - - if prefix_to_suffix: - msg += "\nThe following commands are indexed:\n" - for prefix in sorted(prefix_to_suffix.keys()): - msg += " {}{}[n]:\n ".format(ctx.prefix, prefix) - - for suffix in sorted(map(int, prefix_to_suffix[prefix])): - msg += " {}{}".format(prefix, suffix) - - if len(prefix_to_other[prefix]): - msg += "\n" - for cmd in sorted(prefix_to_other[prefix]): - msg += " {}{}".format(ctx.prefix, cmd) - msg += "\n\n" + if prefix in prefix_to_suffix and prefix not in done_prefixes: + msg += " {}{}[n]:\n ".format(ctx.prefix, prefix) + for suffix in sorted(map(int, prefix_to_suffix[prefix])): + msg += " {}{}".format(prefix, suffix) + msg += '\n' + done_prefixes.append(prefix) + else: + msg += " {}{}\n".format(ctx.prefix, cmd) for page in pagify(msg): await ctx.author.send(box(page)) @@ -826,7 +795,7 @@ async def addwhich(self, ctx, monster_id: int, *, definition): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id op = 'EDITED' if name in self.settings.which() else 'ADDED' @@ -839,9 +808,9 @@ async def rmwhich(self, ctx, *, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format( - m.name_na)): + m.name_en)): return name = m.monster_id @@ -888,7 +857,7 @@ async def debuglookup(self, ctx, *, term: str): term = term.lower().replace('?', '') nm, err, deb = await lookup_named_monster(term) base = nm.group_computed_basename.title() if nm else nm - name = nm.name_na if nm else nm + name = nm.name_en if nm else nm monster_id = nm.monster_id if nm else nm definition = self.settings.which().get(monster_id, None) await ctx.send('Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' @@ -1187,7 +1156,7 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id op = 'EDITED' if name in self.settings.leaderGuide() else 'ADDED' @@ -1200,10 +1169,10 @@ async def rmleaderguide(self, ctx, monster_id: int): m = monster_id_to_monster(monster_id) if m != m.base_monster: m = m.base_monster - await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_na)) + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format( - m.name_na)): + m.name_en)): return name = m.monster_id @@ -1236,7 +1205,7 @@ def check_simple_tree(monster): return False if m.is_equip: return False - if 'awoken' in m.name_na.lower(): + if 'awoken' in m.name_en.lower(): return False return True diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index ee9f84db..c1c8ecb2 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -101,8 +101,8 @@ async def searchdungeon(self, ctx, *, search_text): """Search""" search_text = '%{}%'.format(search_text) with self.get_cursor() as cursor: - sql = ('select dungeon_id, name_na, name_jp, visible from dungeons' - ' where lower(name_na) like %s or lower(name_jp) like %s' + sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' + ' where lower(name_en) like %s or lower(name_ja) like %s' ' order by dungeon_id desc limit 20') cursor.execute(sql, [search_text, search_text]) results = list(cursor.fetchall()) @@ -132,6 +132,13 @@ async def setimageupdatefile(self, ctx, *, image_update_file): self.settings.setImageUpdateFile(image_update_file) await ctx.tick() + @padguidedb.command() + @checks.is_owner() + async def setpythonexecutable(self, ctx, *, python_executable): + """Set the python executable file.""" + self.settings.setPythonExecutable(python_executable) + await ctx.tick() + @padguidedb.command() @checks.is_owner() async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): @@ -166,7 +173,7 @@ async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): async with self.dungeon_load_lock: process = await asyncio.create_subprocess_exec( - '/usr/bin/python3', + self.settings.pythonExecutable(), self.settings.dungeonScriptFile(), '--db_config={}'.format(self.settings.configFile()), '--server={}'.format(server), @@ -181,7 +188,7 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): stdout, stderr = await process.communicate() if stderr: - logger.error("Dungeon Load Error:\n" + stderr.decode()) + logger.error("Dungeon Load Error:\n{}\n\n{}".format(stdout.decode(), stderr.decode())) await tsutils.doubleup(ctx, inline( 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) else: @@ -326,6 +333,7 @@ def make_default_settings(self): 'dungeon_script_file': '', 'full_etl_file': '', 'update_image_file': '', + 'python_executable': '/usr/bin/python3', 'users': {}, } return config @@ -364,19 +372,26 @@ def setDungeonScriptFile(self, dungeon_script_file): self.save_settings() def fullETLFile(self): - return self.bot_settings.get('dungeon_script_file', '') + return self.bot_settings.get('full_etl_file', '') def setFullETLFile(self, full_etl_file): self.bot_settings['full_etl_file'] = full_etl_file self.save_settings() - def updateImageFile(self): - return self.bot_settings.get('dungeon_script_file', '') + def imageUpdateFile(self): + return self.bot_settings.get('update_image_file', '') - def setUpdateImageFile(self, update_image_file): + def setImageUpdateFile(self, update_image_file): self.bot_settings['update_image_file'] = update_image_file self.save_settings() + def pythonExecutable(self): + return self.bot_settings.get('python_executable', '/usr/bin/python3') + + def setPythonExecutable(self, python_executable): + self.bot_settings['python_executable'] = python_executable + self.save_settings() + def setUserInfo(self, server, user_uuid, user_intid): self.bot_settings['users'][server.upper()] = [user_uuid, user_intid] self.save_settings() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index be83bb5d..1d6fb6e9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -46,8 +46,8 @@ RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/{0:03d}cb.png' +ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' +ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' @@ -255,7 +255,7 @@ async def jpname(self, ctx, *, query: str): m, err, debug_info = await self.findMonster(query) if m is not None: await ctx.send(monsterToHeader(m)) - await ctx.send(box(m.name_jp)) + await ctx.send(box(m.name_ja)) else: await ctx.send(self.makeFailureMsg(err)) @@ -457,7 +457,7 @@ async def links(self, ctx, *, query: str): if m is not None: embed = monsterToBaseEmbed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), SKYOZORA_TEMPLATE.format(m.monster_no_jp), INFO_PDX_TEMPLATE.format(m.monster_no_jp), ILMINA_TEMPLATE.format(m.monster_no_jp)) @@ -524,23 +524,27 @@ async def collabscroll(self, ctx, *, query: str): @commands.command(aliases=['leaders', 'leaderskills', 'ls']) @checks.bot_has_permissions(embed_links=True) - async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, bad=None): + async def leaderskill(self, ctx, *, whole_query=''): """Display the multiplier and leaderskills for two monsters If either your left or right query contains spaces, wrap in quotes. e.g.: [p]leaderskill "r sonia" "b sonia" """ - if bad: - await ctx.send(inline('Too many inputs. Try wrapping your queries in quotes.')) - return - - # Handle a very specific failure case, user typing something like "uuvo ragdra" - if ' ' not in left_query and right_query is not None and ' ' not in right_query and bad is None: - combined_query = left_query + ' ' + right_query - nm, err, debug_info = await self._findMonster(combined_query) - if nm and left_query in nm.prefixes: - left_query = combined_query - right_query = None + # order of separators is '/' > '"' > ',' + pref_query, bad, check_prefix = [], [], False + for sep in ('/', '"', ',', ' '): + if sep in whole_query: + ind = whole_query.index(sep) + left_query, right_query = whole_query[:ind].strip(), whole_query[ind + 1:].strip() + if sep == ' ': + # Handle a very specific failure case, user typing something like "uuvo ragdra" + nm, err, debug_info = await self._findMonster(whole_query) + if not err and left_query in nm.prefixes: + left_query = whole_query + right_query = None + break + else: # no separators + left_query, right_query = whole_query, None left_m, left_err, _ = await self.findMonster(left_query) if right_query: @@ -548,7 +552,7 @@ async def leaderskill(self, ctx, left_query: str, right_query: str = None, *, ba else: right_m, right_err, = left_m, left_err - err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, wrap it in quotes.' + err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, try separating the queries with / or wrap with quotes.' if left_err: await ctx.send(inline(err_msg.format('Left', left_query))) return @@ -666,7 +670,7 @@ async def iddiff(self, ctx): [debug_info1, debug_info2] )) if m1 and m2: - await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_na, m2.name_na)) + await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_en, m2.name_en)) await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) file = discord.File(io.BytesIO(json.dumps(f).encode()), filename="diff.json") await ctx.send(file=file) @@ -781,11 +785,11 @@ def voiceDir(self): def monsterToHeader(m: "DgMonster", link=False): - msg = 'No. {} {}'.format(m.monster_no_na, m.name_na) + msg = 'No. {} {}'.format(m.monster_no_na, m.name_en) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg -def monsterToJpSuffix(m: "DgMonster"): +def monsterToJaSuffix(m: "DgMonster"): suffix = "" if m.roma_subname: suffix += ' [{}]'.format(m.roma_subname) @@ -795,7 +799,7 @@ def monsterToJpSuffix(m: "DgMonster"): def monsterToLongHeader(m: "DgMonster", link=False): - msg = monsterToHeader(m) + monsterToJpSuffix(m) + msg = monsterToHeader(m) + monsterToJaSuffix(m) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg @@ -1135,9 +1139,9 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): tbl.add_row([row_name.format(plus), hp, atk, rcv]) body_text += box(tbl.get_string()) - body_text += "\n**JP Name**: {}".format(m.name_jp) + body_text += "\n**JP Name**: {}".format(m.name_ja) body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_jp)), + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), SKYOZORA_TEMPLATE.format(m.monster_no_jp), INFO_PDX_TEMPLATE.format(m.monster_no_jp), ILMINA_TEMPLATE.format(m.monster_no_jp)) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 21e363d5..ff000706 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -60,10 +60,10 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: - msg += '\n\tNo. {} {}'.format(m.monster_no, m.name_na) - if tsutils.containsJp( - m.name_na) and m.name_na_override != m.name_na and m.name_na_override is not None: - msg += ' ({})'.format(m.name_na_override) + msg += '\n\tNo. {} {}'.format(m.monster_no, m.name_en) + if tsutils.containsJa( + m.name_en) and m.name_en_override != m.name_en and m.name_en_override is not None: + msg += ' ({})'.format(m.name_en_override) return msg else: return None @@ -83,7 +83,7 @@ async def announce(self, channel_id, message): for page in pagify(message): await channel.send(box(page)) except Exception as ex: - logger.exception('failed to send message to', channel_id, ' : ', ex) + logger.exception('failed to send message to {}:'.format(channel_id)) @commands.group() @checks.mod_or_permissions(manage_guild=True) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index bc7e2a2d..bd4ecb1d 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -16,7 +16,7 @@ To get more than 10 results, include the word 'all' in your search. Colors can be any of: - fire water wood light dark + fire water wood light dark null Additionally, orb colors can be any of: heart jammer poison mortal and bomb @@ -67,6 +67,7 @@ def filt_timeout(filts, ms): 'wood', 'light', 'dark', + 'null', ] TYPES = [ @@ -133,6 +134,7 @@ def split_csv_orbcolors(value): 'g': 'wood', 'l': 'light', 'd': 'dark', + 'x': 'null', 'heart': 'heal', 'h': 'heal', 'p': 'poison', @@ -790,7 +792,7 @@ async def search(self, ctx, *, filter_spec: str): matched_monsters = matched_monsters[0:200] for m in matched_monsters: - msg += '\n\tNo. {} {}'.format(m.monster_no_na, m.name_na) + msg += '\n\tNo. {} {}'.format(m.monster_no_na, m.name_en) if dm_required: header += '\nList too long to display; sent via DM' diff --git a/padvision/padvision.py b/padvision/padvision.py index d2138fcf..eb8429ad 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -122,7 +122,7 @@ class NeuralClassifierBoardExtractor(object): def __init__(self, model_path, np_img, img_bytes): self.model_path = model_path self.np_img = np_img - self.img = PIL.Image.open(io.BytesIO(img_bytes)) + self.img = PIL.Image.open(io.BytesIO(img_bytes)).convert('RGB') self.processed = False self.results = [['u' for x in range(6)] for y in range(5)] diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 23fcbd0d..43773e19 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -4,14 +4,17 @@ from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check -PC_TEXT = """{name} - Stamina Cost: {stam_cost} +DISCLAIMER = "**Disclaimer**: This is Lumon's data. Use at your own discretion." -{name} - Plus Point Value: {pp_val} -({points} 297 Plus Points) +PC_TEXT = """{name} + - Stamina Cost: {stam_cost} + - Plus Point Value: {pp_val} ({points} 𝟤𝟫𝟩 Plus Points) {foot} """ +def rint(x, p): + return str(round(x, p)).rstrip('0').rstrip('.') class PriceCheck(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -20,7 +23,9 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9213337337) self.config.register_global(pcs={}) self.config.register_channel(dm=False) - self.bot.get_cog("GlobalAdmin").register_perm("pcadmin") + GADMIN_COG = self.bot.get_cog("GlobalAdmin") + if GADMIN_COG: + GADMIN_COG.register_perm("pcadmin") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -49,20 +54,22 @@ async def pricecheck(self, ctx, *, query): return async with self.config.pcs() as pcs: if str(nm.base_monster_no) not in pcs: - await ctx.send("{} does not have PC data.".format(nm.name_na)) + await ctx.send("{} does not have PC data.".format(nm.name_en)) return sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name=nm.name_na, - stam_cost=sc, - pp_val=sc * 83 / 50, - points=sc * 83 / 50 / 297, + pct = PC_TEXT.format(name=nm.name_en, + stam_cost=rint(sc, 2), + pp_val=rint(sc * 83 / 50, 2), + points=rint(sc * 83 / 50 / 297, 2), foot=foot) if await self.config.channel(ctx.channel).dm(): + await ctx.send(DISCLAIMER) for page in pagify(pct): - await ctx.author.send(box(page)) + await ctx.author.send(box(page.replace("'","ʼ"), lang='py')) else: + await ctx.send(DISCLAIMER) for page in pagify(pct): - await ctx.send(box(page)) + await ctx.send(box(page.replace("'","ʼ"), lang='py')) @commands.group() @auth_check('pcadmin') @@ -87,7 +94,7 @@ async def set(self, ctx, stam_cost: float, *, query): if str(nm.base_monster_no) in pcs: foot = pcs[str(nm.base_monster_no)][1] pcs[str(nm.base_monster_no)] = (stam_cost, foot) - await ctx.send(box("Set {} ({}) to {}".format(nm.name_na, nm.base_monster_no, stam_cost))) + await ctx.send(box("Set {} ({}) to {}".format(nm.name_en, nm.base_monster_no, stam_cost))) @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) async def setfooter(self, ctx, query, *, footer=""): @@ -107,7 +114,7 @@ async def setfooter(self, ctx, query, *, footer=""): if str(nm.base_monster_no) in pcs: sc = pcs[str(nm.base_monster_no)][0] pcs[str(nm.base_monster_no)] = (sc, footer.strip('`')) - await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_na, nm.base_monster_no, footer))) + await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_en, nm.base_monster_no, footer))) @pcadmin.command(aliases=['delete', 'del', 'rm']) async def remove(self, ctx, *, query): @@ -124,10 +131,10 @@ async def remove(self, ctx, *, query): return async with self.config.pcs() as pcs: if str(nm.base_monster_no) not in pcs: - await ctx.send("{} does not have PC data.".format(nm.name_na)) + await ctx.send("{} does not have PC data.".format(nm.name_en)) return del pcs[str(nm.base_monster_no)] - await ctx.send(inline("Removed PC data from {}.".format(nm.name_na))) + await ctx.send(inline("Removed PC data from {}.".format(nm.name_en))) @pcadmin.command(aliases=['set-demon-ly']) async def setdmonly(self, ctx, value: bool = True): diff --git a/profile/profile.py b/profile/profile.py index d37d8dd2..76c7ea2f 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -121,7 +121,7 @@ async def getIdMsg(self, ctx, user, server=None, for_other=True): if server is None: return - pad_id = self.settings.getId(user.id, server) + pad_id = str(self.settings.getId(user.id, server)) pad_name = self.settings.getName(user.id, server) profile_text = self.settings.getProfileText(user.id, server) @@ -299,11 +299,11 @@ def getProfile(self, user, server): return profile[server] def setId(self, user, server, id): - self.getProfile(user, server)['id'] = id + self.getProfile(user, server)['id'] = int(id) self.save_settings() def getId(self, user, server): - return self.getProfile(user, server).get('id', '000000000') + return self.getProfile(user, server).get('id', 000000000) def setName(self, user, server, name): self.getProfile(user, server)['name'] = name From a497dfd544cc5661e8a7619f8c2b5fe699a85744 Mon Sep 17 00:00:00 2001 From: muffin-rice Date: Sun, 4 Oct 2020 13:35:46 -0700 Subject: [PATCH 0142/1220] fixed quotes --- padinfo/padinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1d6fb6e9..b8c726af 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -534,8 +534,8 @@ async def leaderskill(self, ctx, *, whole_query=''): pref_query, bad, check_prefix = [], [], False for sep in ('/', '"', ',', ' '): if sep in whole_query: - ind = whole_query.index(sep) - left_query, right_query = whole_query[:ind].strip(), whole_query[ind + 1:].strip() + left_query, *right_query = [x for x in whole_query.split(sep) if x.strip()] + right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" nm, err, debug_info = await self._findMonster(whole_query) From 60cddcd4936c392d064fa21755d796cddb0f441c Mon Sep 17 00:00:00 2001 From: muffin-rice Date: Sun, 4 Oct 2020 13:42:00 -0700 Subject: [PATCH 0143/1220] removed default string --- padinfo/padinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b8c726af..bc905d15 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -524,7 +524,7 @@ async def collabscroll(self, ctx, *, query: str): @commands.command(aliases=['leaders', 'leaderskills', 'ls']) @checks.bot_has_permissions(embed_links=True) - async def leaderskill(self, ctx, *, whole_query=''): + async def leaderskill(self, ctx, *, whole_query): """Display the multiplier and leaderskills for two monsters If either your left or right query contains spaces, wrap in quotes. @@ -535,7 +535,7 @@ async def leaderskill(self, ctx, *, whole_query=''): for sep in ('/', '"', ',', ' '): if sep in whole_query: left_query, *right_query = [x for x in whole_query.split(sep) if x.strip()] - right_query = ' '.join(q for q in right_query) + right_query = ' '.join(q for q in right_query) # might break in some cases, but most of the time it's ok if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" nm, err, debug_info = await self._findMonster(whole_query) From e57b7cbc768a8245703e3899bca7251806dd11bf Mon Sep 17 00:00:00 2001 From: muffin-rice Date: Sun, 4 Oct 2020 14:09:23 -0700 Subject: [PATCH 0144/1220] comments and empty list protection --- padinfo/padinfo.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bc905d15..c7158dad 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -531,18 +531,22 @@ async def leaderskill(self, ctx, *, whole_query): e.g.: [p]leaderskill "r sonia" "b sonia" """ # order of separators is '/' > '"' > ',' - pref_query, bad, check_prefix = [], [], False for sep in ('/', '"', ',', ' '): if sep in whole_query: - left_query, *right_query = [x for x in whole_query.split(sep) if x.strip()] - right_query = ' '.join(q for q in right_query) # might break in some cases, but most of the time it's ok + + left_query, *right_query = [x.strip() for x in whole_query.split(sep) if x.strip()] or ('', '') # or in case of ^ls [sep] which is empty list + # split on first separator, with if x.strip() block to prevent null values from showing up, mainly for quotes support + # right query is the rest of query but in list form because of how .strip() works. bring it back to string form with ' '.join + right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" nm, err, debug_info = await self._findMonster(whole_query) if not err and left_query in nm.prefixes: left_query = whole_query right_query = None + break + else: # no separators left_query, right_query = whole_query, None From 3139de8e623dd3fe4b2d9168b53935765228e382 Mon Sep 17 00:00:00 2001 From: muffin-rice Date: Sun, 4 Oct 2020 16:55:05 -0700 Subject: [PATCH 0145/1220] changed docstring and usage --- padinfo/padinfo.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c7158dad..b2c8b9fc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -522,15 +522,16 @@ async def collabscroll(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command(aliases=['leaders', 'leaderskills', 'ls']) + @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [sep] ") @checks.bot_has_permissions(embed_links=True) async def leaderskill(self, ctx, *, whole_query): """Display the multiplier and leaderskills for two monsters - If either your left or right query contains spaces, wrap in quotes. - e.g.: [p]leaderskill "r sonia" "b sonia" + Gets two monsters separated by a slash, wrapping quotes, a comma, + or spaces (if there's only two words). + e.g. ^ls r sonia / revo lu bu, ^ls r sonia "revo lu bu", ^ls sonia lubu """ - # order of separators is '/' > '"' > ',' + # deliberate order in case of multiple different separators. for sep in ('/', '"', ',', ' '): if sep in whole_query: From cfd7a657dbf3b78b5fbde8e18f3784decdf817c4 Mon Sep 17 00:00:00 2001 From: muffin-rice Date: Mon, 5 Oct 2020 10:19:04 -0700 Subject: [PATCH 0146/1220] changed usage string and docstring --- padinfo/padinfo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b2c8b9fc..1e12407f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -522,14 +522,16 @@ async def collabscroll(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [sep] ") + @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) async def leaderskill(self, ctx, *, whole_query): """Display the multiplier and leaderskills for two monsters Gets two monsters separated by a slash, wrapping quotes, a comma, or spaces (if there's only two words). - e.g. ^ls r sonia / revo lu bu, ^ls r sonia "revo lu bu", ^ls sonia lubu + [p]ls r sonia/ revo lu bu + [p]ls r sonia "revo lu bu" + [p]ls sonia lubu """ # deliberate order in case of multiple different separators. for sep in ('/', '"', ',', ' '): From ca68f40ecf590b8aab3cc96924e5c8b42d75e3de Mon Sep 17 00:00:00 2001 From: muffin-rice Date: Mon, 5 Oct 2020 12:49:49 -0700 Subject: [PATCH 0147/1220] changed ordering of separators --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1e12407f..76160cd8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -534,7 +534,7 @@ async def leaderskill(self, ctx, *, whole_query): [p]ls sonia lubu """ # deliberate order in case of multiple different separators. - for sep in ('/', '"', ',', ' '): + for sep in ('"', '/', ',', ' '): if sep in whole_query: left_query, *right_query = [x.strip() for x in whole_query.split(sep) if x.strip()] or ('', '') # or in case of ^ls [sep] which is empty list From 7ef12f932ca2ad5fb7161c017d3f7f255c8639a5 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 6 Oct 2020 14:30:24 -0700 Subject: [PATCH 0148/1220] Different Language for Pricecheck Boxes --- pricecheck/pricecheck.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 43773e19..66184039 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -65,11 +65,11 @@ async def pricecheck(self, ctx, *, query): if await self.config.channel(ctx.channel).dm(): await ctx.send(DISCLAIMER) for page in pagify(pct): - await ctx.author.send(box(page.replace("'","ʼ"), lang='py')) + await ctx.author.send(box(page.replace("'","ʼ"), lang='q')) else: await ctx.send(DISCLAIMER) for page in pagify(pct): - await ctx.send(box(page.replace("'","ʼ"), lang='py')) + await ctx.send(box(page.replace("'","ʼ"), lang='q')) @commands.group() @auth_check('pcadmin') From 786f8df325ab9a5fd03bd6c15ead07e66973c25d Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 7 Oct 2020 20:45:12 -0700 Subject: [PATCH 0149/1220] first file split --- dadguide/dadguide.py | 1507 +-------------------------------- dadguide/database_manager.py | 1511 ++++++++++++++++++++++++++++++++++ 2 files changed, 1519 insertions(+), 1499 deletions(-) create mode 100644 dadguide/database_manager.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 99b9e0a7..e879ba4d 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -9,24 +9,17 @@ import asyncio import csv import difflib -import discord import json import logging import os -import pytz -import re -import romkan import shutil -import sqlite3 as lite -import traceback import tsutils -from collections import OrderedDict, defaultdict, deque -from datetime import datetime -from enum import Enum +from collections import defaultdict from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.utils import AsyncIter -from redbot.core.utils.chat_formatting import inline + +from .database_manager import * + logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -51,7 +44,6 @@ def _data_file(file_name: str) -> str: DB_DUMP_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/db/dadguide.sqlite' DB_DUMP_FILE = _data_file('dadguide.sqlite') -DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') class Dadguide(commands.Cog): @@ -163,8 +155,10 @@ async def download_and_refresh_nicknames(self): basename_overrides = self._csv_to_tuples(BASENAME_FILE_PATTERN) panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) - self.nickname_overrides = {x[0].lower(): int(x[1]) - for x in nickname_overrides if x[1].isdigit()} + self.nickname_overrides = defaultdict(set) + for nick, id in nickname_overrides: + if id.isdigit(): + self.nickname_overrides[int(id)].add(nick.lower()) self.basename_overrides = defaultdict(set) for x in basename_overrides: @@ -262,1488 +256,3 @@ def data_file(self): def set_data_file(self, data_file): self.bot_settings['data_file'] = data_file self.save_settings() - - -class Attribute(Enum): - """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" - Fire = 0 - Water = 1 - Wood = 2 - Light = 3 - Dark = 4 - Unknown = 5 - Nil = 6 - - -class MonsterType(Enum): - Evolve = 0 - Balance = 1 - Physical = 2 - Healer = 3 - Dragon = 4 - God = 5 - Attacker = 6 - Devil = 7 - Machine = 8 - Awoken = 12 - Enhance = 14 - Vendor = 15 - - -class EvoType(Enum): - """Evo types supported by DadGuide. Numbers correspond to their id values.""" - Base = 0 # Represents monsters who didn't require evo - Evo = 1 - UvoAwoken = 2 - UuvoReincarnated = 3 - - -class InternalEvoType(Enum): - """Evo types unsupported by DadGuide.""" - Base = "" - Normal = "" - Ultimate = "" - Reincarnated = "Reincarnated" - Assist = "Assist" - Pixel = "Pixel" - SuperReincarnated = "Super Reincarnated" - - -class Server(Enum): - JP = 0 - NA = 1 - KR = 2 - - -class DadguideTableNotFound(Exception): - def __init__(self, table_name): - self.message = '{} not found'.format(table_name) - - -def load_database(existing_db): - # Release the handle to the database file if it has one - if existing_db: - existing_db.close() - # Overwrite the working copy so we can open a handle to it without affecting future downloads - if os.path.exists(DB_DUMP_FILE): - shutil.copy2(DB_DUMP_FILE, DB_DUMP_WORKING_FILE) - # Open the new working copy. - return DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) - - -class DadguideDatabase(object): - def __init__(self, data_file): - self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) - self._con.row_factory = lite.Row - - def has_database(self): - return self._con is not None - - def close(self): - self._con.close() - self._con = None - - @staticmethod - def _select_builder(tables, key=None, where=None, order=None, distinct=False): - if distinct: - SELECT_FROM = 'SELECT DISTINCT {fields} FROM {first_table}' - else: - SELECT_FROM = 'SELECT {fields} FROM {first_table}' - WHERE = 'WHERE {condition}' - JOIN = 'LEFT JOIN {other_table} ON {first_table}.{key}={other_table}.{key}' - ORDER = 'ORDER BY {order}' - first_table = None - fields_lst = [] - other_tables = [] - for table, fields in tables.items(): - if fields is not None: - fields_lst.extend(['{}.{}'.format(table, f) for f in fields]) - if first_table is None: - first_table = table - if key is None: - break - else: - other_tables.append(table) - query = [SELECT_FROM.format(first_table=first_table, fields=', '.join(fields_lst))] - prev_table = first_table - if key: - for k, other in zip(key, other_tables): - query.append(JOIN.format(first_table=prev_table, other_table=other, key=k)) - prev_table = other - if where: - query.append(WHERE.format(condition=where)) - if order: - query.append(ORDER.format(order=order)) - return ' '.join(query) - - def _query_one(self, query, param, d_type): - cursor = self._con.cursor() - cursor.execute(query, param) - res = cursor.fetchone() - if res is not None: - if issubclass(d_type, DadguideItem): - return d_type(res, self) - else: - return d_type(res) - return None - - def _as_generator(self, cursor, d_type): - res = cursor.fetchone() - if issubclass(d_type, DadguideItem): - while res is not None: - yield d_type(res, self) - res = cursor.fetchone() - else: - while res is not None: - yield d_type(res) - res = cursor.fetchone() - - def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): - cursor = self._con.cursor() - cursor.execute(query, param) - if cursor.rowcount == 0: - return [] - if as_generator: - return self._as_generator(cursor, d_type) - else: - if idx_key is None: - return [d_type(res, self) for res in cursor.fetchall()] - else: - return DictWithAttrAccess({res[idx_key]: d_type(res, self) for res in cursor.fetchall()}) - - def _max_id(self): - cursor = self._con.cursor() - cursor.execute("SELECT MAX(monster_id) FROM monsters WHERE monster_id < 10000") - return cursor.fetchone()['MAX(monster_id)'] - - def _select_one_entry_by_pk(self, pk, d_type): - return self._query_one( - self._select_builder( - tables={d_type.TABLE: d_type.FIELDS}, - where='{}.{}=?'.format(d_type.TABLE, d_type.PK)), - (pk,), - d_type) - - def _get_table_fields(self, table_name: str): - # SQL inject vulnerable :v - table_info = self._query_many('PRAGMA table_info(' + table_name + ')', (), dict) - pk = None - fields = [] - for c in table_info: - if c['pk'] == 1: - pk = c['name'] - fields.append(c['name']) - if len(fields) == 0: - raise DadguideTableNotFound(table_name) - return fields, pk - - def get_active_skill(self, active_skill_id: int): - return self._select_one_entry_by_pk(active_skill_id, DgActiveSkill) - - def get_leader_skill(self, leader_skill_id: int): - return self._select_one_entry_by_pk(leader_skill_id, DgLeaderSkill) - - def get_awoken_skill(self, awoken_skill_id): - return self._select_one_entry_by_pk(awoken_skill_id, DgAwokenSkill) - - def get_awoken_skill_ids(self): - SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' - return [r.awoken_skill_id for r in - self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] - - def get_monsters_by_awakenings(self, awoken_skill_id: int): - return self._query_many( - self._select_builder( - tables=OrderedDict({ - DgMonster.TABLE: DgMonster.FIELDS, - DgAwakening.TABLE: None, - }), - where='{}.awoken_skill_id=?;'.format(DgAwakening.TABLE), - key=('monster_id',), - distinct=True - ), - (awoken_skill_id,), - DgMonster) - - def get_awakenings_by_monster(self, monster_id, is_super=None): - if is_super is None: - where = '{0}.monster_id=?'.format(DgAwakening.TABLE) - param = (monster_id,) - else: - where = '{0}.monster_id=? AND {0}.is_super=?'.format(DgAwakening.TABLE) - param = (monster_id, is_super) - return self._query_many( - self._select_builder( - tables={DgAwakening.TABLE: DgAwakening.FIELDS}, - where=where, - order='order_idx ASC' - ), - param, - DgAwakening) - - def get_drop_dungeons(self, monster_id): - return self._query_many( - self._select_builder( - tables=OrderedDict({ - DgDungeon.TABLE: DgDungeon.FIELDS, - DgEncounter.TABLE: None, - DgDrop.TABLE: None, - }), - where='{0}.monster_id=?'.format(DgDrop.TABLE), - key=(DgDungeon.PK, DgEncounter.PK) - ), - (monster_id,), - DgDungeon) - - def monster_is_farmable(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgDrop.TABLE: DgDrop.FIELDS}, - where='{0}.monster_id=?'.format(DgDrop.TABLE) - ), - (monster_id,), - DgDrop) is not None - - def monster_in_rem(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('rem_egg',)}, - where='{0}.monster_id=? AND rem_egg=1'.format(DgMonster.TABLE) - ), - (monster_id,), - DgDrop) is not None - - def monster_in_pem(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('pal_egg',)}, - where='{0}.monster_id=? AND pal_egg=1'.format(DgMonster.TABLE) - ), - (monster_id,), - DgDrop) is not None - - def monster_in_mp_shop(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('buy_mp',)}, - where='{0}.monster_id=? AND buy_mp IS NOT NULL'.format(DgMonster.TABLE) - ), - (monster_id,), - DgDrop) is not None - - def get_prev_evolution_by_monster(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where='{}.to_id=?'.format(DgEvolution.TABLE) - ), - (monster_id,), - DgEvolution) - - def get_next_evolutions_by_monster(self, monster_id): - return self._query_many( - self._select_builder( - tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where='{}.from_id=?'.format(DgEvolution.TABLE) - ), - (monster_id,), - DgEvolution, - as_generator=True) - - def get_base_monster_by_monster(self, monster_id): - base = {'from_id': monster_id, 'to_id': monster_id} - lastbase = None - while base != None: - lastbase = base - base = self.get_prev_evolution_by_monster(base['from_id']) - return lastbase - - def get_all_evolutions_by_monster(self, monster_id): - base = self.get_base_monster_by_monster(monster_id) - queue = [base] - - while queue: - curr = queue.pop(0) - yield curr - queue.extend(self.get_next_evolutions_by_monster(curr['to_id'])) - - def get_evolution_by_material(self, monster_id): - return self._query_many( - self._select_builder( - tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where=' OR '.join(['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in range(1, 6)]) - ), - (monster_id,) * 5, - DgEvolution) - - def get_base_monster_ids(self): - SELECT_BASE_MONSTER_ID = ''' - SELECT evolutions.from_id as monster_id FROM evolutions WHERE evolutions.from_id NOT IN (SELECT DISTINCT evolutions.to_id FROM evolutions) - UNION - SELECT monsters.monster_id FROM monsters WHERE monsters.monster_id NOT IN (SELECT evolutions.from_id FROM evolutions UNION SELECT evolutions.to_id FROM evolutions)''' - return self._query_many( - SELECT_BASE_MONSTER_ID, - (), - DictWithAttrAccess, - as_generator=True) - - def get_evolution_tree_ids(self, base_monster_id): - # is not a tree i lied - base_id = base_monster_id - evolution_tree = [base_id] - n_evos = deque() - n_evos.append(base_id) - while len(n_evos) > 0: - n_evo_id = n_evos.popleft() - for e in self.get_next_evolutions_by_monster(n_evo_id): - n_evos.append(e.to_id) - evolution_tree.append(e.to_id) - return evolution_tree - - def monster_id_to_no(self, monster_id, region=Server.JP): - res = self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('monster_no_{}'.format(region.name.lower()),)}, - where='{}.monster_id=?'.format(DgMonster.TABLE) - ), - (monster_id,), - DictWithAttrAccess) - for k in res: - return res[k] - - def get_series(self, series_id: int): - return self._select_one_entry_by_pk(series_id, DgSeries) - - def _get_monsters_where(self, where, param): - return self._query_many( - self._select_builder( - tables={DgMonster.TABLE: DgMonster.FIELDS}, - where=where - ), - param, - DgMonster) - - def get_monsters_by_series(self, series_id: int): - return self._get_monsters_where('{}.series_id=?'.format(DgMonster.TABLE), (series_id,)) - - def get_monsters_by_active(self, active_skill_id: int): - return self._get_monsters_where('{}.active_skill_id=?'.format(DgMonster.TABLE), (active_skill_id,)) - - def get_monster_evo_gem(self, name: str, region='jp'): - gem_suffix = { - 'ja': 'の希石', - 'en': '\'s Gem', - 'ko': ' 의 휘석' - } - if region not in gem_suffix: - return None - non_gem_name = name.replace(gem_suffix[region], '') - if non_gem_name == name: - return None - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: DgMonster.FIELDS}, - where='{0}.name_{1}=? AND {0}.leader_skill_id=10628'.format(DgMonster.TABLE, region) - ), - (non_gem_name,), - DgMonster) - - def get_na_only_monsters(self): - return self._get_monsters_where( - '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format(DgMonster.TABLE), - ()) - - def get_monster(self, monster_id: int): - return self._select_one_entry_by_pk(monster_id, DgMonster) - - def get_all_monster_ja_name(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, - as_generator=as_generator) - - def get_all_monsters(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, - as_generator=as_generator) - - def get_all_events(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), - DgScheduledEvent, - as_generator=as_generator) - - def get_dungeon_by_id(self, dungeon_id: int): - return self._select_one_entry_by_pk(dungeon_id, DgDungeon) - - -def enum_or_none(enum, value, default=None): - if value is not None: - return enum(value) - else: - return default - - -class DictWithAttrAccess(dict): - def __init__(self, item): - super(DictWithAttrAccess, self).__init__(item) - self.__dict__ = self - - -class DadguideItem(DictWithAttrAccess): - """ - Base class for all items loaded from DadGuide. - Is a dict with attr access. - """ - TABLE = None - FIELDS = '*' - PK = None - AS_BOOL = () - - def __init__(self, item, database): - super(DadguideItem, self).__init__(item) - self._database = database - for k in self.AS_BOOL: - self[k] = bool(self[k]) - - def key(self): - return self[self.PK] - - -class DgActiveSkill(DadguideItem): - TABLE = 'active_skills' - PK = 'active_skill_id' - - @property - def monsters(self): - return self._database.get_monsters_by_active(self.active_skill_id) - - @property - def skillups(self): - return list(filter(lambda x: x.farmable, self.monsters)) - - @property - def desc(self): - return self.desc_en or self.desc_ja - - @property - def name(self): - return self.name_en or self.name_ja - - -class DgLeaderSkill(DadguideItem): - TABLE = 'leader_skills' - PK = 'leader_skill_id' - - @property - def data(self): - return self.max_hp, self.max_atk, self.max_rcv, self.max_shield - - @property - def desc(self): - return self.desc_en or self.desc_ja - - @property - def name(self): - return self.name_en or self.name_ja - - -class DgAwakening(DadguideItem): - TABLE = 'awakenings' - PK = 'awakening_id' - AS_BOOL = ['is_super'] - - def __init__(self, item, database): - super(DgAwakening, self).__init__(item, database) - - @property - def skill(self): - return self._database.get_awoken_skill(self.awoken_skill_id) - - @property - def name(self): - return self.skill.name_en if self.skill.name_en is not None else self.skill.name_ja - - -class DgAwokenSkill(DadguideItem): - TABLE = 'awoken_skills' - PK = 'awoken_skill_id' - - @property - def monsters_with_awakening(self): - return self._database.get_monsters_by_awakenings(self.awoken_skill_id) - - -class DgEvolution(DadguideItem): - TABLE = 'evolutions' - PK = 'evolution_id' - - def __init__(self, item, database): - super(DgEvolution, self).__init__(item, database) - self.evolution_type = EvoType(self.evolution_type) - - -class DgSeries(DadguideItem): - TABLE = 'series' - PK = 'series_id' - - @property - def monsters(self): - return self._database.get_monsters_by_series(self.series_id) - - @property - def name(self): - return self.name_en if self.name_en is not None else self.name_ja - - -class DgDungeon(DadguideItem): - TABLE = 'dungeons' - PK = 'dungeon_id' - - -class DgEncounter(DadguideItem): - TABLE = 'encounters' - PK = 'encounter_id' - - -class DgDrop(DadguideItem): - TABLE = 'drops' - PK = 'drop_id' - - -class DgScheduledEvent(DadguideItem): - TABLE = 'schedule' - PK = 'event_id' - - def __init__(self, item, database): - super(DgScheduledEvent, self).__init__(item, database) - self.dungeon = self._database.get_dungeon_by_id(self.dungeon_id) - - @property - def open_datetime(self): - return datetime.utcfromtimestamp(self.start_timestamp).replace(tzinfo=pytz.UTC) - - @open_datetime.setter - def open_datetime(self, value): - self.start_timestamp = int(value.timestamp()) - - @property - def close_datetime(self): - return datetime.utcfromtimestamp(self.end_timestamp).replace(tzinfo=pytz.UTC) - - @close_datetime.setter - def close_datetime(self, value): - self.end_timestamp = int(value.timestamp()) - - -class DgMonster(DadguideItem): - TABLE = 'monsters' - PK = 'monster_id' - AS_BOOL = ('on_jp', 'on_na', 'on_kr', 'has_animation', 'has_hqimage') - - def __init__(self, item, database): - super(DgMonster, self).__init__(item, database) - - self.roma_subname = None - if self.name_en == self.name_ja: - self.roma_subname = make_roma_subname(self.name_ja) - else: - # Remove annoying stuff from NA names, like Jörmungandr - self.name_en = tsutils.rmdiacritics(self.name_en) - - self.name_en = self.name_en_override or self.name_en - - self.attr1 = enum_or_none(Attribute, self.attribute_1_id) - self.attr2 = enum_or_none(Attribute, self.attribute_2_id) - - self.type1 = enum_or_none(MonsterType, self.type_1_id) - self.type2 = enum_or_none(MonsterType, self.type_2_id) - self.type3 = enum_or_none(MonsterType, self.type_3_id) - self.types = list(filter(None, [self.type1, self.type2, self.type3])) - - self.in_pem = bool(self.pal_egg) - self.in_rem = bool(self.rem_egg) - - self.awakenings = self._database.get_awakenings_by_monster(self.monster_id) - self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) - - self.is_inheritable = bool(self.inheritable) - - self.evo_from = self._database.get_prev_evolution_by_monster(self.monster_id) - - self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) - - base_id = self.monster_id - next_base = self._database.get_prev_evolution_by_monster(base_id) - while next_base is not None: - base_id = next_base.from_id - next_base = self._database.get_prev_evolution_by_monster(base_id) - self._base_monster_id = base_id - self._alt_evo_id_list = self._database.get_evolution_tree_ids(self._base_monster_id) - - self.search = MonsterSearchHelper(self) - - @property - def monster_no(self): - return self.monster_id - - def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): - s_min = float(self[key + '_min']) - s_max = float(self[key + '_max']) - if self.level > 1: - s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** self[key + '_scale'] - else: - s_val = s_min - if lv > 99: - s_val *= 1 + (self.limit_mult / 11 * (lv - 99)) / 100 - plus_dict = {'hp': 10, 'atk': 5, 'rcv': 3} - s_val += plus_dict[key] * max(min(plus, 99), 0) - if inherit: - inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} - if not is_plus_297: - s_val -= plus_dict[key] * max(min(plus, 99), 0) - s_val *= inherit_dict[key] - return int(round(s_val)) - - def stats(self, lv=99, plus=0, inherit=False): - is_plus_297 = False - if plus == 297: - plus = (99, 99, 99) - is_plus_297 = True - elif plus == 0: - plus = (0, 0, 0) - hp = self.stat('hp', lv, plus[0], inherit, is_plus_297) - atk = self.stat('atk', lv, plus[1], inherit, is_plus_297) - rcv = self.stat('rcv', lv, plus[2], inherit, is_plus_297) - weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) - return hp, atk, rcv, weighted - - @property - def active_skill(self): - return self._database.get_active_skill(self.active_skill_id) - - @property - def leader_skill(self): - return self._database.get_leader_skill(self.leader_skill_id) - - @property - def cur_evo_type(self): - return self.evo_from.evolution_type if self.evo_from else EvoType.Base - - @property - def true_evo_type(self): - if self == self.base_monster: - return InternalEvoType.Base - elif 5077 in [dgi.monster_id for dgi in self.mats_for_evo]: - return InternalEvoType.SuperReincarnated - elif 3826 in [dgi.monster_id for dgi in self.mats_for_evo]: - return InternalEvoType.Pixel - elif self.is_equip: - return InternalEvoType.Assist - elif self.cur_evo_type == EvoType.UuvoReincarnated: - return InternalEvoType.Reincarnated - elif self.cur_evo_type == EvoType.UvoAwoken: - return InternalEvoType.Ultimate - else: - return InternalEvoType.Normal - - @property - def mats_for_evo(self): - if self.evo_from is None: - return [] - return [self._database.get_monster(self.evo_from['mat_{}_id'.format(i)]) for i in range(1, 6) if - self.evo_from['mat_{}_id'.format(i)] is not None] - - @property - def evo_gem(self): - return self._database.get_monster_evo_gem(self.name_ja) - - @property - def material_of(self): - mat_of = self._database.get_evolution_by_material(self.monster_id) - return [self._database.get_monster(x.to_id) for x in mat_of] - - def _evolutions_to(self): - return self._database.get_next_evolutions_by_monster(self.monster_id) - - @property - def evo_to(self): - return [self._database.get_monster(x.to_id) for x in self._evolutions_to()] - - @property - def base_monster(self): - return self._database.get_monster(self._base_monster_id) - - @property - def alt_evos(self): - return [self._database.get_monster(a) for a in self._alt_evo_id_list] - - @property - def is_base_monster(self): - return self.evo_from is None - - @property - def series(self): - return self._database.get_series(self.series_id) - - @property - def is_gfe(self): - return self.series_id == 34 - - @property - def drop_dungeons(self): - return self._database.get_drop_dungeons(self.monster_id) - - @property - def farmable(self): - return self._database.monster_is_farmable(self.monster_id) - - @property - def farmable_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_is_farmable(e_id): - return True - return False - - @property - def rem_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_in_rem(e_id): - return True - return False - - @property - def pem_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_in_pem(e_id): - return True - return False - - @property - def killers(self): - type_to_killers_map = { - MonsterType.God: ['Devil'], - MonsterType.Devil: ['God'], - MonsterType.Machine: ['God', 'Balance'], - MonsterType.Dragon: ['Machine', 'Healer'], - MonsterType.Physical: ['Machine', 'Healer'], - MonsterType.Attacker: ['Devil', 'Physical'], - MonsterType.Healer: ['Dragon', 'Attacker'], - } - if MonsterType.Balance in self.types: - return ['Any'] - killers = set() - for t in self.types: - killers.update(type_to_killers_map.get(t, [])) - return sorted(killers) - - @property - def in_mpshop(self): - return self.buy_mp is not None - - @property - def mp_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_in_mp_shop(e_id): - return True - return False - - @property - def history_us(self): - return '[{}] New Added'.format(self.reg_date) - - @property - def next_monster(self): - next = None - offset = 1 - while next is None and self.monster_no + offset <= self._database._max_id(): - next = self._database.get_monster(self.monster_no + offset) - offset += 1 - return next - - @property - def prev_monster(self): - next = None - offset = 1 - while next is None and self.monster_no - offset >= 1: - next = self._database.get_monster(self.monster_no - offset) - offset += 1 - return next - - -class MonsterSearchHelper(object): - def __init__(self, m: DgMonster): - - self.name = '{} {}'.format(m.name_en, m.name_ja).lower() - leader_skill = m.leader_skill - self.leader = leader_skill.desc.lower() if leader_skill else '' - active_skill = m.active_skill - self.active_name = active_skill.name.lower() if active_skill else '' - self.active_desc = active_skill.desc.lower() if active_skill else '' - self.active = '{} {}'.format(self.active_name, self.active_desc) - self.active_min = active_skill.turn_min if active_skill else None - self.active_max = active_skill.turn_max if active_skill else None - - self.color = [m.attr1.name.lower()] - self.hascolor = [c.name.lower() for c in [m.attr1, m.attr2] if c] - - self.hp, self.atk, self.rcv, self.weighted_stats = m.stats(lv=110) - - self.types = [t.name for t in m.types] - - def replace_colors(text: str): - return text.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') - - self.leader = replace_colors(self.leader) - self.active = replace_colors(self.active) - self.active_name = replace_colors(self.active_name) - self.active_desc = replace_colors(self.active_desc) - - self.board_change = [] - self.orb_convert = defaultdict(list) - self.row_convert = [] - self.column_convert = [] - - def color_txt_to_list(txt): - txt = txt.replace('and', ' ') - txt = txt.replace(',', ' ') - txt = txt.replace('orbs', ' ') - txt = txt.replace('orb', ' ') - txt = txt.replace('mortal poison', 'mortalpoison') - txt = txt.replace('jammers', 'jammer') - txt = txt.strip() - return txt.split() - - def strip_prev_clause(txt: str, sep: str): - prev_clause_start_idx = txt.find(sep) - if prev_clause_start_idx >= 0: - prev_clause_start_idx += len(sep) - txt = txt[prev_clause_start_idx:] - return txt - - def strip_next_clause(txt: str, sep: str): - next_clause_start_idx = txt.find(sep) - if next_clause_start_idx >= 0: - txt = txt[:next_clause_start_idx] - return txt - - active_desc = self.active_desc - active_desc = active_desc.replace(' rows ', ' row ') - active_desc = active_desc.replace(' columns ', ' column ') - active_desc = active_desc.replace(' into ', ' to ') - active_desc = active_desc.replace('changes orbs to', 'all orbs to') - - board_change_txt = 'all orbs to' - if board_change_txt in active_desc: - txt = strip_prev_clause(active_desc, board_change_txt) - txt = strip_next_clause(txt, 'orbs') - txt = strip_next_clause(txt, ';') - self.board_change = color_txt_to_list(txt) - - txt = active_desc - if 'row' in txt: - parts = re.split(r'\Wand\W|;\W', txt) - for i in range(0, len(parts)): - if 'row' in parts[i]: - self.row_convert.append(strip_next_clause( - strip_prev_clause(parts[i], 'to '), ' orbs')) - - txt = active_desc - if 'column' in txt: - parts = re.split(r'\Wand\W|;\W', txt) - for i in range(0, len(parts)): - if 'column' in parts[i]: - self.column_convert.append(strip_next_clause( - strip_prev_clause(parts[i], 'to '), ' orbs')) - - convert_done = self.board_change or self.row_convert or self.column_convert - - change_txt = 'change ' - if not convert_done and change_txt in active_desc and 'orb' in active_desc: - txt = active_desc - parts = re.split(r'\Wand\W|;\W', txt) - for i in range(0, len(parts)): - parts[i] = strip_prev_clause(parts[i], change_txt) if change_txt in parts[i] else '' - - for part in parts: - sub_parts = part.split(' to ') - if len(sub_parts) > 1: - source_orbs = color_txt_to_list(sub_parts[0]) - dest_orbs = color_txt_to_list(sub_parts[1]) - for so in source_orbs: - for do in dest_orbs: - self.orb_convert[so].append(do) - - -def make_roma_subname(name_ja): - subname = name_ja.replace('=', '') - adjusted_subname = '' - for part in subname.split('・'): - roma_part = romkan.to_roma(part) - if part != roma_part and not tsutils.containsJa(roma_part): - adjusted_subname += ' ' + roma_part.strip('-') - return adjusted_subname.strip() - - -class MonsterIndex(tsutils.aobject): - async def __init__(self, monster_database, nickname_overrides, basename_overrides, - panthname_overrides, accept_filter=None): - # Important not to hold onto anything except IDs here so we don't leak memory - base_monster_ids = monster_database.get_base_monster_ids() - - self.attr_short_prefix_map = { - Attribute.Fire: ['r'], - Attribute.Water: ['b'], - Attribute.Wood: ['g'], - Attribute.Light: ['l'], - Attribute.Dark: ['d'], - Attribute.Unknown: ['h'], - Attribute.Nil: ['x'], - } - self.attr_long_prefix_map = { - Attribute.Fire: ['red', 'fire'], - Attribute.Water: ['blue', 'water'], - Attribute.Wood: ['green', 'wood'], - Attribute.Light: ['light'], - Attribute.Dark: ['dark'], - Attribute.Unknown: ['unknown'], - Attribute.Nil: ['null', 'none'], - } - - self.series_to_prefix_map = { - 130: ['halloween', 'hw', 'h'], - 136: ['xmas', 'christmas'], - 125: ['summer', 'beach'], - 114: ['school', 'academy', 'gakuen'], - 139: ['new years', 'ny'], - 149: ['wedding', 'bride'], - 154: ['padr'], - 175: ['valentines', 'vday', 'v'], - 183: ['gh', 'gungho'], - } - - monster_id_to_nicknames = defaultdict(set) - for nickname, monster_id in nickname_overrides.items(): - monster_id_to_nicknames[monster_id].add(nickname) - - named_monsters = [] - async for base_mon in AsyncIter(base_monster_ids): - base_id = base_mon.monster_id - group_basename_overrides = basename_overrides.get(base_id, []) - evolution_tree = [monster_database.get_monster(m) for m in - monster_database.get_evolution_tree_ids(base_id)] - named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) - for monster in evolution_tree: - if accept_filter and not accept_filter(monster): - continue - prefixes = self.compute_prefixes(monster, evolution_tree) - extra_nicknames = monster_id_to_nicknames[monster.monster_id] - named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames) - named_monsters.append(named_monster) - - # Sort the NamedMonsters into the opposite order we want to accept their nicknames in - # This order is: - # 1) High priority first - # 2) Larger group sizes - # 3) Minimum ID size in the group - # 4) Monsters with higher ID values - def named_monsters_sort(nm: NamedMonster): - return (not nm.is_low_priority, nm.group_size, -1 * - nm.base_monster_no_na, nm.monster_no_na) - - named_monsters.sort(key=named_monsters_sort) - - # set up a set of all pantheon names, a set of all pantheon nicknames, and a dictionary of nickname -> full name - # later we will set up a dictionary of pantheon full name -> monsters - self.all_pantheon_names = set() - self.all_pantheon_names.update(panthname_overrides.values()) - - self.pantheon_nick_to_name = panthname_overrides - self.pantheon_nick_to_name.update(panthname_overrides) - - self.all_pantheon_nicknames = set() - self.all_pantheon_nicknames.update(panthname_overrides.keys()) - - self.all_prefixes = set() - self.pantheons = defaultdict(set) - self.all_entries = {} - self.two_word_entries = {} - for nm in named_monsters: - self.all_prefixes.update(nm.prefixes) - for nickname in nm.final_nicknames: - self.all_entries[nickname] = nm - for nickname in nm.final_two_word_nicknames: - self.two_word_entries[nickname] = nm - if nm.series: - for pantheon in self.all_pantheon_names: - if pantheon.lower() == nm.series.lower(): - self.pantheons[pantheon.lower()].add(nm) - - self.all_monsters = named_monsters - self.all_en_name_to_monsters = {m.name_en.lower(): m for m in named_monsters} - self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} - self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} - - for nickname, monster_id in nickname_overrides.items(): - nm = self.monster_id_to_named_monster.get(monster_id) - if nm: - self.all_entries[nickname] = nm - - def init_index(self): - pass - - def compute_prefixes(self, m: DgMonster, evotree: list): - prefixes = set() - - attr1_short_prefixes = self.attr_short_prefix_map[m.attr1] - attr1_long_prefixes = self.attr_long_prefix_map[m.attr1] - prefixes.update(attr1_short_prefixes) - prefixes.update(attr1_long_prefixes) - - # If no 2nd attribute, use x so we can look those monsters up easier - attr2_short_prefixes = self.attr_short_prefix_map.get(m.attr2, ['x']) - for a1 in attr1_short_prefixes: - for a2 in attr2_short_prefixes: - prefixes.add(a1 + a2) - prefixes.add(a1 + '/' + a2) - - # TODO: add prefixes based on type - - # Chibi monsters have the same NA name, except lowercased - lower_name = m.name_en.lower() - if m.name_en != m.name_ja: - if lower_name == m.name_en: - prefixes.add('chibi') - elif 'ミニ' in m.name_ja: - # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) - prefixes.add('chibi') - - awoken = lower_name.startswith('awoken') or '覚醒' in lower_name - revo = lower_name.startswith('reincarnated') or '転生' in lower_name - srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name - mega = lower_name.startswith('mega awoken') or '極醒' in lower_name - awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega - - # These clauses need to be separate to handle things like 'Awoken Thoth' which are - # actually Evos but have awoken in the name - if awoken: - prefixes.add('a') - prefixes.add('awoken') - - if revo: - prefixes.add('revo') - prefixes.add('reincarnated') - - if mega: - prefixes.add('mega') - prefixes.add('mega awoken') - prefixes.add('awoken') - - if srevo: - prefixes.add('srevo') - prefixes.add('super reincarnated') - - # Prefixes for evo type - if m.cur_evo_type == EvoType.Base: - prefixes.add('base') - elif m.cur_evo_type == EvoType.Evo: - prefixes.add('evo') - elif m.cur_evo_type == EvoType.UvoAwoken and not awoken_or_revo_or_equip_or_mega: - prefixes.add('uvo') - prefixes.add('uevo') - elif m.cur_evo_type == EvoType.UuvoReincarnated and not awoken_or_revo_or_equip_or_mega: - prefixes.add('uuvo') - prefixes.add('uuevo') - - if m.farmable: - prefixes.add('farmable') - - # If any monster in the group is a pixel, add 'nonpixel' to all the versions - # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. - def is_pixel(n): - n = n.name_en.lower() - return n.startswith('pixel') or n.startswith('ドット') - - for gm in evotree: - if is_pixel(gm): - prefixes.update(['pixel'] if is_pixel(m) else ['np', 'nonpixel']) - break - - if m.is_equip: - prefixes.add('assist') - prefixes.add('equip') - - # Collab prefixes - prefixes.update(self.series_to_prefix_map.get(m.series.series_id, [])) - - return prefixes - - def find_monster(self, query): - query = tsutils.rmdiacritics(query).lower().strip() - - # id search - if query.isdigit(): - m = self.monster_no_na_to_named_monster.get(int(query)) - if m is None: - return None, 'Looks like a monster ID but was not found', None - else: - return m, None, "ID lookup" - # special handling for na/jp - - # TODO: need to handle na_only? - - # handle exact nickname match - if query in self.all_entries: - return self.all_entries[query], None, "Exact nickname" - - contains_ja = tsutils.containsJa(query) - if len(query) < 2 and contains_ja: - return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_ja: - return None, 'Your query must be at least 4 letters', None - - # TODO: this should be a length-limited priority queue - matches = set() - - # prefix search for ids, take max id - for nickname, m in self.all_entries.items(): - if query.endswith("base {}".format(m.monster_id)): - matches.add( - discord.utils.find(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) - if len(matches): - return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() - - # prefix search for nicknames, space-preceeded, take max id - for nickname, m in self.all_entries.items(): - if nickname.startswith(query + ' '): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) - - # prefix search for nicknames, take max id - for nickname, m in self.all_entries.items(): - if nickname.startswith(query): - matches.add(m) - if len(matches): - all_names = ",".join(map(lambda x: x.name_en, matches)) - return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( - len(matches), all_names) - - # prefix search for full name, take max id - for nickname, m in self.all_entries.items(): - if (m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query)): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) - - # for nicknames with 2 names, prefix search 2nd word, take max id - if query in self.two_word_entries: - return self.two_word_entries[query], None, "Second-word nickname prefix, max of {}".format(len(matches)) - - # TODO: refactor 2nd search characteristcs for 2nd word - - # full name contains on nickname, take max id - for nickname, m in self.all_entries.items(): - if (query in m.name_en.lower() or query in m.name_ja.lower()): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( - len(matches)) - - # No decent matches. Try near hits on nickname instead - matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) - if len(matches): - match = matches[0] - return self.all_entries[match], None, 'Close nickname match ({})'.format(match) - - # Still no decent matches. Try near hits on full name instead - matches = difflib.get_close_matches( - query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) - if len(matches): - match = matches[0] - return self.all_en_name_to_monsters[match], None, 'Close name match ({})'.format(match) - - # About to give up, try matching all words - matches = set() - for nickname, m in self.all_entries.items(): - if (all(map(lambda x: x in m.name_en.lower(), query.split())) or - all(map(lambda x: x in m.name_ja.lower(), query.split()))): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( - len(matches)) - - # couldn't find anything - return None, "Could not find a match for: " + query, None - - def find_monster2(self, query): - """Search with alternative method for resolving prefixes. - - Implements the lookup for id2, where you are allowed to specify multiple prefixes for a card. - All prefixes are required to be exactly matched by the card. - Follows a similar logic to the regular id but after each check, will remove any potential match that doesn't - contain every single specified prefix. - """ - query = tsutils.rmdiacritics(query).lower().strip() - # id search - if query.isdigit(): - m = self.monster_no_na_to_named_monster.get(int(query)) - if m is None: - return None, 'Looks like a monster ID but was not found', None - else: - return m, None, "ID lookup" - - # handle exact nickname match - if query in self.all_entries: - return self.all_entries[query], None, "Exact nickname" - - contains_ja = tsutils.containsJa(query) - if len(query) < 2 and contains_ja: - return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_ja: - return None, 'Your query must be at least 4 letters', None - - # we want to look up only the main part of the query, and then verify that each result has the prefixes - # so break up the query into an array of prefixes, and a string (new_query) that will be the lookup - query_prefixes = [] - parts_of_query = query.split() - new_query = '' - for i, part in enumerate(parts_of_query): - if part in self.all_prefixes: - query_prefixes.append(part) - else: - new_query = ' '.join(parts_of_query[i:]) - break - - # if we don't have any prefixes, then default to using the regular id lookup - if len(query_prefixes) < 1: - return self.find_monster(query) - - matches = PotentialMatches() - - # prefix search for ids, take max id - for nickname, m in self.all_entries.items(): - if query.endswith("base {}".format(m.monster_id)): - matches.add( - discord.utils.find(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # first try to get matches from nicknames - for nickname, m in self.all_entries.items(): - if new_query in nickname: - matches.add(m) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # if we don't have any candidates yet, pick a new method - if not matches.length(): - # try matching on exact names next - for nickname, m in self.all_en_name_to_monsters.items(): - if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): - matches.add(m) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # check for exact match on pantheon name but only if needed - if not matches.length(): - for pantheon in self.all_pantheon_nicknames: - if new_query == pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, - self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # check for any match on pantheon name, again but only if needed - if not matches.length(): - for pantheon in self.all_pantheon_nicknames: - if new_query in pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, - self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - if matches.length(): - return matches.pick_best_monster(), None, None - return None, "Could not find a match for: " + query, None - - def pickBestMonster(self, named_monster_list): - return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) - - -class PotentialMatches(object): - def __init__(self): - self.match_list = set() - - def add(self, m): - self.match_list.add(m) - - def update(self, monster_list): - self.match_list.update(monster_list) - - def length(self): - return len(self.match_list) - - def remove_potential_matches_without_all_prefixes(self, query_prefixes): - to_remove = set() - for m in self.match_list: - for prefix in query_prefixes: - if prefix not in m.prefixes: - to_remove.add(m) - break - self.match_list.difference_update(to_remove) - - def get_monsters_from_potential_pantheon_match(self, pantheon, pantheon_nick_to_name, pantheons): - full_name = pantheon_nick_to_name[pantheon] - self.update(pantheons[full_name]) - - def pick_best_monster(self): - return max(self.match_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) - - -class NamedMonsterGroup(object): - def __init__(self, evolution_tree: list, basename_overrides: list): - self.is_low_priority = ( - self._is_low_priority_monster(evolution_tree[0]) - or self._is_low_priority_group(evolution_tree)) - - base_monster = evolution_tree[0] - self.group_size = len(evolution_tree) - self.base_monster_no = base_monster.monster_id - self.base_monster_no_na = base_monster.monster_no_na - - self.monster_no_to_basename = { - m.monster_id: self._compute_monster_basename(m) for m in evolution_tree - } - - self.computed_basename = self._compute_group_basename(evolution_tree) - self.computed_basenames = set([self.computed_basename]) - if '-' in self.computed_basename: - self.computed_basenames.add(self.computed_basename.replace('-', ' ')) - - self.basenames = basename_overrides or self.computed_basenames - - def _compute_monster_basename(self, m: DgMonster): - basename = m.name_en.lower() - if ',' in basename: - name_parts = basename.split(',') - if name_parts[1].strip().startswith('the '): - # handle names like 'xxx, the yyy' where xxx is the name - basename = name_parts[0] - else: - # otherwise, grab the chunk after the last comma - basename = name_parts[-1] - - for x in ['awoken', 'reincarnated']: - if basename.startswith(x): - basename = basename.replace(x, '') - - # Fix for DC collab garbage - basename = basename.replace('(comics)', '') - basename = basename.replace('(film)', '') - - return basename.strip() - - def _compute_group_basename(self, monsters): - """Computes the basename for a group of monsters. - - Prefer a basename with the largest count across the group. If all the - groups have equal size, prefer the lowest monster number basename. - This monster in general has better names, particularly when all the - names are unique, e.g. for male/female hunters.""" - - def count_and_id(): - return [0, 0] - - basename_to_info = defaultdict(count_and_id) - - for m in monsters: - basename = self.monster_no_to_basename[m.monster_id] - entry = basename_to_info[basename] - entry[0] += 1 - entry[1] = max(entry[1], m.monster_id) - - entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] - return max(entries)[2] - - def _is_low_priority_monster(self, m: DgMonster): - lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] - lp_substrings = ['tamadra'] - lp_min_rarity = 2 - name = m.name_en.lower() - - failed_type = m.type1 in lp_types - failed_ss = any([x in name for x in lp_substrings]) - failed_rarity = m.rarity < lp_min_rarity - failed_chibi = name == m.name_en and m.name_en != m.name_ja - failed_equip = m.is_equip - return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip - - def _is_low_priority_group(self, mg: list): - lp_grp_min_rarity = 5 - max_rarity = max(m.rarity for m in mg) - failed_max_rarity = max_rarity < lp_grp_min_rarity - return failed_max_rarity - - -class NamedMonster(object): - def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set): - # Must not hold onto monster or monster_group! - - # Hold on to the IDs instead - self.monster_id = monster.monster_id - self.monster_no_na = monster.monster_no_na - self.monster_no_jp = monster.monster_no_jp - - # ID of the root of the tree for this monster - self.base_monster_no = monster_group.base_monster_no - self.base_monster_no_na = monster_group.base_monster_no_na - - # This stuff is important for nickname generation - self.group_basenames = monster_group.basenames - self.prefixes = prefixes - - # Pantheon - self.series = monster.series.name if monster.series else None - - # Data used to determine how to rank the nicknames - self.is_low_priority = monster_group.is_low_priority or monster.is_equip - self.group_size = monster_group.group_size - self.rarity = monster.rarity - - # Used in fallback searches - self.name_en = monster.name_en - self.name_ja = monster.name_ja - - # These are just extra metadata - self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] - self.group_computed_basename = monster_group.computed_basename - self.extra_nicknames = extra_nicknames - - # Compute any extra prefixes - if self.monster_basename in ('ana', 'ace'): - self.prefixes.add(self.monster_basename) - - # Compute extra basenames by checking for two-word basenames and using the second half - self.two_word_basenames = set() - for basename in self.group_basenames: - basename_words = basename.split(' ') - if len(basename_words) == 2: - self.two_word_basenames.add(basename_words[1]) - - # The primary result nicknames - self.final_nicknames = set() - # Set the configured override nicknames - self.final_nicknames.update(self.extra_nicknames) - # Set the roma subname for JP monsters - if monster.roma_subname: - self.final_nicknames.add(monster.roma_subname) - - # For each basename, add nicknames - for basename in self.group_basenames: - # Add the basename directly - self.final_nicknames.add(basename) - # Add the prefix plus basename, and the prefix with a space between basename - for prefix in self.prefixes: - self.final_nicknames.add(prefix + basename) - self.final_nicknames.add(prefix + ' ' + basename) - - self.final_two_word_nicknames = set() - # Slightly different process for two-word basenames. Does this make sense? Who knows. - for basename in self.two_word_basenames: - self.final_two_word_nicknames.add(basename) - # Add the prefix plus basename, and the prefix with a space between basename - for prefix in self.prefixes: - self.final_two_word_nicknames.add(prefix + basename) - self.final_two_word_nicknames.add(prefix + ' ' + basename) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py new file mode 100644 index 00000000..ec2fe37d --- /dev/null +++ b/dadguide/database_manager.py @@ -0,0 +1,1511 @@ +from enum import Enum +from datetime import datetime +import sqlite3 as lite +import shutil +import romkan +import logging +import pytz +import difflib +import os +import re +from discord.utils import find as find_first +from collections import OrderedDict, defaultdict, deque + +from redbot.core.utils import AsyncIter +from redbot.core import data_manager +import tsutils + +logger = logging.getLogger('red.padbot-cogs.dadguide.database_manager') + +def _data_file(file_name: str) -> str: + return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) + +DB_DUMP_FILE = _data_file('dadguide.sqlite') +DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') + + +class Attribute(Enum): + """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" + Fire = 0 + Water = 1 + Wood = 2 + Light = 3 + Dark = 4 + Unknown = 5 + Nil = 6 + + +class MonsterType(Enum): + Evolve = 0 + Balance = 1 + Physical = 2 + Healer = 3 + Dragon = 4 + God = 5 + Attacker = 6 + Devil = 7 + Machine = 8 + Awoken = 12 + Enhance = 14 + Vendor = 15 + + +class EvoType(Enum): + """Evo types supported by DadGuide. Numbers correspond to their id values.""" + Base = 0 # Represents monsters who didn't require evo + Evo = 1 + UvoAwoken = 2 + UuvoReincarnated = 3 + + +class InternalEvoType(Enum): + """Evo types unsupported by DadGuide.""" + Base = "" + Normal = "" + Ultimate = "" + Reincarnated = "Reincarnated" + Assist = "Assist" + Pixel = "Pixel" + SuperReincarnated = "Super Reincarnated" + + +class Server(Enum): + JP = 0 + NA = 1 + KR = 2 + + +class DadguideTableNotFound(Exception): + def __init__(self, table_name): + self.message = '{} not found'.format(table_name) + + +def load_database(existing_db): + # Release the handle to the database file if it has one + if existing_db: + existing_db.close() + # Overwrite the working copy so we can open a handle to it without affecting future downloads + if os.path.exists(DB_DUMP_FILE): + shutil.copy2(DB_DUMP_FILE, DB_DUMP_WORKING_FILE) + # Open the new working copy. + return DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) + + +class DadguideDatabase(object): + def __init__(self, data_file): + self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) + self._con.row_factory = lite.Row + + def has_database(self): + return self._con is not None + + def close(self): + self._con.close() + self._con = None + + @staticmethod + def _select_builder(tables, key=None, where=None, order=None, distinct=False): + if distinct: + SELECT_FROM = 'SELECT DISTINCT {fields} FROM {first_table}' + else: + SELECT_FROM = 'SELECT {fields} FROM {first_table}' + WHERE = 'WHERE {condition}' + JOIN = 'LEFT JOIN {other_table} ON {first_table}.{key}={other_table}.{key}' + ORDER = 'ORDER BY {order}' + first_table = None + fields_lst = [] + other_tables = [] + for table, fields in tables.items(): + if fields is not None: + fields_lst.extend(['{}.{}'.format(table, f) for f in fields]) + if first_table is None: + first_table = table + if key is None: + break + else: + other_tables.append(table) + query = [SELECT_FROM.format(first_table=first_table, fields=', '.join(fields_lst))] + prev_table = first_table + if key: + for k, other in zip(key, other_tables): + query.append(JOIN.format(first_table=prev_table, other_table=other, key=k)) + prev_table = other + if where: + query.append(WHERE.format(condition=where)) + if order: + query.append(ORDER.format(order=order)) + return ' '.join(query) + + def _query_one(self, query, param, d_type): + cursor = self._con.cursor() + cursor.execute(query, param) + res = cursor.fetchone() + if res is not None: + if issubclass(d_type, DadguideItem): + return d_type(res, self) + else: + return d_type(res) + return None + + def _as_generator(self, cursor, d_type): + res = cursor.fetchone() + if issubclass(d_type, DadguideItem): + while res is not None: + yield d_type(res, self) + res = cursor.fetchone() + else: + while res is not None: + yield d_type(res) + res = cursor.fetchone() + + def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): + cursor = self._con.cursor() + cursor.execute(query, param) + if cursor.rowcount == 0: + return [] + if as_generator: + return self._as_generator(cursor, d_type) + else: + if idx_key is None: + return [d_type(res, self) for res in cursor.fetchall()] + else: + return DictWithAttrAccess({res[idx_key]: d_type(res, self) for res in cursor.fetchall()}) + + def _max_id(self): + cursor = self._con.cursor() + cursor.execute("SELECT MAX(monster_id) FROM monsters WHERE monster_id < 10000") + return cursor.fetchone()['MAX(monster_id)'] + + def _select_one_entry_by_pk(self, pk, d_type): + return self._query_one( + self._select_builder( + tables={d_type.TABLE: d_type.FIELDS}, + where='{}.{}=?'.format(d_type.TABLE, d_type.PK)), + (pk,), + d_type) + + def _get_table_fields(self, table_name: str): + # SQL inject vulnerable :v + table_info = self._query_many('PRAGMA table_info(' + table_name + ')', (), dict) + pk = None + fields = [] + for c in table_info: + if c['pk'] == 1: + pk = c['name'] + fields.append(c['name']) + if len(fields) == 0: + raise DadguideTableNotFound(table_name) + return fields, pk + + def get_active_skill(self, active_skill_id: int): + return self._select_one_entry_by_pk(active_skill_id, DgActiveSkill) + + def get_leader_skill(self, leader_skill_id: int): + return self._select_one_entry_by_pk(leader_skill_id, DgLeaderSkill) + + def get_awoken_skill(self, awoken_skill_id): + return self._select_one_entry_by_pk(awoken_skill_id, DgAwokenSkill) + + def get_awoken_skill_ids(self): + SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' + return [r.awoken_skill_id for r in + self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] + + def get_monsters_by_awakenings(self, awoken_skill_id: int): + return self._query_many( + self._select_builder( + tables=OrderedDict({ + DgMonster.TABLE: DgMonster.FIELDS, + DgAwakening.TABLE: None, + }), + where='{}.awoken_skill_id=?;'.format(DgAwakening.TABLE), + key=('monster_id',), + distinct=True + ), + (awoken_skill_id,), + DgMonster) + + def get_awakenings_by_monster(self, monster_id, is_super=None): + if is_super is None: + where = '{0}.monster_id=?'.format(DgAwakening.TABLE) + param = (monster_id,) + else: + where = '{0}.monster_id=? AND {0}.is_super=?'.format(DgAwakening.TABLE) + param = (monster_id, is_super) + return self._query_many( + self._select_builder( + tables={DgAwakening.TABLE: DgAwakening.FIELDS}, + where=where, + order='order_idx ASC' + ), + param, + DgAwakening) + + def get_drop_dungeons(self, monster_id): + return self._query_many( + self._select_builder( + tables=OrderedDict({ + DgDungeon.TABLE: DgDungeon.FIELDS, + DgEncounter.TABLE: None, + DgDrop.TABLE: None, + }), + where='{0}.monster_id=?'.format(DgDrop.TABLE), + key=(DgDungeon.PK, DgEncounter.PK) + ), + (monster_id,), + DgDungeon) + + def monster_is_farmable(self, monster_id): + return self._query_one( + self._select_builder( + tables={DgDrop.TABLE: DgDrop.FIELDS}, + where='{0}.monster_id=?'.format(DgDrop.TABLE) + ), + (monster_id,), + DgDrop) is not None + + def monster_in_rem(self, monster_id): + return self._query_one( + self._select_builder( + tables={DgMonster.TABLE: ('rem_egg',)}, + where='{0}.monster_id=? AND rem_egg=1'.format(DgMonster.TABLE) + ), + (monster_id,), + DgDrop) is not None + + def monster_in_pem(self, monster_id): + return self._query_one( + self._select_builder( + tables={DgMonster.TABLE: ('pal_egg',)}, + where='{0}.monster_id=? AND pal_egg=1'.format(DgMonster.TABLE) + ), + (monster_id,), + DgDrop) is not None + + def monster_in_mp_shop(self, monster_id): + return self._query_one( + self._select_builder( + tables={DgMonster.TABLE: ('buy_mp',)}, + where='{0}.monster_id=? AND buy_mp IS NOT NULL'.format(DgMonster.TABLE) + ), + (monster_id,), + DgDrop) is not None + + def get_prev_evolution_by_monster(self, monster_id): + return self._query_one( + self._select_builder( + tables={DgEvolution.TABLE: DgEvolution.FIELDS}, + where='{}.to_id=?'.format(DgEvolution.TABLE) + ), + (monster_id,), + DgEvolution) + + def get_next_evolutions_by_monster(self, monster_id): + return self._query_many( + self._select_builder( + tables={DgEvolution.TABLE: DgEvolution.FIELDS}, + where='{}.from_id=?'.format(DgEvolution.TABLE) + ), + (monster_id,), + DgEvolution, + as_generator=True) + + def get_base_monster_by_monster(self, monster_id): + base = {'from_id': monster_id, 'to_id': monster_id} + lastbase = None + while base != None: + lastbase = base + base = self.get_prev_evolution_by_monster(base['from_id']) + return lastbase + + def get_all_evolutions_by_monster(self, monster_id): + base = self.get_base_monster_by_monster(monster_id) + queue = [base] + + while queue: + curr = queue.pop(0) + yield curr + queue.extend(self.get_next_evolutions_by_monster(curr['to_id'])) + + def get_evolution_by_material(self, monster_id): + return self._query_many( + self._select_builder( + tables={DgEvolution.TABLE: DgEvolution.FIELDS}, + where=' OR '.join(['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in range(1, 6)]) + ), + (monster_id,) * 5, + DgEvolution) + + def get_base_monster_ids(self): + SELECT_BASE_MONSTER_ID = ''' + SELECT evolutions.from_id as monster_id FROM evolutions WHERE evolutions.from_id NOT IN (SELECT DISTINCT evolutions.to_id FROM evolutions) + UNION + SELECT monsters.monster_id FROM monsters WHERE monsters.monster_id NOT IN (SELECT evolutions.from_id FROM evolutions UNION SELECT evolutions.to_id FROM evolutions)''' + return self._query_many( + SELECT_BASE_MONSTER_ID, + (), + DictWithAttrAccess, + as_generator=True) + + def get_evolution_tree_ids(self, base_monster_id): + # is not a tree i lied + base_id = base_monster_id + evolution_tree = [base_id] + n_evos = deque() + n_evos.append(base_id) + while len(n_evos) > 0: + n_evo_id = n_evos.popleft() + for e in self.get_next_evolutions_by_monster(n_evo_id): + n_evos.append(e.to_id) + evolution_tree.append(e.to_id) + return evolution_tree + + def monster_id_to_no(self, monster_id, region=Server.JP): + res = self._query_one( + self._select_builder( + tables={DgMonster.TABLE: ('monster_no_{}'.format(region.name.lower()),)}, + where='{}.monster_id=?'.format(DgMonster.TABLE) + ), + (monster_id,), + DictWithAttrAccess) + for k in res: + return res[k] + + def get_series(self, series_id: int): + return self._select_one_entry_by_pk(series_id, DgSeries) + + def _get_monsters_where(self, where, param): + return self._query_many( + self._select_builder( + tables={DgMonster.TABLE: DgMonster.FIELDS}, + where=where + ), + param, + DgMonster) + + def get_monsters_by_series(self, series_id: int): + return self._get_monsters_where('{}.series_id=?'.format(DgMonster.TABLE), (series_id,)) + + def get_monsters_by_active(self, active_skill_id: int): + return self._get_monsters_where('{}.active_skill_id=?'.format(DgMonster.TABLE), (active_skill_id,)) + + def get_monster_evo_gem(self, name: str, region='jp'): + gem_suffix = { + 'ja': 'の希石', + 'en': '\'s Gem', + 'ko': ' 의 휘석' + } + if region not in gem_suffix: + return None + non_gem_name = name.replace(gem_suffix[region], '') + if non_gem_name == name: + return None + return self._query_one( + self._select_builder( + tables={DgMonster.TABLE: DgMonster.FIELDS}, + where='{0}.name_{1}=? AND {0}.leader_skill_id=10628'.format(DgMonster.TABLE, region) + ), + (non_gem_name,), + DgMonster) + + def get_na_only_monsters(self): + return self._get_monsters_where( + '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format(DgMonster.TABLE), + ()) + + def get_monster(self, monster_id: int): + return self._select_one_entry_by_pk(monster_id, DgMonster) + + def get_all_monster_ja_name(self, as_generator=True): + return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, + as_generator=as_generator) + + def get_all_monsters(self, as_generator=True): + return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, + as_generator=as_generator) + + def get_all_events(self, as_generator=True): + return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), + DgScheduledEvent, + as_generator=as_generator) + + def get_dungeon_by_id(self, dungeon_id: int): + return self._select_one_entry_by_pk(dungeon_id, DgDungeon) + + +def enum_or_none(enum, value, default=None): + if value is not None: + return enum(value) + else: + return default + + +class DictWithAttrAccess(dict): + def __init__(self, item): + super(DictWithAttrAccess, self).__init__(item) + self.__dict__ = self + + +class DadguideItem(DictWithAttrAccess): + """ + Base class for all items loaded from DadGuide. + Is a dict with attr access. + """ + TABLE = None + FIELDS = '*' + PK = None + AS_BOOL = () + + def __init__(self, item, database): + super(DadguideItem, self).__init__(item) + self._database = database + for k in self.AS_BOOL: + self[k] = bool(self[k]) + + def key(self): + return self[self.PK] + + +class DgActiveSkill(DadguideItem): + TABLE = 'active_skills' + PK = 'active_skill_id' + + @property + def monsters(self): + return self._database.get_monsters_by_active(self.active_skill_id) + + @property + def skillups(self): + return list(filter(lambda x: x.farmable, self.monsters)) + + @property + def desc(self): + return self.desc_en or self.desc_ja + + @property + def name(self): + return self.name_en or self.name_ja + + +class DgLeaderSkill(DadguideItem): + TABLE = 'leader_skills' + PK = 'leader_skill_id' + + @property + def data(self): + return self.max_hp, self.max_atk, self.max_rcv, self.max_shield + + @property + def desc(self): + return self.desc_en or self.desc_ja + + @property + def name(self): + return self.name_en or self.name_ja + + +class DgAwakening(DadguideItem): + TABLE = 'awakenings' + PK = 'awakening_id' + AS_BOOL = ['is_super'] + + def __init__(self, item, database): + super(DgAwakening, self).__init__(item, database) + + @property + def skill(self): + return self._database.get_awoken_skill(self.awoken_skill_id) + + @property + def name(self): + return self.skill.name_en if self.skill.name_en is not None else self.skill.name_ja + + +class DgAwokenSkill(DadguideItem): + TABLE = 'awoken_skills' + PK = 'awoken_skill_id' + + @property + def monsters_with_awakening(self): + return self._database.get_monsters_by_awakenings(self.awoken_skill_id) + + +class DgEvolution(DadguideItem): + TABLE = 'evolutions' + PK = 'evolution_id' + + def __init__(self, item, database): + super(DgEvolution, self).__init__(item, database) + self.evolution_type = EvoType(self.evolution_type) + + +class DgSeries(DadguideItem): + TABLE = 'series' + PK = 'series_id' + + @property + def monsters(self): + return self._database.get_monsters_by_series(self.series_id) + + @property + def name(self): + return self.name_en if self.name_en is not None else self.name_ja + + +class DgDungeon(DadguideItem): + TABLE = 'dungeons' + PK = 'dungeon_id' + + +class DgEncounter(DadguideItem): + TABLE = 'encounters' + PK = 'encounter_id' + + +class DgDrop(DadguideItem): + TABLE = 'drops' + PK = 'drop_id' + + +class DgScheduledEvent(DadguideItem): + TABLE = 'schedule' + PK = 'event_id' + + def __init__(self, item, database): + super(DgScheduledEvent, self).__init__(item, database) + self.dungeon = self._database.get_dungeon_by_id(self.dungeon_id) + + @property + def open_datetime(self): + return datetime.utcfromtimestamp(self.start_timestamp).replace(tzinfo=pytz.UTC) + + @open_datetime.setter + def open_datetime(self, value): + self.start_timestamp = int(value.timestamp()) + + @property + def close_datetime(self): + return datetime.utcfromtimestamp(self.end_timestamp).replace(tzinfo=pytz.UTC) + + @close_datetime.setter + def close_datetime(self, value): + self.end_timestamp = int(value.timestamp()) + + +class DgMonster(DadguideItem): + TABLE = 'monsters' + PK = 'monster_id' + AS_BOOL = ('on_jp', 'on_na', 'on_kr', 'has_animation', 'has_hqimage') + + def __init__(self, item, database): + super(DgMonster, self).__init__(item, database) + + self.roma_subname = None + if self.name_en == self.name_ja: + self.roma_subname = make_roma_subname(self.name_ja) + else: + # Remove annoying stuff from NA names, like Jörmungandr + self.name_en = tsutils.rmdiacritics(self.name_en) + + self.name_en = self.name_en_override or self.name_en + + self.attr1 = enum_or_none(Attribute, self.attribute_1_id, Attribute.Nil) + self.attr2 = enum_or_none(Attribute, self.attribute_2_id, Attribute.Nil) + + self.type1 = enum_or_none(MonsterType, self.type_1_id) + self.type2 = enum_or_none(MonsterType, self.type_2_id) + self.type3 = enum_or_none(MonsterType, self.type_3_id) + self.types = list(filter(None, [self.type1, self.type2, self.type3])) + + self.in_pem = bool(self.pal_egg) + self.in_rem = bool(self.rem_egg) + + self.awakenings = self._database.get_awakenings_by_monster(self.monster_id) + self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) + + self.is_inheritable = bool(self.inheritable) + + self.evo_from = self._database.get_prev_evolution_by_monster(self.monster_id) + + self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) + + base_id = self.monster_id + next_base = self._database.get_prev_evolution_by_monster(base_id) + while next_base is not None: + base_id = next_base.from_id + next_base = self._database.get_prev_evolution_by_monster(base_id) + self._base_monster_id = base_id + self._alt_evo_id_list = self._database.get_evolution_tree_ids(self._base_monster_id) + + self.search = MonsterSearchHelper(self) + + + @property + def monster_no(self): + return self.monster_id + + def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): + s_min = float(self[key + '_min']) + s_max = float(self[key + '_max']) + if self.level > 1: + s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** self[key + '_scale'] + else: + s_val = s_min + if lv > 99: + s_val *= 1 + (self.limit_mult / 11 * (lv - 99)) / 100 + plus_dict = {'hp': 10, 'atk': 5, 'rcv': 3} + s_val += plus_dict[key] * max(min(plus, 99), 0) + if inherit: + inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} + if not is_plus_297: + s_val -= plus_dict[key] * max(min(plus, 99), 0) + s_val *= inherit_dict[key] + return int(round(s_val)) + + def stats(self, lv=99, plus=0, inherit=False): + is_plus_297 = False + if plus == 297: + plus = (99, 99, 99) + is_plus_297 = True + elif plus == 0: + plus = (0, 0, 0) + hp = self.stat('hp', lv, plus[0], inherit, is_plus_297) + atk = self.stat('atk', lv, plus[1], inherit, is_plus_297) + rcv = self.stat('rcv', lv, plus[2], inherit, is_plus_297) + weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) + return hp, atk, rcv, weighted + + @property + def active_skill(self): + return self._database.get_active_skill(self.active_skill_id) + + @property + def leader_skill(self): + return self._database.get_leader_skill(self.leader_skill_id) + + @property + def cur_evo_type(self): + return self.evo_from.evolution_type if self.evo_from else EvoType.Base + + @property + def true_evo_type(self): + if self == self.base_monster: + return InternalEvoType.Base + elif 5077 in [dgi.monster_id for dgi in self.mats_for_evo]: + return InternalEvoType.SuperReincarnated + elif 3826 in [dgi.monster_id for dgi in self.mats_for_evo]: + return InternalEvoType.Pixel + elif self.is_equip: + return InternalEvoType.Assist + elif self.cur_evo_type == EvoType.UuvoReincarnated: + return InternalEvoType.Reincarnated + elif self.cur_evo_type == EvoType.UvoAwoken: + return InternalEvoType.Ultimate + else: + return InternalEvoType.Normal + + @property + def mats_for_evo(self): + if self.evo_from is None: + return [] + return [self._database.get_monster(self.evo_from['mat_{}_id'.format(i)]) for i in range(1, 6) if + self.evo_from['mat_{}_id'.format(i)] is not None] + + @property + def evo_gem(self): + return self._database.get_monster_evo_gem(self.name_ja) + + @property + def material_of(self): + mat_of = self._database.get_evolution_by_material(self.monster_id) + return [self._database.get_monster(x.to_id) for x in mat_of] + + def _evolutions_to(self): + return self._database.get_next_evolutions_by_monster(self.monster_id) + + @property + def evo_to(self): + return [self._database.get_monster(x.to_id) for x in self._evolutions_to()] + + @property + def base_monster(self): + return self._database.get_monster(self._base_monster_id) + + @property + def alt_evos(self): + return [self._database.get_monster(a) for a in self._alt_evo_id_list] + + @property + def is_base_monster(self): + return self.evo_from is None + + @property + def series(self): + return self._database.get_series(self.series_id) + + @property + def is_gfe(self): + return self.series_id == 34 + + @property + def drop_dungeons(self): + return self._database.get_drop_dungeons(self.monster_id) + + @property + def farmable(self): + return self._database.monster_is_farmable(self.monster_id) + + @property + def farmable_evo(self): + for e_id in self._alt_evo_id_list: + if self._database.monster_is_farmable(e_id): + return True + return False + + @property + def rem_evo(self): + for e_id in self._alt_evo_id_list: + if self._database.monster_in_rem(e_id): + return True + return False + + @property + def pem_evo(self): + for e_id in self._alt_evo_id_list: + if self._database.monster_in_pem(e_id): + return True + return False + + @property + def killers(self): + type_to_killers_map = { + MonsterType.God: ['Devil'], + MonsterType.Devil: ['God'], + MonsterType.Machine: ['God', 'Balance'], + MonsterType.Dragon: ['Machine', 'Healer'], + MonsterType.Physical: ['Machine', 'Healer'], + MonsterType.Attacker: ['Devil', 'Physical'], + MonsterType.Healer: ['Dragon', 'Attacker'], + } + if MonsterType.Balance in self.types: + return ['Any'] + killers = set() + for t in self.types: + killers.update(type_to_killers_map.get(t, [])) + return sorted(killers) + + @property + def in_mpshop(self): + return self.buy_mp is not None + + @property + def mp_evo(self): + for e_id in self._alt_evo_id_list: + if self._database.monster_in_mp_shop(e_id): + return True + return False + + @property + def history_us(self): + return '[{}] New Added'.format(self.reg_date) + + @property + def next_monster(self): + next = None + offset = 1 + while next is None and self.monster_no + offset <= self._database._max_id(): + next = self._database.get_monster(self.monster_no + offset) + offset += 1 + return next + + @property + def prev_monster(self): + next = None + offset = 1 + while next is None and self.monster_no - offset >= 1: + next = self._database.get_monster(self.monster_no - offset) + offset += 1 + return next + + +class MonsterSearchHelper(object): + def __init__(self, m: DgMonster): + + self.name = '{} {}'.format(m.name_en, m.name_ja).lower() + leader_skill = m.leader_skill + self.leader = leader_skill.desc.lower() if leader_skill else '' + active_skill = m.active_skill + self.active_name = active_skill.name.lower() if active_skill else '' + self.active_desc = active_skill.desc.lower() if active_skill else '' + self.active = '{} {}'.format(self.active_name, self.active_desc) + self.active_min = active_skill.turn_min if active_skill else None + self.active_max = active_skill.turn_max if active_skill else None + + self.color = [m.attr1.name.lower()] + self.hascolor = [c.name.lower() for c in [m.attr1, m.attr2] if c] + + self.hp, self.atk, self.rcv, self.weighted_stats = m.stats(lv=110) + + self.types = [t.name for t in m.types] + + def replace_colors(text: str): + return text.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') + + self.leader = replace_colors(self.leader) + self.active = replace_colors(self.active) + self.active_name = replace_colors(self.active_name) + self.active_desc = replace_colors(self.active_desc) + + self.board_change = [] + self.orb_convert = defaultdict(list) + self.row_convert = [] + self.column_convert = [] + + def color_txt_to_list(txt): + txt = txt.replace('and', ' ') + txt = txt.replace(',', ' ') + txt = txt.replace('orbs', ' ') + txt = txt.replace('orb', ' ') + txt = txt.replace('mortal poison', 'mortalpoison') + txt = txt.replace('jammers', 'jammer') + txt = txt.strip() + return txt.split() + + def strip_prev_clause(txt: str, sep: str): + prev_clause_start_idx = txt.find(sep) + if prev_clause_start_idx >= 0: + prev_clause_start_idx += len(sep) + txt = txt[prev_clause_start_idx:] + return txt + + def strip_next_clause(txt: str, sep: str): + next_clause_start_idx = txt.find(sep) + if next_clause_start_idx >= 0: + txt = txt[:next_clause_start_idx] + return txt + + active_desc = self.active_desc + active_desc = active_desc.replace(' rows ', ' row ') + active_desc = active_desc.replace(' columns ', ' column ') + active_desc = active_desc.replace(' into ', ' to ') + active_desc = active_desc.replace('changes orbs to', 'all orbs to') + + board_change_txt = 'all orbs to' + if board_change_txt in active_desc: + txt = strip_prev_clause(active_desc, board_change_txt) + txt = strip_next_clause(txt, 'orbs') + txt = strip_next_clause(txt, ';') + self.board_change = color_txt_to_list(txt) + + txt = active_desc + if 'row' in txt: + parts = re.split(r'\Wand\W|;\W', txt) + for i in range(0, len(parts)): + if 'row' in parts[i]: + self.row_convert.append(strip_next_clause( + strip_prev_clause(parts[i], 'to '), ' orbs')) + + txt = active_desc + if 'column' in txt: + parts = re.split(r'\Wand\W|;\W', txt) + for i in range(0, len(parts)): + if 'column' in parts[i]: + self.column_convert.append(strip_next_clause( + strip_prev_clause(parts[i], 'to '), ' orbs')) + + convert_done = self.board_change or self.row_convert or self.column_convert + + change_txt = 'change ' + if not convert_done and change_txt in active_desc and 'orb' in active_desc: + txt = active_desc + parts = re.split(r'\Wand\W|;\W', txt) + for i in range(0, len(parts)): + parts[i] = strip_prev_clause(parts[i], change_txt) if change_txt in parts[i] else '' + + for part in parts: + sub_parts = part.split(' to ') + if len(sub_parts) > 1: + source_orbs = color_txt_to_list(sub_parts[0]) + dest_orbs = color_txt_to_list(sub_parts[1]) + for so in source_orbs: + for do in dest_orbs: + self.orb_convert[so].append(do) + + +def make_roma_subname(name_ja): + subname = name_ja.replace('=', '') + adjusted_subname = '' + for part in subname.split('・'): + roma_part = romkan.to_roma(part) + if part != roma_part and not tsutils.containsJa(roma_part): + adjusted_subname += ' ' + roma_part.strip('-') + return adjusted_subname.strip() + + +class MonsterIndex(tsutils.aobject): + async def __init__(self, monster_database, nickname_overrides, basename_overrides, + panthname_overrides, accept_filter=None): + # Important not to hold onto anything except IDs here so we don't leak memory + base_monster_ids = monster_database.get_base_monster_ids() + + self.attr_short_prefix_map = { + Attribute.Fire: ['r'], + Attribute.Water: ['b'], + Attribute.Wood: ['g'], + Attribute.Light: ['l'], + Attribute.Dark: ['d'], + Attribute.Unknown: ['h'], + Attribute.Nil: ['x'], + } + self.attr_long_prefix_map = { + Attribute.Fire: ['red', 'fire'], + Attribute.Water: ['blue', 'water'], + Attribute.Wood: ['green', 'wood'], + Attribute.Light: ['light'], + Attribute.Dark: ['dark'], + Attribute.Unknown: ['unknown'], + Attribute.Nil: ['null', 'none'], + } + + self.series_to_prefix_map = { + 130: ['halloween', 'hw', 'h'], + 136: ['xmas', 'christmas'], + 125: ['summer', 'beach'], + 114: ['school', 'academy', 'gakuen'], + 139: ['new years', 'ny'], + 149: ['wedding', 'bride'], + 154: ['padr'], + 175: ['valentines', 'vday', 'v'], + 183: ['gh', 'gungho'], + } + + monster_id_to_nicknames = defaultdict(set) + for monster_id, nicknames in nickname_overrides.items(): + monster_id_to_nicknames[monster_id] = nicknames + + named_monsters = [] + async for base_mon in AsyncIter(base_monster_ids): + base_id = base_mon.monster_id + group_basename_overrides = basename_overrides.get(base_id, []) + evolution_tree = [monster_database.get_monster(m) for m in + monster_database.get_evolution_tree_ids(base_id)] + named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) + for monster in evolution_tree: + if accept_filter and not accept_filter(monster): + continue + prefixes = self.compute_prefixes(monster, evolution_tree) + extra_nicknames = monster_id_to_nicknames[monster.monster_id] + named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames) + named_monsters.append(named_monster) + + # Sort the NamedMonsters into the opposite order we want to accept their nicknames in + # This order is: + # 1) High priority first + # 2) Larger group sizes + # 3) Minimum ID size in the group + # 4) Monsters with higher ID values + def named_monsters_sort(nm: NamedMonster): + return (not nm.is_low_priority, nm.group_size, -1 * + nm.base_monster_no_na, nm.monster_no_na) + + named_monsters.sort(key=named_monsters_sort) + + # set up a set of all pantheon names, a set of all pantheon nicknames, and a dictionary of nickname -> full name + # later we will set up a dictionary of pantheon full name -> monsters + self.all_pantheon_names = set() + self.all_pantheon_names.update(panthname_overrides.values()) + + self.pantheon_nick_to_name = panthname_overrides + self.pantheon_nick_to_name.update(panthname_overrides) + + self.all_pantheon_nicknames = set() + self.all_pantheon_nicknames.update(panthname_overrides.keys()) + + self.all_prefixes = set() + self.pantheons = defaultdict(set) + self.all_entries = {} + self.two_word_entries = {} + for nm in named_monsters: + self.all_prefixes.update(nm.prefixes) + for nickname in nm.final_nicknames: + self.all_entries[nickname] = nm + for nickname in nm.final_two_word_nicknames: + self.two_word_entries[nickname] = nm + if nm.series: + for pantheon in self.all_pantheon_names: + if pantheon.lower() == nm.series.lower(): + self.pantheons[pantheon.lower()].add(nm) + + self.all_monsters = named_monsters + self.all_en_name_to_monsters = {m.name_en.lower(): m for m in named_monsters} + self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} + self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} + + for monster_id, nicknames in nickname_overrides.items(): + nm = self.monster_id_to_named_monster.get(monster_id) + if nm: + for nickname in nicknames: + self.all_entries[nickname] = nm + + def init_index(self): + pass + + def compute_prefixes(self, m: DgMonster, evotree: list): + prefixes = set() + + attr1_short_prefixes = self.attr_short_prefix_map[m.attr1] + attr1_long_prefixes = self.attr_long_prefix_map[m.attr1] + prefixes.update(attr1_short_prefixes) + prefixes.update(attr1_long_prefixes) + + # If no 2nd attribute, use x so we can look those monsters up easier + attr2_short_prefixes = self.attr_short_prefix_map.get(m.attr2, ['x']) + for a1 in attr1_short_prefixes: + for a2 in attr2_short_prefixes: + prefixes.add(a1 + a2) + prefixes.add(a1 + '/' + a2) + + # TODO: add prefixes based on type + + # Chibi monsters have the same NA name, except lowercased + lower_name = m.name_en.lower() + if m.name_en != m.name_ja: + if lower_name == m.name_en: + prefixes.add('chibi') + elif 'ミニ' in m.name_ja: + # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) + prefixes.add('chibi') + + awoken = lower_name.startswith('awoken') or '覚醒' in lower_name + revo = lower_name.startswith('reincarnated') or '転生' in lower_name + srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name + mega = lower_name.startswith('mega awoken') or '極醒' in lower_name + awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega + + # These clauses need to be separate to handle things like 'Awoken Thoth' which are + # actually Evos but have awoken in the name + if awoken: + prefixes.add('a') + prefixes.add('awoken') + + if revo: + prefixes.add('revo') + prefixes.add('reincarnated') + + if mega: + prefixes.add('mega') + prefixes.add('mega awoken') + prefixes.add('awoken') + + if srevo: + prefixes.add('srevo') + prefixes.add('super reincarnated') + + # Prefixes for evo type + if m.cur_evo_type == EvoType.Base: + prefixes.add('base') + elif m.cur_evo_type == EvoType.Evo: + prefixes.add('evo') + elif m.cur_evo_type == EvoType.UvoAwoken and not awoken_or_revo_or_equip_or_mega: + prefixes.add('uvo') + prefixes.add('uevo') + elif m.cur_evo_type == EvoType.UuvoReincarnated and not awoken_or_revo_or_equip_or_mega: + prefixes.add('uuvo') + prefixes.add('uuevo') + + if m.farmable: + prefixes.add('farmable') + + # If any monster in the group is a pixel, add 'nonpixel' to all the versions + # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. + def is_pixel(n): + n = n.name_en.lower() + return n.startswith('pixel') or n.startswith('ドット') + + for gm in evotree: + if is_pixel(gm): + prefixes.update(['pixel'] if is_pixel(m) else ['np', 'nonpixel']) + break + + if m.is_equip: + prefixes.add('assist') + prefixes.add('equip') + + # Collab prefixes + prefixes.update(self.series_to_prefix_map.get(m.series.series_id, [])) + + return prefixes + + def find_monster(self, query): + query = tsutils.rmdiacritics(query).lower().strip() + + # id search + if query.isdigit(): + m = self.monster_no_na_to_named_monster.get(int(query)) + if m is None: + return None, 'Looks like a monster ID but was not found', None + else: + return m, None, "ID lookup" + # special handling for na/jp + + # TODO: need to handle na_only? + + # handle exact nickname match + if query in self.all_entries: + return self.all_entries[query], None, "Exact nickname" + + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: + return None, 'Japanese queries must be at least 2 characters', None + elif len(query) < 4 and not contains_ja: + return None, 'Your query must be at least 4 letters', None + + # TODO: this should be a length-limited priority queue + matches = set() + + # prefix search for ids, take max id + for nickname, m in self.all_entries.items(): + if query.endswith("base {}".format(m.monster_id)): + matches.add( + find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) + if len(matches): + return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() + + # prefix search for nicknames, space-preceeded, take max id + for nickname, m in self.all_entries.items(): + if nickname.startswith(query + ' '): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) + + # prefix search for nicknames, take max id + for nickname, m in self.all_entries.items(): + if nickname.startswith(query): + matches.add(m) + if len(matches): + all_names = ",".join(map(lambda x: x.name_en, matches)) + return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( + len(matches), all_names) + + # prefix search for full name, take max id + for nickname, m in self.all_entries.items(): + if (m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query)): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) + + # for nicknames with 2 names, prefix search 2nd word, take max id + if query in self.two_word_entries: + return self.two_word_entries[query], None, "Second-word nickname prefix, max of {}".format(len(matches)) + + # TODO: refactor 2nd search characteristcs for 2nd word + + # full name contains on nickname, take max id + for nickname, m in self.all_entries.items(): + if (query in m.name_en.lower() or query in m.name_ja.lower()): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( + len(matches)) + + # No decent matches. Try near hits on nickname instead + matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) + if len(matches): + match = matches[0] + return self.all_entries[match], None, 'Close nickname match ({})'.format(match) + + # Still no decent matches. Try near hits on full name instead + matches = difflib.get_close_matches( + query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) + if len(matches): + match = matches[0] + return self.all_en_name_to_monsters[match], None, 'Close name match ({})'.format(match) + + # About to give up, try matching all words + matches = set() + for nickname, m in self.all_entries.items(): + if (all(map(lambda x: x in m.name_en.lower(), query.split())) or + all(map(lambda x: x in m.name_ja.lower(), query.split()))): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( + len(matches)) + + # couldn't find anything + return None, "Could not find a match for: " + query, None + + def find_monster2(self, query): + """Search with alternative method for resolving prefixes. + + Implements the lookup for id2, where you are allowed to specify multiple prefixes for a card. + All prefixes are required to be exactly matched by the card. + Follows a similar logic to the regular id but after each check, will remove any potential match that doesn't + contain every single specified prefix. + """ + query = tsutils.rmdiacritics(query).lower().strip() + # id search + if query.isdigit(): + m = self.monster_no_na_to_named_monster.get(int(query)) + if m is None: + return None, 'Looks like a monster ID but was not found', None + else: + return m, None, "ID lookup" + + # handle exact nickname match + if query in self.all_entries: + return self.all_entries[query], None, "Exact nickname" + + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: + return None, 'Japanese queries must be at least 2 characters', None + elif len(query) < 4 and not contains_ja: + return None, 'Your query must be at least 4 letters', None + + # we want to look up only the main part of the query, and then verify that each result has the prefixes + # so break up the query into an array of prefixes, and a string (new_query) that will be the lookup + query_prefixes = [] + parts_of_query = query.split() + new_query = '' + for i, part in enumerate(parts_of_query): + if part in self.all_prefixes: + query_prefixes.append(part) + else: + new_query = ' '.join(parts_of_query[i:]) + break + + # if we don't have any prefixes, then default to using the regular id lookup + if len(query_prefixes) < 1: + return self.find_monster(query) + + matches = PotentialMatches() + + # prefix search for ids, take max id + for nickname, m in self.all_entries.items(): + if query.endswith("base {}".format(m.monster_id)): + matches.add( + find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # first try to get matches from nicknames + for nickname, m in self.all_entries.items(): + if new_query in nickname: + matches.add(m) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # if we don't have any candidates yet, pick a new method + if not matches.length(): + # try matching on exact names next + for nickname, m in self.all_en_name_to_monsters.items(): + if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): + matches.add(m) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # check for exact match on pantheon name but only if needed + if not matches.length(): + for pantheon in self.all_pantheon_nicknames: + if new_query == pantheon.lower(): + matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, + self.pantheons) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # check for any match on pantheon name, again but only if needed + if not matches.length(): + for pantheon in self.all_pantheon_nicknames: + if new_query in pantheon.lower(): + matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, + self.pantheons) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + if matches.length(): + return matches.pick_best_monster(), None, None + return None, "Could not find a match for: " + query, None + + def pickBestMonster(self, named_monster_list): + return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) + + +class PotentialMatches(object): + def __init__(self): + self.match_list = set() + + def add(self, m): + self.match_list.add(m) + + def update(self, monster_list): + self.match_list.update(monster_list) + + def length(self): + return len(self.match_list) + + def remove_potential_matches_without_all_prefixes(self, query_prefixes): + to_remove = set() + for m in self.match_list: + for prefix in query_prefixes: + if prefix not in m.prefixes: + to_remove.add(m) + break + self.match_list.difference_update(to_remove) + + def get_monsters_from_potential_pantheon_match(self, pantheon, pantheon_nick_to_name, pantheons): + full_name = pantheon_nick_to_name[pantheon] + self.update(pantheons[full_name]) + + def pick_best_monster(self): + return max(self.match_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) + + +class NamedMonsterGroup(object): + def __init__(self, evolution_tree: list, basename_overrides: list): + self.is_low_priority = ( + self._is_low_priority_monster(evolution_tree[0]) + or self._is_low_priority_group(evolution_tree)) + + base_monster = evolution_tree[0] + self.group_size = len(evolution_tree) + self.base_monster_no = base_monster.monster_id + self.base_monster_no_na = base_monster.monster_no_na + + self.monster_no_to_basename = { + m.monster_id: self._compute_monster_basename(m) for m in evolution_tree + } + + self.computed_basename = self._compute_group_basename(evolution_tree) + self.computed_basenames = set([self.computed_basename]) + if '-' in self.computed_basename: + self.computed_basenames.add(self.computed_basename.replace('-', ' ')) + + self.basenames = basename_overrides or self.computed_basenames + + def _compute_monster_basename(self, m: DgMonster): + basename = m.name_en.lower() + if ',' in basename: + name_parts = basename.split(',') + if name_parts[1].strip().startswith('the '): + # handle names like 'xxx, the yyy' where xxx is the name + basename = name_parts[0] + else: + # otherwise, grab the chunk after the last comma + basename = name_parts[-1] + + for x in ['awoken', 'reincarnated']: + if basename.startswith(x): + basename = basename.replace(x, '') + + # Fix for DC collab garbage + basename = basename.replace('(comics)', '') + basename = basename.replace('(film)', '') + + return basename.strip() + + def _compute_group_basename(self, monsters): + """Computes the basename for a group of monsters. + + Prefer a basename with the largest count across the group. If all the + groups have equal size, prefer the lowest monster number basename. + This monster in general has better names, particularly when all the + names are unique, e.g. for male/female hunters.""" + + def count_and_id(): + return [0, 0] + + basename_to_info = defaultdict(count_and_id) + + for m in monsters: + basename = self.monster_no_to_basename[m.monster_id] + entry = basename_to_info[basename] + entry[0] += 1 + entry[1] = max(entry[1], m.monster_id) + + entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] + return max(entries)[2] + + def _is_low_priority_monster(self, m: DgMonster): + lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] + lp_substrings = ['tamadra'] + lp_min_rarity = 2 + name = m.name_en.lower() + + failed_type = m.type1 in lp_types + failed_ss = any([x in name for x in lp_substrings]) + failed_rarity = m.rarity < lp_min_rarity + failed_chibi = name == m.name_en and m.name_en != m.name_ja + failed_equip = m.is_equip + return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip + + def _is_low_priority_group(self, mg: list): + lp_grp_min_rarity = 5 + max_rarity = max(m.rarity for m in mg) + failed_max_rarity = max_rarity < lp_grp_min_rarity + return failed_max_rarity + + +class NamedMonster(object): + def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set): + # Must not hold onto monster or monster_group! + + # Hold on to the IDs instead + self.monster_id = monster.monster_id + self.monster_no_na = monster.monster_no_na + self.monster_no_jp = monster.monster_no_jp + + # ID of the root of the tree for this monster + self.base_monster_no = monster_group.base_monster_no + self.base_monster_no_na = monster_group.base_monster_no_na + + # This stuff is important for nickname generation + self.group_basenames = monster_group.basenames + self.prefixes = prefixes + + # Pantheon + self.series = monster.series.name if monster.series else None + + # Data used to determine how to rank the nicknames + self.is_low_priority = monster_group.is_low_priority or monster.is_equip + self.group_size = monster_group.group_size + self.rarity = monster.rarity + + # Used in fallback searches + self.name_en = monster.name_en + self.name_ja = monster.name_ja + + # These are just extra metadata + self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] + self.group_computed_basename = monster_group.computed_basename + self.extra_nicknames = extra_nicknames + + # Compute any extra prefixes + if self.monster_basename in ('ana', 'ace'): + self.prefixes.add(self.monster_basename) + + # Compute extra basenames by checking for two-word basenames and using the second half + self.two_word_basenames = set() + for basename in self.group_basenames: + basename_words = basename.split(' ') + if len(basename_words) == 2: + self.two_word_basenames.add(basename_words[1]) + + # The primary result nicknames + self.final_nicknames = set() + # Set the configured override nicknames + self.final_nicknames.update(self.extra_nicknames) + # Set the roma subname for JP monsters + if monster.roma_subname: + self.final_nicknames.add(monster.roma_subname) + + # For each basename, add nicknames + for basename in self.group_basenames: + # Add the basename directly + self.final_nicknames.add(basename) + # Add the prefix plus basename, and the prefix with a space between basename + for prefix in self.prefixes: + self.final_nicknames.add(prefix + basename) + self.final_nicknames.add(prefix + ' ' + basename) + + self.final_two_word_nicknames = set() + # Slightly different process for two-word basenames. Does this make sense? Who knows. + for basename in self.two_word_basenames: + self.final_two_word_nicknames.add(basename) + # Add the prefix plus basename, and the prefix with a space between basename + for prefix in self.prefixes: + self.final_two_word_nicknames.add(prefix + basename) + self.final_two_word_nicknames.add(prefix + ' ' + basename) From bc44dc2338565fa5a43b9d2e4754cda2d32e4703 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 10 Oct 2020 01:32:44 -0700 Subject: [PATCH 0150/1220] PDB Series Manipulation Draft --- padguidedb/padguidedb.py | 138 ++++++++++++++++++++++++++++++--------- 1 file changed, 107 insertions(+), 31 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index c1c8ecb2..72f61ed4 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -5,6 +5,7 @@ import json import logging import pymysql +from datetime import datetime from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify @@ -21,6 +22,13 @@ def is_padguidedb_admin_check(ctx): def is_padguidedb_admin(): return commands.check(is_padguidedb_admin_check) +TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) + +SERIES_KEYS = { + "name_en": 'Untranslated', + "name_ja": 'Untranslated', + "name_ko": 'Untranslated', +} class PadGuideDb(commands.Cog): """PadGuide Database manipulator""" @@ -96,7 +104,6 @@ async def setconfigfile(self, ctx, *, config_file): await ctx.tick() @padguidedb.command() - @is_padguidedb_admin() async def searchdungeon(self, ctx, *, search_text): """Search""" search_text = '%{}%'.format(search_text) @@ -111,40 +118,74 @@ async def searchdungeon(self, ctx, *, search_text): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.command() - @checks.is_owner() - async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): - """Set the dungeon script file.""" - self.settings.setDungeonScriptFile(dungeon_script_file) - await ctx.tick() + @padguidedb.group() + async def series(self, ctx): + """Series""" - @padguidedb.command() - @checks.is_owner() - async def setfulletlfile(self, ctx, *, full_etl_file): - """Set the full ETL file.""" - self.settings.setFullETLFile(full_etl_file) - await ctx.tick() + @series.command(name="search") + async def series_search(self, ctx, *, search_text): + """Search""" + search_text = '%{}%'.format(search_text) + with self.get_cursor() as cursor: + sql = ('select series_id, name_en, name_ja from series' + ' where lower(name_en) like %s or lower(name_ja) like %s' + ' order by series_id desc limit 20') + cursor.execute(sql, [search_text, search_text]) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(sql)) + for page in pagify(msg): + await ctx.send(box(page)) - @padguidedb.command() - @checks.is_owner() - async def setimageupdatefile(self, ctx, *, image_update_file): - """Set the image update file.""" - self.settings.setImageUpdateFile(image_update_file) - await ctx.tick() + @series.command(name="add") + async def series_add(self, ctx, *, elements: TokenConverter): + """Add""" + if not all(x in SERIES_KEYS for x in elements): + await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + return - @padguidedb.command() - @checks.is_owner() - async def setpythonexecutable(self, ctx, *, python_executable): - """Set the python executable file.""" - self.settings.setPythonExecutable(python_executable) - await ctx.tick() + EXTRAS = {} + with self.get_cursor() as cursor: + cursor.execute("SELECT MAX(series_id) FROM series") + max_val = cursor.fetchall()[0]['MAX(series_id)'] + EXTRAS['series_id'] = max_val + 1 + EXTRAS['tstamp'] = int(datetime.now().timestamp()) + elements = {**SERIES_KEYS, **EXTRAS, **elements} + + key_infix = ", ".join(elements.keys()) + value_infix = ", ".join("%s" for v in elements.values()) + with self.get_cursor() as cursor: + sql = ('INSERT INTO series ({})' + ' VALUES ({})').format(key_infix, value_infix) + affected = cursor.execute(sql, (*elements.values(),)) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) added.".format(affected)) + + @series.command(name="edit") + async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): + """Edit""" + if not all(x in SERIES_KEYS for x in elements): + await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + return - @padguidedb.command() - @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): - """Set the dungeon script.""" - self.settings.setUserInfo(server, user_uuid, user_intid) - await ctx.tick() + replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) + with self.get_cursor() as cursor: + sql = ('UPDATE series' + ' SET {}' + ' WHERE series_id = %s').format(replacement_infix) + affected = cursor.execute(sql, (*elements.values(),) + (series_id,)) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) changed.".format(affected)) + + @series.command(name="delete") + async def series_delete(self, ctx, series_id: int): + """Delete""" + with self.get_cursor() as cursor: + sql = ('DELETE FROM series' + ' WHERE series_id = %s') + affected = cursor.execute(sql, series_id) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) deleted.".format(affected)) @padguidedb.command() @is_padguidedb_admin() @@ -324,6 +365,41 @@ async def extractimages(self, ctx): return await ctx.send(inline('Image extract finished')) + @padguidedb.command() + @checks.is_owner() + async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): + """Set the dungeon script file.""" + self.settings.setDungeonScriptFile(dungeon_script_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setfulletlfile(self, ctx, *, full_etl_file): + """Set the full ETL file.""" + self.settings.setFullETLFile(full_etl_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setimageupdatefile(self, ctx, *, image_update_file): + """Set the image update file.""" + self.settings.setImageUpdateFile(image_update_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setpythonexecutable(self, ctx, *, python_executable): + """Set the python executable file.""" + self.settings.setPythonExecutable(python_executable) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): + """Set the dungeon script.""" + self.settings.setUserInfo(server, user_uuid, user_intid) + await ctx.tick() + class PadGuideDbSettings(CogSettings): def make_default_settings(self): From 23a4b1eee01dd957eca4c310477a0f7fcc58a037 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 10 Oct 2020 03:48:03 -0500 Subject: [PATCH 0151/1220] Revert "PDB Series Manipulation Draft" --- padguidedb/padguidedb.py | 138 +++++++++------------------------------ 1 file changed, 31 insertions(+), 107 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 72f61ed4..c1c8ecb2 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -5,7 +5,6 @@ import json import logging import pymysql -from datetime import datetime from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify @@ -22,13 +21,6 @@ def is_padguidedb_admin_check(ctx): def is_padguidedb_admin(): return commands.check(is_padguidedb_admin_check) -TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) - -SERIES_KEYS = { - "name_en": 'Untranslated', - "name_ja": 'Untranslated', - "name_ko": 'Untranslated', -} class PadGuideDb(commands.Cog): """PadGuide Database manipulator""" @@ -104,6 +96,7 @@ async def setconfigfile(self, ctx, *, config_file): await ctx.tick() @padguidedb.command() + @is_padguidedb_admin() async def searchdungeon(self, ctx, *, search_text): """Search""" search_text = '%{}%'.format(search_text) @@ -118,74 +111,40 @@ async def searchdungeon(self, ctx, *, search_text): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.group() - async def series(self, ctx): - """Series""" + @padguidedb.command() + @checks.is_owner() + async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): + """Set the dungeon script file.""" + self.settings.setDungeonScriptFile(dungeon_script_file) + await ctx.tick() - @series.command(name="search") - async def series_search(self, ctx, *, search_text): - """Search""" - search_text = '%{}%'.format(search_text) - with self.get_cursor() as cursor: - sql = ('select series_id, name_en, name_ja from series' - ' where lower(name_en) like %s or lower(name_ja) like %s' - ' order by series_id desc limit 20') - cursor.execute(sql, [search_text, search_text]) - results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) - await ctx.send(inline(sql)) - for page in pagify(msg): - await ctx.send(box(page)) + @padguidedb.command() + @checks.is_owner() + async def setfulletlfile(self, ctx, *, full_etl_file): + """Set the full ETL file.""" + self.settings.setFullETLFile(full_etl_file) + await ctx.tick() - @series.command(name="add") - async def series_add(self, ctx, *, elements: TokenConverter): - """Add""" - if not all(x in SERIES_KEYS for x in elements): - await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) - return + @padguidedb.command() + @checks.is_owner() + async def setimageupdatefile(self, ctx, *, image_update_file): + """Set the image update file.""" + self.settings.setImageUpdateFile(image_update_file) + await ctx.tick() - EXTRAS = {} - with self.get_cursor() as cursor: - cursor.execute("SELECT MAX(series_id) FROM series") - max_val = cursor.fetchall()[0]['MAX(series_id)'] - EXTRAS['series_id'] = max_val + 1 - EXTRAS['tstamp'] = int(datetime.now().timestamp()) - elements = {**SERIES_KEYS, **EXTRAS, **elements} - - key_infix = ", ".join(elements.keys()) - value_infix = ", ".join("%s" for v in elements.values()) - with self.get_cursor() as cursor: - sql = ('INSERT INTO series ({})' - ' VALUES ({})').format(key_infix, value_infix) - affected = cursor.execute(sql, (*elements.values(),)) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) added.".format(affected)) - - @series.command(name="edit") - async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): - """Edit""" - if not all(x in SERIES_KEYS for x in elements): - await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) - return + @padguidedb.command() + @checks.is_owner() + async def setpythonexecutable(self, ctx, *, python_executable): + """Set the python executable file.""" + self.settings.setPythonExecutable(python_executable) + await ctx.tick() - replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) - with self.get_cursor() as cursor: - sql = ('UPDATE series' - ' SET {}' - ' WHERE series_id = %s').format(replacement_infix) - affected = cursor.execute(sql, (*elements.values(),) + (series_id,)) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) changed.".format(affected)) - - @series.command(name="delete") - async def series_delete(self, ctx, series_id: int): - """Delete""" - with self.get_cursor() as cursor: - sql = ('DELETE FROM series' - ' WHERE series_id = %s') - affected = cursor.execute(sql, series_id) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) deleted.".format(affected)) + @padguidedb.command() + @checks.is_owner() + async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): + """Set the dungeon script.""" + self.settings.setUserInfo(server, user_uuid, user_intid) + await ctx.tick() @padguidedb.command() @is_padguidedb_admin() @@ -365,41 +324,6 @@ async def extractimages(self, ctx): return await ctx.send(inline('Image extract finished')) - @padguidedb.command() - @checks.is_owner() - async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): - """Set the dungeon script file.""" - self.settings.setDungeonScriptFile(dungeon_script_file) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setfulletlfile(self, ctx, *, full_etl_file): - """Set the full ETL file.""" - self.settings.setFullETLFile(full_etl_file) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setimageupdatefile(self, ctx, *, image_update_file): - """Set the image update file.""" - self.settings.setImageUpdateFile(image_update_file) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setpythonexecutable(self, ctx, *, python_executable): - """Set the python executable file.""" - self.settings.setPythonExecutable(python_executable) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): - """Set the dungeon script.""" - self.settings.setUserInfo(server, user_uuid, user_intid) - await ctx.tick() - class PadGuideDbSettings(CogSettings): def make_default_settings(self): From 34227777b9415e446fb079d6fec761ebf0e01e3b Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 10 Oct 2020 01:53:03 -0700 Subject: [PATCH 0152/1220] Revert "Revert "PDB Series Manipulation Draft"" This reverts commit 23a4b1eee01dd957eca4c310477a0f7fcc58a037. --- padguidedb/padguidedb.py | 138 ++++++++++++++++++++++++++++++--------- 1 file changed, 107 insertions(+), 31 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index c1c8ecb2..72f61ed4 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -5,6 +5,7 @@ import json import logging import pymysql +from datetime import datetime from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify @@ -21,6 +22,13 @@ def is_padguidedb_admin_check(ctx): def is_padguidedb_admin(): return commands.check(is_padguidedb_admin_check) +TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) + +SERIES_KEYS = { + "name_en": 'Untranslated', + "name_ja": 'Untranslated', + "name_ko": 'Untranslated', +} class PadGuideDb(commands.Cog): """PadGuide Database manipulator""" @@ -96,7 +104,6 @@ async def setconfigfile(self, ctx, *, config_file): await ctx.tick() @padguidedb.command() - @is_padguidedb_admin() async def searchdungeon(self, ctx, *, search_text): """Search""" search_text = '%{}%'.format(search_text) @@ -111,40 +118,74 @@ async def searchdungeon(self, ctx, *, search_text): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.command() - @checks.is_owner() - async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): - """Set the dungeon script file.""" - self.settings.setDungeonScriptFile(dungeon_script_file) - await ctx.tick() + @padguidedb.group() + async def series(self, ctx): + """Series""" - @padguidedb.command() - @checks.is_owner() - async def setfulletlfile(self, ctx, *, full_etl_file): - """Set the full ETL file.""" - self.settings.setFullETLFile(full_etl_file) - await ctx.tick() + @series.command(name="search") + async def series_search(self, ctx, *, search_text): + """Search""" + search_text = '%{}%'.format(search_text) + with self.get_cursor() as cursor: + sql = ('select series_id, name_en, name_ja from series' + ' where lower(name_en) like %s or lower(name_ja) like %s' + ' order by series_id desc limit 20') + cursor.execute(sql, [search_text, search_text]) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(sql)) + for page in pagify(msg): + await ctx.send(box(page)) - @padguidedb.command() - @checks.is_owner() - async def setimageupdatefile(self, ctx, *, image_update_file): - """Set the image update file.""" - self.settings.setImageUpdateFile(image_update_file) - await ctx.tick() + @series.command(name="add") + async def series_add(self, ctx, *, elements: TokenConverter): + """Add""" + if not all(x in SERIES_KEYS for x in elements): + await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + return - @padguidedb.command() - @checks.is_owner() - async def setpythonexecutable(self, ctx, *, python_executable): - """Set the python executable file.""" - self.settings.setPythonExecutable(python_executable) - await ctx.tick() + EXTRAS = {} + with self.get_cursor() as cursor: + cursor.execute("SELECT MAX(series_id) FROM series") + max_val = cursor.fetchall()[0]['MAX(series_id)'] + EXTRAS['series_id'] = max_val + 1 + EXTRAS['tstamp'] = int(datetime.now().timestamp()) + elements = {**SERIES_KEYS, **EXTRAS, **elements} + + key_infix = ", ".join(elements.keys()) + value_infix = ", ".join("%s" for v in elements.values()) + with self.get_cursor() as cursor: + sql = ('INSERT INTO series ({})' + ' VALUES ({})').format(key_infix, value_infix) + affected = cursor.execute(sql, (*elements.values(),)) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) added.".format(affected)) + + @series.command(name="edit") + async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): + """Edit""" + if not all(x in SERIES_KEYS for x in elements): + await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + return - @padguidedb.command() - @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): - """Set the dungeon script.""" - self.settings.setUserInfo(server, user_uuid, user_intid) - await ctx.tick() + replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) + with self.get_cursor() as cursor: + sql = ('UPDATE series' + ' SET {}' + ' WHERE series_id = %s').format(replacement_infix) + affected = cursor.execute(sql, (*elements.values(),) + (series_id,)) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) changed.".format(affected)) + + @series.command(name="delete") + async def series_delete(self, ctx, series_id: int): + """Delete""" + with self.get_cursor() as cursor: + sql = ('DELETE FROM series' + ' WHERE series_id = %s') + affected = cursor.execute(sql, series_id) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) deleted.".format(affected)) @padguidedb.command() @is_padguidedb_admin() @@ -324,6 +365,41 @@ async def extractimages(self, ctx): return await ctx.send(inline('Image extract finished')) + @padguidedb.command() + @checks.is_owner() + async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): + """Set the dungeon script file.""" + self.settings.setDungeonScriptFile(dungeon_script_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setfulletlfile(self, ctx, *, full_etl_file): + """Set the full ETL file.""" + self.settings.setFullETLFile(full_etl_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setimageupdatefile(self, ctx, *, image_update_file): + """Set the image update file.""" + self.settings.setImageUpdateFile(image_update_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setpythonexecutable(self, ctx, *, python_executable): + """Set the python executable file.""" + self.settings.setPythonExecutable(python_executable) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): + """Set the dungeon script.""" + self.settings.setUserInfo(server, user_uuid, user_intid) + await ctx.tick() + class PadGuideDbSettings(CogSettings): def make_default_settings(self): From e02a0cb6ffd70311e318e7c7ba0d45110a41dc93 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 10 Oct 2020 21:27:40 -0700 Subject: [PATCH 0153/1220] PDE Fix --- padevents/padevents.py | 51 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 20cdd34f..f7555dd1 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -2,6 +2,7 @@ import datetime import discord import logging +import itertools import prettytable import pytz import re @@ -84,7 +85,7 @@ async def check_started(self): daily_refresh_servers = set() for e in events: self.started_events.add(e.key) - if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek]: + if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, EventType.Week]: for gr in list(self.settings.listGuerrillaReg()): if e.server == gr['server']: try: @@ -271,6 +272,7 @@ def makeActiveText(self, server): msg = "Listing all events for " + server + """ special_events = active_events.withType( EventType.Special).itemsByCloseTime() if len(special_events) > 0: @@ -304,7 +306,43 @@ def makeActiveText(self, server): msg += "\n\n" + \ self.makeFullGuerrillaOutput( 'Guerrilla Events', guerrilla_events, starter_guerilla=True) + """ + active_cdo_events = active_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped(True).items() + if len(active_cdo_events) > 0: + msg += "\n\n" + \ + self.makeActiveOutput( + 'Active Events', active_cdo_events) + """ + cdo_events = pending_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped(False).items() + if len(cdo_events) > 0: + msg += "\n\n" + \ + self.makeActiveOutput( + 'Reward Events', cdo_events) + """ + active_grouped_cdo_events = active_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped().items() + if len(active_grouped_cdo_events) > 0: + msg += "\n\n" + \ + self.makeActiveGuerrillaOutput( + 'Active Guerrillas', active_grouped_cdo_events) + grouped_cdo_events = pending_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped().items() + if len(grouped_cdo_events) > 0: + msg += "\n\n" + \ + self.makeFullGuerrillaOutput( + 'Guerrillas', grouped_cdo_events, starter_guerilla=True) + + active_etc_events = active_events.withDungeonType(DungeonType.Etc).items() + if len(active_etc_events) > 0: + msg += "\n\n" + \ + self.makeActiveOutput( + 'Active Other Events', active_etc_events) + """ + etc_events = pending_events.withDungeonType(DungeonType.Etc).items() + if len(etc_events) > 0: + msg += "\n\n" + \ + self.makeActiveOutput( + 'Other Events', etc_events) + """ # clean up long headers msg = msg.replace('-------------------------------------', '-----------------------') @@ -353,7 +391,7 @@ def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False events = sorted(events, key=lambda e: e.open_datetime) events_by_group = defaultdict(list) for e in events: - events_by_group[e.group].append(e) + events_by_group[e.group.upper()].append(e) done = False while not done: @@ -408,7 +446,8 @@ async def doPartial(self, ctx, server): events = EventList(self.events) events = events.withServer(server) - events = events.inType([EventType.Guerrilla, EventType.SpecialWeek]) + events = events.inDungeonType([DungeonType.Etc, DungeonType.CoinDailyOther]) + events = events.isGrouped() active_events = events.activeOnly().itemsByOpenTime(reverse=True) pending_events = events.pendingOnly().itemsByOpenTime(reverse=True) @@ -602,6 +641,12 @@ def inType(self, event_types): def withDungeonType(self, dungeon_type, exclude=False): return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) + def inDungeonType(self, dungeon_types, exclude=False): + return self.withFunc(lambda e: e.dungeon_type in dungeon_types, exclude) + + def isGrouped(self, exclude=False): + return self.withFunc(lambda e: e.group is not None, exclude) + def withNameContains(self, name, exclude=False): return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) From ee583ac871d473177aefd6c7b53f44e531eee94d Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 10 Oct 2020 22:43:51 -0700 Subject: [PATCH 0154/1220] Dadguide Draft Commit #1 Added non-query monster generation. Added id3 start --- dadguide/dadguide.py | 2 +- dadguide/database_manager.py | 19 ++++++++++++++++-- padinfo/find_monster.py | 29 ++++++++++++++++++++++++++++ padinfo/padinfo.py | 37 ++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 padinfo/find_monster.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index e879ba4d..f30511b5 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -34,8 +34,8 @@ def _data_file(file_name: str) -> str: TRANSLATEDNAMES_EXPORT_PATH = _data_file('translated_names.json') SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/pub?gid={}&single=true&output=csv' -GROUP_BASENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') +GROUP_BASENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') NICKNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('nicknames')) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index ec2fe37d..25c8341e 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -96,6 +96,10 @@ def __init__(self, data_file): self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) self._con.row_factory = lite.Row + + self._monsters = None + self.expiry = 0 + def has_database(self): return self._con is not None @@ -413,17 +417,28 @@ def get_na_only_monsters(self): '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format(DgMonster.TABLE), ()) - def get_monster(self, monster_id: int): + def get_monster_query(self, monster_id: int): return self._select_one_entry_by_pk(monster_id, DgMonster) + def get_monster(self, monster_id: int): + for monster in self.get_all_monsters(): + if monster['monster_id'] == monster_id: + return DgMonster(monster, self) + def get_all_monster_ja_name(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, as_generator=as_generator) - def get_all_monsters(self, as_generator=True): + def get_all_monsters_query(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, as_generator=as_generator) + def get_all_monsters(self): + if self._monsters == None or self.expiry < datetime.now().timestamp() + 60*60: + self._monsters = self.get_all_monsters_query(False) + self.expiry = int(datetime.now().timestamp()) + return self._monsters + def get_all_events(self, as_generator=True): return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), DgScheduledEvent, diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py new file mode 100644 index 00000000..6192e0c2 --- /dev/null +++ b/padinfo/find_monster.py @@ -0,0 +1,29 @@ +import re + +COLOR_MAP = {} + +for r in ('r','red','fire'): COLOR_MAP[r] = 'r' +for b in ('b','blue','water'): COLOR_MAP[b] = 'b' +for g in ('g','green','wood'): COLOR_MAP[g] = 'g' +for l in ('l','light','yellow', 'white'): COLOR_MAP[l] = 'l' +for d in ('d','dark','purple', 'black'): COLOR_MAP[d] = 'd' +for x in ('x','none','null', 'nil'): COLOR_MAP[x] = 'x' + +COLORS = 'rbgld?x' + +def prefix_to_filter(prefix): + prefix = prefix.lower() + + # light, nil, wood + if prefix in COLOR_MAP: + m = COLORS.index(COLOR_MAP[prefix]) + def filter_via_attribute(monster): + return monster.attr1.value == m + + # light/red, l/r, yellow/fire + match = re.match(r'^({0})/?({0})$'.format('|'.join(COLOR_MAP)), prefix) + if match: + m, s = [COLORS.index(COLOR_MAP[a]) for a in match.groups()] + def filter_via_attributes(monster): + return monster.attr1.value == m and monster.attr2.value == s + return filter_via_attributes diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 76160cd8..1f627518 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -15,6 +15,8 @@ from redbot.core.utils.chat_formatting import box, inline from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json +from .find_monster import prefix_to_filter + logger = logging.getLogger('red.padbot-cogs.padinfo') HELP_MSG = """ @@ -311,6 +313,19 @@ async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): else: await ctx.send(self.makeFailureMsg(err)) + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def id3(self, ctx, *, query: str): + """Monster info (main tab)""" + await self._do_id3(ctx, query) + + async def _do_id3(self, ctx, query: str): + m = await self.findMonster3(query) + if m is not None: + await self._do_idmenu(ctx, m, self.id_emoji) + else: + await ctx.send("whoops") + @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): @@ -744,6 +759,28 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster2(query) + async def findMonster3(self, query): + return await self._findMonster3(query) + + async def _findMonster3(self, query): + DGCOG = self.bot.get_cog("Dadguide") + if DGCOG is None: + raise ValueError("Dadguide cog is not loaded") + + query = rmdiacritics(query).split() + monstergen = DGCOG.database.get_all_monsters() + for c, token in enumerate(query): + try: + filt = prefix_to_filter(token) + except: + print(token) + raise + if filt is None: + monster_name = " ".join(query[c:]) + break + monstergen = filter(filt, monstergen) + return max(monstergen, key=lambda x: (not x.is_equip, x.rarity, x.monster_no_na)) + class PadInfoSettings(CogSettings): def make_default_settings(self): From 471a9b18ee77fe22f6df12ba6b53fe78b44e754a Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 10 Oct 2020 23:58:45 -0700 Subject: [PATCH 0155/1220] Fix generator and expiry --- dadguide/database_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 25c8341e..aae41653 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -433,10 +433,12 @@ def get_all_monsters_query(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, as_generator=as_generator) - def get_all_monsters(self): - if self._monsters == None or self.expiry < datetime.now().timestamp() + 60*60: + def get_all_monsters(self, as_generator=False): + if self._monsters == None or self.expiry < datetime.now().timestamp(): self._monsters = self.get_all_monsters_query(False) - self.expiry = int(datetime.now().timestamp()) + self.expiry = int(datetime.now().timestamp()) + 60*60 + if as_generator: + return (m for m in self._monsters) return self._monsters def get_all_events(self, as_generator=True): From 61a032e022c89040116dec419a8ed0a44ce41206 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Oct 2020 01:35:26 -0700 Subject: [PATCH 0156/1220] Refresh Monster --- dadguide/database_manager.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index aae41653..3b307636 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -421,22 +421,26 @@ def get_monster_query(self, monster_id: int): return self._select_one_entry_by_pk(monster_id, DgMonster) def get_monster(self, monster_id: int): - for monster in self.get_all_monsters(): - if monster['monster_id'] == monster_id: - return DgMonster(monster, self) + self.refresh_monsters() + m = self._monsters.get(monster_id) + if m is not None: + return DgMonster(m, self) def get_all_monster_ja_name(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, as_generator=as_generator) + def refresh_monsters(self): + if self._monsters == None or self.expiry < datetime.now().timestamp(): + self._monsters = {m.monster_id: m for m in self.get_all_monsters_query(False)} + self.expiry = int(datetime.now().timestamp()) + 60*60 + def get_all_monsters_query(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, as_generator=as_generator) def get_all_monsters(self, as_generator=False): - if self._monsters == None or self.expiry < datetime.now().timestamp(): - self._monsters = self.get_all_monsters_query(False) - self.expiry = int(datetime.now().timestamp()) + 60*60 + self.refresh_monsters() if as_generator: return (m for m in self._monsters) return self._monsters From 5e8121ce7b55fc2b75beb3a4c5b60b1b724cf3e9 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Oct 2020 10:30:00 -0700 Subject: [PATCH 0157/1220] add autoping role events --- padevents/padevents.py | 38 ++++++++++++++++++++++++++++++++++++-- padinfo/padinfo.py | 2 +- padmonitor/padmonitor.py | 2 +- padsearch/padsearch.py | 2 +- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 20cdd34f..5a6fb519 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -10,14 +10,14 @@ from datetime import timedelta from enum import Enum from redbot.core import checks -from redbot.core import commands +from redbot.core import commands, Config from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.padevents') SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] - +GROUPS = ['red', 'blue', 'green'] class PadEvents(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -25,6 +25,8 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.settings = PadEventSettings("padevents") + self.config = Config.get_conf(self, identifier=940373775) + self.config.register_global(pingroles={}) # Load event data self.events = list() @@ -111,6 +113,19 @@ async def check_started(self): if not e.dungeon_type in [DungeonType.Normal]: daily_refresh_servers.add(e.server) + pingroles = await self.config.pingroles() + if e.dungeon_name in pingroles: + for cid in pingroles[e.dungeon_name]: + channel = self.bot.get_channel(int(cid)) + if channel is not None: + index = GROUPS.index(e.group) + if channel.guild.get_role(pingroles[e.dungeon_name][cid][index]): + ment = channel.guild.get_role(pingroles[e.dungeon_name][cid][index]).mention + else: + ment = "#deleted-role" + await channel.send("{} {}".format(e.dungeon_name, ment)) + + for server in daily_refresh_servers: msg = self.makeActiveText(server) for daily_registration in list(self.settings.listDailyReg()): @@ -232,6 +247,25 @@ async def rmchanneldaily(self, ctx, server): self.settings.removeDailyReg(channel_id, server) await ctx.send("Channel deactivated.") + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def autoeventping(self, ctx, name, red: discord.Role, blue: discord.Role, green: discord.Role): + async with self.config.pingroles() as pingroles: + pingroles[name] = {ctx.channel.id: (red.id, blue.id, green.id)} + await ctx.tick() + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def rmautoeventping(self, ctx, *, name): + async with self.config.pingroles() as pingroles: + if name not in pingroles or ctx.guild.id not in pingroles[name]: + await ctx.send("That name is not set.") + return + n = pingroles[name][ctx.guild.id] + await ctx.tick() + @padevents.command(name="listchannels") @checks.mod_or_permissions(manage_guild=True) async def _listchannel(self, ctx): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1f627518..3533034e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -768,7 +768,7 @@ async def _findMonster3(self, query): raise ValueError("Dadguide cog is not loaded") query = rmdiacritics(query).split() - monstergen = DGCOG.database.get_all_monsters() + monstergen = DGCOG.database.get_all_monsters().values() for c, token in enumerate(query): try: filt = prefix_to_filter(token) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index ff000706..ca5a715f 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -39,7 +39,7 @@ async def check_seen(self): """Refresh the monster indexes.""" pg_cog = self.bot.get_cog('Dadguide') await pg_cog.wait_until_ready() - all_monsters = pg_cog.database.get_all_monsters(as_generator=False) + all_monsters = pg_cog.database.get_all_monsters(as_generator=False).values() jp_monster_map = {m.monster_no: m for m in all_monsters if m.on_jp} na_monster_map = {m.monster_no: m for m in all_monsters if m.on_na} diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index bd4ecb1d..073e2fc9 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -760,7 +760,7 @@ async def search(self, ctx, *, filter_spec: str): if dg_cog == None: await ctx.send("Dadguide cog not loaded.") return - monsters = dg_cog.database.get_all_monsters() + monsters = dg_cog.database.get_all_monsters().values() rmvGemFilter = self._make_search_config('remove( gem)') monsters = list(filter(rmvGemFilter.check_filters, monsters)) From e196f3da365c055bb53070dd5f1afd8cf5d67e8b Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Oct 2020 10:30:40 -0700 Subject: [PATCH 0158/1220] Revert "add autoping role events" This reverts commit 5e8121ce7b55fc2b75beb3a4c5b60b1b724cf3e9. --- padevents/padevents.py | 38 ++------------------------------------ padinfo/padinfo.py | 2 +- padmonitor/padmonitor.py | 2 +- padsearch/padsearch.py | 2 +- 4 files changed, 5 insertions(+), 39 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 5a6fb519..20cdd34f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -10,14 +10,14 @@ from datetime import timedelta from enum import Enum from redbot.core import checks -from redbot.core import commands, Config +from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.padevents') SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] -GROUPS = ['red', 'blue', 'green'] + class PadEvents(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -25,8 +25,6 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.settings = PadEventSettings("padevents") - self.config = Config.get_conf(self, identifier=940373775) - self.config.register_global(pingroles={}) # Load event data self.events = list() @@ -113,19 +111,6 @@ async def check_started(self): if not e.dungeon_type in [DungeonType.Normal]: daily_refresh_servers.add(e.server) - pingroles = await self.config.pingroles() - if e.dungeon_name in pingroles: - for cid in pingroles[e.dungeon_name]: - channel = self.bot.get_channel(int(cid)) - if channel is not None: - index = GROUPS.index(e.group) - if channel.guild.get_role(pingroles[e.dungeon_name][cid][index]): - ment = channel.guild.get_role(pingroles[e.dungeon_name][cid][index]).mention - else: - ment = "#deleted-role" - await channel.send("{} {}".format(e.dungeon_name, ment)) - - for server in daily_refresh_servers: msg = self.makeActiveText(server) for daily_registration in list(self.settings.listDailyReg()): @@ -247,25 +232,6 @@ async def rmchanneldaily(self, ctx, server): self.settings.removeDailyReg(channel_id, server) await ctx.send("Channel deactivated.") - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def autoeventping(self, ctx, name, red: discord.Role, blue: discord.Role, green: discord.Role): - async with self.config.pingroles() as pingroles: - pingroles[name] = {ctx.channel.id: (red.id, blue.id, green.id)} - await ctx.tick() - - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmautoeventping(self, ctx, *, name): - async with self.config.pingroles() as pingroles: - if name not in pingroles or ctx.guild.id not in pingroles[name]: - await ctx.send("That name is not set.") - return - n = pingroles[name][ctx.guild.id] - await ctx.tick() - @padevents.command(name="listchannels") @checks.mod_or_permissions(manage_guild=True) async def _listchannel(self, ctx): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3533034e..1f627518 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -768,7 +768,7 @@ async def _findMonster3(self, query): raise ValueError("Dadguide cog is not loaded") query = rmdiacritics(query).split() - monstergen = DGCOG.database.get_all_monsters().values() + monstergen = DGCOG.database.get_all_monsters() for c, token in enumerate(query): try: filt = prefix_to_filter(token) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index ca5a715f..ff000706 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -39,7 +39,7 @@ async def check_seen(self): """Refresh the monster indexes.""" pg_cog = self.bot.get_cog('Dadguide') await pg_cog.wait_until_ready() - all_monsters = pg_cog.database.get_all_monsters(as_generator=False).values() + all_monsters = pg_cog.database.get_all_monsters(as_generator=False) jp_monster_map = {m.monster_no: m for m in all_monsters if m.on_jp} na_monster_map = {m.monster_no: m for m in all_monsters if m.on_na} diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 073e2fc9..bd4ecb1d 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -760,7 +760,7 @@ async def search(self, ctx, *, filter_spec: str): if dg_cog == None: await ctx.send("Dadguide cog not loaded.") return - monsters = dg_cog.database.get_all_monsters().values() + monsters = dg_cog.database.get_all_monsters() rmvGemFilter = self._make_search_config('remove( gem)') monsters = list(filter(rmvGemFilter.check_filters, monsters)) From d26e9e118b9e98e0d25214e88d1c6f67e4e5823e Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Oct 2020 10:31:17 -0700 Subject: [PATCH 0159/1220] add autoping role events --- padevents/padevents.py | 38 ++++++++++++++++++++++++++++++++++++-- padmonitor/padmonitor.py | 2 +- padsearch/padsearch.py | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index f7555dd1..330a30a8 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -11,14 +11,14 @@ from datetime import timedelta from enum import Enum from redbot.core import checks -from redbot.core import commands +from redbot.core import commands, Config from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings logger = logging.getLogger('red.padbot-cogs.padevents') SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] - +GROUPS = ['red', 'blue', 'green'] class PadEvents(commands.Cog): def __init__(self, bot, *args, **kwargs): @@ -26,6 +26,8 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.settings = PadEventSettings("padevents") + self.config = Config.get_conf(self, identifier=940373775) + self.config.register_global(pingroles={}) # Load event data self.events = list() @@ -112,6 +114,19 @@ async def check_started(self): if not e.dungeon_type in [DungeonType.Normal]: daily_refresh_servers.add(e.server) + pingroles = await self.config.pingroles() + if e.dungeon_name in pingroles: + for cid in pingroles[e.dungeon_name]: + channel = self.bot.get_channel(int(cid)) + if channel is not None: + index = GROUPS.index(e.group) + if channel.guild.get_role(pingroles[e.dungeon_name][cid][index]): + ment = channel.guild.get_role(pingroles[e.dungeon_name][cid][index]).mention + else: + ment = "#deleted-role" + await channel.send("{} {}".format(e.dungeon_name, ment)) + + for server in daily_refresh_servers: msg = self.makeActiveText(server) for daily_registration in list(self.settings.listDailyReg()): @@ -233,6 +248,25 @@ async def rmchanneldaily(self, ctx, server): self.settings.removeDailyReg(channel_id, server) await ctx.send("Channel deactivated.") + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def autoeventping(self, ctx, name, red: discord.Role, blue: discord.Role, green: discord.Role): + async with self.config.pingroles() as pingroles: + pingroles[name] = {ctx.channel.id: (red.id, blue.id, green.id)} + await ctx.tick() + + @padevents.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def rmautoeventping(self, ctx, *, name): + async with self.config.pingroles() as pingroles: + if name not in pingroles or ctx.guild.id not in pingroles[name]: + await ctx.send("That name is not set.") + return + n = pingroles[name][ctx.guild.id] + await ctx.tick() + @padevents.command(name="listchannels") @checks.mod_or_permissions(manage_guild=True) async def _listchannel(self, ctx): diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index ff000706..ca5a715f 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -39,7 +39,7 @@ async def check_seen(self): """Refresh the monster indexes.""" pg_cog = self.bot.get_cog('Dadguide') await pg_cog.wait_until_ready() - all_monsters = pg_cog.database.get_all_monsters(as_generator=False) + all_monsters = pg_cog.database.get_all_monsters(as_generator=False).values() jp_monster_map = {m.monster_no: m for m in all_monsters if m.on_jp} na_monster_map = {m.monster_no: m for m in all_monsters if m.on_na} diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index bd4ecb1d..073e2fc9 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -760,7 +760,7 @@ async def search(self, ctx, *, filter_spec: str): if dg_cog == None: await ctx.send("Dadguide cog not loaded.") return - monsters = dg_cog.database.get_all_monsters() + monsters = dg_cog.database.get_all_monsters().values() rmvGemFilter = self._make_search_config('remove( gem)') monsters = list(filter(rmvGemFilter.check_filters, monsters)) From 269d47c85c41709558bd98277beb8b19b0d6b0de Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Oct 2020 13:24:47 -0700 Subject: [PATCH 0160/1220] pingroles fix --- padevents/padevents.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 330a30a8..8e00b4d7 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -85,6 +85,7 @@ async def check_started(self): events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) daily_refresh_servers = set() + pingroles = await self.config.pingroles() for e in events: self.started_events.add(e.key) if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, EventType.Week]: @@ -114,14 +115,14 @@ async def check_started(self): if not e.dungeon_type in [DungeonType.Normal]: daily_refresh_servers.add(e.server) - pingroles = await self.config.pingroles() if e.dungeon_name in pingroles: for cid in pingroles[e.dungeon_name]: channel = self.bot.get_channel(int(cid)) if channel is not None: index = GROUPS.index(e.group) - if channel.guild.get_role(pingroles[e.dungeon_name][cid][index]): - ment = channel.guild.get_role(pingroles[e.dungeon_name][cid][index]).mention + role = channel.guild.get_role(pingroles[e.dungeon_name][cid][index]) + if role: + ment = role.mention else: ment = "#deleted-role" await channel.send("{} {}".format(e.dungeon_name, ment)) From 8594babd82b4c96ba9e53d50090c71b8ab4077d3 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 12 Oct 2020 10:26:33 -0700 Subject: [PATCH 0161/1220] quiet padsearch and padmonitor bc somehow i put these into the wrong branch --- padmonitor/padmonitor.py | 2 +- padsearch/padsearch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index ca5a715f..ff000706 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -39,7 +39,7 @@ async def check_seen(self): """Refresh the monster indexes.""" pg_cog = self.bot.get_cog('Dadguide') await pg_cog.wait_until_ready() - all_monsters = pg_cog.database.get_all_monsters(as_generator=False).values() + all_monsters = pg_cog.database.get_all_monsters(as_generator=False) jp_monster_map = {m.monster_no: m for m in all_monsters if m.on_jp} na_monster_map = {m.monster_no: m for m in all_monsters if m.on_na} diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 073e2fc9..bd4ecb1d 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -760,7 +760,7 @@ async def search(self, ctx, *, filter_spec: str): if dg_cog == None: await ctx.send("Dadguide cog not loaded.") return - monsters = dg_cog.database.get_all_monsters().values() + monsters = dg_cog.database.get_all_monsters() rmvGemFilter = self._make_search_config('remove( gem)') monsters = list(filter(rmvGemFilter.check_filters, monsters)) From 6cd9b28e03aa8d60f98dd07f0791999871e0a9d3 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 12 Oct 2020 10:33:39 -0700 Subject: [PATCH 0162/1220] Use shortened name for matching --- padevents/padevents.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 8e00b4d7..08aab295 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -115,17 +115,17 @@ async def check_started(self): if not e.dungeon_type in [DungeonType.Normal]: daily_refresh_servers.add(e.server) - if e.dungeon_name in pingroles: - for cid in pingroles[e.dungeon_name]: + if e.name_and_modifier in pingroles: + for cid in pingroles[e.name_and_modifier]: channel = self.bot.get_channel(int(cid)) if channel is not None: index = GROUPS.index(e.group) - role = channel.guild.get_role(pingroles[e.dungeon_name][cid][index]) + role = channel.guild.get_role(pingroles[e.name_and_modifier][cid][index]) if role: ment = role.mention else: ment = "#deleted-role" - await channel.send("{} {}".format(e.dungeon_name, ment)) + await channel.send("{} {}".format(e.name_and_modifier, ment)) for server in daily_refresh_servers: From 16743cfb1d84ca2b952ddfd4ef01f07c4e38c8d9 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 12 Oct 2020 12:15:00 -0700 Subject: [PATCH 0163/1220] allow role mentions in pde --- padevents/padevents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 08aab295..b0d937e7 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -106,7 +106,7 @@ async def check_started(self): except: pass # do nothing if role is missing - await channel.send(message) + await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) except Exception as ex: # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) logger.exception("caught exception while sending guerrilla msg:") @@ -125,7 +125,7 @@ async def check_started(self): ment = role.mention else: ment = "#deleted-role" - await channel.send("{} {}".format(e.name_and_modifier, ment)) + await channel.send("{} {}".format(e.name_and_modifier, ment), allowed_mentions=discord.AllowedMentions(roles=True)) for server in daily_refresh_servers: From d3c758a13a0d6e804d36a57b280bfb1e1c5ebfcd Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 12 Oct 2020 18:27:39 -0700 Subject: [PATCH 0164/1220] allow role mentions in pde (#111) --- padevents/padevents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 08aab295..b0d937e7 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -106,7 +106,7 @@ async def check_started(self): except: pass # do nothing if role is missing - await channel.send(message) + await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) except Exception as ex: # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) logger.exception("caught exception while sending guerrilla msg:") @@ -125,7 +125,7 @@ async def check_started(self): ment = role.mention else: ment = "#deleted-role" - await channel.send("{} {}".format(e.name_and_modifier, ment)) + await channel.send("{} {}".format(e.name_and_modifier, ment), allowed_mentions=discord.AllowedMentions(roles=True)) for server in daily_refresh_servers: From 940f47bda429f3fa5cc5979ddade037c8715ce6a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 12 Oct 2020 23:43:13 -0700 Subject: [PATCH 0165/1220] write crud --- crud/__init__.py | 7 ++ crud/crud.py | 201 +++++++++++++++++++++++++++++++++++++++ crud/info.json | 26 +++++ padguidedb/padguidedb.py | 69 -------------- 4 files changed, 234 insertions(+), 69 deletions(-) create mode 100644 crud/__init__.py create mode 100644 crud/crud.py create mode 100644 crud/info.json diff --git a/crud/__init__.py b/crud/__init__.py new file mode 100644 index 00000000..56a24ce2 --- /dev/null +++ b/crud/__init__.py @@ -0,0 +1,7 @@ +from .crud import * + +__red_end_user_data_statement__ = "No personal data is stored." + + +def setup(bot): + bot.add_cog(Crud(bot)) diff --git a/crud/crud.py b/crud/crud.py new file mode 100644 index 00000000..105e91e6 --- /dev/null +++ b/crud/crud.py @@ -0,0 +1,201 @@ +import asyncio +import csv +import discord +import io +import json +import logging +import pymysql +from datetime import datetime +from redbot.core import checks, commands, Config, errors +from redbot.core.utils.chat_formatting import box, inline, pagify +from tsutils import auth_check, confirm_message + +logger = logging.getLogger('red.padbot-cogs.crud') + +TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) + +SERIES_KEYS = { + "name_en": 'Untranslated', + "name_ja": 'Untranslated', + "name_ko": 'Untranslated', +} + +class Crud(commands.Cog): + """PadGuide CRUD""" + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.config = Config.get_conf(self, identifier=3270) + self.config.register_global(config_file=None) + + GADMIN_COG = self.bot.get_cog("GlobalAdmin") + if GADMIN_COG: + GADMIN_COG.register_perm("crud") + else: + raise errors.CogLoadError("Global Administration cog must be loaded.") + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + + async def get_cursor(self): + with open(await self.config.config_file(), 'r') as db_config: + return self.connect(json.load(db_config)).cursor() + + def connect(self, db_config): + return pymysql.connect(host=db_config['host'], + user=db_config['user'], + password=db_config['password'], + db=db_config['db'], + charset=db_config['charset'], + cursorclass=pymysql.cursors.DictCursor, + autocommit=True) + + @commands.group(aliases=['hera-ur']) + @auth_check('crud') + async def crud(self, ctx): + """PadGuide database CRUD.""" + + @crud.command() + async def searchdungeon(self, ctx, *, search_text): + """Search""" + search_text = '%{}%'.format(search_text) + with await self.get_cursor() as cursor: + sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' + ' where lower(name_en) like %s or lower(name_ja) like %s' + ' order by dungeon_id desc limit 20') + cursor.execute(sql, [search_text, search_text]) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(sql)) + for page in pagify(msg): + await ctx.send(box(page)) + + @crud.group() + async def series(self, ctx): + """Series""" + + @series.command(name="search") + async def series_search(self, ctx, *, search_text): + """Search""" + search_text = '%{}%'.format(search_text) + with await self.get_cursor() as cursor: + sql = ('select series_id, name_en, name_ja from series' + ' where lower(name_en) like %s or lower(name_ja) like %s' + ' order by series_id desc limit 20') + cursor.execute(sql, [search_text, search_text]) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(sql)) + for page in pagify(msg): + await ctx.send(box(page)) + + @series.command(name="add") + async def series_add(self, ctx, *, elements: TokenConverter): + """Add""" + if not all(x in SERIES_KEYS for x in elements): + await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + return + + EXTRAS = {} + with await self.get_cursor() as cursor: + cursor.execute("SELECT MAX(series_id) FROM series") + max_val = cursor.fetchall()[0]['MAX(series_id)'] + EXTRAS['series_id'] = max_val + 1 + EXTRAS['tstamp'] = int(datetime.now().timestamp()) + elements = {**SERIES_KEYS, **EXTRAS, **elements} + + key_infix = ", ".join(elements.keys()) + value_infix = ", ".join("%s" for v in elements.values()) + with await self.get_cursor() as cursor: + sql = ('INSERT INTO series ({})' + ' VALUES ({})').format(key_infix, value_infix) + affected = cursor.execute(sql, (*elements.values(),)) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) added.".format(affected)) + + @series.command(name="edit") + async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): + """Edit""" + if not all(x in SERIES_KEYS for x in elements): + await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + return + + replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) + with await self.get_cursor() as cursor: + sql = ('UPDATE series' + ' SET {}' + ' WHERE series_id = %s').format(replacement_infix) + affected = cursor.execute(sql, (*elements.values(),) + (series_id,)) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) changed.".format(affected)) + + @series.command(name="delete") + async def series_delete(self, ctx, series_id: int): + """Delete""" + with await self.get_cursor() as cursor: + sql = ('DELETE FROM series' + ' WHERE series_id = %s') + affected = cursor.execute(sql, series_id) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) deleted.".format(affected)) + + @crud.command() + async def editmonsname(self, ctx, monster_id: int, *, name): + """Change a monster's name_en_override""" + with await self.get_cursor() as cursor: + cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) + old_val = cursor.fetchall() + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['name_en_override'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " english override from `{}` to `{}`?").format(monster_id, + old_val, name)): + return + + sql = ('UPDATE monsters' + ' SET name_en_override = %s' + ' WHERE monster_id = %s') + affected = cursor.execute(sql, (name, monster_id)) + await ctx.send("Changed `{}` to `{}` via:".format(old_val, name)) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) changed.".format(affected)) + + @crud.command() + async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): + """Change a monster's series_id""" + with await self.get_cursor() as cursor: + cursor.execute("SELECT series_id FROM monsters WHERE monster_id = %s", (monster_id,)) + old_val = cursor.fetchall() + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['series_id'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " series_id from `{}` to `{}`?").format(monster_id, + old_val, series_id)): + return + + sql = ('UPDATE monsters' + ' SET series_id = %s' + ' WHERE monster_id = %s') + affected = cursor.execute(sql, (series_id, monster_id)) + await ctx.send("Changed `{}` to `{}` via:\n".format(old_val, series_id, inline(cursor._executed))) + await ctx.send("{} row(s) changed.".format(affected)) + + @crud.command() + @checks.is_owner() + async def setconfig(self, ctx, path): + await self.config.config_file.set(path) + await ctx.tick() diff --git a/crud/info.json b/crud/info.json new file mode 100644 index 00000000..037a7719 --- /dev/null +++ b/crud/info.json @@ -0,0 +1,26 @@ +{ + "author": [ + "Aradia Megido#2552" + ], + "description": "Crud for the Pipeline Database.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Pipeline CRUD", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "pymysql" + ], + "tags": [ + "PAD" + ] +} diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 72f61ed4..4c10bafe 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -118,75 +118,6 @@ async def searchdungeon(self, ctx, *, search_text): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.group() - async def series(self, ctx): - """Series""" - - @series.command(name="search") - async def series_search(self, ctx, *, search_text): - """Search""" - search_text = '%{}%'.format(search_text) - with self.get_cursor() as cursor: - sql = ('select series_id, name_en, name_ja from series' - ' where lower(name_en) like %s or lower(name_ja) like %s' - ' order by series_id desc limit 20') - cursor.execute(sql, [search_text, search_text]) - results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) - await ctx.send(inline(sql)) - for page in pagify(msg): - await ctx.send(box(page)) - - @series.command(name="add") - async def series_add(self, ctx, *, elements: TokenConverter): - """Add""" - if not all(x in SERIES_KEYS for x in elements): - await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) - return - - EXTRAS = {} - with self.get_cursor() as cursor: - cursor.execute("SELECT MAX(series_id) FROM series") - max_val = cursor.fetchall()[0]['MAX(series_id)'] - EXTRAS['series_id'] = max_val + 1 - EXTRAS['tstamp'] = int(datetime.now().timestamp()) - elements = {**SERIES_KEYS, **EXTRAS, **elements} - - key_infix = ", ".join(elements.keys()) - value_infix = ", ".join("%s" for v in elements.values()) - with self.get_cursor() as cursor: - sql = ('INSERT INTO series ({})' - ' VALUES ({})').format(key_infix, value_infix) - affected = cursor.execute(sql, (*elements.values(),)) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) added.".format(affected)) - - @series.command(name="edit") - async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): - """Edit""" - if not all(x in SERIES_KEYS for x in elements): - await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) - return - - replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) - with self.get_cursor() as cursor: - sql = ('UPDATE series' - ' SET {}' - ' WHERE series_id = %s').format(replacement_infix) - affected = cursor.execute(sql, (*elements.values(),) + (series_id,)) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) changed.".format(affected)) - - @series.command(name="delete") - async def series_delete(self, ctx, series_id: int): - """Delete""" - with self.get_cursor() as cursor: - sql = ('DELETE FROM series' - ' WHERE series_id = %s') - affected = cursor.execute(sql, series_id) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) deleted.".format(affected)) - @padguidedb.command() @is_padguidedb_admin() async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): From bff18c61a3cd3dacfb477589a028117c867a5c46 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 01:22:29 -0700 Subject: [PATCH 0166/1220] padinfo alt evos --- padinfo/padinfo.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1f627518..d9090c48 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1157,6 +1157,13 @@ def monsterToEmbed(m: "DgMonster", emoji_list): ls_header += " [ {} ]".format(multiplier_text) embed.add_field(name=ls_header, value=ls_row, inline=False) + evos_header = "Alternate Evos" + evos_body = ", ".join(f"**{m2.monster_id}**" + if m2.monster_id==m.monster_id + else f"[{m2.monster_id}]({get_pdx_url(m2)})" + for m2 in m.alt_evos) + embed.add_field(name=evos_header, value=evos_body, inline=False) + return embed From 67b7080d3c0ee48b6d00511a6729d5a7a754ea53 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 01:22:42 -0700 Subject: [PATCH 0167/1220] tokenize monsters --- dadguide/database_manager.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 3b307636..fed73e70 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -422,9 +422,7 @@ def get_monster_query(self, monster_id: int): def get_monster(self, monster_id: int): self.refresh_monsters() - m = self._monsters.get(monster_id) - if m is not None: - return DgMonster(m, self) + return self._monsters.get(monster_id) def get_all_monster_ja_name(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, @@ -453,6 +451,12 @@ def get_all_events(self, as_generator=True): def get_dungeon_by_id(self, dungeon_id: int): return self._select_one_entry_by_pk(dungeon_id, DgDungeon) + def tokenize_monsters(self): + tokens = defaultdict(list) + for mid, monster in self.get_all_monsters().items(): + for token in monster.name_en.split(): + tokens[token.lower()].append(monster) + return tokens def enum_or_none(enum, value, default=None): if value is not None: @@ -848,6 +852,9 @@ def prev_monster(self): offset += 1 return next + def __repr__(self): + return "DgMonster<{} ({})>".format(self.name_en, self.monster_no) + class MonsterSearchHelper(object): def __init__(self, m: DgMonster): From c57ebefbfae2b2a2a0dbda93dd06e5ae0b105369 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 01:37:04 -0700 Subject: [PATCH 0168/1220] expand eventsna --- padevents/padevents.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index b0d937e7..347ff368 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -490,8 +490,8 @@ async def doPartial(self, ctx, server): group_to_active_event = {e.group: e for e in active_events} group_to_pending_event = {e.group: e for e in pending_events} - active_events = list(group_to_active_event.values()) - pending_events = list(group_to_pending_event.values()) + #active_events = list(group_to_active_event.values()) + #pending_events = list(group_to_pending_event.values()) active_events.sort(key=lambda e: e.group, reverse=True) pending_events.sort(key=lambda e: e.group, reverse=True) @@ -512,7 +512,8 @@ async def doPartial(self, ctx, server): for e in pending_events: output += "\n" + e.toPartialEvent(self) - await ctx.send(box(output)) + for page in pagify(output): + await ctx.send(box(page)) def makeChannelReg(channel_id, server): From f08d7f38704eae0d0f9f85d57f8f4ed8fed8c7c3 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 01:46:56 -0700 Subject: [PATCH 0169/1220] id alt evos add --- padinfo/padinfo.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 76160cd8..9c0b26e4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1120,6 +1120,13 @@ def monsterToEmbed(m: "DgMonster", emoji_list): ls_header += " [ {} ]".format(multiplier_text) embed.add_field(name=ls_header, value=ls_row, inline=False) + evos_header = "Alternate Evos" + evos_body = ", ".join(f"**{m2.monster_id}**" + if m2.monster_id==m.monster_id + else f"[{m2.monster_id}]({get_pdx_url(m2)})" + for m2 in m.alt_evos) + embed.add_field(name=evos_header, value=evos_body, inline=False) + return embed From 1a1d5e22ac320a983a0d9069a5cc83c55afd4142 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 03:36:41 -0700 Subject: [PATCH 0170/1220] id evo scroll --- padinfo/padinfo.py | 68 ++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9c0b26e4..87dae767 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -87,37 +87,39 @@ def __init__(self, emoji_to_embed, m: "DgMonster" = None, def on_update(self, ctx, selected_emoji): if self.pad_info.settings.checkEvoID(ctx.author.id): + if selected_emoji == self.pad_info.previous_monster_emoji: + if self.m.prev_monster is None: + return False + self.m = self.m.prev_monster + elif selected_emoji == self.pad_info.next_monster_emoji: + if self.m.next_monster is None: + return False + self.m = self.m.next_monster + else: + self.selected_emoji = selected_emoji + return True + else: DGCOG = self.bot.get_cog('Dadguide') - evos = list(map(lambda x: [x['from_id'], x['to_id']], - DGCOG.database.get_all_evolutions_by_monster(self.m.monster_id))) - evos = list(set(sum(evos, []))) + evos = self.m._alt_evo_id_list evos.sort() index = evos.index(self.m.monster_id) if selected_emoji == self.pad_info.previous_monster_emoji: if index == 0: - self.m = DGCOG.get_monster_by_id(evos[-1]) + newm = DGCOG.get_monster_by_id(evos[-1]) else: - self.m = DGCOG.get_monster_by_id(evos[index - 1]) + newm = DGCOG.get_monster_by_id(evos[index - 1]) elif selected_emoji == self.pad_info.next_monster_emoji: if index == len(evos) - 1: - self.m = DGCOG.get_monster_by_id(evos[0]) + newm = DGCOG.get_monster_by_id(evos[0]) else: - self.m = DGCOG.get_monster_by_id(evos[index + 1]) - else: - self.selected_emoji = selected_emoji - return True - else: - if selected_emoji == self.pad_info.previous_monster_emoji: - if self.m.prev_monster is None: - return False - self.m = self.m.prev_monster - elif selected_emoji == self.pad_info.next_monster_emoji: - if self.m.next_monster is None: - return False - self.m = self.m.next_monster + newm = DGCOG.get_monster_by_id(evos[index + 1]) else: self.selected_emoji = selected_emoji return True + if newm.monster_id == self.m.monster_id: + return False + self.m = newm + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m) return True @@ -170,10 +172,10 @@ def __init__(self, bot, *args, **kwargs): self.menu = Menu(bot) # These emojis are the keys into the idmenu submenus - self.id_emoji = '\N{INFORMATION SOURCE}' + self.id_emoji = '\N{HOUSE BUILDING}' self.evo_emoji = char_to_emoji('e') self.mats_emoji = char_to_emoji('m') - self.ls_emoji = '\N{INFORMATION SOURCE}' + self.ls_emoji = '\N{HOUSE BUILDING}' self.left_emoji = char_to_emoji('l') self.right_emoji = char_to_emoji('r') self.pantheon_emoji = '\N{CLASSICAL BUILDING}' @@ -620,18 +622,18 @@ async def padsay(self, ctx, server, *, query: str = None): @commands.command() async def idmode(self, ctx, id_type): """Switch between number mode and evo mode""" - if id_type in ['evo']: - if self.settings.setEvoID(ctx.author.id): + if id_type in ['evo', 'default']: + if self.settings.rmEvoID(ctx.author.id): await ctx.tick() else: await ctx.send(inline("You're already using evo mode")) - elif id_type in ['number', 'default']: - if self.settings.rmEvoID(ctx.author.id): + elif id_type in ['number']: + if self.settings.setEvoID(ctx.author.id): await ctx.tick() else: await ctx.send(inline("You're already using number mode")) else: - await ctx.send(inline("id_type must be 'number' or 'evo'")) + await ctx.send(inline("id_type must be `number` or `evo`")) @commands.group() @checks.is_owner() @@ -749,7 +751,7 @@ class PadInfoSettings(CogSettings): def make_default_settings(self): config = { 'animation_dir': '', - 'evo_id_users': [], + 'evo_id_optout': [], 'voice_dir_path': '', } return config @@ -767,21 +769,21 @@ def setEmojiServers(self, emoji_servers): self.save_settings() def setEvoID(self, user_id): - if user_id not in self.bot_settings['evo_id_users']: - self.bot_settings['evo_id_users'].append(user_id) + if user_id not in self.bot_settings['evo_id_optout']: + self.bot_settings['evo_id_optout'].append(user_id) self.save_settings() return True return False def rmEvoID(self, user_id): - if user_id in self.bot_settings['evo_id_users']: - self.bot_settings['evo_id_users'].remove(user_id) + if user_id in self.bot_settings['evo_id_optout']: + self.bot_settings['evo_id_optout'].remove(user_id) self.save_settings() return True return False def checkEvoID(self, user_id): - return user_id in self.bot_settings['evo_id_users'] + return user_id in self.bot_settings['evo_id_optout'] def setVoiceDir(self, path): self.bot_settings['voice_dir_path'] = path @@ -1126,7 +1128,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): else f"[{m2.monster_id}]({get_pdx_url(m2)})" for m2 in m.alt_evos) embed.add_field(name=evos_header, value=evos_body, inline=False) - + return embed From 316a661d6f2a47465cc6324008a6825f83f511ac Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 03:39:54 -0700 Subject: [PATCH 0171/1220] sort ids by monster_id --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 87dae767..9ec629e4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1126,7 +1126,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): evos_body = ", ".join(f"**{m2.monster_id}**" if m2.monster_id==m.monster_id else f"[{m2.monster_id}]({get_pdx_url(m2)})" - for m2 in m.alt_evos) + for m2 in sorted(m.alt_evos, key=lambda m: m.monster_id)) embed.add_field(name=evos_header, value=evos_body, inline=False) return embed From cd8d893a5e93584301f0c931178ab7394d04497b Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 03:51:11 -0700 Subject: [PATCH 0172/1220] idmode doc --- padinfo/padinfo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9ec629e4..bd982059 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -621,7 +621,10 @@ async def padsay(self, ctx, server, *, query: str = None): @commands.command() async def idmode(self, ctx, id_type): - """Switch between number mode and evo mode""" + """Switch between number mode and evo mode + + [p]idmode number + [p]idmode evo""" if id_type in ['evo', 'default']: if self.settings.rmEvoID(ctx.author.id): await ctx.tick() From 3ddaac426ef34d94780e2dddfb6b884595edb2d7 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 16:15:26 -0700 Subject: [PATCH 0173/1220] Allow set to null --- crud/crud.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crud/crud.py b/crud/crud.py index 105e91e6..5295fc9f 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -152,6 +152,8 @@ async def series_delete(self, ctx, series_id: int): @crud.command() async def editmonsname(self, ctx, monster_id: int, *, name): """Change a monster's name_en_override""" + if name.lower() in ["none", "null"]: + name = None with await self.get_cursor() as cursor: cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) old_val = cursor.fetchall() From 0f53f6ebf3043f99a042f748035c3f80be02806d Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 16:21:16 -0700 Subject: [PATCH 0174/1220] Add docstrings --- crud/crud.py | 20 ++++++++++++++------ padguidedb/padguidedb.py | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 5295fc9f..1fba6efe 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -67,7 +67,7 @@ async def crud(self, ctx): @crud.command() async def searchdungeon(self, ctx, *, search_text): - """Search""" + """Search for a dungeon via its jp or na name""" search_text = '%{}%'.format(search_text) with await self.get_cursor() as cursor: sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' @@ -82,11 +82,11 @@ async def searchdungeon(self, ctx, *, search_text): @crud.group() async def series(self, ctx): - """Series""" + """Series related commands""" @series.command(name="search") async def series_search(self, ctx, *, search_text): - """Search""" + """Search for a series via its jp or na name""" search_text = '%{}%'.format(search_text) with await self.get_cursor() as cursor: sql = ('select series_id, name_en, name_ja from series' @@ -101,7 +101,11 @@ async def series_search(self, ctx, *, search_text): @series.command(name="add") async def series_add(self, ctx, *, elements: TokenConverter): - """Add""" + """Add a new series. + Valid element keys are: `name_en`, `name_kr`, `name_jp` + + [p]crud series add key1 "Value1" key2 "Value2" + """ if not all(x in SERIES_KEYS for x in elements): await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) return @@ -125,7 +129,11 @@ async def series_add(self, ctx, *, elements: TokenConverter): @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): - """Edit""" + """Edit an existing series series. + Valid element keys are: `name_en`, `name_kr`, `name_jp` + + [p]crud series edit key1 "Value1" key2 "Value2" + """ if not all(x in SERIES_KEYS for x in elements): await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) return @@ -141,7 +149,7 @@ async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): @series.command(name="delete") async def series_delete(self, ctx, series_id: int): - """Delete""" + """Delete an existing series""" with await self.get_cursor() as cursor: sql = ('DELETE FROM series' ' WHERE series_id = %s') diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 4c10bafe..5e4149a4 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -105,7 +105,7 @@ async def setconfigfile(self, ctx, *, config_file): @padguidedb.command() async def searchdungeon(self, ctx, *, search_text): - """Search""" + """Search for a dungeon via its jp or na name""" search_text = '%{}%'.format(search_text) with self.get_cursor() as cursor: sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' From c32d258636ccfeee92a2cb81fe74656038821e93 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 16:27:57 -0700 Subject: [PATCH 0175/1220] format help --- crud/crud.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 1fba6efe..b6c5cad5 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -102,12 +102,14 @@ async def series_search(self, ctx, *, search_text): @series.command(name="add") async def series_add(self, ctx, *, elements: TokenConverter): """Add a new series. + Valid element keys are: `name_en`, `name_kr`, `name_jp` + Example Usage: [p]crud series add key1 "Value1" key2 "Value2" """ if not all(x in SERIES_KEYS for x in elements): - await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + await ctx.send_help() return EXTRAS = {} @@ -130,12 +132,14 @@ async def series_add(self, ctx, *, elements: TokenConverter): @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): """Edit an existing series series. + Valid element keys are: `name_en`, `name_kr`, `name_jp` + Example Usage: [p]crud series edit key1 "Value1" key2 "Value2" """ if not all(x in SERIES_KEYS for x in elements): - await ctx.send("Invalid key. Valid keys are: `{}`".format("` `".join(SERIES_KEYS))) + await ctx.send_help() return replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) From f536aaf80d7bb1b2cf792189735acd53b6042f57 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 16:58:51 -0700 Subject: [PATCH 0176/1220] refactor --- crud/crud.py | 144 ++++++++++++++++++++++++--------------------------- 1 file changed, 69 insertions(+), 75 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index b6c5cad5..4098ab77 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -60,6 +60,21 @@ def connect(self, db_config): cursorclass=pymysql.cursors.DictCursor, autocommit=True) + async def execute_edit(self, ctx, sql, replacements): + with await self.get_cursor() as cursor: + affected = cursor.execute(sql, replacements) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) affected.".format(affected)) + + async def execute_read(self, ctx, sql, replacements): + with await self.get_cursor() as cursor: + cursor.execute(sql, replacements) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(cursor._executed)) + for page in pagify(msg): + await ctx.send(box(page)) + @commands.group(aliases=['hera-ur']) @auth_check('crud') async def crud(self, ctx): @@ -68,17 +83,11 @@ async def crud(self, ctx): @crud.command() async def searchdungeon(self, ctx, *, search_text): """Search for a dungeon via its jp or na name""" - search_text = '%{}%'.format(search_text) - with await self.get_cursor() as cursor: - sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' - ' where lower(name_en) like %s or lower(name_ja) like %s' - ' order by dungeon_id desc limit 20') - cursor.execute(sql, [search_text, search_text]) - results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) - await ctx.send(inline(sql)) - for page in pagify(msg): - await ctx.send(box(page)) + search_text = '%{}%'.format(search_text).lower() + sql = ('SELECT dungeon_id, name_en, name_ja, visible FROM dungeons' + ' WHERE lower(name_en) LIKE %s OR lower(name_ja) LIKE %s' + ' ORDER BY dungeon_id DESC LIMIT 20') + await self.execute_read(ctx, sql, [search_text, search_text]) @crud.group() async def series(self, ctx): @@ -87,17 +96,11 @@ async def series(self, ctx): @series.command(name="search") async def series_search(self, ctx, *, search_text): """Search for a series via its jp or na name""" - search_text = '%{}%'.format(search_text) - with await self.get_cursor() as cursor: - sql = ('select series_id, name_en, name_ja from series' - ' where lower(name_en) like %s or lower(name_ja) like %s' - ' order by series_id desc limit 20') - cursor.execute(sql, [search_text, search_text]) - results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) - await ctx.send(inline(sql)) - for page in pagify(msg): - await ctx.send(box(page)) + search_text = '%{}%'.format(search_text).lower() + sql = ('SELECT series_id, name_en, name_ja, name_ko FROM series' + ' WHERE lower(name_en) LIKE %s OR lower(name_ja) LIKE %s' + ' ORDER BY series_id DESC LIMIT 20') + await self.execute_read(ctx, sql, [search_text, search_text]) @series.command(name="add") async def series_add(self, ctx, *, elements: TokenConverter): @@ -114,20 +117,18 @@ async def series_add(self, ctx, *, elements: TokenConverter): EXTRAS = {} with await self.get_cursor() as cursor: - cursor.execute("SELECT MAX(series_id) FROM series") - max_val = cursor.fetchall()[0]['MAX(series_id)'] + cursor.execute("SELECT MAX(series_id) AS max_id FROM series") + max_val = cursor.fetchall()[0]['max_id'] EXTRAS['series_id'] = max_val + 1 EXTRAS['tstamp'] = int(datetime.now().timestamp()) elements = {**SERIES_KEYS, **EXTRAS, **elements} key_infix = ", ".join(elements.keys()) value_infix = ", ".join("%s" for v in elements.values()) - with await self.get_cursor() as cursor: - sql = ('INSERT INTO series ({})' - ' VALUES ({})').format(key_infix, value_infix) - affected = cursor.execute(sql, (*elements.values(),)) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) added.".format(affected)) + sql = ('INSERT INTO series ({})' + ' VALUES ({})').format(key_infix, value_infix) + + await self.execute_edit(ctx, sql, (*elements.values(),)) @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): @@ -136,30 +137,26 @@ async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): Valid element keys are: `name_en`, `name_kr`, `name_jp` Example Usage: - [p]crud series edit key1 "Value1" key2 "Value2" + [p]crud series edit 100 key1 "Value1" key2 "Value2" """ if not all(x in SERIES_KEYS for x in elements): await ctx.send_help() return replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) - with await self.get_cursor() as cursor: - sql = ('UPDATE series' - ' SET {}' - ' WHERE series_id = %s').format(replacement_infix) - affected = cursor.execute(sql, (*elements.values(),) + (series_id,)) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) changed.".format(affected)) + sql = ('UPDATE series' + ' SET {}' + ' WHERE series_id = %s').format(replacement_infix) + + await self.execute_edit(ctx, sql, (*elements.values(), series_id)) @series.command(name="delete") async def series_delete(self, ctx, series_id: int): """Delete an existing series""" - with await self.get_cursor() as cursor: - sql = ('DELETE FROM series' - ' WHERE series_id = %s') - affected = cursor.execute(sql, series_id) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) deleted.".format(affected)) + sql = ('DELETE FROM series' + ' WHERE series_id = %s') + + await self.execute_edit(ctx, sql, series_id) @crud.command() async def editmonsname(self, ctx, monster_id: int, *, name): @@ -169,22 +166,20 @@ async def editmonsname(self, ctx, monster_id: int, *, name): with await self.get_cursor() as cursor: cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) old_val = cursor.fetchall() - if not old_val: - await ctx.send("There is no monster with id: {}".format(monster_id)) - return - old_val = old_val[0]['name_en_override'] - if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" - " english override from `{}` to `{}`?").format(monster_id, - old_val, name)): - return - - sql = ('UPDATE monsters' - ' SET name_en_override = %s' - ' WHERE monster_id = %s') - affected = cursor.execute(sql, (name, monster_id)) - await ctx.send("Changed `{}` to `{}` via:".format(old_val, name)) - await ctx.send(inline(cursor._executed)) - await ctx.send("{} row(s) changed.".format(affected)) + + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['name_en_override'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " english override from `{}` to `{}`?" + "").format(monster_id, old_val, name)): + return + + sql = ('UPDATE monsters' + ' SET name_en_override = %s' + ' WHERE monster_id = %s') + await self.execute_edit(ctx, sql, (name, monster_id)) @crud.command() async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): @@ -192,21 +187,20 @@ async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): with await self.get_cursor() as cursor: cursor.execute("SELECT series_id FROM monsters WHERE monster_id = %s", (monster_id,)) old_val = cursor.fetchall() - if not old_val: - await ctx.send("There is no monster with id: {}".format(monster_id)) - return - old_val = old_val[0]['series_id'] - if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" - " series_id from `{}` to `{}`?").format(monster_id, - old_val, series_id)): - return - - sql = ('UPDATE monsters' - ' SET series_id = %s' - ' WHERE monster_id = %s') - affected = cursor.execute(sql, (series_id, monster_id)) - await ctx.send("Changed `{}` to `{}` via:\n".format(old_val, series_id, inline(cursor._executed))) - await ctx.send("{} row(s) changed.".format(affected)) + + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['series_id'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " series id from `{}` to `{}`?" + "").format(monster_id, old_val, series_id)): + return + + sql = ('UPDATE monsters' + ' SET series_id = %s' + ' WHERE monster_id = %s') + await self.execute_edit(ctx, sql, (series_id, monster_id)) @crud.command() @checks.is_owner() From cefea4f8f1ca5bbf3ca807dc68959e36afa0c359 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 13 Oct 2020 17:05:55 -0700 Subject: [PATCH 0177/1220] delete = owner_only --- crud/crud.py | 1 + 1 file changed, 1 insertion(+) diff --git a/crud/crud.py b/crud/crud.py index 4098ab77..7d436d93 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -151,6 +151,7 @@ async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): await self.execute_edit(ctx, sql, (*elements.values(), series_id)) @series.command(name="delete") + @checks.is_owner() async def series_delete(self, ctx, series_id: int): """Delete an existing series""" sql = ('DELETE FROM series' From 8ec15a6e12d40a6f369e633713a72861887fcbc3 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 14 Oct 2020 09:51:06 -0700 Subject: [PATCH 0178/1220] PDE cleaning --- padevents/padevents.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 347ff368..16c4713a 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -17,7 +17,7 @@ logger = logging.getLogger('red.padbot-cogs.padevents') -SUPPORTED_SERVERS = ["JP", "NA", "KR", "UK"] +SUPPORTED_SERVERS = ["JP", "NA", "KR"] GROUPS = ['red', 'blue', 'green'] class PadEvents(commands.Cog): @@ -114,17 +114,13 @@ async def check_started(self): else: if not e.dungeon_type in [DungeonType.Normal]: daily_refresh_servers.add(e.server) - - if e.name_and_modifier in pingroles: + if e.clean_dungeon_name in pingroles: for cid in pingroles[e.name_and_modifier]: channel = self.bot.get_channel(int(cid)) if channel is not None: index = GROUPS.index(e.group) role = channel.guild.get_role(pingroles[e.name_and_modifier][cid][index]) - if role: - ment = role.mention - else: - ment = "#deleted-role" + ment = role.mention if role else "#deleted-role" await channel.send("{} {}".format(e.name_and_modifier, ment), allowed_mentions=discord.AllowedMentions(roles=True)) @@ -167,15 +163,21 @@ async def testevent(self, ctx, server, seconds: int = 0): te = dg_module.DgScheduledEvent({'dungeon_id': 1}, dg_cog.database) te.server = server + te.server_id = SUPPORTED_SERVERS.index(server) te.event_type_id = EventType.Guerrilla.value te.event_seq = 0 fuid = self.fake_uid = self.fake_uid - 1 te.key = lambda: fuid - te.group_name = 'Yellow' + te.group_name = 'red' te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) te.close_datetime = te.open_datetime + timedelta(minutes=1) - te.dungeon_name = 'fake_dungeon_name' + + class Dungeon: pass + d = Dungeon() + d.name_en = 'fake_dungeon_name' + d.dungeon_type = DungeonType.Unknown7 + te.dungeon = d te.event_modifier = 'fake_event_modifier' self.events.append(Event(te)) @@ -262,10 +264,10 @@ async def autoeventping(self, ctx, name, red: discord.Role, blue: discord.Role, @checks.mod_or_permissions(manage_guild=True) async def rmautoeventping(self, ctx, *, name): async with self.config.pingroles() as pingroles: - if name not in pingroles or ctx.guild.id not in pingroles[name]: + if name not in pingroles or str(ctx.channel.id) not in pingroles[name]: await ctx.send("That name is not set.") return - n = pingroles[name][ctx.guild.id] + del pingroles[name][str(ctx.channel.id)] await ctx.tick() @padevents.command(name="listchannels") @@ -493,8 +495,8 @@ async def doPartial(self, ctx, server): #active_events = list(group_to_active_event.values()) #pending_events = list(group_to_pending_event.values()) - active_events.sort(key=lambda e: e.group, reverse=True) - pending_events.sort(key=lambda e: e.group, reverse=True) + active_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) + pending_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) if len(active_events) == 0 and len(pending_events) == 0: await ctx.send("No events available for " + server) @@ -503,12 +505,12 @@ async def doPartial(self, ctx, server): output = "Events for {}".format(server) if len(active_events) > 0: - output += "\n\n" + "G Remaining Dungeon" + output += "\n\n" + " Remaining Dungeon" for e in active_events: output += "\n" + e.toPartialEvent(self) if len(pending_events) > 0: - output += "\n\n" + "G PT ET ETA Dungeon" + output += "\n\n" + " PT ET ETA Dungeon" for e in pending_events: output += "\n" + e.toPartialEvent(self) From 97bfae5c46782f803d1976b983e09ec699b909ef Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 03:38:39 -0700 Subject: [PATCH 0179/1220] PadEvents Server Fix --- padevents/padevents.py | 46 +++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 16c4713a..300a1411 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -87,24 +87,20 @@ async def check_started(self): daily_refresh_servers = set() pingroles = await self.config.pingroles() for e in events: + # await self.bot.get_channel(765847219177521192).send(inline(str((e.clean_dungeon_name, fmtTime(e.open_datetime), e.server, e.key)))) self.started_events.add(e.key) if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, EventType.Week]: for gr in list(self.settings.listGuerrillaReg()): if e.server == gr['server']: try: - message = box("Server " + e.server + ", group " + - e.groupLongName() + " : " + e.name_and_modifier) channel = self.bot.get_channel(int(gr['channel_id'])) - try: - role_name = '{}_group_{}'.format( - e.server, e.groupLongName()) - role = get_role(channel.guild.roles, role_name) - if role and role.mentionable: - message = "{} `: {} is starting`".format( - role.mention, e.name_and_modifier) - except: - pass # do nothing if role is missing + role_name = '{}_group_{}'.format(e.server, e.groupLongName()) + role = channel.guild.get_role(channel.guild.roles, role_name) + if role and role.mentionable: + message = "{} `: {} is starting`".format(role.mention, e.name_and_modifier) + else: + message = box("Server " + e.server + ", group " + e.groupLongName() + " : " + e.name_and_modifier) await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) except Exception as ex: @@ -114,12 +110,12 @@ async def check_started(self): else: if not e.dungeon_type in [DungeonType.Normal]: daily_refresh_servers.add(e.server) - if e.clean_dungeon_name in pingroles: - for cid in pingroles[e.name_and_modifier]: + if e.server + e.clean_dungeon_name in pingroles: + for cid in pingroles[e.server + e.clean_dungeon_name]: channel = self.bot.get_channel(int(cid)) if channel is not None: index = GROUPS.index(e.group) - role = channel.guild.get_role(pingroles[e.name_and_modifier][cid][index]) + role = channel.guild.get_role(pingroles[e.server + e.clean_dungeon_name][cid][index]) ment = role.mention if role else "#deleted-role" await channel.send("{} {}".format(e.name_and_modifier, ment), allowed_mentions=discord.AllowedMentions(roles=True)) @@ -141,7 +137,7 @@ async def check_started(self): logger.exception("caught exception while checking guerrillas:") await asyncio.sleep(10) - logger.info("done check_started") + logger.info("done check_started (cog probably unloaded)") @commands.group(aliases=['pde']) @checks.mod_or_permissions(manage_guild=True) @@ -254,20 +250,28 @@ async def rmchanneldaily(self, ctx, server): @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def autoeventping(self, ctx, name, red: discord.Role, blue: discord.Role, green: discord.Role): + async def autoeventping(self, ctx, server, name, red: discord.Role, blue: discord.Role, green: discord.Role): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return async with self.config.pingroles() as pingroles: - pingroles[name] = {ctx.channel.id: (red.id, blue.id, green.id)} + pingroles[server+name] = {ctx.channel.id: (red.id, blue.id, green.id)} await ctx.tick() @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmautoeventping(self, ctx, *, name): + async def rmautoeventping(self, ctx, *, server, name): + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return async with self.config.pingroles() as pingroles: - if name not in pingroles or str(ctx.channel.id) not in pingroles[name]: + if server+name not in pingroles or str(ctx.channel.id) not in pingroles[server+name]: await ctx.send("That name is not set.") return - del pingroles[name][str(ctx.channel.id)] + del pingroles[server+name][str(ctx.channel.id)] await ctx.tick() @padevents.command(name="listchannels") @@ -646,7 +650,7 @@ def groupShortName(self): return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') def groupLongName(self): - return self.group.upper() + return self.group.upper() if self.group is not None else "UNGROUPED" def toPartialEvent(self, pe): group = self.groupShortName() From 90c25c4806d13a5275dbd332a7993670daf9f2bb Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 04:40:03 -0700 Subject: [PATCH 0180/1220] Uncomment logging --- padevents/padevents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 300a1411..a4cb7723 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -87,7 +87,7 @@ async def check_started(self): daily_refresh_servers = set() pingroles = await self.config.pingroles() for e in events: - # await self.bot.get_channel(765847219177521192).send(inline(str((e.clean_dungeon_name, fmtTime(e.open_datetime), e.server, e.key)))) + await self.bot.get_channel(765847219177521192).send(inline(str((e.clean_dungeon_name, fmtTime(e.open_datetime), e.server, e.key)))) self.started_events.add(e.key) if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, EventType.Week]: for gr in list(self.settings.listGuerrillaReg()): From 421c0df59ee948586a7642492a8b58f5ae27cb37 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 06:37:35 -0700 Subject: [PATCH 0181/1220] Padevents AEP rewrite --- padevents/padevents.py | 237 +++++++++++++++++++++++++++++++++-------- 1 file changed, 195 insertions(+), 42 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index a4cb7723..8c9f807a 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -27,11 +27,12 @@ def __init__(self, bot, *args, **kwargs): self.settings = PadEventSettings("padevents") self.config = Config.get_conf(self, identifier=940373775) - self.config.register_global(pingroles={}) + self.config.register_guild(pingroles={}) # Load event data self.events = list() self.started_events = set() + self.rolepinged_events = set() self.fake_uid = -999 @@ -77,15 +78,38 @@ async def refresh_data(self): self.events = new_events self.started_events = {ev.key for ev in new_events if ev.is_started()} + self.rolepinged_events = set() async def check_started(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadEvents'): try: - events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) + events = filter(lambda e: not e.key in self.started_events, self.events) + for e in events: + for gid, data in (await self.config.all_guilds()).items(): + guild = self.bot.get_guild(gid) + for key, aep in data.get('pingroles', {}).items(): + if e.start_from_now_sec() > aep['offset'] * 60 \ + or not aep['enabled'] \ + or e.server != aep['server'] \ + or (key, e.key) in self.rolepinged_events: + continue + elif aep['regex']: + matches = re.search(aep['searchstr'], e.clean_dungeon_name) + else: + matches = aep['searchstr'] in e.clean_dungeon_name + + self.rolepinged_events.add((key, e.key)) + if matches: + index = GROUPS.index(e.group) + channel = guild.get_channel(aep['channels'][index]) + if channel is not None: + role = guild.get_role(aep['roles'][index]) + ment = role.mention if role else "" + await channel.send("{} {}".format(e.name_and_modifier, ment), allowed_mentions=discord.AllowedMentions(roles=True)) + events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) daily_refresh_servers = set() - pingroles = await self.config.pingroles() for e in events: await self.bot.get_channel(765847219177521192).send(inline(str((e.clean_dungeon_name, fmtTime(e.open_datetime), e.server, e.key)))) self.started_events.add(e.key) @@ -96,7 +120,7 @@ async def check_started(self): channel = self.bot.get_channel(int(gr['channel_id'])) role_name = '{}_group_{}'.format(e.server, e.groupLongName()) - role = channel.guild.get_role(channel.guild.roles, role_name) + role = channel.guild.get_role(role_name) if role and role.mentionable: message = "{} `: {} is starting`".format(role.mention, e.name_and_modifier) else: @@ -109,29 +133,17 @@ async def check_started(self): else: if not e.dungeon_type in [DungeonType.Normal]: - daily_refresh_servers.add(e.server) - if e.server + e.clean_dungeon_name in pingroles: - for cid in pingroles[e.server + e.clean_dungeon_name]: - channel = self.bot.get_channel(int(cid)) - if channel is not None: - index = GROUPS.index(e.group) - role = channel.guild.get_role(pingroles[e.server + e.clean_dungeon_name][cid][index]) - ment = role.mention if role else "#deleted-role" - await channel.send("{} {}".format(e.name_and_modifier, ment), allowed_mentions=discord.AllowedMentions(roles=True)) - - - for server in daily_refresh_servers: - msg = self.makeActiveText(server) - for daily_registration in list(self.settings.listDailyReg()): - try: - if server == daily_registration['server']: - await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), - msg, channel_id=daily_registration['channel_id']) - logger.info("daily_reg server") - except Exception as ex: - # self.settings.removeDailyReg( - # daily_registration['channel_id'], daily_registration['server']) - logger.exception("caught exception while sending daily msg:") + msg = self.makeActiveText(server) + for daily_registration in list(self.settings.listDailyReg()): + try: + if server == daily_registration['server']: + await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), + msg, channel_id=daily_registration['channel_id']) + logger.info("daily_reg server") + except Exception as ex: + # self.settings.removeDailyReg( + # daily_registration['channel_id'], daily_registration['server']) + logger.exception("caught exception while sending daily msg:") except Exception as ex: logger.exception("caught exception while checking guerrillas:") @@ -247,31 +259,172 @@ async def rmchanneldaily(self, ctx, server): self.settings.removeDailyReg(channel_id, server) await ctx.send("Channel deactivated.") - @padevents.command() + @commands.group(aliases=['aep']) @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def autoeventping(self, ctx, server, name, red: discord.Role, blue: discord.Role, green: discord.Role): - server = normalizeServer(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") + async def autoeventping(self, ctx): + """Auto Event Pings""" + + @autoeventping.command(name="add") + async def aep_add(self, ctx, key, server = None, searchstr = None, red: discord.Role = None, blue: discord.Role = None, green: discord.Role = None): + if green is None and server is not None: + await ctx.send("Multi-word keys must be in quotes.") return - async with self.config.pingroles() as pingroles: - pingroles[server+name] = {ctx.channel.id: (red.id, blue.id, green.id)} + + default = { + 'roles': [None, None, None], + 'channels': [None, None, None], + 'server': 'NA', + 'searchstr': None, + 'regex': False, + 'enabled': False, + 'offset': 0, + } + + if green is not None: + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + default.update({ + 'roles': [red.id, blue.id, green.id], + 'channels': [ctx.channel.id] * 3, + 'server': server, + 'searchstr': searchstr, + 'enabled': True, + }) + + async with self.config.guild(ctx.guild).pingroles() as pingroles: + pingroles[key] = default await ctx.tick() - @padevents.command() - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def rmautoeventping(self, ctx, *, server, name): + @autoeventping.command(name="remove", aliases=['rm', 'delete']) + async def aep_remove(self, ctx, *, key): + async with self.config.guild(ctx.guild).pingroles() as pingroles: + if key not in pingroles: + await ctx.send("That key does not exist.") + return + del pingroles[key] + await ctx.tick() + + @autoeventping.command(name="show") + async def aep_show(self, ctx, *, key): + pingroles = await self.config.guild(ctx.guild).pingroles() + if key not in pingroles: + await ctx.send("That key does not exist.") + return + pr = pingroles[key] + await ctx.send(str(pr)+"\n\nTODO: Make this readable") + + @autoeventping.command(name="list") + async def aep_list(self, ctx): + pingroles = await self.config.guild(ctx.guild).pingroles() + await ctx.send(box('\n'.join(pingroles))) + + @autoeventping.group(name="set") + async def aep_set(self, ctx): + """Set specific parts of an autoeventping""" + + async def aepc(self, ctx, key, k, f): + async with self.config.guild(ctx.guild).pingroles() as pingroles: + if key not in pingroles: + await ctx.send("That key does not exist.") + return + pingroles[key][k] = f(pingroles[key][k]) + + async def aeps(self, ctx, key, k, v): + await self.aepc(ctx, key, k, lambda x: v) + + async def aepg(self, ctx, key): + pingroles = await self.config.guild(ctx.guild).pingroles() + if key not in pingroles: + await ctx.send("That key does not exist.") + return + return pingroles[key] + + @aep_set.command(name="channel") + async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): + await self.aeps(ctx, key, 'channels', [channel.id]*3) + await ctx.tick() + + @aep_set.command(name="redchannel") + async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel): + await self.aepc(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) + await ctx.tick() + @aep_set.command(name="bluechannel") + async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel): + await self.aepc(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) + await ctx.tick() + @aep_set.command(name="greenchannel") + async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel): + await self.aepc(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) + await ctx.tick() + + @aep_set.command(name="roles") + async def aep_s_roles(self, ctx, key, red: discord.Role, blue: discord.Role, green: discord.Role): + await self.aeps(ctx, key, 'roles', [red.id, blue.id, green.id]) + await ctx.tick() + + @aep_set.command(name="redrole") + async def aep_s_redrole(self, ctx, key, role: discord.Role): + await self.aepc(ctx, key, 'roles', lambda x: [role.id, x[1], x[2]]) + await ctx.tick() + @aep_set.command(name="bluerole") + async def aep_s_bluerole(self, ctx, key, role: discord.Role): + await self.aepc(ctx, key, 'roles', lambda x: [x[0], role.id, x[2]]) + await ctx.tick() + @aep_set.command(name="greenrole") + async def aep_s_greenrole(self, ctx, key, role: discord.Role): + await self.aepc(ctx, key, 'roles', lambda x: [x[0], x[1], role.id]) + await ctx.tick() + + @aep_set.command(name="server") + async def aep_s_server(self, ctx, key, server): server = normalizeServer(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return - async with self.config.pingroles() as pingroles: - if server+name not in pingroles or str(ctx.channel.id) not in pingroles[server+name]: - await ctx.send("That name is not set.") + await self.aeps(ctx, key, 'server', server) + await ctx.tick() + + @aep_set.command(name="searchstr") + async def aep_s_searchstr(self, ctx, key, *, searchstr): + if (await self.aepg(ctx, key))['regex']: + try: + re.compile(searchstr) + except re.error: + await ctx.send("Invalid regex searchstr. (`{}`)".format(searchstr)) return - del pingroles[server+name][str(ctx.channel.id)] + await self.aeps(ctx, key, 'searchstr', searchstr) + await ctx.tick() + + @aep_set.command(name="regex") + async def aep_s_regex(self, ctx, key, regex: bool): + if regex: + try: + re.compile((await self.aepg(ctx, key))['searchstr']) + except re.error: + await ctx.send("Invalid regex searchstr. (`{}`)".format((await self.aepg(ctx, key))['searchstr'])) + return + await self.aeps(ctx, key, 'regex', regex) + await ctx.tick() + + @aep_set.command(name="enabled", aliases=['enable']) + async def aep_s_enabled(self, ctx, key, enabled: bool = True): + await self.aeps(ctx, key, 'enabled', enabled) + await ctx.tick() + + @aep_set.command(name="disabled", aliases=['disable']) + async def aep_s_disabled(self, ctx, key, disabled: bool = True): + await self.aeps(ctx, key, 'enabled', not disabled) + await ctx.tick() + + @aep_set.command(name="offset") + async def aep_s_disabled(self, ctx, key, offset: int): + if offset < 0: + await ctx.send("Offset cannot be negative.") + return + await self.aeps(ctx, key, 'offset', offset) await ctx.tick() @padevents.command(name="listchannels") From 7a83ca44ddcbfd476f6c506e8634d0e0a6e25330 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 06:42:02 -0700 Subject: [PATCH 0182/1220] Docstrings --- padevents/padevents.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/padevents/padevents.py b/padevents/padevents.py index 8c9f807a..b1738b63 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -267,6 +267,7 @@ async def autoeventping(self, ctx): @autoeventping.command(name="add") async def aep_add(self, ctx, key, server = None, searchstr = None, red: discord.Role = None, blue: discord.Role = None, green: discord.Role = None): + """Add a new autoeventping""" if green is None and server is not None: await ctx.send("Multi-word keys must be in quotes.") return @@ -300,6 +301,7 @@ async def aep_add(self, ctx, key, server = None, searchstr = None, red: discord. @autoeventping.command(name="remove", aliases=['rm', 'delete']) async def aep_remove(self, ctx, *, key): + """Remove an autoeventping""" async with self.config.guild(ctx.guild).pingroles() as pingroles: if key not in pingroles: await ctx.send("That key does not exist.") @@ -309,6 +311,7 @@ async def aep_remove(self, ctx, *, key): @autoeventping.command(name="show") async def aep_show(self, ctx, *, key): + """Show specifics of an autoeventping""" pingroles = await self.config.guild(ctx.guild).pingroles() if key not in pingroles: await ctx.send("That key does not exist.") @@ -318,6 +321,7 @@ async def aep_show(self, ctx, *, key): @autoeventping.command(name="list") async def aep_list(self, ctx): + """List all autoeventpings""" pingroles = await self.config.guild(ctx.guild).pingroles() await ctx.send(box('\n'.join(pingroles))) @@ -344,42 +348,51 @@ async def aepg(self, ctx, key): @aep_set.command(name="channel") async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): + """Set channel to ping for all groups""" await self.aeps(ctx, key, 'channels', [channel.id]*3) await ctx.tick() @aep_set.command(name="redchannel") async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel): + """Sets channel to ping when event is red""" await self.aepc(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) await ctx.tick() @aep_set.command(name="bluechannel") async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel): + """Sets channel to ping when event is blue""" await self.aepc(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) await ctx.tick() @aep_set.command(name="greenchannel") async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel): + """Sets channel to ping when event is green""" await self.aepc(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) await ctx.tick() @aep_set.command(name="roles") async def aep_s_roles(self, ctx, key, red: discord.Role, blue: discord.Role, green: discord.Role): + """Sets roles to ping""" await self.aeps(ctx, key, 'roles', [red.id, blue.id, green.id]) await ctx.tick() @aep_set.command(name="redrole") async def aep_s_redrole(self, ctx, key, role: discord.Role): + """Sets role to ping when event is red""" await self.aepc(ctx, key, 'roles', lambda x: [role.id, x[1], x[2]]) await ctx.tick() @aep_set.command(name="bluerole") async def aep_s_bluerole(self, ctx, key, role: discord.Role): + """Sets role to ping when event is blue""" await self.aepc(ctx, key, 'roles', lambda x: [x[0], role.id, x[2]]) await ctx.tick() @aep_set.command(name="greenrole") async def aep_s_greenrole(self, ctx, key, role: discord.Role): + """Sets role to ping when event is green""" await self.aepc(ctx, key, 'roles', lambda x: [x[0], x[1], role.id]) await ctx.tick() @aep_set.command(name="server") async def aep_s_server(self, ctx, key, server): + """Set which server to listen to events in""" server = normalizeServer(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") @@ -389,6 +402,7 @@ async def aep_s_server(self, ctx, key, server): @aep_set.command(name="searchstr") async def aep_s_searchstr(self, ctx, key, *, searchstr): + """Set what string is tested against event name""" if (await self.aepg(ctx, key))['regex']: try: re.compile(searchstr) @@ -400,6 +414,7 @@ async def aep_s_searchstr(self, ctx, key, *, searchstr): @aep_set.command(name="regex") async def aep_s_regex(self, ctx, key, regex: bool): + """Set whether searchstr is calculated via regex""" if regex: try: re.compile((await self.aepg(ctx, key))['searchstr']) @@ -411,16 +426,19 @@ async def aep_s_regex(self, ctx, key, regex: bool): @aep_set.command(name="enabled", aliases=['enable']) async def aep_s_enabled(self, ctx, key, enabled: bool = True): + """Set whether or not ping is enabled""" await self.aeps(ctx, key, 'enabled', enabled) await ctx.tick() @aep_set.command(name="disabled", aliases=['disable']) async def aep_s_disabled(self, ctx, key, disabled: bool = True): + """Set whether or not ping is disabled""" await self.aeps(ctx, key, 'enabled', not disabled) await ctx.tick() @aep_set.command(name="offset") async def aep_s_disabled(self, ctx, key, offset: int): + """Set how many minutes before event should ping happen""" if offset < 0: await ctx.send("Offset cannot be negative.") return From 5dd1701b5b38b2cdf263c0e03f31b2d5eaa29dd7 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 06:43:23 -0700 Subject: [PATCH 0183/1220] Stop bullying korea --- padevents/padevents.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index b1738b63..c43fc6b8 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -214,7 +214,7 @@ async def addchannel(self, ctx, server): async def rmchannel(self, ctx, server): server = normalizeServer(server) if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + await ctx.send("Unsupported server, pick one of NA, KR, JP") return channel_id = ctx.channel.id @@ -231,7 +231,7 @@ async def rmchannel(self, ctx, server): async def addchanneldaily(self, ctx, server): server = normalizeServer(server) if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + await ctx.send("Unsupported server, pick one of NA, KR, JP") return channel_id = ctx.channel.id @@ -248,7 +248,7 @@ async def addchanneldaily(self, ctx, server): async def rmchanneldaily(self, ctx, server): server = normalizeServer(server) if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, ~~KR~~, JP") + await ctx.send("Unsupported server, pick one of NA, KR, JP") return channel_id = ctx.channel.id @@ -267,7 +267,8 @@ async def autoeventping(self, ctx): @autoeventping.command(name="add") async def aep_add(self, ctx, key, server = None, searchstr = None, red: discord.Role = None, blue: discord.Role = None, green: discord.Role = None): - """Add a new autoeventping""" + """Add a new autoeventping"""@chasehult +Padevents AEP rewrite if green is None and server is not None: await ctx.send("Multi-word keys must be in quotes.") return From 38f1b7fb1f3c2f5a17e01088ae2b30cc5dc9933e Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 06:45:43 -0700 Subject: [PATCH 0184/1220] Aparently i added random text when I tried to name the last PR --- padevents/padevents.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index c43fc6b8..e2090b76 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -267,8 +267,7 @@ async def autoeventping(self, ctx): @autoeventping.command(name="add") async def aep_add(self, ctx, key, server = None, searchstr = None, red: discord.Role = None, blue: discord.Role = None, green: discord.Role = None): - """Add a new autoeventping"""@chasehult -Padevents AEP rewrite + """Add a new autoeventping""" if green is None and server is not None: await ctx.send("Multi-word keys must be in quotes.") return From 16a2ca7e9173dee554b5f9c2ab34751e1fe23ee3 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 07:06:54 -0700 Subject: [PATCH 0185/1220] Docstring tense and different message when offset --- padevents/padevents.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index e2090b76..ea529ba8 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -106,7 +106,10 @@ async def check_started(self): if channel is not None: role = guild.get_role(aep['roles'][index]) ment = role.mention if role else "" - await channel.send("{} {}".format(e.name_and_modifier, ment), allowed_mentions=discord.AllowedMentions(roles=True)) + offsetstr = "" + if aep['offset']: + offsetstr = " in {} minute(s)".format(aep['offset']) + await channel.send("{}{} {}".format(e.name_and_modifier, offsetstr, ment), allowed_mentions=discord.AllowedMentions(roles=True)) events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) daily_refresh_servers = set() @@ -327,7 +330,7 @@ async def aep_list(self, ctx): @autoeventping.group(name="set") async def aep_set(self, ctx): - """Set specific parts of an autoeventping""" + """Sets specific parts of an autoeventping""" async def aepc(self, ctx, key, k, f): async with self.config.guild(ctx.guild).pingroles() as pingroles: @@ -348,7 +351,7 @@ async def aepg(self, ctx, key): @aep_set.command(name="channel") async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): - """Set channel to ping for all groups""" + """Sets channel to ping for all groups""" await self.aeps(ctx, key, 'channels', [channel.id]*3) await ctx.tick() @@ -392,7 +395,7 @@ async def aep_s_greenrole(self, ctx, key, role: discord.Role): @aep_set.command(name="server") async def aep_s_server(self, ctx, key, server): - """Set which server to listen to events in""" + """Sets which server to listen to events in""" server = normalizeServer(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") @@ -402,7 +405,7 @@ async def aep_s_server(self, ctx, key, server): @aep_set.command(name="searchstr") async def aep_s_searchstr(self, ctx, key, *, searchstr): - """Set what string is tested against event name""" + """Sets what string is tested against event name""" if (await self.aepg(ctx, key))['regex']: try: re.compile(searchstr) @@ -414,7 +417,7 @@ async def aep_s_searchstr(self, ctx, key, *, searchstr): @aep_set.command(name="regex") async def aep_s_regex(self, ctx, key, regex: bool): - """Set whether searchstr is calculated via regex""" + """Sets whether searchstr is calculated via regex""" if regex: try: re.compile((await self.aepg(ctx, key))['searchstr']) @@ -426,19 +429,19 @@ async def aep_s_regex(self, ctx, key, regex: bool): @aep_set.command(name="enabled", aliases=['enable']) async def aep_s_enabled(self, ctx, key, enabled: bool = True): - """Set whether or not ping is enabled""" + """Sets whether or not ping is enabled""" await self.aeps(ctx, key, 'enabled', enabled) await ctx.tick() @aep_set.command(name="disabled", aliases=['disable']) async def aep_s_disabled(self, ctx, key, disabled: bool = True): - """Set whether or not ping is disabled""" + """Sets whether or not ping is disabled""" await self.aeps(ctx, key, 'enabled', not disabled) await ctx.tick() @aep_set.command(name="offset") async def aep_s_disabled(self, ctx, key, offset: int): - """Set how many minutes before event should ping happen""" + """Sets how many minutes before event should ping happen""" if offset < 0: await ctx.send("Offset cannot be negative.") return From 219dab3fbbc9ed385c8fd4a767a96157d0448d20 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 12:12:18 -0700 Subject: [PATCH 0186/1220] evo scroll remove arrows --- padinfo/padinfo.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bd982059..75c56a2e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -86,7 +86,8 @@ def __init__(self, emoji_to_embed, m: "DgMonster" = None, self.bot = bot def on_update(self, ctx, selected_emoji): - if self.pad_info.settings.checkEvoID(ctx.author.id): + evoID = self.pad_info.settings.checkEvoID(ctx.author.id) + if evoID: if selected_emoji == self.pad_info.previous_monster_emoji: if self.m.prev_monster is None: return False @@ -120,7 +121,7 @@ def on_update(self, ctx, selected_emoji): return False self.m = newm - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=evoID) return True @@ -358,7 +359,7 @@ async def skillups(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=self.settings.checkEvoID(ctx.author.id)) return await self._do_menu( ctx, starting_menu_emoji, @@ -402,10 +403,12 @@ def get_id_emoji_options(self, m=None, scroll=False): # IdEmojiUpdater won't allow it, however they have to be defined # so that the buttons display in the first place - if scroll: emoji_to_embed[self.first_monster_emoji] = None - emoji_to_embed[self.previous_monster_emoji] = None - emoji_to_embed[self.next_monster_emoji] = None - if scroll: emoji_to_embed[self.last_monster_emoji] = None + if scroll and len(m.alt_evos) > 1: + emoji_to_embed[self.first_monster_emoji] = None + emoji_to_embed[self.last_monster_emoji] = None + if not (len(m.alt_evos) == 1 and scroll): + emoji_to_embed[self.previous_monster_emoji] = None + emoji_to_embed[self.next_monster_emoji] = None # remove emoji needs to be last emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message @@ -786,7 +789,7 @@ def rmEvoID(self, user_id): return False def checkEvoID(self, user_id): - return user_id in self.bot_settings['evo_id_optout'] + return user_id not in self.bot_settings['evo_id_optout'] def setVoiceDir(self, path): self.bot_settings['voice_dir_path'] = path From 9d7d3635c004f3496eee14b2c8df9797f1a037fa Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 13:49:13 -0700 Subject: [PATCH 0187/1220] Add Evoscroll (and fix id scroll) --- dadguide/database_manager.py | 6 ++ padinfo/padinfo.py | 107 ++++++++++++++++++++--------------- 2 files changed, 66 insertions(+), 47 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index ec2fe37d..c9f93da8 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -827,6 +827,12 @@ def prev_monster(self): offset += 1 return next + def __eq__(self, other): + return isinstance(other, DgMonster) and self.monster_id == other.monster_id + + def __hash__(self): + return hash(("DgMonster", self.monster_id)) + class MonsterSearchHelper(object): def __init__(self, m: DgMonster): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 75c56a2e..96f8cbac 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -88,18 +88,6 @@ def __init__(self, emoji_to_embed, m: "DgMonster" = None, def on_update(self, ctx, selected_emoji): evoID = self.pad_info.settings.checkEvoID(ctx.author.id) if evoID: - if selected_emoji == self.pad_info.previous_monster_emoji: - if self.m.prev_monster is None: - return False - self.m = self.m.prev_monster - elif selected_emoji == self.pad_info.next_monster_emoji: - if self.m.next_monster is None: - return False - self.m = self.m.next_monster - else: - self.selected_emoji = selected_emoji - return True - else: DGCOG = self.bot.get_cog('Dadguide') evos = self.m._alt_evo_id_list evos.sort() @@ -120,8 +108,20 @@ def on_update(self, ctx, selected_emoji): if newm.monster_id == self.m.monster_id: return False self.m = newm + else: + if selected_emoji == self.pad_info.previous_monster_emoji: + if self.m.prev_monster is None: + return False + self.m = self.m.prev_monster + elif selected_emoji == self.pad_info.next_monster_emoji: + if self.m.next_monster is None: + return False + self.m = self.m.next_monster + else: + self.selected_emoji = selected_emoji + return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=evoID) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted(self.m.alt_evos, key=lambda x: x.monster_id) if evoID else []) return True @@ -137,24 +137,24 @@ def __init__(self, emoji_to_embed, m: "DgMonster" = None, def on_update(self, ctx, selected_emoji): DGCOG = self.bot.get_cog('Dadguide') - index = self.ms.index(self.m.monster_id) + index = self.ms.index(self.m) if selected_emoji == self.pad_info.first_monster_emoji: - self.m = DGCOG.get_monster_by_id(self.ms[0]) + self.m = self.ms[0] elif selected_emoji == self.pad_info.previous_monster_emoji: - self.m = DGCOG.get_monster_by_id(self.ms[index - 1]) + self.m = self.ms[index - 1] elif selected_emoji == self.pad_info.next_monster_emoji: if index == len(self.ms) - 1: - self.m = DGCOG.get_monster_by_id(self.ms[0]) + self.m = self.ms[0] else: - self.m = DGCOG.get_monster_by_id(self.ms[index + 1]) + self.m = self.ms[index + 1] elif selected_emoji == self.pad_info.last_monster_emoji: - self.m = DGCOG.get_monster_by_id(self.ms[-1]) + self.m = self.ms[-1] else: self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=True) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=self.ms) return True @@ -359,7 +359,7 @@ async def skillups(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=self.settings.checkEvoID(ctx.author.id)) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted(m.alt_evos, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else []) return await self._do_menu( ctx, starting_menu_emoji, @@ -368,7 +368,7 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): ) async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=True) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=ms) return await self._do_menu( ctx, starting_menu_emoji, @@ -376,7 +376,7 @@ async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): m=m, ms=ms, selected_emoji=starting_menu_emoji) ) - def get_id_emoji_options(self, m=None, scroll=False): + def get_id_emoji_options(self, m=None, scroll=[]): id_embed = monsterToEmbed(m, self.get_emojis()) evo_embed = monsterToEvoEmbed(m) mats_embed = monsterToEvoMatsEmbed(m) @@ -403,12 +403,14 @@ def get_id_emoji_options(self, m=None, scroll=False): # IdEmojiUpdater won't allow it, however they have to be defined # so that the buttons display in the first place - if scroll and len(m.alt_evos) > 1: + + if len(scroll) > 1: emoji_to_embed[self.first_monster_emoji] = None - emoji_to_embed[self.last_monster_emoji] = None - if not (len(m.alt_evos) == 1 and scroll): + if len(scroll) != 1: emoji_to_embed[self.previous_monster_emoji] = None emoji_to_embed[self.next_monster_emoji] = None + if len(scroll) > 1: + emoji_to_embed[self.last_monster_emoji] = None # remove emoji needs to be last emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message @@ -512,21 +514,32 @@ async def collabscroll(self, ctx, *, query: str): ms = DGCOG.database.get_monsters_by_series(m.series.series_id) ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) - ms = [m._alt_evo_id_list for m in ms - if m._base_monster_id == m.monster_id + ms = [sorted({*m.alt_evos}, key=lambda m: m.monster_id) for m in ms + if m.base_monster == m and m.sell_mp >= 3000] ms = [m for ml in ms for m in ml] - if m._base_monster_id != m.monster_id: - m = self.get_monster_by_id(m._base_monster_id) - if m.monster_id not in ms: - m = m if m.monster_id in ms else self.get_monster_by_id(ms[0]) + if m.base_monster != m: + m = m.base_monster + if m not in ms: + m = m if m in ms else ms[0] if m is not None: await self._do_scrollmenu(ctx, m, ms, self.id_emoji) else: await ctx.send(self.makeFailureMsg(err)) + @commands.command(aliases=['es']) + @checks.bot_has_permissions(embed_links=True) + async def evoscroll(self, ctx, *, query: str): + """Scroll through the monsters in a collab""" + m, err, debug_info = await self.findMonster(query) + + if m is not None: + await self._do_scrollmenu(ctx, m, sorted({*m.alt_evos}, key=lambda x: x.monster_id), self.id_emoji) + else: + await ctx.send(self.makeFailureMsg(err)) + @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) async def leaderskill(self, ctx, *, whole_query): @@ -629,12 +642,12 @@ async def idmode(self, ctx, id_type): [p]idmode number [p]idmode evo""" if id_type in ['evo', 'default']: - if self.settings.rmEvoID(ctx.author.id): + if self.settings.setEvoID(ctx.author.id): await ctx.tick() else: await ctx.send(inline("You're already using evo mode")) elif id_type in ['number']: - if self.settings.setEvoID(ctx.author.id): + if self.settings.rmEvoID(ctx.author.id): await ctx.tick() else: await ctx.send(inline("You're already using number mode")) @@ -757,7 +770,7 @@ class PadInfoSettings(CogSettings): def make_default_settings(self): config = { 'animation_dir': '', - 'evo_id_optout': [], + 'alt_id_optout': [], 'voice_dir_path': '', } return config @@ -775,21 +788,21 @@ def setEmojiServers(self, emoji_servers): self.save_settings() def setEvoID(self, user_id): - if user_id not in self.bot_settings['evo_id_optout']: - self.bot_settings['evo_id_optout'].append(user_id) - self.save_settings() - return True - return False + if self.checkEvoID(user_id): + return False + self.bot_settings['alt_id_optout'].remove(user_id) + self.save_settings() + return True def rmEvoID(self, user_id): - if user_id in self.bot_settings['evo_id_optout']: - self.bot_settings['evo_id_optout'].remove(user_id) - self.save_settings() - return True - return False + if not self.checkEvoID(user_id): + return False + self.bot_settings['alt_id_optout'].append(user_id) + self.save_settings() + return True def checkEvoID(self, user_id): - return user_id not in self.bot_settings['evo_id_optout'] + return user_id not in self.bot_settings['alt_id_optout'] def setVoiceDir(self, path): self.bot_settings['voice_dir_path'] = path @@ -1132,7 +1145,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): evos_body = ", ".join(f"**{m2.monster_id}**" if m2.monster_id==m.monster_id else f"[{m2.monster_id}]({get_pdx_url(m2)})" - for m2 in sorted(m.alt_evos, key=lambda m: m.monster_id)) + for m2 in sorted({*m.alt_evos}, key=lambda m: m.monster_id)) embed.add_field(name=evos_header, value=evos_body, inline=False) return embed From 3ea4af32554fa351209348be4c844d8026138bad Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 14:43:14 -0700 Subject: [PATCH 0188/1220] Add Survey Link --- padinfo/padinfo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 96f8cbac..bab6bd1d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -284,6 +284,12 @@ async def idjp(self, ctx, *, query: str): async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): m, err, debug_info = await self.findMonster(query, server_filter=server_filter) + params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': m.name_en}) + url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params + async def send_survey_after(): + asyncio.sleep(3) + await ctx.send(f"Was this the monster you wanted? If not, fill out this survey to help the Tsubaki team!\n <{url}>") + asyncio.create_task(send_survey_after()) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: From 1f31e74be1351fb875fc59952ca3a703c5b5e7bf Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 16:03:47 -0700 Subject: [PATCH 0189/1220] padinfo logging --- padinfo/padinfo.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 13860fc5..9ec23e82 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -87,8 +87,11 @@ def __init__(self, emoji_to_embed, m: "DgMonster" = None, self.selected_emoji = selected_emoji self.bot = bot + self.pad_info.settings.log_emoji("start_"+selected_emoji) + def on_update(self, ctx, selected_emoji): evoID = self.pad_info.settings.checkEvoID(ctx.author.id) + self.pad_info.settings.log_emoji(selected_emoji) if evoID: DGCOG = self.bot.get_cog('Dadguide') evos = self.m._alt_evo_id_list @@ -809,6 +812,7 @@ def make_default_settings(self): 'animation_dir': '', 'alt_id_optout': [], 'voice_dir_path': '', + 'emoji_use': {}, } return config @@ -848,6 +852,10 @@ def setVoiceDir(self, path): def voiceDir(self): return self.bot_settings['voice_dir_path'] + def log_emoji(self, emote): + self.bot_settings['emoji_use'][emote] = self.bot_settings['emoji_use'].get(emote, 0) + 1 + self.save_settings() + def monsterToHeader(m: "DgMonster", link=False): msg = 'No. {} {}'.format(m.monster_no_na, m.name_en) From 33744b20c3f958c502c36b560252776fcabfc89e Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 16:07:12 -0700 Subject: [PATCH 0190/1220] reset expiry on fir --- dadguide/database_manager.py | 14 +++++++------- padglobal/padglobal.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index e063c807..a3aa7bb6 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -97,7 +97,7 @@ def __init__(self, data_file): self._con.row_factory = lite.Row - self._monsters = None + self.cachedmonsters = None self.expiry = 0 def has_database(self): @@ -422,15 +422,15 @@ def get_monster_query(self, monster_id: int): def get_monster(self, monster_id: int): self.refresh_monsters() - return self._monsters.get(monster_id) + return self.cachedmonsters.get(monster_id) def get_all_monster_ja_name(self, as_generator=True): return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, as_generator=as_generator) def refresh_monsters(self): - if self._monsters == None or self.expiry < datetime.now().timestamp(): - self._monsters = {m.monster_id: m for m in self.get_all_monsters_query(False)} + if self.cachedmonsters == None or self.expiry < datetime.now().timestamp(): + self.cachedmonsters = {m.monster_id: m for m in self.get_all_monsters_query(False)} self.expiry = int(datetime.now().timestamp()) + 60*60 def get_all_monsters_query(self, as_generator=True): @@ -440,8 +440,8 @@ def get_all_monsters_query(self, as_generator=True): def get_all_monsters(self, as_generator=False): self.refresh_monsters() if as_generator: - return (m for m in self._monsters) - return self._monsters + return (m for m in self.cachedmonsters) + return self.cachedmonsters def get_all_events(self, as_generator=True): return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), @@ -854,7 +854,7 @@ def prev_monster(self): def __repr__(self): return "DgMonster<{} ({})>".format(self.name_en, self.monster_no) - + def __eq__(self, other): return isinstance(other, DgMonster) and self.monster_id == other.monster_id diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 64e9e776..8975a5bc 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -291,6 +291,7 @@ async def forceindexreload(self, ctx): async with ctx.typing(): await ctx.send('starting reload') dadguide_cog = self.bot.get_cog('Dadguide') + dadguide_cog.expiry = 0 await dadguide_cog.reload_config_files() padinfo_cog = self.bot.get_cog('PadInfo') await padinfo_cog.refresh_index() From e9cb2cbae14e132a46229040c11dffa4bc94c6ea Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 16:35:16 -0700 Subject: [PATCH 0191/1220] remove first and last emotes from evo --- dadguide/database_manager.py | 7 ++++--- padinfo/padinfo.py | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index a3aa7bb6..adb8cb91 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -440,8 +440,8 @@ def get_all_monsters_query(self, as_generator=True): def get_all_monsters(self, as_generator=False): self.refresh_monsters() if as_generator: - return (m for m in self.cachedmonsters) - return self.cachedmonsters + return (m for m in self.cachedmonsters.values()) + return self.cachedmonsters.values() def get_all_events(self, as_generator=True): return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), @@ -452,8 +452,9 @@ def get_dungeon_by_id(self, dungeon_id: int): return self._select_one_entry_by_pk(dungeon_id, DgDungeon) def tokenize_monsters(self): + self.refresh_monsters() tokens = defaultdict(list) - for mid, monster in self.get_all_monsters().items(): + for mid, monster in self.cachedmonsters: for token in monster.name_en.split(): tokens[token.lower()].append(monster) return tokens diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9ec23e82..dbf63736 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -126,7 +126,7 @@ def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted(self.m.alt_evos, key=lambda x: x.monster_id) if evoID else []) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted(self.m.alt_evos, key=lambda x: x.monster_id) if evoID else [], 1) return True @@ -377,7 +377,7 @@ async def skillups(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted(m.alt_evos, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else []) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted(m.alt_evos, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], 1) return await self._do_menu( ctx, starting_menu_emoji, @@ -394,7 +394,7 @@ async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): m=m, ms=ms, selected_emoji=starting_menu_emoji) ) - def get_id_emoji_options(self, m=None, scroll=[]): + def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): id_embed = monsterToEmbed(m, self.get_emojis()) evo_embed = monsterToEvoEmbed(m) mats_embed = monsterToEvoMatsEmbed(m) @@ -422,12 +422,12 @@ def get_id_emoji_options(self, m=None, scroll=[]): # so that the buttons display in the first place - if len(scroll) > 1: + if len(scroll) > 1 and menu_type != 1: emoji_to_embed[self.first_monster_emoji] = None if len(scroll) != 1: emoji_to_embed[self.previous_monster_emoji] = None emoji_to_embed[self.next_monster_emoji] = None - if len(scroll) > 1: + if len(scroll) > 1 and menu_type != 1: emoji_to_embed[self.last_monster_emoji] = None # remove emoji needs to be last From ac10650fed1faaf70f2af956bd86818f11322d99 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 18:25:37 -0700 Subject: [PATCH 0192/1220] use menu_type as kwarg --- padinfo/padinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index dbf63736..975b041e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -126,7 +126,7 @@ def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted(self.m.alt_evos, key=lambda x: x.monster_id) if evoID else [], 1) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted(self.m.alt_evos, key=lambda x: x.monster_id) if evoID else [], menu_type = 1) return True @@ -377,7 +377,7 @@ async def skillups(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted(m.alt_evos, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], 1) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted(m.alt_evos, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) return await self._do_menu( ctx, starting_menu_emoji, From 9eb91ef59b4afa071f0a4020468382271c96867f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 19:13:59 -0700 Subject: [PATCH 0193/1220] execute_edit -> execute_write --- crud/crud.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 7d436d93..c0f7df48 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -60,7 +60,7 @@ def connect(self, db_config): cursorclass=pymysql.cursors.DictCursor, autocommit=True) - async def execute_edit(self, ctx, sql, replacements): + async def execute_write(self, ctx, sql, replacements): with await self.get_cursor() as cursor: affected = cursor.execute(sql, replacements) await ctx.send(inline(cursor._executed)) @@ -128,7 +128,7 @@ async def series_add(self, ctx, *, elements: TokenConverter): sql = ('INSERT INTO series ({})' ' VALUES ({})').format(key_infix, value_infix) - await self.execute_edit(ctx, sql, (*elements.values(),)) + await self.execute_write(ctx, sql, (*elements.values(),)) @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): @@ -148,7 +148,7 @@ async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): ' SET {}' ' WHERE series_id = %s').format(replacement_infix) - await self.execute_edit(ctx, sql, (*elements.values(), series_id)) + await self.execute_write(ctx, sql, (*elements.values(), series_id)) @series.command(name="delete") @checks.is_owner() @@ -157,7 +157,7 @@ async def series_delete(self, ctx, series_id: int): sql = ('DELETE FROM series' ' WHERE series_id = %s') - await self.execute_edit(ctx, sql, series_id) + await self.execute_write(ctx, sql, series_id) @crud.command() async def editmonsname(self, ctx, monster_id: int, *, name): @@ -180,7 +180,7 @@ async def editmonsname(self, ctx, monster_id: int, *, name): sql = ('UPDATE monsters' ' SET name_en_override = %s' ' WHERE monster_id = %s') - await self.execute_edit(ctx, sql, (name, monster_id)) + await self.execute_write(ctx, sql, (name, monster_id)) @crud.command() async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): @@ -201,7 +201,7 @@ async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): sql = ('UPDATE monsters' ' SET series_id = %s' ' WHERE monster_id = %s') - await self.execute_edit(ctx, sql, (series_id, monster_id)) + await self.execute_write(ctx, sql, (series_id, monster_id)) @crud.command() @checks.is_owner() From 4f9a941c9a1a5b328f1f01de1095be6b973f8f71 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 19:14:53 -0700 Subject: [PATCH 0194/1220] PDB Series Manipulation Draft (#106) * Revert "Revert "PDB Series Manipulation Draft"" This reverts commit 23a4b1eee01dd957eca4c310477a0f7fcc58a037. * write crud * Allow set to null * Add docstrings * format help * refactor * delete = owner_only * execute_edit -> execute_write --- crud/__init__.py | 7 ++ crud/crud.py | 210 +++++++++++++++++++++++++++++++++++++++ crud/info.json | 26 +++++ padguidedb/padguidedb.py | 81 ++++++++------- 4 files changed, 287 insertions(+), 37 deletions(-) create mode 100644 crud/__init__.py create mode 100644 crud/crud.py create mode 100644 crud/info.json diff --git a/crud/__init__.py b/crud/__init__.py new file mode 100644 index 00000000..56a24ce2 --- /dev/null +++ b/crud/__init__.py @@ -0,0 +1,7 @@ +from .crud import * + +__red_end_user_data_statement__ = "No personal data is stored." + + +def setup(bot): + bot.add_cog(Crud(bot)) diff --git a/crud/crud.py b/crud/crud.py new file mode 100644 index 00000000..c0f7df48 --- /dev/null +++ b/crud/crud.py @@ -0,0 +1,210 @@ +import asyncio +import csv +import discord +import io +import json +import logging +import pymysql +from datetime import datetime +from redbot.core import checks, commands, Config, errors +from redbot.core.utils.chat_formatting import box, inline, pagify +from tsutils import auth_check, confirm_message + +logger = logging.getLogger('red.padbot-cogs.crud') + +TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) + +SERIES_KEYS = { + "name_en": 'Untranslated', + "name_ja": 'Untranslated', + "name_ko": 'Untranslated', +} + +class Crud(commands.Cog): + """PadGuide CRUD""" + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.config = Config.get_conf(self, identifier=3270) + self.config.register_global(config_file=None) + + GADMIN_COG = self.bot.get_cog("GlobalAdmin") + if GADMIN_COG: + GADMIN_COG.register_perm("crud") + else: + raise errors.CogLoadError("Global Administration cog must be loaded.") + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + + async def get_cursor(self): + with open(await self.config.config_file(), 'r') as db_config: + return self.connect(json.load(db_config)).cursor() + + def connect(self, db_config): + return pymysql.connect(host=db_config['host'], + user=db_config['user'], + password=db_config['password'], + db=db_config['db'], + charset=db_config['charset'], + cursorclass=pymysql.cursors.DictCursor, + autocommit=True) + + async def execute_write(self, ctx, sql, replacements): + with await self.get_cursor() as cursor: + affected = cursor.execute(sql, replacements) + await ctx.send(inline(cursor._executed)) + await ctx.send("{} row(s) affected.".format(affected)) + + async def execute_read(self, ctx, sql, replacements): + with await self.get_cursor() as cursor: + cursor.execute(sql, replacements) + results = list(cursor.fetchall()) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + await ctx.send(inline(cursor._executed)) + for page in pagify(msg): + await ctx.send(box(page)) + + @commands.group(aliases=['hera-ur']) + @auth_check('crud') + async def crud(self, ctx): + """PadGuide database CRUD.""" + + @crud.command() + async def searchdungeon(self, ctx, *, search_text): + """Search for a dungeon via its jp or na name""" + search_text = '%{}%'.format(search_text).lower() + sql = ('SELECT dungeon_id, name_en, name_ja, visible FROM dungeons' + ' WHERE lower(name_en) LIKE %s OR lower(name_ja) LIKE %s' + ' ORDER BY dungeon_id DESC LIMIT 20') + await self.execute_read(ctx, sql, [search_text, search_text]) + + @crud.group() + async def series(self, ctx): + """Series related commands""" + + @series.command(name="search") + async def series_search(self, ctx, *, search_text): + """Search for a series via its jp or na name""" + search_text = '%{}%'.format(search_text).lower() + sql = ('SELECT series_id, name_en, name_ja, name_ko FROM series' + ' WHERE lower(name_en) LIKE %s OR lower(name_ja) LIKE %s' + ' ORDER BY series_id DESC LIMIT 20') + await self.execute_read(ctx, sql, [search_text, search_text]) + + @series.command(name="add") + async def series_add(self, ctx, *, elements: TokenConverter): + """Add a new series. + + Valid element keys are: `name_en`, `name_kr`, `name_jp` + + Example Usage: + [p]crud series add key1 "Value1" key2 "Value2" + """ + if not all(x in SERIES_KEYS for x in elements): + await ctx.send_help() + return + + EXTRAS = {} + with await self.get_cursor() as cursor: + cursor.execute("SELECT MAX(series_id) AS max_id FROM series") + max_val = cursor.fetchall()[0]['max_id'] + EXTRAS['series_id'] = max_val + 1 + EXTRAS['tstamp'] = int(datetime.now().timestamp()) + elements = {**SERIES_KEYS, **EXTRAS, **elements} + + key_infix = ", ".join(elements.keys()) + value_infix = ", ".join("%s" for v in elements.values()) + sql = ('INSERT INTO series ({})' + ' VALUES ({})').format(key_infix, value_infix) + + await self.execute_write(ctx, sql, (*elements.values(),)) + + @series.command(name="edit") + async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): + """Edit an existing series series. + + Valid element keys are: `name_en`, `name_kr`, `name_jp` + + Example Usage: + [p]crud series edit 100 key1 "Value1" key2 "Value2" + """ + if not all(x in SERIES_KEYS for x in elements): + await ctx.send_help() + return + + replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) + sql = ('UPDATE series' + ' SET {}' + ' WHERE series_id = %s').format(replacement_infix) + + await self.execute_write(ctx, sql, (*elements.values(), series_id)) + + @series.command(name="delete") + @checks.is_owner() + async def series_delete(self, ctx, series_id: int): + """Delete an existing series""" + sql = ('DELETE FROM series' + ' WHERE series_id = %s') + + await self.execute_write(ctx, sql, series_id) + + @crud.command() + async def editmonsname(self, ctx, monster_id: int, *, name): + """Change a monster's name_en_override""" + if name.lower() in ["none", "null"]: + name = None + with await self.get_cursor() as cursor: + cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) + old_val = cursor.fetchall() + + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['name_en_override'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " english override from `{}` to `{}`?" + "").format(monster_id, old_val, name)): + return + + sql = ('UPDATE monsters' + ' SET name_en_override = %s' + ' WHERE monster_id = %s') + await self.execute_write(ctx, sql, (name, monster_id)) + + @crud.command() + async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): + """Change a monster's series_id""" + with await self.get_cursor() as cursor: + cursor.execute("SELECT series_id FROM monsters WHERE monster_id = %s", (monster_id,)) + old_val = cursor.fetchall() + + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['series_id'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " series id from `{}` to `{}`?" + "").format(monster_id, old_val, series_id)): + return + + sql = ('UPDATE monsters' + ' SET series_id = %s' + ' WHERE monster_id = %s') + await self.execute_write(ctx, sql, (series_id, monster_id)) + + @crud.command() + @checks.is_owner() + async def setconfig(self, ctx, path): + await self.config.config_file.set(path) + await ctx.tick() diff --git a/crud/info.json b/crud/info.json new file mode 100644 index 00000000..037a7719 --- /dev/null +++ b/crud/info.json @@ -0,0 +1,26 @@ +{ + "author": [ + "Aradia Megido#2552" + ], + "description": "Crud for the Pipeline Database.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Pipeline CRUD", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "require_cogs": {}, + "requirements": [ + "tsutils", + "pymysql" + ], + "tags": [ + "PAD" + ] +} diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index c1c8ecb2..5e4149a4 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -5,6 +5,7 @@ import json import logging import pymysql +from datetime import datetime from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify @@ -21,6 +22,13 @@ def is_padguidedb_admin_check(ctx): def is_padguidedb_admin(): return commands.check(is_padguidedb_admin_check) +TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) + +SERIES_KEYS = { + "name_en": 'Untranslated', + "name_ja": 'Untranslated', + "name_ko": 'Untranslated', +} class PadGuideDb(commands.Cog): """PadGuide Database manipulator""" @@ -96,9 +104,8 @@ async def setconfigfile(self, ctx, *, config_file): await ctx.tick() @padguidedb.command() - @is_padguidedb_admin() async def searchdungeon(self, ctx, *, search_text): - """Search""" + """Search for a dungeon via its jp or na name""" search_text = '%{}%'.format(search_text) with self.get_cursor() as cursor: sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' @@ -111,41 +118,6 @@ async def searchdungeon(self, ctx, *, search_text): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.command() - @checks.is_owner() - async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): - """Set the dungeon script file.""" - self.settings.setDungeonScriptFile(dungeon_script_file) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setfulletlfile(self, ctx, *, full_etl_file): - """Set the full ETL file.""" - self.settings.setFullETLFile(full_etl_file) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setimageupdatefile(self, ctx, *, image_update_file): - """Set the image update file.""" - self.settings.setImageUpdateFile(image_update_file) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setpythonexecutable(self, ctx, *, python_executable): - """Set the python executable file.""" - self.settings.setPythonExecutable(python_executable) - await ctx.tick() - - @padguidedb.command() - @checks.is_owner() - async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): - """Set the dungeon script.""" - self.settings.setUserInfo(server, user_uuid, user_intid) - await ctx.tick() - @padguidedb.command() @is_padguidedb_admin() async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): @@ -324,6 +296,41 @@ async def extractimages(self, ctx): return await ctx.send(inline('Image extract finished')) + @padguidedb.command() + @checks.is_owner() + async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): + """Set the dungeon script file.""" + self.settings.setDungeonScriptFile(dungeon_script_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setfulletlfile(self, ctx, *, full_etl_file): + """Set the full ETL file.""" + self.settings.setFullETLFile(full_etl_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setimageupdatefile(self, ctx, *, image_update_file): + """Set the image update file.""" + self.settings.setImageUpdateFile(image_update_file) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setpythonexecutable(self, ctx, *, python_executable): + """Set the python executable file.""" + self.settings.setPythonExecutable(python_executable) + await ctx.tick() + + @padguidedb.command() + @checks.is_owner() + async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): + """Set the dungeon script.""" + self.settings.setUserInfo(server, user_uuid, user_intid) + await ctx.tick() + class PadGuideDbSettings(CogSettings): def make_default_settings(self): From 3a3667ea57012e7ae943c6b8ead11f90f60f8a9f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 15 Oct 2020 20:19:36 -0700 Subject: [PATCH 0195/1220] database expiry --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8975a5bc..3bb61561 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -291,7 +291,7 @@ async def forceindexreload(self, ctx): async with ctx.typing(): await ctx.send('starting reload') dadguide_cog = self.bot.get_cog('Dadguide') - dadguide_cog.expiry = 0 + dadguide_cog.database.expiry = 0 await dadguide_cog.reload_config_files() padinfo_cog = self.bot.get_cog('PadInfo') await padinfo_cog.refresh_index() From 8692bbbcfb532e83a8645114039f56a997b9ce59 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 16 Oct 2020 02:58:56 -0700 Subject: [PATCH 0196/1220] AED and Crud Channel --- crud/crud.py | 19 +++++- padevents/padevents.py | 137 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index c0f7df48..7b387993 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -20,6 +20,10 @@ "name_ko": 'Untranslated', } +async def check_crud_channel(ctx): + chan = await ctx.bot.get_cog("Crud").config.chan() + return chan is None or chan == ctx.channel.id or ctx.author.id in ctx.bot.owner_ids + class Crud(commands.Cog): """PadGuide CRUD""" @@ -27,7 +31,7 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.config = Config.get_conf(self, identifier=3270) - self.config.register_global(config_file=None) + self.config.register_global(config_file=None, chan=None) GADMIN_COG = self.bot.get_cog("GlobalAdmin") if GADMIN_COG: @@ -77,6 +81,7 @@ async def execute_read(self, ctx, sql, replacements): @commands.group(aliases=['hera-ur']) @auth_check('crud') + @commands.check(check_crud_channel) async def crud(self, ctx): """PadGuide database CRUD.""" @@ -208,3 +213,15 @@ async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): async def setconfig(self, ctx, path): await self.config.config_file.set(path) await ctx.tick() + + @crud.command() + @checks.is_owner() + async def setchan(self, ctx, channel: discord.TextChannel): + await self.config.chan.set(channel.id) + await ctx.tick() + + @crud.command() + @checks.is_owner() + async def rmchan(self, ctx): + await self.config.chan.set(None) + await ctx.tick() diff --git a/padevents/padevents.py b/padevents/padevents.py index ea529ba8..65a2f77f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -13,7 +13,7 @@ from redbot.core import checks from redbot.core import commands, Config from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import CogSettings +from tsutils import CogSettings, confirm_message logger = logging.getLogger('red.padbot-cogs.padevents') @@ -28,6 +28,7 @@ def __init__(self, bot, *args, **kwargs): self.settings = PadEventSettings("padevents") self.config = Config.get_conf(self, identifier=940373775) self.config.register_guild(pingroles={}) + self.config.register_user(dmevents=[]) # Load event data self.events = list() @@ -88,6 +89,7 @@ async def check_started(self): for e in events: for gid, data in (await self.config.all_guilds()).items(): guild = self.bot.get_guild(gid) + if guild is None: continue for key, aep in data.get('pingroles', {}).items(): if e.start_from_now_sec() > aep['offset'] * 60 \ or not aep['enabled'] \ @@ -109,7 +111,29 @@ async def check_started(self): offsetstr = "" if aep['offset']: offsetstr = " in {} minute(s)".format(aep['offset']) - await channel.send("{}{} {}".format(e.name_and_modifier, offsetstr, ment), allowed_mentions=discord.AllowedMentions(roles=True)) + try: + await channel.send("{}{} {}".format(e.name_and_modifier, offsetstr, ment), allowed_mentions=discord.AllowedMentions(roles=True)) + except Exception: + logger.exception("Failed to send AEP in channel {}".format(channel.id)) + + for uid, data in (await self.config.all_users()).items(): + user = self.bot.get_user(uid) + if user is None: continue + for aed in data.get('dmevents', []): + if e.start_from_now_sec() > aed['offset'] * 60 \ + or e.group != aed['group'] \ + or e.server != aed['server'] \ + or (aed['key'], e.key) in self.rolepinged_events: + continue + self.rolepinged_events.add((aed['key'], e.key)) + if aed['searchstr'] in e.clean_dungeon_name: + offsetstr = " starts now!" + if aed['offset']: + offsetstr = " starts in {} minute(s)!".format(aed['offset']) + try: + await user.send(e.clean_dungeon_name + offsetstr) + except Exception: + logger.exception("Failed to send AED to user {}".format(user.id)) events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) daily_refresh_servers = set() @@ -448,6 +472,115 @@ async def aep_s_disabled(self, ctx, key, offset: int): await self.aeps(ctx, key, 'offset', offset) await ctx.tick() + @commands.group(aliases=['aed']) + async def autoeventdm(self, ctx): + """Auto Event DMs""" + + @autoeventdm.command(name="add") + async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): + """Add a new autoeventdm""" + server = normalizeServer(server) + if server not in SUPPORTED_SERVERS: + await ctx.send("Unsupported server, pick one of NA, KR, JP") + return + group = group.lower() + if group not in GROUPS: + await ctx.send("Unsupported group, pick one of red, blue, green") + return + if offset < 0: + await ctx.send("Offset cannot be negative") + return + elif offset > 0: + DONOR_COG = self.bot.get_cog("Donations") + if DONOR_COG is None: + await ctx.send(inline("Donor Cog not loaded. Please contact a bot admin.")) + elif not DONOR_COG.is_donor(ctx): + await ctx.send(("AED offset is a donor only feature due to server loads." + " Your auto event DM will be created, but the offset will not" + " be in affect until you're a donor. You can donate any time" + " at https://www.patreon.com/tsubaki_bot. Use `{}donate` to" + " view this link at any time").format(ctx.prefix)) + + default = { + 'key': datetime.datetime.now().timestamp(), + 'server': server, + 'group': group, + 'searchstr': searchstr, + 'offset': offset, + } + + async with self.config.user(ctx.author).dmevents() as dmevents: + dmevents.append(default) + await ctx.tick() + + @autoeventdm.command(name="remove", aliases=['rm', 'delete']) + async def aed_remove(self, ctx, index: int): + """Remove an autoeventdm""" + async with self.config.user(ctx.author).dmevents() as dmevents: + if not 0 <= index < len(dmevents): + await ctx.send("That isn't a valid index.") + return + if not await confirm_message(ctx, ("Are you sure you want to delete autoeventdm with searchstring '{}'" + "").format(dmevents[index]['searchstr'])): + return + dmevents.pop(index) + await ctx.tick() + + @autoeventdm.command(name="list") + async def aed_list(self, ctx, index: int): + """List current autoeventdms""" + dmevents = await self.config.user(ctx.author).dmevents() + msg = "\n".join("{}. {}".format(c, aed['searchstr']) for c, aed in enumerate(dmevents)) + if msg: + await ctx.send(box(msg)) + else: + await ctx.send("You have no autoeventdms set up.") + + @autoeventdm.command(name="purge") + async def aed_purge(self, ctx, index: int): + """Remove an autoeventdm""" + if not await confirm_message(ctx, "Are you sure you want to purge your autoeventdms?"): + return + await self.config.user(ctx.author).dmevents.set([]) + await ctx.tick() + + @autoeventdm.group(name="edit") + async def aed_e(self, ctx): + """Edit a property of the autoeventdm""" + + @aed_e.command(name="offset") + async def aed_e_offset(self, ctx, index, offset): + """Set offset of an autoeventdm (Donor only)""" + if offset < 0: + await ctx.send("Offset cannot be negative") + return + elif offset > 0: + DONOR_COG = self.bot.get_cog("Donations") + if DONOR_COG is None: + await ctx.send(inline("Donor Cog not loaded. Please contact a bot admin.")) + elif not DONOR_COG.is_donor(ctx): + await ctx.send(("AED offset is a donor only feature due to server loads." + " Your auto event DM will be created, but the offset will not" + " be in affect until you're a donor. You can donate any time" + " at https://www.patreon.com/tsubaki_bot. Use `{}donate` to" + " view this link at any time").format(ctx.prefix)) + async with self.config.user(ctx.author).dmevents() as dmevents: + if not 0 <= index < len(dmevents): + await ctx.send("That isn't a valid index.") + return + dmevents[index]['offset'] = offset + await ctx.tick() + + @aed_e.command(name="searchstr") + async def aed_e_searchstr(self, ctx, index, *, searchstr): + """Set search string of an autoeventdm""" + async with self.config.user(ctx.author).dmevents() as dmevents: + if not 0 <= index < len(dmevents): + await ctx.send("That isn't a valid index.") + return + dmevents[index]['searchstr'] = searchstr + await ctx.tick() + @padevents.command(name="listchannels") @checks.mod_or_permissions(manage_guild=True) async def _listchannel(self, ctx): From 5a3797b3e8705624bcfaa4455382fa1a83ed9fed Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 16 Oct 2020 05:19:01 -0700 Subject: [PATCH 0197/1220] Issue Fixes --- padevents/padevents.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 65a2f77f..b3f11d36 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -517,27 +517,27 @@ async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): async def aed_remove(self, ctx, index: int): """Remove an autoeventdm""" async with self.config.user(ctx.author).dmevents() as dmevents: - if not 0 <= index < len(dmevents): + if not 0 < index <= len(dmevents): await ctx.send("That isn't a valid index.") return if not await confirm_message(ctx, ("Are you sure you want to delete autoeventdm with searchstring '{}'" - "").format(dmevents[index]['searchstr'])): + "").format(dmevents[index-1]['searchstr'])): return - dmevents.pop(index) + dmevents.pop(index-1) await ctx.tick() @autoeventdm.command(name="list") - async def aed_list(self, ctx, index: int): + async def aed_list(self, ctx): """List current autoeventdms""" dmevents = await self.config.user(ctx.author).dmevents() - msg = "\n".join("{}. {}".format(c, aed['searchstr']) for c, aed in enumerate(dmevents)) + msg = "\n".join("{}. {}".format(c, aed['searchstr']) for c, aed in enumerate(dmevents, 1)) if msg: await ctx.send(box(msg)) else: await ctx.send("You have no autoeventdms set up.") @autoeventdm.command(name="purge") - async def aed_purge(self, ctx, index: int): + async def aed_purge(self, ctx): """Remove an autoeventdm""" if not await confirm_message(ctx, "Are you sure you want to purge your autoeventdms?"): return @@ -565,20 +565,20 @@ async def aed_e_offset(self, ctx, index, offset): " at https://www.patreon.com/tsubaki_bot. Use `{}donate` to" " view this link at any time").format(ctx.prefix)) async with self.config.user(ctx.author).dmevents() as dmevents: - if not 0 <= index < len(dmevents): + if not 0 < index <= len(dmevents): await ctx.send("That isn't a valid index.") return - dmevents[index]['offset'] = offset + dmevents[index-1]['offset'] = offset await ctx.tick() @aed_e.command(name="searchstr") async def aed_e_searchstr(self, ctx, index, *, searchstr): """Set search string of an autoeventdm""" async with self.config.user(ctx.author).dmevents() as dmevents: - if not 0 <= index < len(dmevents): + if not 0 < index <= len(dmevents): await ctx.send("That isn't a valid index.") return - dmevents[index]['searchstr'] = searchstr + dmevents[index-1]['searchstr'] = searchstr await ctx.tick() @padevents.command(name="listchannels") From 5dc143c990f25ef554d74a8a606c1ef5c345aa97 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 16 Oct 2020 05:23:52 -0700 Subject: [PATCH 0198/1220] fail on empty purge --- padevents/padevents.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/padevents/padevents.py b/padevents/padevents.py index b3f11d36..2fa39347 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -539,6 +539,9 @@ async def aed_list(self, ctx): @autoeventdm.command(name="purge") async def aed_purge(self, ctx): """Remove an autoeventdm""" + if not await self.config.user(ctx.author).dmevents(): + await ctx.send("You don't have any autoeventdms.") + return if not await confirm_message(ctx, "Are you sure you want to purge your autoeventdms?"): return await self.config.user(ctx.author).dmevents.set([]) From 7c8eaab17870871c8de99e1b99f8ac01661cd8bb Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 17 Oct 2020 21:37:29 -0700 Subject: [PATCH 0199/1220] mega awoken prefixes --- dadguide/database_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index a3aa7bb6..1cfa6892 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -1130,6 +1130,8 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('mega') prefixes.add('mega awoken') prefixes.add('awoken') + prefixes.add('mawo') + prefixes.add('ma') if srevo: prefixes.add('srevo') From d15761288d86ba3642695a6f040f2ecebcb2c90f Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 17 Oct 2020 21:42:16 -0700 Subject: [PATCH 0200/1220] no more mawo --- dadguide/database_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 1cfa6892..0282b33e 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -1130,7 +1130,6 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('mega') prefixes.add('mega awoken') prefixes.add('awoken') - prefixes.add('mawo') prefixes.add('ma') if srevo: From 420c780e0aef65530734bc19fc9bc4ab78f43547 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 18 Oct 2020 05:50:10 -0700 Subject: [PATCH 0201/1220] GFE Revo ID --- dadguide/database_manager.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 0282b33e..d3b9d9fe 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -1148,6 +1148,12 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('uuvo') prefixes.add('uuevo') + # True Evo Type Prefixes + if m.trueEvoType == InternalEvoType.Reincarnated: + prefixes.add('revo') + prefixes.add('reincarnated') + + # Other Prefixes if m.farmable: prefixes.add('farmable') From 7d8e26e589fbbc55ea743aa76fcf19616b806e67 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 18 Oct 2020 06:24:16 -0700 Subject: [PATCH 0202/1220] scroll fix --- dadguide/database_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index d3b9d9fe..83c18389 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -177,7 +177,7 @@ def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): def _max_id(self): cursor = self._con.cursor() - cursor.execute("SELECT MAX(monster_id) FROM monsters WHERE monster_id < 10000") + cursor.execute("SELECT MAX(monster_id) FROM monsters") return cursor.fetchone()['MAX(monster_id)'] def _select_one_entry_by_pk(self, pk, d_type): @@ -1149,7 +1149,7 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('uuevo') # True Evo Type Prefixes - if m.trueEvoType == InternalEvoType.Reincarnated: + if m.true_evo_type == InternalEvoType.Reincarnated: prefixes.add('revo') prefixes.add('reincarnated') From 17cb4d29f0fef3e9ece4581950d80e12ff2bb8d9 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 18 Oct 2020 06:32:51 -0700 Subject: [PATCH 0203/1220] auto fir fix --- dadguide/dadguide.py | 10 ++++++---- padglobal/padglobal.py | 1 - padinfo/padinfo.py | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index f30511b5..6035c150 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -169,16 +169,18 @@ async def download_and_refresh_nicknames(self): self.panthname_overrides = {x[0].lower(): x[1].lower() for x in panthname_overrides} self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) - logger.info('Loading dg database') + logger.debug('Loading dg database') self.database = load_database(self.database) - logger.info('Building dg monster index') + logger.debug('Building dg monster index') self.index = await MonsterIndex(self.database, self.nickname_overrides, self.basename_overrides, self.panthname_overrides) - logger.info('Writing dg monster computed names') + logger.debug('Writing dg monster computed names') self.write_monster_computed_names() - logger.info('Done refreshing dg data') + self.database.expiry = 0 + logger.debug('Done refreshing dg data') + def write_monster_computed_names(self): results = {} diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 3bb61561..64e9e776 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -291,7 +291,6 @@ async def forceindexreload(self, ctx): async with ctx.typing(): await ctx.send('starting reload') dadguide_cog = self.bot.get_cog('Dadguide') - dadguide_cog.database.expiry = 0 await dadguide_cog.reload_config_files() padinfo_cog = self.bot.get_cog('PadInfo') await padinfo_cog.refresh_index() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9ec23e82..837b04f3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -242,13 +242,13 @@ async def refresh_index(self): await dg_cog.wait_until_ready() async with self.index_lock: - logger.info('Loading ALL index') + logger.debug('Loading ALL index') self.index_all = await dg_cog.create_index() - logger.info('Loading NA index') + logger.debug('Loading NA index') self.index_na = await dg_cog.create_index(lambda m: m.on_na) - logger.info('Loading JP index') + logger.debug('Loading JP index') self.index_jp = await dg_cog.create_index(lambda m: m.on_jp) logger.info('Done refreshing indexes') From f7ff19684fba9ad98fd1fbed35307fcbb3d608a2 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 18 Oct 2020 07:12:36 -0700 Subject: [PATCH 0204/1220] evo mode id fix (it's fast now and it works) --- padinfo/padinfo.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 78da6b41..7f3b98c0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -94,19 +94,15 @@ def on_update(self, ctx, selected_emoji): self.pad_info.settings.log_emoji(selected_emoji) if evoID: DGCOG = self.bot.get_cog('Dadguide') - evos = self.m._alt_evo_id_list - evos.sort() - index = evos.index(self.m.monster_id) + evos = sorted({*self.m.alt_evos}, key=lambda m: m.monster_id) + index = evos.index(self.m) if selected_emoji == self.pad_info.previous_monster_emoji: - if index == 0: - newm = DGCOG.get_monster_by_id(evos[-1]) - else: - newm = DGCOG.get_monster_by_id(evos[index - 1]) + newm = evos[index - 1] elif selected_emoji == self.pad_info.next_monster_emoji: if index == len(evos) - 1: - newm = DGCOG.get_monster_by_id(evos[0]) + newm = evos[0] else: - newm = DGCOG.get_monster_by_id(evos[index + 1]) + newm = evos[index + 1] else: self.selected_emoji = selected_emoji return True @@ -126,7 +122,7 @@ def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted(self.m.alt_evos, key=lambda x: x.monster_id) if evoID else [], menu_type = 1) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted({*self.m.alt_evos}, key=lambda x: x.monster_id) if evoID else [], menu_type = 1) return True @@ -377,7 +373,7 @@ async def skillups(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted(m.alt_evos, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted({*m.alt_evos}, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) return await self._do_menu( ctx, starting_menu_emoji, @@ -878,7 +874,7 @@ def monsterToLongHeader(m: "DgMonster", link=False): def monsterToEvoText(m: "DgMonster"): output = monsterToLongHeader(m) - for ae in sorted(m.alt_evos, key=lambda x: int(x.monster_id)): + for ae in sorted({*m.alt_evos}, key=lambda x: int(x.monster_id)): output += "\n\t- {}".format(monsterToLongHeader(ae)) return output From ced357c7b17626ae0560bd24335eb68ee8ba9099 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 19 Oct 2020 02:57:59 -0700 Subject: [PATCH 0205/1220] Dadguide DgMonster creation change --- dadguide/database_manager.py | 117 ++++++++++++++--------------------- 1 file changed, 48 insertions(+), 69 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index adb8cb91..13fb27aa 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -216,6 +216,7 @@ def get_awoken_skill_ids(self): self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] def get_monsters_by_awakenings(self, awoken_skill_id: int): + # TODO: Make this not make monsters via query return self._query_many( self._select_builder( tables=OrderedDict({ @@ -269,31 +270,17 @@ def monster_is_farmable(self, monster_id): DgDrop) is not None def monster_in_rem(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('rem_egg',)}, - where='{0}.monster_id=? AND rem_egg=1'.format(DgMonster.TABLE) - ), - (monster_id,), - DgDrop) is not None + m = self.get_monster(monster_id) + return m is not None and m.rem_egg == 1 def monster_in_pem(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('pal_egg',)}, - where='{0}.monster_id=? AND pal_egg=1'.format(DgMonster.TABLE) - ), - (monster_id,), - DgDrop) is not None + m = self.get_monster(monster_id) + return m is not None and m.pem_egg == 1 + def monster_in_mp_shop(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('buy_mp',)}, - where='{0}.monster_id=? AND buy_mp IS NOT NULL'.format(DgMonster.TABLE) - ), - (monster_id,), - DgDrop) is not None + m = self.get_monster(monster_id) + return m is not None and m.buy_mp is not None def get_prev_evolution_by_monster(self, monster_id): return self._query_one( @@ -365,83 +352,75 @@ def get_evolution_tree_ids(self, base_monster_id): return evolution_tree def monster_id_to_no(self, monster_id, region=Server.JP): - res = self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('monster_no_{}'.format(region.name.lower()),)}, - where='{}.monster_id=?'.format(DgMonster.TABLE) - ), - (monster_id,), - DictWithAttrAccess) - for k in res: - return res[k] + m = self.get_monster(monster_id) + if m: + return getattr(m, 'monster_no_{}'.format(region.name.lower())) def get_series(self, series_id: int): return self._select_one_entry_by_pk(series_id, DgSeries) - def _get_monsters_where(self, where, param): - return self._query_many( - self._select_builder( - tables={DgMonster.TABLE: DgMonster.FIELDS}, - where=where - ), - param, - DgMonster) + def get_monsters_where(self, f): + return [m for m in self.get_all_monsters() if f(m)] + + def get_first_monster_where(self, f): + ms = self.get_monsters_where(f) + if ms: + return min(ms, key=lambda m: m.monster_id) def get_monsters_by_series(self, series_id: int): - return self._get_monsters_where('{}.series_id=?'.format(DgMonster.TABLE), (series_id,)) + return self.get_monsters_where(lambda m: m.series_id == series_id) def get_monsters_by_active(self, active_skill_id: int): - return self._get_monsters_where('{}.active_skill_id=?'.format(DgMonster.TABLE), (active_skill_id,)) + return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id) - def get_monster_evo_gem(self, name: str, region='jp'): + def get_monster_evo_gem(self, name: str, region='ja'): gem_suffix = { 'ja': 'の希石', 'en': '\'s Gem', 'ko': ' 의 휘석' } + if region not in gem_suffix: return None - non_gem_name = name.replace(gem_suffix[region], '') + non_gem_name = name.replace(gem_suffix.get(region), '') if non_gem_name == name: return None - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: DgMonster.FIELDS}, - where='{0}.name_{1}=? AND {0}.leader_skill_id=10628'.format(DgMonster.TABLE, region) - ), - (non_gem_name,), - DgMonster) + + return self.get_first_monster_where(lambda m: getattr(m, 'name_{}'.format(region)) == non_gem_name and m.leader_skill_id == 10628) + def get_na_only_monsters(self): - return self._get_monsters_where( - '{0}.monster_id != {0}.monster_no_na AND {0}.monster_no_jp == {0}.monster_no_na '.format(DgMonster.TABLE), - ()) + return self.get_monsters_where(lambda m: m.monster_id != m.monster_no_na and m.monster_no_jp == m.monster_no_na) def get_monster_query(self, monster_id: int): - return self._select_one_entry_by_pk(monster_id, DgMonster) + monster = self._select_one_entry_by_pk(monster_id, DgMonster) + if monster is not None: + self.cachedmonsters[monster.monster_id] = monster + return monster def get_monster(self, monster_id: int): self.refresh_monsters() - return self.cachedmonsters.get(monster_id) + if monster_id in self.cachedmonsters: + return self.cachedmonsters.get(monster_id) + return self.get_monster_query(monster_id) - def get_all_monster_ja_name(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: ('name_ja',)}), (), DictWithAttrAccess, + def get_all_monster_ids_query(self, as_generator=True): + query = self._query_many(self._select_builder(tables={DgMonster.TABLE: ('monster_id',)}), (), DictWithAttrAccess, as_generator=as_generator) + if as_generator: + return map(lambda m: m.monster_id, query) + return [m.monster_id for m in query] def refresh_monsters(self): - if self.cachedmonsters == None or self.expiry < datetime.now().timestamp(): - self.cachedmonsters = {m.monster_id: m for m in self.get_all_monsters_query(False)} + if self.expiry < datetime.now().timestamp(): self.expiry = int(datetime.now().timestamp()) + 60*60 + self.cachedmonsters = {} - def get_all_monsters_query(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, - as_generator=as_generator) - - def get_all_monsters(self, as_generator=False): - self.refresh_monsters() - if as_generator: - return (m for m in self.cachedmonsters.values()) - return self.cachedmonsters.values() + def get_all_monsters(self, as_generator=True): + monsters = (self.get_monster(mid) for mid in self.get_all_monster_ids_query()) + if not as_generator: + return [*monsters] + return monsters def get_all_events(self, as_generator=True): return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), @@ -452,9 +431,9 @@ def get_dungeon_by_id(self, dungeon_id: int): return self._select_one_entry_by_pk(dungeon_id, DgDungeon) def tokenize_monsters(self): - self.refresh_monsters() tokens = defaultdict(list) - for mid, monster in self.cachedmonsters: + for mid in self.get_all_monster_ids_query(): + monster = self.get_monster(mid) for token in monster.name_en.split(): tokens[token.lower()].append(monster) return tokens From 550a5f3f492fcc725f36eda1c52d96f379200f02 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 19 Oct 2020 03:02:50 -0700 Subject: [PATCH 0206/1220] pem -> pal --- dadguide/database_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 3742ef04..3e5a6fcf 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -275,7 +275,7 @@ def monster_in_rem(self, monster_id): def monster_in_pem(self, monster_id): m = self.get_monster(monster_id) - return m is not None and m.pem_egg == 1 + return m is not None and m.pal_egg == 1 def monster_in_mp_shop(self, monster_id): From 470a44a64c7392797c7bf73d011120910d38f4a1 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 03:12:11 -0700 Subject: [PATCH 0207/1220] Transform --- dadguide/dadguide.py | 1 - dadguide/database_manager.py | 66 ++++++++++++++++++++++++------------ padglobal/padglobal.py | 2 +- padinfo/padinfo.py | 20 +++++------ 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 6035c150..dbe4c229 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -178,7 +178,6 @@ async def download_and_refresh_nicknames(self): logger.debug('Writing dg monster computed names') self.write_monster_computed_names() - self.database.expiry = 0 logger.debug('Done refreshing dg data') diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 3e5a6fcf..165ff229 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -277,7 +277,6 @@ def monster_in_pem(self, monster_id): m = self.get_monster(monster_id) return m is not None and m.pal_egg == 1 - def monster_in_mp_shop(self, monster_id): m = self.get_monster(monster_id) return m is not None and m.buy_mp is not None @@ -301,22 +300,46 @@ def get_next_evolutions_by_monster(self, monster_id): DgEvolution, as_generator=True) - def get_base_monster_by_monster(self, monster_id): - base = {'from_id': monster_id, 'to_id': monster_id} - lastbase = None - while base != None: - lastbase = base - base = self.get_prev_evolution_by_monster(base['from_id']) - return lastbase + def get_prev_transformations_by_monster(self, monster_id): + return self._query_many( + self._select_builder( + tables={DgMonster.TABLE: ('monster_id AS from_id', 'linked_monster_id AS to_id')}, + where='{}.linked_monster_id=?'.format(DgMonster.TABLE) + ), + (monster_id,), + DictWithAttrAccess, + as_generator=True) - def get_all_evolutions_by_monster(self, monster_id): - base = self.get_base_monster_by_monster(monster_id) - queue = [base] + def get_next_transformation_by_monster(self, monster_id): + return self._query_one( + self._select_builder( + tables={DgMonster.TABLE: ('monster_id AS from_id', 'linked_monster_id AS to_id')}, + where='{}.monster_id=?'.format(DgMonster.TABLE) + ), + (monster_id,), + DictWithAttrAccess) + + def build_id_set(self, monster_id): + to_check = {monster_id} + all_ids = {monster_id} + while to_check: + mid = to_check.pop() + linked_monsters = set() + linked_monsters.add(getattr(self.get_prev_evolution_by_monster(mid), 'from_id', None)) + linked_monsters.update({e.to_id for e in self.get_next_evolutions_by_monster(mid)}) + linked_monsters.update({e.from_id for e in self.get_prev_transformations_by_monster(mid)}) + linked_monsters.add(getattr(self.get_next_transformation_by_monster(mid), 'to_id', None)) + linked_monsters.discard(None) + + linked_monsters.difference_update(all_ids) - while queue: - curr = queue.pop(0) - yield curr - queue.extend(self.get_next_evolutions_by_monster(curr['to_id'])) + to_check |= linked_monsters + all_ids |= linked_monsters + return sorted(all_ids) + + + def get_base_monster_by_id(self, monster_id): + return self.build_id_set(monster_id)[0] def get_evolution_by_material(self, monster_id): return self._query_many( @@ -604,6 +627,7 @@ class DgMonster(DadguideItem): def __init__(self, item, database): super(DgMonster, self).__init__(item, database) + print(self.monster_id) self.roma_subname = None if self.name_en == self.name_ja: @@ -634,12 +658,8 @@ def __init__(self, item, database): self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) - base_id = self.monster_id - next_base = self._database.get_prev_evolution_by_monster(base_id) - while next_base is not None: - base_id = next_base.from_id - next_base = self._database.get_prev_evolution_by_monster(base_id) - self._base_monster_id = base_id + self._alt_version_id_list = self._database.build_id_set(self.monster_id) + self._base_monster_id = self._alt_version_id_list[0] self._alt_evo_id_list = self._database.get_evolution_tree_ids(self._base_monster_id) self.search = MonsterSearchHelper(self) @@ -740,6 +760,10 @@ def base_monster(self): def alt_evos(self): return [self._database.get_monster(a) for a in self._alt_evo_id_list] + @property + def alt_versions(self): + return [self._database.get_monster(a) for a in self._alt_version_id_list] + @property def is_base_monster(self): return self.evo_from is None diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 64e9e776..87df5e10 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1200,7 +1200,7 @@ def emojify(self, message): def check_simple_tree(monster): attr1 = monster.attr1 active_skill = monster.active_skill - for m in monster.alt_evos: + for m in monster.alt_versions: if m.attr1 != attr1 or m.active_skill != active_skill: return False if m.is_equip: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7f3b98c0..98e10835 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -94,7 +94,7 @@ def on_update(self, ctx, selected_emoji): self.pad_info.settings.log_emoji(selected_emoji) if evoID: DGCOG = self.bot.get_cog('Dadguide') - evos = sorted({*self.m.alt_evos}, key=lambda m: m.monster_id) + evos = sorted({*self.m.alt_versions}, key=lambda m: m.monster_id) index = evos.index(self.m) if selected_emoji == self.pad_info.previous_monster_emoji: newm = evos[index - 1] @@ -122,7 +122,7 @@ def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted({*self.m.alt_evos}, key=lambda x: x.monster_id) if evoID else [], menu_type = 1) + self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted({*self.m.alt_versions}, key=lambda x: x.monster_id) if evoID else [], menu_type = 1) return True @@ -373,7 +373,7 @@ async def skillups(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted({*m.alt_evos}, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted({*m.alt_versions}, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) return await self._do_menu( ctx, starting_menu_emoji, @@ -431,7 +431,7 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): return emoji_to_embed async def _do_evolistmenu(self, ctx, sm): - monsters = sm.alt_evos + monsters = sm.alt_versions monsters.sort(key=lambda m: m.monster_id) emoji_to_embed = OrderedDict() @@ -528,7 +528,7 @@ async def collabscroll(self, ctx, *, query: str): ms = DGCOG.database.get_monsters_by_series(m.series.series_id) ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) - ms = [sorted({*m.alt_evos}, key=lambda m: m.monster_id) for m in ms + ms = [sorted({*m.alt_versions}, key=lambda m: m.monster_id) for m in ms if m.base_monster == m and m.sell_mp >= 3000] ms = [m for ml in ms for m in ml] @@ -550,7 +550,7 @@ async def evoscroll(self, ctx, *, query: str): m, err, debug_info = await self.findMonster(query) if m is not None: - await self._do_scrollmenu(ctx, m, sorted({*m.alt_evos}, key=lambda x: x.monster_id), self.id_emoji) + await self._do_scrollmenu(ctx, m, sorted({*m.alt_versions}, key=lambda x: x.monster_id), self.id_emoji) else: await ctx.send(self.makeFailureMsg(err)) @@ -874,7 +874,7 @@ def monsterToLongHeader(m: "DgMonster", link=False): def monsterToEvoText(m: "DgMonster"): output = monsterToLongHeader(m) - for ae in sorted({*m.alt_evos}, key=lambda x: int(x.monster_id)): + for ae in sorted({*m.alt_versions}, key=lambda x: int(x.monster_id)): output += "\n\t- {}".format(monsterToLongHeader(ae)) return output @@ -907,11 +907,11 @@ def addEvoListFields(list_of_monsters, embed, name): def monsterToEvoEmbed(m: "DgMonster"): embed = monsterToBaseEmbed(m) - if not len(m.alt_evos) and not m.evo_gem: + if not len(m.alt_versions) and not m.evo_gem: embed.description = 'No alternate evos or evo gem' return embed - addEvoListFields(m.alt_evos, embed, '{} alternate evo(s)') + addEvoListFields(m.alt_versions, embed, '{} alternate evo(s)') if not m.evo_gem: return embed addEvoListFields([m.evo_gem], embed, '{} evo gem(s)') @@ -1186,7 +1186,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): evos_body = ", ".join(f"**{m2.monster_id}**" if m2.monster_id==m.monster_id else f"[{m2.monster_id}]({get_pdx_url(m2)})" - for m2 in sorted({*m.alt_evos}, key=lambda m: m.monster_id)) + for m2 in sorted({*m.alt_versions}, key=lambda m: m.monster_id)) embed.add_field(name=evos_header, value=evos_body, inline=False) return embed From c0ee31aac621db61f6f989036bd537633f2697b9 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 03:15:05 -0700 Subject: [PATCH 0208/1220] Remove logging --- dadguide/database_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 165ff229..b6f285f3 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -627,7 +627,6 @@ class DgMonster(DadguideItem): def __init__(self, item, database): super(DgMonster, self).__init__(item, database) - print(self.monster_id) self.roma_subname = None if self.name_en == self.name_ja: From 5ad26880c915cce5ae85999459c9822914ecd76b Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 03:40:59 -0700 Subject: [PATCH 0209/1220] God this took me way too long to do --- padevents/padevents.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index ea529ba8..c761b96f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -114,7 +114,6 @@ async def check_started(self): events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) daily_refresh_servers = set() for e in events: - await self.bot.get_channel(765847219177521192).send(inline(str((e.clean_dungeon_name, fmtTime(e.open_datetime), e.server, e.key)))) self.started_events.add(e.key) if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, EventType.Week]: for gr in list(self.settings.listGuerrillaReg()): From fa259a76ba0ee564e3b32da47e298357ffc70139 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 03:57:45 -0700 Subject: [PATCH 0210/1220] Readable AEP show --- padevents/padevents.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index c761b96f..0f95003f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -318,8 +318,26 @@ async def aep_show(self, ctx, *, key): if key not in pingroles: await ctx.send("That key does not exist.") return - pr = pingroles[key] - await ctx.send(str(pr)+"\n\nTODO: Make this readable") + roles = [ctx.guild.get_role(role).mention + if ctx.guild.get_role(role) is not None + else "No Role" + for role + in pingroles[key]['roles'] + ] + chans = [ctx.bot.get_channel(c).mention + if ctx.bot.get_channel(c) is not None + else "No Channel" + for c + in pingroles[key]['channels'] + ] + pr = (f"{key} ({'enabled' if pingroles[key]['enabled'] else 'disabled'})\n" + f" Search String: '{pingroles[key]['searchstr']}' {'(regex search)' if pingroles[key]['regex'] else ''}\n" + f" Server: {pingroles[key]['server']}\n" + f" Red: {roles[0]} (In {chans[0]})\n" + f" Blue: {roles[1]} (In {chans[1]})\n" + f" Green: {roles[2]} (In {chans[2]})\n" + f" Offset: {pingroles[key]['offset']} minutes") + await ctx.send(pr) @autoeventping.command(name="list") async def aep_list(self, ctx): From f17b9af6a1a8e04ddd399955a99f2483f1a7b052 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 04:15:31 -0700 Subject: [PATCH 0211/1220] lss --- padinfo/padinfo.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 98e10835..f8ccc72b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -606,6 +606,20 @@ async def leaderskill(self, ctx, *, whole_query): await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) + @commands.command(aliases=['lss']) + @checks.bot_has_permissions(embed_links=True) + async def leaderskillsingle(self, ctx, *, query): + m, err, _ = await self.findMonster(query) + if err: + await ctx.send(err) + return + emoji_to_embed = OrderedDict() + emoji_to_embed[self.ls_emoji] = monstersToLssEmbed(m) + emoji_to_embed[self.left_emoji] = monsterToEmbed(m, self.get_emojis()) + + await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) + + @commands.command(aliases=['helppic', 'helpimg']) @checks.bot_has_permissions(embed_links=True) async def helpid(self, ctx): @@ -1069,6 +1083,26 @@ def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): return embed +def monstersToLssEmbed(m: "DgMonster"): + ls = m.leader_skill + + if ls: + hp, atk, rcv, resist = ls.data + else: + hp, atk, rcv, resist = 1, 1, 1, 0 + + multiplier_text = createSingleMultiplierText(hp, atk, rcv, resist) + + embed = discord.Embed() + embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) + description = '' + description += '\n**{}**\n{}'.format( + monsterToHeader(m, link=True), + m.leader_skill.desc if m.leader_skill else 'None') + embed.description = description + + return embed + def monsterToHeaderEmbed(m: "DgMonster"): header = monsterToLongHeader(m, link=True) @@ -1330,3 +1364,11 @@ def fmtNum(val): if resist1 > 0 or resist2 > 0: text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) return text + +def createSingleMultiplierText(hp, atk, rcv, resist): + def fmtNum(val): + return ('{:.2f}').format(val).strip('0').rstrip('.') + text = "{}/{}/{}".format(fmtNum(hp), fmtNum(atk), fmtNum(rcv)) + if resist > 0: + text += ' Resist {}%'.format(fmtNum(100 * (resist))) + return text From eb45f19afbbca3bd80df8c460f5d0c4ade9dc37c Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 04:35:50 -0700 Subject: [PATCH 0212/1220] Fix subname and restrict listchannels to owners --- dadguide/database_manager.py | 3 ++- padevents/padevents.py | 2 +- padinfo/padinfo.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index b6f285f3..c50e71e0 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -970,7 +970,8 @@ def strip_next_clause(txt: str, sep: str): def make_roma_subname(name_ja): - subname = name_ja.replace('=', '') + subname = re.sub(r'[=]', '', name_ja) + subname = re.sub(r'[「」]', '・', subname) adjusted_subname = '' for part in subname.split('・'): roma_part = romkan.to_roma(part) diff --git a/padevents/padevents.py b/padevents/padevents.py index 2fa39347..7694301a 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -585,7 +585,7 @@ async def aed_e_searchstr(self, ctx, index, *, searchstr): await ctx.tick() @padevents.command(name="listchannels") - @checks.mod_or_permissions(manage_guild=True) + @checks.is_owner() async def _listchannel(self, ctx): msg = 'Following daily channels are registered:\n' msg += self.makeChannelList(self.settings.listDailyReg()) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f8ccc72b..975f8252 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -606,7 +606,7 @@ async def leaderskill(self, ctx, *, whole_query): await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) - @commands.command(aliases=['lss']) + @commands.command(aliases=['lssingle']) @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): m, err, _ = await self.findMonster(query) From dd1c96039543ecb8c56a2da77c78fcdcfd5f16af Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 04:49:19 -0700 Subject: [PATCH 0213/1220] listchannels per server fix --- padevents/padevents.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 7694301a..2a1c5093 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -584,9 +584,9 @@ async def aed_e_searchstr(self, ctx, index, *, searchstr): dmevents[index-1]['searchstr'] = searchstr await ctx.tick() - @padevents.command(name="listchannels") + @padevents.command(name="listallchannels") @checks.is_owner() - async def _listchannel(self, ctx): + async def _listallchannel(self, ctx): msg = 'Following daily channels are registered:\n' msg += self.makeChannelList(self.settings.listDailyReg()) msg += "\n" @@ -594,14 +594,27 @@ async def _listchannel(self, ctx): msg += self.makeChannelList(self.settings.listGuerrillaReg()) await self.pageOutput(ctx, msg) - def makeChannelList(self, reg_list): + @padevents.command(name="listchannels") + @checks.mod_or_permissions(manage_guild=True) + async def _listchannel(self, ctx): + msg = 'Following daily channels are registered:\n' + msg += self.makeChannelList(self.settings.listDailyReg(), lambda c: c in ctx.guild.channels) + msg += "\n" + msg += 'Following guerilla channels are registered:\n' + msg += self.makeChannelList(self.settings.listGuerrillaReg(), lambda c: c in ctx.guild.channels) + await self.pageOutput(ctx, msg) + + def makeChannelList(self, reg_list, filt=None): + if filt is None: + filt = lambda x: x msg = "" for cr in reg_list: reg_channel_id = cr['channel_id'] channel = self.bot.get_channel(int(reg_channel_id)) - channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' - server_name = channel.guild.name if channel else 'Unknown server' - msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' + if filt(channel): + channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' + server_name = channel.guild.name if channel else 'Unknown server' + msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' return msg @padevents.command() From a9ccdcaed921927bf69f208655e6d4b7b05504fe Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 05:37:35 -0700 Subject: [PATCH 0214/1220] crud edit json --- crud/crud.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- crud/info.json | 3 ++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 7b387993..935f723a 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -1,7 +1,9 @@ import asyncio +import aiofiles import csv import discord import io +import os import json import logging import pymysql @@ -31,7 +33,7 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.config = Config.get_conf(self, identifier=3270) - self.config.register_global(config_file=None, chan=None) + self.config.register_global(config_file=None, pipeline_base=None, chan=None) GADMIN_COG = self.bot.get_cog("GlobalAdmin") if GADMIN_COG: @@ -52,8 +54,8 @@ async def red_delete_data_for_user(self, *, requester, user_id): return async def get_cursor(self): - with open(await self.config.config_file(), 'r') as db_config: - return self.connect(json.load(db_config)).cursor() + async with aiofiles.open(await self.config.config_file(), 'r') as db_config: + return self.connect(json.loads(await db_config.read())).cursor() def connect(self, db_config): return pymysql.connect(host=db_config['host'], @@ -135,6 +137,19 @@ async def series_add(self, ctx, *, elements: TokenConverter): await self.execute_write(ctx, sql, (*elements.values(),)) + fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/series.json') + async with aiofiles.open(fn, 'r') as f: + j = json.loads(await f.read()) + j.append({ + 'name_ja': elements['name_ja'], + 'name_en': elements['name_en'], + 'name_ko': elements['name_ko'], + 'series_id': elements['series_id'] + }) + async with aiofiles.open(fn, 'w') as f: + await f.write(json.dumps(j, indent=4, ensure_ascii=False)) + + @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): """Edit an existing series series. @@ -155,6 +170,15 @@ async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): await self.execute_write(ctx, sql, (*elements.values(), series_id)) + fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/series.json') + async with aiofiles.open(fn, 'r') as f: + j = json.loads(await f.read()) + for e in j: + if e['series_id'] == series_id: + e.update(elements) + async with aiofiles.open(fn, 'w') as f: + await f.write(json.dumps(j, indent=4, ensure_ascii=False)) + @series.command(name="delete") @checks.is_owner() async def series_delete(self, ctx, series_id: int): @@ -164,6 +188,15 @@ async def series_delete(self, ctx, series_id: int): await self.execute_write(ctx, sql, series_id) + fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/series.json') + async with aiofiles.open(fn, 'r') as f: + j = json.loads(await f.read()) + for e in j[:]: + if e['series_id'] == series_id: + j.remove(e) + async with aiofiles.open(fn, 'w') as f: + await f.write(json.dumps(j, indent=4, ensure_ascii=False)) + @crud.command() async def editmonsname(self, ctx, monster_id: int, *, name): """Change a monster's name_en_override""" @@ -214,6 +247,12 @@ async def setconfig(self, ctx, path): await self.config.config_file.set(path) await ctx.tick() + @crud.command() + @checks.is_owner() + async def pipeline_base(self, ctx, path): + await self.config.pipeline_base.set(path) + await ctx.tick() + @crud.command() @checks.is_owner() async def setchan(self, ctx, channel: discord.TextChannel): diff --git a/crud/info.json b/crud/info.json index 037a7719..fc765cfe 100644 --- a/crud/info.json +++ b/crud/info.json @@ -18,7 +18,8 @@ "require_cogs": {}, "requirements": [ "tsutils", - "pymysql" + "pymysql", + "aiofiles" ], "tags": [ "PAD" From a49958aaf2fd1613f84b5eabb685a74ce1be31cc Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 05:38:58 -0700 Subject: [PATCH 0215/1220] fix typos --- crud/crud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 935f723a..27d065b9 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -113,7 +113,7 @@ async def series_search(self, ctx, *, search_text): async def series_add(self, ctx, *, elements: TokenConverter): """Add a new series. - Valid element keys are: `name_en`, `name_kr`, `name_jp` + Valid element keys are: `name_en`, `name_ko`, `name_ja` Example Usage: [p]crud series add key1 "Value1" key2 "Value2" @@ -154,7 +154,7 @@ async def series_add(self, ctx, *, elements: TokenConverter): async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): """Edit an existing series series. - Valid element keys are: `name_en`, `name_kr`, `name_jp` + Valid element keys are: `name_en`, `name_ko`, `name_ja` Example Usage: [p]crud series edit 100 key1 "Value1" key2 "Value2" From 3c4e2c65b333e76bd167cf66eddad7d3234c1aa9 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 05:48:58 -0700 Subject: [PATCH 0216/1220] fix TokenConverter --- crud/crud.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 27d065b9..9a8c791f 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -14,8 +14,6 @@ logger = logging.getLogger('red.padbot-cogs.crud') -TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) - SERIES_KEYS = { "name_en": 'Untranslated', "name_ja": 'Untranslated', @@ -110,7 +108,7 @@ async def series_search(self, ctx, *, search_text): await self.execute_read(ctx, sql, [search_text, search_text]) @series.command(name="add") - async def series_add(self, ctx, *, elements: TokenConverter): + async def series_add(self, ctx, *elements): """Add a new series. Valid element keys are: `name_en`, `name_ko`, `name_ja` @@ -118,6 +116,11 @@ async def series_add(self, ctx, *, elements: TokenConverter): Example Usage: [p]crud series add key1 "Value1" key2 "Value2" """ + if len(elements) % 2 != 0: + await ctx.send_help() + return + elements = {elements[i]: elements[i+1] for i in range(0, len(elements), 2)} + if not all(x in SERIES_KEYS for x in elements): await ctx.send_help() return @@ -151,7 +154,7 @@ async def series_add(self, ctx, *, elements: TokenConverter): @series.command(name="edit") - async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): + async def series_edit(self, ctx, series_id: int, *elements): """Edit an existing series series. Valid element keys are: `name_en`, `name_ko`, `name_ja` @@ -159,6 +162,11 @@ async def series_edit(self, ctx, series_id: int, *, elements: TokenConverter): Example Usage: [p]crud series edit 100 key1 "Value1" key2 "Value2" """ + if len(elements) % 2 != 0: + await ctx.send_help() + return + elements = {elements[i]: elements[i+1] for i in range(0, len(elements), 2)} + if not all(x in SERIES_KEYS for x in elements): await ctx.send_help() return From 4bf3119ebeecec728d467b2296884214b7073bef Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 20 Oct 2020 10:36:57 -0700 Subject: [PATCH 0217/1220] pdb pipeline run doesn't lie --- padguidedb/padguidedb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 5e4149a4..3d70ea3a 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -265,7 +265,7 @@ async def fulletl(self, ctx): ) stdout, stderr = await process.communicate() - if stderr: + if "Pipeline completed successfully!" not in stderr: logger.error("Full ETL Error:\n" + stderr.decode()) await ctx.send(inline('Full ETL failed')) return From e6c0e8ce081a7edefcc46159be042c6d622f2c60 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 04:14:30 -0700 Subject: [PATCH 0218/1220] A-O --- azurelane/__init__.py | 2 +- azurelane/azurelane.py | 1 + azurelane/info.json | 6 +++--- chronomagia/chronomagia.py | 1 + chronomagia/info.json | 4 ++-- crud/__init__.py | 2 +- crud/crud.py | 2 +- crud/info.json | 4 ++-- dadguide/__init__.py | 2 +- dadguide/dadguide.py | 2 ++ dadguide/info.json | 6 +++--- damagecalc/__init__.py | 2 +- damagecalc/damagecalc.py | 3 ++- damagecalc/info.json | 4 ++-- feedback/__init__.py | 2 +- feedback/feedback.py | 2 ++ feedback/info.json | 6 +++--- 17 files changed, 29 insertions(+), 22 deletions(-) diff --git a/azurelane/__init__.py b/azurelane/__init__.py index 7b3c1723..f3650fcb 100644 --- a/azurelane/__init__.py +++ b/azurelane/__init__.py @@ -1,4 +1,4 @@ -from .azurelane import * +from .azurelane import AzureLane __red_end_user_data_statement__ = "No personal data is stored." diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 53e88318..793c881e 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -3,6 +3,7 @@ import discord import json import logging +from io import BytesIO from collections import OrderedDict from redbot.core import checks, commands from redbot.core.utils.chat_formatting import inline diff --git a/azurelane/info.json b/azurelane/info.json index 66626808..7700c4c0 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -4,7 +4,7 @@ ], "description": "Displays data about Azur Lane.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]alid to get started. NOTE: This cog is no longer supported.", "short": "Shows Azur Lane data", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -13,9 +13,9 @@ 8, 1 ], - "hidden": false, + "hidden": true, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils", "aiohttp" diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index d274ff96..93a68fd1 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -7,6 +7,7 @@ import traceback import tsutils import urllib.parse +from io import BytesIO from collections import OrderedDict from redbot.core import checks, commands, data_manager from redbot.core.utils.chat_formatting import inline diff --git a/chronomagia/info.json b/chronomagia/info.json index c136d0cd..a7a23454 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -4,7 +4,7 @@ ], "description": "Displays data about Chrono Ma:Gia.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]cmid to get started.", "short": "Shows Chrono Ma:Gia data", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils", "aiohttp" diff --git a/crud/__init__.py b/crud/__init__.py index 56a24ce2..e7f08ed1 100644 --- a/crud/__init__.py +++ b/crud/__init__.py @@ -1,4 +1,4 @@ -from .crud import * +from .crud import Crud __red_end_user_data_statement__ = "No personal data is stored." diff --git a/crud/crud.py b/crud/crud.py index 9a8c791f..e18dbfc2 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -2,11 +2,11 @@ import aiofiles import csv import discord -import io import os import json import logging import pymysql +from io import BytesIO from datetime import datetime from redbot.core import checks, commands, Config, errors from redbot.core.utils.chat_formatting import box, inline, pagify diff --git a/crud/info.json b/crud/info.json index fc765cfe..86771d7f 100644 --- a/crud/info.json +++ b/crud/info.json @@ -4,7 +4,7 @@ ], "description": "Crud for the Pipeline Database.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]crud to get started.", "short": "Pipeline CRUD", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils", "pymysql", diff --git a/dadguide/__init__.py b/dadguide/__init__.py index 1159ca75..dbe92ee4 100644 --- a/dadguide/__init__.py +++ b/dadguide/__init__.py @@ -1,4 +1,4 @@ -from .dadguide import * +from .dadguide import Dadguide __red_end_user_data_statement__ = "No personal data is stored." diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index dbe4c229..01de89cf 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -14,6 +14,7 @@ import os import shutil import tsutils +from io import BytesIO from collections import defaultdict from redbot.core import checks, data_manager from redbot.core import commands @@ -47,6 +48,7 @@ def _data_file(file_name: str) -> str: class Dadguide(commands.Cog): + """Dadguide database manager""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot diff --git a/dadguide/info.json b/dadguide/info.json index 8823f056..46024be3 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -4,8 +4,8 @@ ], "description": "No commands, but loads all the monster data stored in Tsubaki.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Loads monster data (LOAD THIS SECOND)", + "install_msg": "Use [p]dadguide setdatafile if it's applicable to you.", + "short": "Loads monster data (LOAD THIS FIRST)", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", "min_python_version": [ @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils", "pytz", diff --git a/damagecalc/__init__.py b/damagecalc/__init__.py index 177e57ab..58a095fc 100644 --- a/damagecalc/__init__.py +++ b/damagecalc/__init__.py @@ -1,4 +1,4 @@ -from .damagecalc import * +from .damagecalc import DamageCalc __red_end_user_data_statement__ = "No personal data is stored." diff --git a/damagecalc/damagecalc.py b/damagecalc/damagecalc.py index c0dd510d..8ffe97d2 100644 --- a/damagecalc/damagecalc.py +++ b/damagecalc/damagecalc.py @@ -1,6 +1,7 @@ import math from ply import lex +from io import BytesIO from redbot.core import commands @@ -250,7 +251,7 @@ async def helpdamage(self, ctx): Resistance, defense, loading by monster id, killers, etc coming soon """ - @commands.command() + @commands.command(aliases=['damagecalc']) async def damage(self, ctx, *, damage_spec): """Computes damage for the provided damage_spec diff --git a/damagecalc/info.json b/damagecalc/info.json index 4594d9f8..b2ba2304 100644 --- a/damagecalc/info.json +++ b/damagecalc/info.json @@ -4,7 +4,7 @@ ], "description": "Calculate Puzzle & Dragons data", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]damage to get started.", "short": "Calculates Puzzle & Dragons data", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "ply" ], diff --git a/feedback/__init__.py b/feedback/__init__.py index 975a3c07..1cc4f2ff 100644 --- a/feedback/__init__.py +++ b/feedback/__init__.py @@ -1,4 +1,4 @@ -from .feedback import * +from .feedback import Feedback __red_end_user_data_statement__ = "No personal data is stored." diff --git a/feedback/feedback.py b/feedback/feedback.py index 20c55fe3..54e33ea5 100644 --- a/feedback/feedback.py +++ b/feedback/feedback.py @@ -1,10 +1,12 @@ import discord +from io import BytesIO from redbot.core import checks, commands, modlog from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings class Feedback(commands.Cog): + """Feedback Cog""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot diff --git a/feedback/info.json b/feedback/info.json index f34d128a..3b0f7e7d 100644 --- a/feedback/info.json +++ b/feedback/info.json @@ -4,8 +4,8 @@ ], "description": "Tsubaki feedback and credits", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "P&D Phonebook", + "install_msg": "Use [p]setfeedbackchannel and [p]setblogfeedbackchannel to set the feedback channels.", + "short": "Feedback and credits", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", "min_python_version": [ @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils" ], From b1990cbe654ed1adb02c1493621beedbcd6df8e2 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 04:36:54 -0700 Subject: [PATCH 0219/1220] P --- chronomagia/__init__.py | 2 +- padboard/__init__.py | 2 +- padboard/info.json | 4 ++-- padboard/padboard.py | 2 ++ padbuildimg/__init__.py | 2 +- padbuildimg/info.json | 4 ++-- padbuildimg/padbuildimg.py | 4 ++-- padbuilds/__init__.py | 2 +- padbuilds/info.json | 4 ++-- padbuilds/padbuilds.py | 3 ++- padevents/__init__.py | 2 +- padevents/info.json | 4 ++-- padevents/padevents.py | 2 ++ padglobal/__init__.py | 2 +- padglobal/info.json | 4 ++-- padglobal/padglobal.py | 6 +++--- padguidedb/__init__.py | 2 +- padguidedb/info.json | 4 ++-- padguidedb/padguidedb.py | 27 +-------------------------- padinfo/__init__.py | 2 +- padinfo/info.json | 6 +++--- padinfo/padinfo.py | 5 +++-- padmonitor/__init__.py | 2 +- padmonitor/info.json | 4 ++-- padmonitor/padmonitor.py | 2 ++ padsearch/__init__.py | 2 +- padsearch/info.json | 4 ++-- padsearch/padsearch.py | 1 + padvision/__init__.py | 2 +- padvision/info.json | 4 ++-- padvision/padvision.py | 5 +++-- pricecheck/__init__.py | 2 +- pricecheck/info.json | 4 ++-- pricecheck/pricecheck.py | 2 ++ profile/__init__.py | 2 +- profile/info.json | 4 ++-- profile/profile.py | 2 ++ 37 files changed, 63 insertions(+), 74 deletions(-) diff --git a/chronomagia/__init__.py b/chronomagia/__init__.py index 230cbca9..67acde36 100644 --- a/chronomagia/__init__.py +++ b/chronomagia/__init__.py @@ -1,4 +1,4 @@ -from .chronomagia import * +from .chronomagia import ChronoMagia __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padboard/__init__.py b/padboard/__init__.py index ddbada96..37824925 100644 --- a/padboard/__init__.py +++ b/padboard/__init__.py @@ -1,4 +1,4 @@ -from .padboard import * +from .padboard import PadBoard __red_end_user_data_statement__ = "No personal data is stored persistantly." diff --git a/padboard/info.json b/padboard/info.json index 1adc8002..577d542d 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -4,7 +4,7 @@ ], "description": "Turn a screenshot into a P&D simulation via DawnGlare.", "end_user_data_statement": "No personal data is stored persistantly.", - "install_msg": "", + "install_msg": "Use [p]set_tflite_path to set up your tflite path. (You need to get this from The Tsubaki Bot Team)", "short": "Dawnglare utils", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "padvision": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padboard/padboard.py b/padboard/padboard.py index 6a293ded..e63e4ee5 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -3,6 +3,7 @@ import discord import numpy as np import os +from io import BytesIO from collections import defaultdict from collections import deque from redbot.core import Config, checks, commands @@ -16,6 +17,7 @@ class PadBoard(commands.Cog): + """Dawnglare Utilities""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot diff --git a/padbuildimg/__init__.py b/padbuildimg/__init__.py index e8e6d36f..b52bc970 100644 --- a/padbuildimg/__init__.py +++ b/padbuildimg/__init__.py @@ -1,4 +1,4 @@ -from .padbuildimg import * +from .padbuildimg import PadBuildImage __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padbuildimg/info.json b/padbuildimg/info.json index a1ef892d..c2bd9a49 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -5,7 +5,7 @@ ], "description": "Build a PAD team image via commands.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]helpbuildimg for help!", "short": "PDChu cog", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -16,7 +16,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index ab20726e..8fbdba0e 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -1,11 +1,11 @@ import aiohttp import csv import discord -import io import math import os import re import urllib +from io import BytesIO from PIL import Image from PIL import ImageChops from PIL import ImageDraw @@ -776,7 +776,7 @@ async def padbuildimg(self, ctx, *, build_str: str): return -1 if pbg.build_img is not None: - with io.BytesIO() as build_io: + with BytesIO() as build_io: pbg.build_img.save(build_io, format='PNG') build_io.seek(0) if ctx.guild and self.settings.dmOnly(ctx.guild.id): diff --git a/padbuilds/__init__.py b/padbuilds/__init__.py index 655f0aca..61784182 100644 --- a/padbuilds/__init__.py +++ b/padbuilds/__init__.py @@ -1,4 +1,4 @@ -from .padbuilds import * +from .padbuilds import PadBuilds __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padbuilds/info.json b/padbuilds/info.json index 76282200..f3939156 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -4,7 +4,7 @@ ], "description": "View user-set builds for various dungeons.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]builds to get started.", "short": "Builds to help you through dungeons", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils" ], diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index a5a31c2a..d7ecccc9 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -1,5 +1,6 @@ import discord import re +from io import BytesIO from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify @@ -28,7 +29,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return - @commands.group(aliases=["build"]) + @commands.group(aliases=["build", "padbuilds"]) @commands.guild_only() async def builds(self, ctx): """PAD Builds management""" diff --git a/padevents/__init__.py b/padevents/__init__.py index 7f7dbc01..d6ef7327 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -1,4 +1,4 @@ -from .padevents import * +from .padevents import PadEvents __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padevents/info.json b/padevents/info.json index f0144455..c65bfe70 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -4,7 +4,7 @@ ], "description": "Stay up to date on PAD dungeons.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]padevents to get started.", "short": "PAD Event tracker", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padevents/padevents.py b/padevents/padevents.py index b345bc52..050d5ccd 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -7,6 +7,7 @@ import pytz import re import traceback +from io import BytesIO from collections import defaultdict from datetime import timedelta from enum import Enum @@ -21,6 +22,7 @@ GROUPS = ['red', 'blue', 'green'] class PadEvents(commands.Cog): + """Pad Event Tracker""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot diff --git a/padglobal/__init__.py b/padglobal/__init__.py index 04aa3de7..94f35cca 100644 --- a/padglobal/__init__.py +++ b/padglobal/__init__.py @@ -1,4 +1,4 @@ -from .padglobal import * +from .padglobal import PadGlobal __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padglobal/info.json b/padglobal/info.json index df8836e6..f837387c 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -4,7 +4,7 @@ ], "description": "The holy text of Tsubaki guides. For the official PadGlobal data, please contact a Tsubaki owner.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]padglobal to get started.", "short": "Tsubaki guides and tips written by P&D professionals", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 87df5e10..49ad5da5 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -4,13 +4,13 @@ import datetime import difflib import discord -import io import json import logging import os import prettytable import re import tsutils +from io import StringIO, BytesIO from collections import defaultdict from redbot.core import checks, data_manager from redbot.core import commands @@ -198,11 +198,11 @@ async def debugiddump(self, ctx): mi = padinfo_cog.index_all async def write_send(nn_map, file_name): - data_holder = io.StringIO() + data_holder = StringIO() writer = csv.writer(data_holder) for nn, nm in nn_map.items(): writer.writerow([nn, nm.monster_no_na, nm.name_en]) - bytes_data = io.BytesIO(data_holder.getvalue().encode()) + bytes_data = BytesIO(data_holder.getvalue().encode()) await ctx.channel.send(file=discord.File(bytes_data, file_name)) await write_send(mi.all_entries, 'all_entries.csv') diff --git a/padguidedb/__init__.py b/padguidedb/__init__.py index 86d4637e..402038c5 100644 --- a/padguidedb/__init__.py +++ b/padguidedb/__init__.py @@ -1,4 +1,4 @@ -from .padguidedb import * +from .padguidedb import PadGuideDb __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padguidedb/info.json b/padguidedb/info.json index ae9e2e31..4a2946b8 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -4,7 +4,7 @@ ], "description": "Dungeon scraper and general database manager.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]padguidedb to get started", "short": "Dungeon data scraper", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils", "pymysql" diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 3d70ea3a..2c41c089 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -1,10 +1,10 @@ import asyncio import csv import discord -import io import json import logging import pymysql +from io import BytesIO from datetime import datetime from redbot.core import checks from redbot.core import commands @@ -22,13 +22,6 @@ def is_padguidedb_admin_check(ctx): def is_padguidedb_admin(): return commands.check(is_padguidedb_admin_check) -TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) - -SERIES_KEYS = { - "name_en": 'Untranslated', - "name_ja": 'Untranslated', - "name_ko": 'Untranslated', -} class PadGuideDb(commands.Cog): """PadGuide Database manipulator""" @@ -223,24 +216,6 @@ async def cleardungeon(self, ctx, dungeon_id: int): cursor.execute(sql) await ctx.tick() - # @padguidedb.command() - @is_padguidedb_admin() - async def dungeondata(self, ctx, dungeon_id: int): - with self.get_cursor() as cursor: - sql = "SELECT * FROM wave_data WHERE dungeon_id = {}".format(int(dungeon_id)) - cursor.execute(sql) - results = list(cursor.fetchall()) - order = sorted(results[0]) - rows = [list(map(lambda x: row[x], order)) for row in results] - fauxfile = io.StringIO() - writer = csv.writer(fauxfile) - writer.writerow(order) - writer.writerows(rows) - fauxfile.seek(0) - m = await ctx.send(file=discord.File(fauxfile, filename="dungeondata.csv")) - await asyncio.sleep(10) - await m.delete() - @padguidedb.group() @is_padguidedb_admin() async def pipeline(self, context): diff --git a/padinfo/__init__.py b/padinfo/__init__.py index 0505758e..054e04e4 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -1,4 +1,4 @@ -from .padinfo import * +from .padinfo import PadInfo __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padinfo/info.json b/padinfo/info.json index 40c5aae7..32585688 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -4,8 +4,8 @@ ], "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", - "short": "Serves monster data (LOAD THIS THIRD)", + "install_msg": "Use [p]helpid for help", + "short": "Serves monster data (LOAD THIS SECOND)", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", "min_python_version": [ @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 975f8252..c35ee5ec 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,6 +1,5 @@ import asyncio import discord -import io import json import logging import os @@ -8,6 +7,7 @@ import traceback import tsutils import urllib.parse +from io import BytesIO from collections import OrderedDict from enum import Enum from redbot.core import checks, commands, data_manager @@ -160,6 +160,7 @@ def on_update(self, ctx, selected_emoji): class PadInfo(commands.Cog): + """Info for PAD Cards""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -728,7 +729,7 @@ async def iddiff(self, ctx): if m1 and m2: await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_en, m2.name_en)) await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) - file = discord.File(io.BytesIO(json.dumps(f).encode()), filename="diff.json") + file = discord.File(BytesIO(json.dumps(f).encode()), filename="diff.json") await ctx.send(file=file) def get_emojis(self): diff --git a/padmonitor/__init__.py b/padmonitor/__init__.py index b9489397..4191866a 100644 --- a/padmonitor/__init__.py +++ b/padmonitor/__init__.py @@ -1,4 +1,4 @@ -from .padmonitor import * +from .padmonitor import PadMonitor __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padmonitor/info.json b/padmonitor/info.json index ad76ff92..2a392d7e 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -4,7 +4,7 @@ ], "description": "Checks for and announces new monsters.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]addnewchannel to set up a monster monitor.", "short": "Announce new monsters", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index ff000706..97eee435 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,6 +1,7 @@ import asyncio import logging import tsutils +from io import BytesIO from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline, pagify @@ -8,6 +9,7 @@ class PadMonitor(commands.Cog): + """Monitor PAD and announce new cards""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot diff --git a/padsearch/__init__.py b/padsearch/__init__.py index faf836f2..8e3dd63d 100644 --- a/padsearch/__init__.py +++ b/padsearch/__init__.py @@ -1,4 +1,4 @@ -from .padsearch import * +from .padsearch import PadSearch __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padsearch/info.json b/padsearch/info.json index b9105701..d4695ac6 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -4,7 +4,7 @@ ], "description": "Search for cards using advanced filters.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]helpsearch for help.", "short": "Card search", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index bd4ecb1d..cedb8aac 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -1,6 +1,7 @@ import json import logging import re +from io import BytesIO from fnmatch import fnmatch from ply import lex from redbot.core import checks diff --git a/padvision/__init__.py b/padvision/__init__.py index ca87a397..f54c9a53 100644 --- a/padvision/__init__.py +++ b/padvision/__init__.py @@ -1,4 +1,4 @@ -from .padvision import * +from .padvision import PadVision __red_end_user_data_statement__ = "No personal data is stored." diff --git a/padvision/info.json b/padvision/info.json index 86713d17..f49e6849 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -4,7 +4,7 @@ ], "description": "A beefy helper cog for neural networks.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Make sure to use padboard with this cog!", "short": "Neural net helper", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "Pillow" ], diff --git a/padvision/padvision.py b/padvision/padvision.py index eb8429ad..d8d3b35b 100644 --- a/padvision/padvision.py +++ b/padvision/padvision.py @@ -1,7 +1,7 @@ import PIL.Image -import io import logging import traceback +from io import BytesIO from redbot.core import commands ORB_IMG_SIZE = 40 @@ -10,6 +10,7 @@ class PadVision(commands.Cog): + """Empty cog for PadVision help""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -122,7 +123,7 @@ class NeuralClassifierBoardExtractor(object): def __init__(self, model_path, np_img, img_bytes): self.model_path = model_path self.np_img = np_img - self.img = PIL.Image.open(io.BytesIO(img_bytes)).convert('RGB') + self.img = PIL.Image.open(BytesIO(img_bytes)).convert('RGB') self.processed = False self.results = [['u' for x in range(6)] for y in range(5)] diff --git a/pricecheck/__init__.py b/pricecheck/__init__.py index 520f0547..0544a15c 100644 --- a/pricecheck/__init__.py +++ b/pricecheck/__init__.py @@ -1,4 +1,4 @@ -from .pricecheck import * +from .pricecheck import PriceCheck __red_end_user_data_statement__ = "No personal data is stored." diff --git a/pricecheck/info.json b/pricecheck/info.json index 879a94df..d3084402 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -4,7 +4,7 @@ ], "description": "A cog that enables the display of pricing information for evolution material gems.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]pcadmin to set up pricechecks.", "short": "Price Check", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": { + "required_cogs": { "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 66184039..7a94fb17 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -1,5 +1,6 @@ import discord import re +from io import BytesIO from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check @@ -17,6 +18,7 @@ def rint(x, p): return str(round(x, p)).rstrip('0').rstrip('.') class PriceCheck(commands.Cog): + """A pricechecking cog for Lumon""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot diff --git a/profile/__init__.py b/profile/__init__.py index da213419..166c8b44 100644 --- a/profile/__init__.py +++ b/profile/__init__.py @@ -1,4 +1,4 @@ -from .profile import * +from .profile import Profile __red_end_user_data_statement__ = "Profile data is stored persistantly." diff --git a/profile/info.json b/profile/info.json index 88dbb00e..2fa3eabe 100644 --- a/profile/info.json +++ b/profile/info.json @@ -4,7 +4,7 @@ ], "description": "Holds all of your Puzzle & Dragons accounts in one place", "end_user_data_statement": "Profile data is stored persistantly.", - "install_msg": "", + "install_msg": "Use [p]profile to get started", "short": "P&D Phonebook", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils" ], diff --git a/profile/profile.py b/profile/profile.py index 76c7ea2f..c8151513 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -1,6 +1,7 @@ import discord import logging import re +from io import BytesIO from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings @@ -39,6 +40,7 @@ def computeOldGroup(str_id): class Profile(commands.Cog): + """PAD Profile Cog""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot From 47dd98f94b38d9f13639168ea13603bc0801df89 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 04:38:45 -0700 Subject: [PATCH 0220/1220] Q-Z --- schoolidol/__init__.py | 2 +- schoolidol/info.json | 4 ++-- schoolidol/schoolidol.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/schoolidol/__init__.py b/schoolidol/__init__.py index 2379fffc..8b204c43 100644 --- a/schoolidol/__init__.py +++ b/schoolidol/__init__.py @@ -1,4 +1,4 @@ -from .schoolidol import * +from .schoolidol import SchoolIdol __red_end_user_data_statement__ = "No personal data is stored." diff --git a/schoolidol/info.json b/schoolidol/info.json index 8e50f075..b678befe 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -4,7 +4,7 @@ ], "description": "Displays data about School Idol.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "", + "install_msg": "Use [p]sifid to get started.", "short": "Shows School Idol data", "min_bot_version": "3.4.0", "max_bot_version": "3.4.0", @@ -15,7 +15,7 @@ ], "hidden": false, "disabled": false, - "require_cogs": {}, + "required_cogs": {}, "requirements": [ "tsutils", "aiohttp" diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index 17fb5def..e57ecbc5 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -3,6 +3,7 @@ import discord import json import logging +from io import BytesIO from collections import OrderedDict from redbot.core import checks, commands from redbot.core.utils.chat_formatting import inline From e76f4eaebbf83f7e17ced92e52025a6218ce818f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 04:51:10 -0700 Subject: [PATCH 0221/1220] Move Channelmod :( --- channelmod/__init__.py | 8 + channelmod/channelmod.py | 434 +++++++++++++++++++++++++++++++++++++++ channelmod/info.json | 27 +++ 3 files changed, 469 insertions(+) create mode 100644 channelmod/__init__.py create mode 100644 channelmod/channelmod.py create mode 100644 channelmod/info.json diff --git a/channelmod/__init__.py b/channelmod/__init__.py new file mode 100644 index 00000000..532573f1 --- /dev/null +++ b/channelmod/__init__.py @@ -0,0 +1,8 @@ +from .channelmod import ChannelMod + +__red_end_user_data_statement__ = "No personal data is stored." + + +def setup(bot): + n = ChannelMod(bot) + bot.add_cog(n) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py new file mode 100644 index 00000000..2f8ffb80 --- /dev/null +++ b/channelmod/channelmod.py @@ -0,0 +1,434 @@ +import aiohttp +import discord +import logging +import re +import time +import traceback +from datetime import datetime +from io import BytesIO +from redbot.core import checks +from redbot.core import commands +from redbot.core.utils.chat_formatting import inline +from tsutils import CogSettings, auth_check, box, replace_emoji_names_with_code, fix_emojis_for_server + +logger = logging.getLogger('red.misc-cogs.channelmod') + +# Three hour cooldown +ATTRIBUTION_TIME_SECONDS = 60 * 60 * 3 + +frMESSAGE_LINK = r'(https://discordapp\.com/channels/{0.guild.id}/{0.id}/(\d+)/?)' +MESSAGE_LINK = 'https://discordapp.com/channels/{0.guild.id}/{0.channel.id}/{0.id}' + + +class ChannelMod(commands.Cog): + """Channel moderation tools.""" + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.settings = ChannelModSettings("channelmod") + self.channel_last_spoke = {} + + GACOG = self.bot.get_cog("GlobalAdmin") + if GACOG: self.bot.get_cog("GlobalAdmin").register_perm("channelmod") + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + + @commands.group() + async def channelmod(self, ctx): + """Manage channel moderation settings""" + + @channelmod.command() + @checks.is_owner() + async def addmirror(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): + """Set mirroring between two channels.""" + if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): + await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) + return + self.settings.add_mirrored_channel(source_channel_id, dest_channel_id) + await ctx.tick() + + @channelmod.command() + @checks.is_owner() + async def rmmirror(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): + """Remove mirroring between two channels.""" + if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): + await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) + return + self.settings.rm_mirrored_channel(source_channel_id, dest_channel_id) + await ctx.tick() + + @channelmod.command() + @checks.is_owner() + async def mirrorconfig(self, ctx): + """List mirror config.""" + mirrored_channels = self.settings.mirrored_channels() + msg = 'Mirrored channels\n' + for mc_id, config in mirrored_channels.items(): + channel = self.bot.get_channel(mc_id) + channel_name = channel.name if channel else 'unknown' + msg += '\n{} ({})'.format(mc_id, channel_name) + for channel_id in config['channels']: + channel = self.bot.get_channel(channel_id) + channel_name = channel.name if channel else 'unknown' + msg += '\n\t{} ({})'.format(channel_id, channel_name) + await ctx.send(box(msg)) + + @channelmod.command() + async def countreactions(self, ctx, message: discord.Message): + """Count reactions on a message and all of its mirrors. + + The message can be a link, a message id (if used in the same + channel as the message), or the channel_id and message_id + separated by a dash (channel_id-message-id)""" + mirrored_messages = self.settings.get_mirrored_messages(message.channel.id, message.id) + if not mirrored_messages: + await ctx.send("This message isn't mirrored!") + reacts = {} + for react in message.reactions: + reacts[str(react)] = react.count - 1 + for (chid, mid) in mirrored_messages: + dest_channel = self.bot.get_channel(chid) + if not dest_channel: + logger.warning('could not locate channel {}'.format(chid)) + continue + dest_message = await dest_channel.fetch_message(mid) + if not dest_message: + logger.warning('could not locate message {}'.format(mid)) + continue + dest_reaction = discord.utils.find(lambda r: r == react, dest_message.reactions) + if not dest_reaction: + logger.warning('could not locate reaction {}'.format(react)) + continue + reacts[str(react)] += dest_reaction.count - 1 + o = "" + maxlen = len(str(max(reacts.values(), key=lambda x: len(str(x))))) + for r, c in reacts.items(): + o += "{{}}: {{:{}}}\n".format(maxlen).format(r, c) + await ctx.send(o) + + @channelmod.command() + @auth_check('channelmod') + async def catchup(self, ctx, channel, from_message, to_message=None): + """Catch up a mirror for all messages after from_message (inclusive)""" + if channel.isdigit(): + channel = self.bot.get_channel(int(channel)) + else: + channel = await self.catchup.do_conversion(ctx, discord.TextChannel, channel, "channel") + + if from_message.isdigit(): + from_message = await channel.fetch_message(int(from_message)) + else: + from_message = await self.catchup.do_conversion(ctx, discord.Message, from_message, "from_message") + + if to_message is None: + pass + elif to_message.isdigit(): + to_message = await channel.fetch_message(int(to_message)) + else: + to_message = await self.catchup.do_conversion(ctx, discord.Message, to_message, "to_message") + + async with ctx.typing(): + await self.mirror_msg(from_message) + async for message in channel.history(limit=None, after=from_message, before=to_message): + await self.mirror_msg(message) + if to_message: + await self.mirror_msg(to_message) + await ctx.tick() + + @commands.Cog.listener('on_message_without_command') + async def mirror_msg(self, message): + if message.author.id == self.bot.user.id: + return + + channel = message.channel + mirrored_channels = self.settings.get_mirrored_channels(channel.id) + + if not mirrored_channels: + return + + last_spoke, last_spoke_timestamp = self.settings.get_last_spoke(channel.id) + now_time = datetime.utcnow() + last_spoke_time = datetime.utcfromtimestamp( + last_spoke_timestamp) if last_spoke_timestamp else now_time + attribution_required = last_spoke != message.author.id + attribution_required |= (now_time - last_spoke_time).total_seconds() > ATTRIBUTION_TIME_SECONDS + self.settings.set_last_spoke(channel.id, message.author.id) + + attachment_bytes = None + filename = None + if message.attachments: + # If we know we're copying a message and that message has an attachment, + # pre download it and reuse it for every upload. + attachment = message.attachments[0] + if hasattr(attachment, 'url') and hasattr(attachment, 'filename'): + url = attachment.url + filename = attachment.filename + attachment_bytes = BytesIO(await attachment.read()) + + for dest_channel_id in mirrored_channels: + dest_channel = self.bot.get_channel(dest_channel_id) + if not dest_channel: + continue + try: + if attribution_required: + msg = self.makeheader(message) + await dest_channel.send(msg) + + fmessage = await self.mformat(message.content, message.channel, dest_channel) + + if attachment_bytes and filename: + attachment_bytes.seek(0) + dest_message = await dest_channel.send(file=discord.File(attachment_bytes, filename), + content=fmessage) + elif message.content: + dest_message = await dest_channel.send(fmessage) + else: + logger.warning('Failed to mirror message from {} no action to take'.format(channel.id)) + continue + + self.settings.add_mirrored_message( + channel.id, message.id, dest_channel.id, dest_message.id) + except Exception as ex: + if dest_channel.guild.owner: + try: + message = ("Hi, {1.guild.owner}! This is an automated message from the Tsubaki team to let" + " you know that your server, {1.guild.name}, has been configured to mirror" + " messages from {0.name} (from {0.guild.name}) to {1.name}, but your channel" + " doesn't give me manage message permissions! Please do make sure to allow" + " me permissions to send messages, embed links, and attach files! It's also" + " okay to turn off message mirroring from your channel. If you need help, contact" + " us via `{2}feedback`!" + "").format(channel, dest_channel, (await self.bot.get_valid_prefixes())[0]) + await dest_channel.guild.owner.send(message) + except Exception: + logger.exception("Owner message failed.") + logger.exception( + 'Failed to mirror message from {} to {}: {}'.format(channel.id, dest_channel_id, str(ex))) + + if attachment_bytes: + attachment_bytes.close() + + @commands.Cog.listener('on_message_edit') + async def mirror_msg_edit(self, message, new_message): + await self.mirror_msg_mod(message, new_message_content=new_message.content) + + @commands.Cog.listener('on_message_delete') + async def mirror_msg_delete(self, message): + await self.mirror_msg_mod(message, delete_message_content=True) + + @commands.Cog.listener('on_reaction_add') + async def mirror_reaction_add(self, reaction, user): + message = reaction.message + if message.author.id != user.id: + return + await self.mirror_msg_mod(message, new_message_reaction=reaction.emoji) + + @commands.Cog.listener('on_reaction_remove') + async def mirror_reaction_remove(self, reaction, user): + message = reaction.message + if message.author.id != user.id: + return + await self.mirror_msg_mod(message, delete_message_reaction=reaction.emoji) + + async def mirror_msg_mod(self, message, + new_message_content: str = None, + delete_message_content: bool = False, + new_message_reaction=None, + delete_message_reaction=None): + if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): + return + + channel = message.channel + mirrored_messages = self.settings.get_mirrored_messages(channel.id, message.id) + for (dest_channel_id, dest_message_id) in mirrored_messages: + try: + dest_channel = self.bot.get_channel(dest_channel_id) + if not dest_channel: + logger.warning('could not locate channel to mod') + continue + dest_message = await dest_channel.fetch_message(dest_message_id) + if not dest_message: + logger.warning('could not locate message to mod') + continue + + if new_message_content: + fcontent = await self.mformat(new_message_content, channel, dest_message.channel) + await dest_message.edit(content=fcontent) + elif new_message_reaction: + await dest_message.add_reaction(new_message_reaction) + elif delete_message_content: + await dest_message.delete() + elif delete_message_reaction: + await dest_message.remove_reaction(delete_message_reaction, dest_message.guild.me) + except Exception as ex: + logger.exception('Failed to mirror message edit from {} to {}:'.format(channel.id, dest_channel_id)) + + def makeheader(self, message): + return 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name, + message.guild.name, + message.channel.name, + message.jump_url) + + async def mformat(self, text, from_channel, dest_channel): + # LINKS + for link, mid in re.findall(frMESSAGE_LINK.format(from_channel), text): + from_link = await from_channel.fetch_message(mid) + if not from_link: + logger.warning('could not locate link to copy') + continue + mirrored_messages = self.settings.get_mirrored_messages(from_link.channel.id, from_link.id) + to_link_id = [dmid for dcid, dmid in mirrored_messages if dcid == dest_channel.id] + if not to_link_id: + logger.warning('could not locate link to mod') + continue + to_link_id = to_link_id[0] + dest_link = await dest_channel.fetch_message(to_link_id) + + newlink = MESSAGE_LINK.format(dest_link) + text = text.replace(link, newlink) + # ROLES + for rtext, rid in re.findall(r'(<@&(\d+)>)', text): + target = from_channel.guild.get_role(int(rid)) + if target is None: + logger.warning('could not locate role to mod') + continue + dest = discord.utils.get(dest_channel.guild.roles, name=target.name) + if dest is None: + repl = "\\@" + target.name + else: + repl = "<@&{}>".format(dest.id) + text = text.replace(rtext, repl) + # MENTIONS + for utext, uid in re.findall(r'(<@!(\d+)>)', text): + target = from_channel.guild.get_member(int(uid)) + if target is None: + logger.warning('could not locate user to mod') + continue + text = text.replace(utext, target.name) + # CHANNELS + for ctext, cid in re.findall(r'(<#(\d+)>)', text): + target = from_channel.guild.get_channel(int(cid)) + if target is None: + logger.warning('could not locate channel to mod') + continue + text = text.replace(ctext, "\\#" + target.name) + # EVERYONE + text = re.sub(r"@everyone\b", "@\u200beveryone", text) + text = re.sub(r"@here\b", "@\u200bhere", text) + # EMOJI + # text = self.emojify(text) + return text + + def emojify(self, message): + emojis = list() + for guild in self.bot.guilds: + emojis.extend(guild.emojis) + message = replace_emoji_names_with_code(emojis, message) + return fix_emojis_for_server(emojis, message) + + +class ChannelModSettings(CogSettings): + def make_default_settings(self): + config = { + 'servers': {}, + 'mirrored_channels': {}, + 'max_mirrored_messages': 20, + } + return config + + def servers(self): + return self.bot_settings['servers'] + + def get_server(self, server_id: int): + servers = self.servers() + if server_id not in servers: + servers[server_id] = {} + return servers[server_id] + + def max_mirrored_messages(self): + return self.bot_settings['max_mirrored_messages'] + + def mirrored_channels(self): + # Mirrored channels looks like: + # : { + # 'last_spoke: '', + # 'channels': [dest_channel_id_1, dest_channel_id2], + # 'messages': { + # : [ + # (dest_channel_id, dest_channel_msg], + # ] + # } + # } + return self.bot_settings['mirrored_channels'] + + def get_mirrored_channels(self, source_channel: str): + return self.mirrored_channels().get(source_channel, {}).get('channels', []) + + def get_last_spoke(self, source_channel: str): + spoke_values = self.mirrored_channels().get(source_channel, {}) + return spoke_values.get('last_spoke', None), spoke_values.get('last_spoke_timestamp', None) + + def set_last_spoke(self, source_channel: str, last_spoke: str): + spoke_values = self.mirrored_channels().get(source_channel) + spoke_values['last_spoke'] = last_spoke + spoke_values['last_spoke_timestamp'] = time.time() + self.save_settings() + + def add_mirrored_channel(self, source_channel: int, dest_channel: int): + channels = self.mirrored_channels() + if source_channel == dest_channel: + raise commands.UserFeedbackCheckFailure('Cannot mirror a channel to itself') + if dest_channel in channels: + raise commands.UserFeedbackCheckFailure('Destination channel is already a source channel') + if source_channel not in channels: + channels[source_channel] = { + 'channels': [], + 'messages': {}, + } + if dest_channel in channels[source_channel]['channels']: + raise commands.UserFeedbackCheckFailure('This mirror already exists') + channels[source_channel]['channels'].append(dest_channel) + self.save_settings() + + def rm_mirrored_channel(self, source_channel: int, dest_channel: int): + channels = self.mirrored_channels() + config = channels.get(source_channel) + if config and dest_channel in config['channels']: + dest_channel_config = config['channels'] + dest_channel_config.remove(dest_channel) + if not dest_channel_config: + channels.pop(source_channel) + self.save_settings() + + def add_mirrored_message(self, source_channel: int, source_message: str, dest_channel: int, dest_message: str): + channel_config = self.mirrored_channels()[source_channel] + messages = channel_config['messages'] + if source_message not in messages: + messages[source_message] = [] + + targets = messages[source_message] + new_entry = [dest_channel, dest_message] + if new_entry not in targets: + targets.append(new_entry) + self.save_settings() + + if len(messages) > self.max_mirrored_messages(): + oldest_msg = min(messages.keys()) + messages.pop(oldest_msg) + self.save_settings() + + def get_mirrored_messages(self, source_channel: int, source_message: str): + """Returns [(channel_id, message_id), ...]""" + channel_config = self.mirrored_channels().get(source_channel, {}) + return channel_config.get('messages', {}).get(source_message, []) diff --git a/channelmod/info.json b/channelmod/info.json new file mode 100644 index 00000000..d7eb3687 --- /dev/null +++ b/channelmod/info.json @@ -0,0 +1,27 @@ +{ + "author": [ + "The Tsubaki Bot Team" + ], + "description": "Allows you to mirror channels across servers.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "Use [p]channelmod to get started", + "short": "Mirror channels across servers.", + "min_bot_version": "3.4.0", + "max_bot_version": "3.4.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "required_cogs": {}, + "requirements": [ + "tsutils", + "aiohttp" + ], + "tags": [ + "administration", + "mirror" + ] +} From 21e5c0bbf05c14d56381390a59ef807d8f09c9ec Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 05:56:51 -0700 Subject: [PATCH 0222/1220] Padmonitor add channel as argument --- padmonitor/padmonitor.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 97eee435..f7857f68 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -87,7 +87,7 @@ async def announce(self, channel_id, message): except Exception as ex: logger.exception('failed to send message to {}:'.format(channel_id)) - @commands.group() + @commands.group(aliases=['pdm']) @checks.mod_or_permissions(manage_guild=True) async def padmonitor(self, ctx): """PAD info monitoring""" @@ -96,17 +96,21 @@ async def padmonitor(self, ctx): @padmonitor.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addnewchannel(self, ctx): + async def addchannel(self, ctx, channel: discord.TextChannel = None): """Sets announcements for the current channel.""" - self.settings.add_new_monster_channel(ctx.channel.id) + if channel is None: + channel = ctx.channel + self.settings.add_new_monster_channel(channel.id) await ctx.tick() @padmonitor.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmnewchannel(self, ctx): + async def rmchannel(self, ctx, channel: discord.TextChannel = None): """Removes announcements for the current channel.""" - self.settings.rm_new_monster_channel(ctx.channel.id) + if channel is None: + channel = ctx.channel + self.settings.rm_new_monster_channel(channel.id) await ctx.tick() From 3c7be00b369e499570e86b8081de42ed696349ec Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 05:57:41 -0700 Subject: [PATCH 0223/1220] padmonitor import discord --- padmonitor/padmonitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index f7857f68..15345894 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,6 +1,7 @@ import asyncio import logging import tsutils +import discord from io import BytesIO from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline, pagify From 4ebbfa580271224b998cf77449e96556a3f91288 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 10:28:32 -0700 Subject: [PATCH 0224/1220] Unalias es from collabscroll --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c35ee5ec..ff5dc6df 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -544,7 +544,7 @@ async def collabscroll(self, ctx, *, query: str): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command(aliases=['es']) + @commands.command() @checks.bot_has_permissions(embed_links=True) async def evoscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" From 27859bdfadfbb1e19a50074e305b0612df6d568b Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 11:08:30 -0700 Subject: [PATCH 0225/1220] seriesscroll --- padinfo/padinfo.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ff5dc6df..864bc7bb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -522,20 +522,19 @@ async def evolist(self, ctx, *, query): @commands.command() @checks.bot_has_permissions(embed_links=True) - async def collabscroll(self, ctx, *, query: str): + async def seriesscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" DGCOG = self.bot.get_cog("Dadguide") m, err, debug_info = await self.findMonster(query) ms = DGCOG.database.get_monsters_by_series(m.series.series_id) - ms.sort(key=lambda m: m.rarity * 100000 + m.monster_id) - ms = [sorted({*m.alt_versions}, key=lambda m: m.monster_id) for m in ms - if m.base_monster == m - and m.sell_mp >= 3000] - ms = [m for ml in ms for m in ml] + ms.sort(key=lambda m: m.monster_id) + ms = [m for m in ms if m.sell_mp >= 100] + + if not ms: + await ctx.send("There are no monsters in that series worth more than 99 monster points.") + return - if m.base_monster != m: - m = m.base_monster if m not in ms: m = m if m in ms else ms[0] From dab819b1cd5cc77661416043d6494c9aaf00fdb4 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Oct 2020 11:12:25 -0700 Subject: [PATCH 0226/1220] alias to collabscroll --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 864bc7bb..78c77dda 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -520,7 +520,7 @@ async def evolist(self, ctx, *, query): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command() + @commands.command(aliases=['collabscroll']) @checks.bot_has_permissions(embed_links=True) async def seriesscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" From ee4894340cc5df44371cf31f521c0d536d409f5f Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 24 Oct 2020 12:40:54 -0700 Subject: [PATCH 0227/1220] Surveymode --- padinfo/padinfo.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bab6bd1d..4ab584fc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -5,12 +5,13 @@ import logging import os import prettytable +import random import traceback import tsutils import urllib.parse from collections import OrderedDict from enum import Enum -from redbot.core import checks, commands, data_manager +from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json @@ -195,6 +196,9 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) + self.config = Config.get_conf(self, identifier=9401770) + self.config.register_user(survey_mode=1.0) + def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.index_all = None @@ -286,9 +290,16 @@ async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): m, err, debug_info = await self.findMonster(query, server_filter=server_filter) params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': m.name_en}) url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params + async def send_survey_after(): - asyncio.sleep(3) - await ctx.send(f"Was this the monster you wanted? If not, fill out this survey to help the Tsubaki team!\n <{url}>") + if random.random() < await self.config.user(ctx.author).survey_mode(): + await asyncio.sleep(1) + m = await ctx.send(f"Was this the monster you wanted? If not, fill out this" + f" survey to help the Tsubaki team!\nUse `{ctx.prefix}idmode" + f" survey` to adjust how often this shows.\n<{url}>") + await asyncio.sleep(15) + await m.delete() + asyncio.create_task(send_survey_after()) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) @@ -641,7 +652,7 @@ async def padsay(self, ctx, server, *, query: str = None): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command() + @commands.group(invoke_without_command=True) async def idmode(self, ctx, id_type): """Switch between number mode and evo mode @@ -660,6 +671,20 @@ async def idmode(self, ctx, id_type): else: await ctx.send(inline("id_type must be `number` or `evo`")) + @idmode.command() + async def survey(self, ctx, value): + """Change how often you see the id survey + + [p]idmode survey always (Always see survey after using id) + [p]idmode survey sometimes (See survey 20% of the time after using id) + [p]idmode survey never (Never see survey after using id D:)""" + vals = {'always': 1.0, 'sometimes': 0.2, 'never': 0.0} + if value in vals: + await self.config.user(ctx.author).survey_mode.set(vals[value]) + await ctx.tick() + else: + await ctx.send(inline("value must be `always`, `sometimes`, or `never`")) + @commands.group() @checks.is_owner() async def padinfo(self, ctx): From d9a644543ead3b2c51f076f3c9a1c2c77802fb71 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 24 Oct 2020 12:50:54 -0700 Subject: [PATCH 0228/1220] Set sometimes percent --- padinfo/padinfo.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a790bc68..1753b555 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -199,7 +199,8 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) self.config = Config.get_conf(self, identifier=9401770) - self.config.register_user(survey_mode=1.0) + self.config.register_user(survey_mode=0) + self.config.register_global(sometimes_perc=20) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -294,7 +295,9 @@ async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params async def send_survey_after(): - if random.random() < await self.config.user(ctx.author).survey_mode(): + sm = await self.config.user(ctx.author).survey_mode() + sms = [1, await self.config.sometimes_perc()/100, 0][sm] + if random.random() < sms: await asyncio.sleep(1) m = await ctx.send(f"Was this the monster you wanted? If not, fill out this" f" survey to help the Tsubaki team!\nUse `{ctx.prefix}idmode" @@ -706,9 +709,9 @@ async def survey(self, ctx, value): [p]idmode survey always (Always see survey after using id) [p]idmode survey sometimes (See survey 20% of the time after using id) [p]idmode survey never (Never see survey after using id D:)""" - vals = {'always': 1.0, 'sometimes': 0.2, 'never': 0.0} + vals = ['always', 'sometimes', 'never'] if value in vals: - await self.config.user(ctx.author).survey_mode.set(vals[value]) + await self.config.user(ctx.author).survey_mode.set(vals.index(value)) await ctx.tick() else: await ctx.send(inline("value must be `always`, `sometimes`, or `never`")) From b5064fe8255fa601b875716c191b2bbab2611171 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 24 Oct 2020 21:07:59 -0700 Subject: [PATCH 0229/1220] Dadguide rewrite Graph Addition (#165) --- dadguide/__init__.py | 2 +- dadguide/dadguide.py | 4 +- dadguide/database_manager.py | 231 ++++++++++++++++++++--------------- dadguide/info.json | 3 +- padglobal/padglobal.py | 6 +- padvision/__init__.py | 2 +- 6 files changed, 143 insertions(+), 105 deletions(-) diff --git a/dadguide/__init__.py b/dadguide/__init__.py index dbe92ee4..1159ca75 100644 --- a/dadguide/__init__.py +++ b/dadguide/__init__.py @@ -1,4 +1,4 @@ -from .dadguide import Dadguide +from .dadguide import * __red_end_user_data_statement__ = "No personal data is stored." diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 01de89cf..de616da9 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -67,7 +67,7 @@ def __init__(self, bot, *args, **kwargs): # Map of google-translated JP names to EN names self.translated_names = {} - self.database = load_database(None) + self.database = None self.index = None # type: MonsterIndex async def wait_until_ready(self): @@ -115,7 +115,7 @@ async def reload_data_task(self): await self.bot.wait_until_ready() # We already had a copy of the database at startup, signal that we're ready now. - if self.database.has_database(): + if self.database and self.database.has_database(): logger.info('Using stored database at load') while self == self.bot.get_cog('Dadguide'): diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index c50e71e0..bb5994ca 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -4,6 +4,7 @@ import shutil import romkan import logging +import networkx as nx import pytz import difflib import os @@ -74,6 +75,8 @@ class Server(Enum): NA = 1 KR = 2 +def get_edges(node, type): + return {mid for mid, edge in node.items() if edge.get('type') == type} class DadguideTableNotFound(Exception): def __init__(self, table_name): @@ -96,15 +99,23 @@ def __init__(self, data_file): self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) self._con.row_factory = lite.Row + self.graph = None + self.build_graph() self.cachedmonsters = None self.expiry = 0 + self.generate_all_monsters() + + def __del__(self): + self.close() + logger.info("Garbage Collecting Old Database") def has_database(self): return self._con is not None def close(self): - self._con.close() + if self._con: + self._con.close() self._con = None @staticmethod @@ -153,14 +164,12 @@ def _query_one(self, query, param, d_type): def _as_generator(self, cursor, d_type): res = cursor.fetchone() - if issubclass(d_type, DadguideItem): - while res is not None: + while res is not None: + if issubclass(d_type, DadguideItem): yield d_type(res, self) - res = cursor.fetchone() - else: - while res is not None: + else: yield d_type(res) - res = cursor.fetchone() + res = cursor.fetchone() def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): cursor = self._con.cursor() @@ -168,12 +177,21 @@ def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): if cursor.rowcount == 0: return [] if as_generator: - return self._as_generator(cursor, d_type) + return (d_type(res, self) + if issubclass(d_type, DadguideItem) + else d_type(res) + for res in cursor.fetchall()) else: if idx_key is None: - return [d_type(res, self) for res in cursor.fetchall()] + if issubclass(d_type, DadguideItem): + return [d_type(res, self) for res in cursor.fetchall()] + else: + return [d_type(res) for res in cursor.fetchall()] else: - return DictWithAttrAccess({res[idx_key]: d_type(res, self) for res in cursor.fetchall()}) + if issubclass(d_type, DadguideItem): + return DictWithAttrAccess({res[idx_key]: d_type(res, self) for res in cursor.fetchall()}) + else: + return DictWithAttrAccess({res[idx_key]: d_type(res) for res in cursor.fetchall()}) def _max_id(self): cursor = self._con.cursor() @@ -201,10 +219,80 @@ def _get_table_fields(self, table_name: str): raise DadguideTableNotFound(table_name) return fields, pk - def get_active_skill(self, active_skill_id: int): + def build_graph(self): + self.graph = nx.DiGraph() + + ms = self._query_many("SELECT * FROM monsters", (), DictWithAttrAccess) + es = self._query_many("SELECT * FROM evolutions", (), DictWithAttrAccess) + aws = self._query_many("SELECT * FROM awakenings", (), DgAwakening) + lss = self._query_many("SELECT * FROM leader_skills", (), DgLeaderSkill, idx_key='leader_skill_id') + ass = self._query_many("SELECT * FROM active_skills", (), DgActiveSkill, idx_key='active_skill_id') + ss = self._query_many("SELECT * FROM series", (), DgSeries, idx_key='series_id') + + mtoawo = defaultdict(list) + for a in aws: + mtoawo[a.monster_id].append(a) + + for m in ms: + self.graph.add_node(m.monster_id, + awakenings=mtoawo[m.monster_id] + leader_skill=lss.get(m.leader_skill_id), + active_skill=ass.get(m.active_skill_id), + series=ss.get(m.series_id)) + if m.linked_monster_id: + self.graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') + self.graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') + + for e in es: + self.graph.add_edge(e.from_id, e.to_id, type='evolution', **e) + self.graph.add_edge(e.to_id, e.from_id, type='back_evolution', **e) + + + def get_evo_tree(self, monster_id): + ids = set() + to_check = {monster_id} + while to_check: + mid = to_check.pop() + if mid in ids: + continue + n = self.graph[mid] + to_check.update(get_edges(n, 'evolution')) + to_check.update(get_edges(n, 'back_evolution')) + ids.add(mid) + return ids + + def get_transform_tree(self, monster_id): + ids = set() + to_check = {monster_id} + while to_check: + mid = to_check.pop() + if mid in ids: + continue + n = self.graph[mid] + to_check.update(get_edges(n, 'transformation')) + to_check.update(get_edges(n, 'back_transformation')) + ids.add(mid) + return ids + + def get_alt_cards(self, monster_id): + ids = set() + to_check = {monster_id} + while to_check: + mid = to_check.pop() + if mid in ids: + continue + n = self.graph[mid] + to_check.update(get_edges(n, 'evolution')) + to_check.update(get_edges(n, 'transformation')) + to_check.update(get_edges(n, 'back_evolution')) + to_check.update(get_edges(n, 'back_transformation')) + ids.add(mid) + return ids + + def get_active_skill_query(self, active_skill_id: int): return self._select_one_entry_by_pk(active_skill_id, DgActiveSkill) - def get_leader_skill(self, leader_skill_id: int): + def get_leader_skill_query(self, leader_skill_id: int): return self._select_one_entry_by_pk(leader_skill_id, DgLeaderSkill) def get_awoken_skill(self, awoken_skill_id): @@ -230,22 +318,6 @@ def get_monsters_by_awakenings(self, awoken_skill_id: int): (awoken_skill_id,), DgMonster) - def get_awakenings_by_monster(self, monster_id, is_super=None): - if is_super is None: - where = '{0}.monster_id=?'.format(DgAwakening.TABLE) - param = (monster_id,) - else: - where = '{0}.monster_id=? AND {0}.is_super=?'.format(DgAwakening.TABLE) - param = (monster_id, is_super) - return self._query_many( - self._select_builder( - tables={DgAwakening.TABLE: DgAwakening.FIELDS}, - where=where, - order='order_idx ASC' - ), - param, - DgAwakening) - def get_drop_dungeons(self, monster_id): return self._query_many( self._select_builder( @@ -282,64 +354,14 @@ def monster_in_mp_shop(self, monster_id): return m is not None and m.buy_mp is not None def get_prev_evolution_by_monster(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where='{}.to_id=?'.format(DgEvolution.TABLE) - ), - (monster_id,), - DgEvolution) + bes = get_edges(self.graph[monster_id], 'back_evolution') + if bes: return bes.pop() def get_next_evolutions_by_monster(self, monster_id): - return self._query_many( - self._select_builder( - tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where='{}.from_id=?'.format(DgEvolution.TABLE) - ), - (monster_id,), - DgEvolution, - as_generator=True) - - def get_prev_transformations_by_monster(self, monster_id): - return self._query_many( - self._select_builder( - tables={DgMonster.TABLE: ('monster_id AS from_id', 'linked_monster_id AS to_id')}, - where='{}.linked_monster_id=?'.format(DgMonster.TABLE) - ), - (monster_id,), - DictWithAttrAccess, - as_generator=True) - - def get_next_transformation_by_monster(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgMonster.TABLE: ('monster_id AS from_id', 'linked_monster_id AS to_id')}, - where='{}.monster_id=?'.format(DgMonster.TABLE) - ), - (monster_id,), - DictWithAttrAccess) - - def build_id_set(self, monster_id): - to_check = {monster_id} - all_ids = {monster_id} - while to_check: - mid = to_check.pop() - linked_monsters = set() - linked_monsters.add(getattr(self.get_prev_evolution_by_monster(mid), 'from_id', None)) - linked_monsters.update({e.to_id for e in self.get_next_evolutions_by_monster(mid)}) - linked_monsters.update({e.from_id for e in self.get_prev_transformations_by_monster(mid)}) - linked_monsters.add(getattr(self.get_next_transformation_by_monster(mid), 'to_id', None)) - linked_monsters.discard(None) - - linked_monsters.difference_update(all_ids) - - to_check |= linked_monsters - all_ids |= linked_monsters - return sorted(all_ids) - + return get_edges(self.graph[monster_id], 'evolution') def get_base_monster_by_id(self, monster_id): - return self.build_id_set(monster_id)[0] + return min(self.get_alt_cards(monster_id)) def get_evolution_by_material(self, monster_id): return self._query_many( @@ -370,8 +392,8 @@ def get_evolution_tree_ids(self, base_monster_id): while len(n_evos) > 0: n_evo_id = n_evos.popleft() for e in self.get_next_evolutions_by_monster(n_evo_id): - n_evos.append(e.to_id) - evolution_tree.append(e.to_id) + n_evos.append(e) + evolution_tree.append(e) return evolution_tree def monster_id_to_no(self, monster_id, region=Server.JP): @@ -379,9 +401,6 @@ def monster_id_to_no(self, monster_id, region=Server.JP): if m: return getattr(m, 'monster_no_{}'.format(region.name.lower())) - def get_series(self, series_id: int): - return self._select_one_entry_by_pk(series_id, DgSeries) - def get_monsters_where(self, f): return [m for m in self.get_all_monsters() if f(m)] @@ -436,8 +455,7 @@ def get_all_monster_ids_query(self, as_generator=True): def refresh_monsters(self): if self.expiry < datetime.now().timestamp(): - self.expiry = int(datetime.now().timestamp()) + 60*60 - self.cachedmonsters = {} + self.generate_all_monsters() def get_all_monsters(self, as_generator=True): monsters = (self.get_monster(mid) for mid in self.get_all_monster_ids_query()) @@ -445,6 +463,15 @@ def get_all_monsters(self, as_generator=True): return [*monsters] return monsters + def generate_all_monsters(self): + self.cachedmonsters = {m.monster_id: m for m in self._query_many( + self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), + (), + DgMonster, + as_generator=True)} + self.expiry = int(datetime.now().timestamp()) + 60*60 + + def get_all_events(self, as_generator=True): return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), DgScheduledEvent, @@ -648,22 +675,29 @@ def __init__(self, item, database): self.in_pem = bool(self.pal_egg) self.in_rem = bool(self.rem_egg) - self.awakenings = self._database.get_awakenings_by_monster(self.monster_id) + self.awakenings = self.node['awakenings'] self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) self.is_inheritable = bool(self.inheritable) - self.evo_from = self._database.get_prev_evolution_by_monster(self.monster_id) + self.evo_from_id = self._database.get_prev_evolution_by_monster(self.monster_id) + self.evo_from = None + if self.evo_from_id: + self.evo_from = self._database.graph.edges[self.evo_from_id, self.monster_id] self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) - self._alt_version_id_list = self._database.build_id_set(self.monster_id) + self._alt_version_id_list = sorted(self._database.get_alt_cards(self.monster_id)) self._base_monster_id = self._alt_version_id_list[0] - self._alt_evo_id_list = self._database.get_evolution_tree_ids(self._base_monster_id) + self._alt_evo_id_list = sorted(self._database.get_evo_tree(self.monster_id)) self.search = MonsterSearchHelper(self) + @property + def node(self): + return self._database.graph.nodes[self.monster_id] + @property def monster_no(self): return self.monster_id @@ -701,15 +735,15 @@ def stats(self, lv=99, plus=0, inherit=False): @property def active_skill(self): - return self._database.get_active_skill(self.active_skill_id) + return self.node['active_skill'] @property def leader_skill(self): - return self._database.get_leader_skill(self.leader_skill_id) + return self.node['leader_skill'] @property def cur_evo_type(self): - return self.evo_from.evolution_type if self.evo_from else EvoType.Base + return self.evo_from['evolution_type'] if self.evo_from else EvoType.Base @property def true_evo_type(self): @@ -749,7 +783,7 @@ def _evolutions_to(self): @property def evo_to(self): - return [self._database.get_monster(x.to_id) for x in self._evolutions_to()] + return [self._database.get_monster(x) for x in self._evolutions_to()] @property def base_monster(self): @@ -769,7 +803,7 @@ def is_base_monster(self): @property def series(self): - return self._database.get_series(self.series_id) + return self.node['series'] @property def is_gfe(self): @@ -1015,6 +1049,7 @@ async def __init__(self, monster_database, nickname_overrides, basename_override 154: ['padr'], 175: ['valentines', 'vday', 'v'], 183: ['gh', 'gungho'], + 117: ['gh', 'gungho'], } monster_id_to_nicknames = defaultdict(set) diff --git a/dadguide/info.json b/dadguide/info.json index 46024be3..592cfab5 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -19,7 +19,8 @@ "requirements": [ "tsutils", "pytz", - "romkan" + "romkan", + "networkx" ], "tags": [ "PAD" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 49ad5da5..890c5101 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -9,6 +9,7 @@ import os import prettytable import re +import time import tsutils from io import StringIO, BytesIO from collections import defaultdict @@ -289,12 +290,13 @@ def list_or_none(l): @is_padglobal_admin() async def forceindexreload(self, ctx): async with ctx.typing(): - await ctx.send('starting reload') + start = time.perf_counter() + await ctx.send('Starting reload...') dadguide_cog = self.bot.get_cog('Dadguide') await dadguide_cog.reload_config_files() padinfo_cog = self.bot.get_cog('PadInfo') await padinfo_cog.refresh_index() - await ctx.send('finished reload') + await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) @commands.group(aliases=['pdg']) @is_padglobal_admin() diff --git a/padvision/__init__.py b/padvision/__init__.py index f54c9a53..0a59bc47 100644 --- a/padvision/__init__.py +++ b/padvision/__init__.py @@ -1,4 +1,4 @@ -from .padvision import PadVision +from .padvision import PadVision, NeuralClassifierBoardExtractor __red_end_user_data_statement__ = "No personal data is stored." From b8a073081c676da4bd114cf164e2a3b4e7dcb580 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 24 Oct 2020 21:30:41 -0700 Subject: [PATCH 0230/1220] Comma (#166) --- dadguide/database_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index bb5994ca..2ff9f6a4 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -235,7 +235,7 @@ def build_graph(self): for m in ms: self.graph.add_node(m.monster_id, - awakenings=mtoawo[m.monster_id] + awakenings=mtoawo[m.monster_id], leader_skill=lss.get(m.leader_skill_id), active_skill=ass.get(m.active_skill_id), series=ss.get(m.series_id)) From 51c1ad7bcf7d11411ccee29802503f57c174936d Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 25 Oct 2020 00:47:07 -0700 Subject: [PATCH 0231/1220] Fix formatting and make sometimes percent vague --- padinfo/padinfo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1753b555..9ed8d63b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -700,21 +700,21 @@ async def idmode(self, ctx, id_type): else: await ctx.send(inline("You're already using number mode")) else: - await ctx.send(inline("id_type must be `number` or `evo`")) + await ctx.send("id_type must be `number` or `evo`") @idmode.command() async def survey(self, ctx, value): """Change how often you see the id survey [p]idmode survey always (Always see survey after using id) - [p]idmode survey sometimes (See survey 20% of the time after using id) + [p]idmode survey sometimes (See survey some of the time after using id) [p]idmode survey never (Never see survey after using id D:)""" vals = ['always', 'sometimes', 'never'] if value in vals: await self.config.user(ctx.author).survey_mode.set(vals.index(value)) await ctx.tick() else: - await ctx.send(inline("value must be `always`, `sometimes`, or `never`")) + await ctx.send("value must be `always`, `sometimes`, or `never`") @commands.group() @checks.is_owner() From 54e60dc405fab3412ee3d3356aa674b0cecd70a6 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 25 Oct 2020 01:16:49 -0700 Subject: [PATCH 0232/1220] commands, kdi, and unnesting --- padinfo/padinfo.py | 62 +++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9ed8d63b..4f694d8d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -14,7 +14,7 @@ from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline -from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json +from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, confirm_message from .find_monster import prefix_to_filter @@ -200,7 +200,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0) - self.config.register_global(sometimes_perc=20) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -291,26 +291,54 @@ async def idjp(self, ctx, *, query: str): async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): m, err, debug_info = await self.findMonster(query, server_filter=server_filter) - params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': m.name_en}) - url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params - - async def send_survey_after(): - sm = await self.config.user(ctx.author).survey_mode() - sms = [1, await self.config.sometimes_perc()/100, 0][sm] - if random.random() < sms: - await asyncio.sleep(1) - m = await ctx.send(f"Was this the monster you wanted? If not, fill out this" - f" survey to help the Tsubaki team!\nUse `{ctx.prefix}idmode" - f" survey` to adjust how often this shows.\n<{url}>") - await asyncio.sleep(15) - await m.delete() - - asyncio.create_task(send_survey_after()) + + if await self.config.do_survey(): + asyncio.create_task(self.send_survey_after(ctx, query, m.name_en)) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: await ctx.send(self.makeFailureMsg(err)) + async def send_survey_after(self, ctx, query, result): + sm = await self.config.user(ctx.author).survey_mode() + sms = [1, await self.config.sometimes_perc()/100, 0][sm] + if random.random() < sms: + params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': result}) + url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params + await asyncio.sleep(1) + if await tsutils.confirm_message(ctx, "Was this the monster you were looking for?"): + await self.config.good.set(await self.config.good() + 1) + return + await self.config.bad.set(await self.config.bad() + 1) + m = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" + f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idmode" + f" survey` to adjust how often this shows.\n\n<{url}>") + await asyncio.sleep(15) + await m.delete() + + @commands.group() + async def idsurvey(self, ctx): + """Commands pertaining to the id survey""" + + @idsurvey.command() + async def dosurvey(self, ctx, do_survey: bool): + """Toggle the survey avalibility""" + await self.config.do_survey.set(do_survey) + await ctx.tick() + + @idsurvey.command() + async def sometimesperc(self, ctx, percent: int): + """Change what 'sometimes' means""" + await self.config.sometimes_perc.set(percent) + await ctx.tick() + + @idsurvey.command() + async def checkbadness(self, ctx): + """Check how good id is according to end users""" + good = await self.config.good() + bad = await self.config.good() + await ctx.send(f"{bad}/{good+bad} ({bad/(good+bad) if good or bad else 'NaN'}%)") + @commands.command() @checks.bot_has_permissions(embed_links=True) async def id2(self, ctx, *, query: str): From 1f9a1cc5fe635187d2e7d4ce8a4204f3cf353991 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 25 Oct 2020 14:36:36 -0700 Subject: [PATCH 0233/1220] Remove speak check --- padinfo/padinfo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4f694d8d..19c706e0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -675,7 +675,6 @@ async def helpid(self, ctx): await ctx.author.send(box(HELP_MSG.format(ctx))) @commands.command() - @checks.bot_has_permissions(speak=True) async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" voice = ctx.author.voice From 60a27504bb4ac4e42dd5e88a4272c61a96c1da35 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 25 Oct 2020 15:37:26 -0700 Subject: [PATCH 0234/1220] Survey Timeout Fix --- padinfo/padinfo.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 19c706e0..442df7cb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -306,15 +306,16 @@ async def send_survey_after(self, ctx, query, result): params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': result}) url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params await asyncio.sleep(1) - if await tsutils.confirm_message(ctx, "Was this the monster you were looking for?"): + userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?") + if userres is True: await self.config.good.set(await self.config.good() + 1) - return - await self.config.bad.set(await self.config.bad() + 1) - m = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" - f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idmode" - f" survey` to adjust how often this shows.\n\n<{url}>") - await asyncio.sleep(15) - await m.delete() + elif userres is False: + await self.config.bad.set(await self.config.bad() + 1) + m = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" + f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idmode" + f" survey` to adjust how often this shows.\n\n<{url}>") + await asyncio.sleep(15) + await m.delete() @commands.group() async def idsurvey(self, ctx): From 229dfe00b71588b6a6637dadb222978ffb4cd719 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 25 Oct 2020 15:45:00 -0700 Subject: [PATCH 0235/1220] update requirement --- padinfo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/info.json b/padinfo/info.json index 32585688..aa578496 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils", + "tsutils>=1.1.0", "prettytable" ], "tags": [ From 90bb88f00144d810c53fd6ec0a56556fbf938287 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 25 Oct 2020 16:05:27 -0700 Subject: [PATCH 0236/1220] Allow for no monster returned --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 442df7cb..be93c6b5 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -293,7 +293,7 @@ async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): m, err, debug_info = await self.findMonster(query, server_filter=server_filter) if await self.config.do_survey(): - asyncio.create_task(self.send_survey_after(ctx, query, m.name_en)) + asyncio.create_task(self.send_survey_after(ctx, query, m.name_en if m else "None")) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: From 3c43d24bb4170a65e839cb0a3542774aa9a12111 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 26 Oct 2020 10:41:14 -0700 Subject: [PATCH 0237/1220] PDB fix and PDI survey emoji change --- padguidedb/padguidedb.py | 2 +- padinfo/padinfo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 2c41c089..56a272f1 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -240,7 +240,7 @@ async def fulletl(self, ctx): ) stdout, stderr = await process.communicate() - if "Pipeline completed successfully!" not in stderr: + if b"Pipeline completed successfully!" not in stderr: logger.error("Full ETL Error:\n" + stderr.decode()) await ctx.send(inline('Full ETL failed')) return diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index be93c6b5..eca6faf4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -306,7 +306,7 @@ async def send_survey_after(self, ctx, query, result): params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': result}) url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params await asyncio.sleep(1) - userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?") + userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", yemoji=char_to_emoji('y'), nemoji=char_to_emoji('n')) if userres is True: await self.config.good.set(await self.config.good() + 1) elif userres is False: From faa99eed438fa08f9d09f08d09877ccae10844d9 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 26 Oct 2020 11:42:04 -0700 Subject: [PATCH 0238/1220] PDB fix and PDI survey emoji change (#171) --- padguidedb/padguidedb.py | 2 +- padinfo/padinfo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 2c41c089..56a272f1 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -240,7 +240,7 @@ async def fulletl(self, ctx): ) stdout, stderr = await process.communicate() - if "Pipeline completed successfully!" not in stderr: + if b"Pipeline completed successfully!" not in stderr: logger.error("Full ETL Error:\n" + stderr.decode()) await ctx.send(inline('Full ETL failed')) return diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index be93c6b5..eca6faf4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -306,7 +306,7 @@ async def send_survey_after(self, ctx, query, result): params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': result}) url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params await asyncio.sleep(1) - userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?") + userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", yemoji=char_to_emoji('y'), nemoji=char_to_emoji('n')) if userres is True: await self.config.good.set(await self.config.good() + 1) elif userres is False: From d854e88290d75f2e98d294522376ca3a1b497d09 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 27 Oct 2020 03:52:02 -0500 Subject: [PATCH 0239/1220] [dadguide] Split out db context, graph, and loader classes, add tests (#167) * split out db wrapper stuff, but code will NOT run atm tests of just database_context.py are (mostly) passing - is_rem/is_pem I'm not sure, are these based on current JP rotation? - I fixed get_monster_evo_gem - get_evolution_tree_ids does not include the base ID - I didn't test get_all_events or get_dungeon_by_id however absolutely definitely this won't actually run since MonsterIndex requires stuff that's no longer in the database manager class. I briefly tried to make that work by further splitting up the files but that didn't go well so I just gave up so here's what I did so far hf * pull old_monster_index to own file (untested) * remove unused functions * create MonsterGraph, change Dadguide signatures DadguideItem and everything that subclasses it now has **kwargs so that DgMonster can have a graph added to its signature, also the signatures of query_one, query_many, etc, have all been changed so that now the DbContext is used instead of the DadguideDatabase for DgMonster (and everything else). Updated database_context accordingly. * make MonsterGraph._get_edges private * create tests dir, add db_context tests * create db_loader, make dg use it, it should run now * send graph & db to query_many in MonsterGraph * workaround for not having DbContext in graphs * undo attempt to fix graph db * fix database workaround for monster graphs * fix more instances of query_many * fix last d_type * pycharm code reformat --- dadguide/dadguide.py | 5 +- dadguide/database_context.py | 226 +++++++ dadguide/database_loader.py | 33 ++ dadguide/database_manager.py | 749 ++---------------------- dadguide/monster_graph.py | 110 ++++ dadguide/old_monster_index.py | 406 +++++++++++++ tests/dadguide/database_context_test.py | 35 ++ 7 files changed, 847 insertions(+), 717 deletions(-) create mode 100644 dadguide/database_context.py create mode 100644 dadguide/database_loader.py create mode 100644 dadguide/monster_graph.py create mode 100644 dadguide/old_monster_index.py create mode 100644 tests/dadguide/database_context_test.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index de616da9..0dd6b3a0 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -20,7 +20,8 @@ from redbot.core import commands from .database_manager import * - +from .old_monster_index import MonsterIndex +from .database_loader import load_database logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -49,6 +50,7 @@ def _data_file(file_name: str) -> str: class Dadguide(commands.Cog): """Dadguide database manager""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -182,7 +184,6 @@ async def download_and_refresh_nicknames(self): logger.debug('Done refreshing dg data') - def write_monster_computed_names(self): results = {} for name, nm in self.index.all_entries.items(): diff --git a/dadguide/database_context.py b/dadguide/database_context.py new file mode 100644 index 00000000..dbb9de05 --- /dev/null +++ b/dadguide/database_context.py @@ -0,0 +1,226 @@ +from datetime import datetime +from collections import OrderedDict, defaultdict, deque + +from .database_manager import DadguideDatabase +from .monster_graph import MonsterGraph +from .database_manager import DgActiveSkill +from .database_manager import DgLeaderSkill +from .database_manager import DgAwokenSkill +from .database_manager import DadguideItem +from .database_manager import DgMonster +from .database_manager import DgAwakening +from .database_manager import DgDungeon +from .database_manager import DgEncounter +from .database_manager import DgDrop +from .database_manager import DgEvolution +from .database_manager import DictWithAttrAccess +from .database_manager import DgScheduledEvent + + +class DbContext(object): + def __init__(self, database: DadguideDatabase, graph: MonsterGraph): + self.database = database + self.graph = graph + self.graph.set_database(self) + self.cachedmonsters = None + self.expiry = 0 + # self.generate_all_monsters() + + def generate_all_monsters(self): + self.cachedmonsters = {m.monster_id: m for m in self.database.query_many( + self.database.select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), + (), + DgMonster, + as_generator=True, db_context=self, graph=self.graph)} + self.expiry = int(datetime.now().timestamp()) + 60 * 60 + + def get_active_skill_query(self, active_skill_id: int): + return self.database.select_one_entry_by_pk( + active_skill_id, DgActiveSkill, db_context=self) + + def get_leader_skill_query(self, leader_skill_id: int): + return self.database.select_one_entry_by_pk( + leader_skill_id, DgLeaderSkill, db_context=self) + + def get_awoken_skill(self, awoken_skill_id): + return self.database.select_one_entry_by_pk( + awoken_skill_id, DgAwokenSkill, db_context=self) + + def get_awoken_skill_ids(self): + SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' + return [r.awoken_skill_id for r in + self.database.query_many( + SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True, + db_context=self, graph=self.graph)] + + def get_monsters_by_awakenings(self, awoken_skill_id: int): + # TODO: Make this not make monsters via query + return self.database.query_many( + self.database.select_builder( + tables=OrderedDict({ + DgMonster.TABLE: DgMonster.FIELDS, + DgAwakening.TABLE: None, + }), + where='{}.awoken_skill_id=?;'.format(DgAwakening.TABLE), + key=('monster_id',), + distinct=True + ), + (awoken_skill_id,), + DgMonster, db_context=self, graph=self.graph) + + def get_drop_dungeons(self, monster_id): + return self.database.query_many( + self.database.select_builder( + tables=OrderedDict({ + DgDungeon.TABLE: DgDungeon.FIELDS, + DgEncounter.TABLE: None, + DgDrop.TABLE: None, + }), + where='{0}.monster_id=?'.format(DgDrop.TABLE), + key=(DgDungeon.PK, DgEncounter.PK) + ), + (monster_id,), + DgDungeon, db_context=self) + + def monster_is_farmable(self, monster_id): + return self.database.query_one( + self.database.select_builder( + tables={DgDrop.TABLE: DgDrop.FIELDS}, + where='{0}.monster_id=?'.format(DgDrop.TABLE) + ), + (monster_id,), + DgDrop, db_context=self) is not None + + def monster_in_rem(self, monster_id): + m = self.get_monster(monster_id) + return m is not None and m.rem_egg == 1 + + def monster_in_pem(self, monster_id): + m = self.get_monster(monster_id) + return m is not None and m.pal_egg == 1 + + def monster_in_mp_shop(self, monster_id): + m = self.get_monster(monster_id) + return m is not None and m.buy_mp is not None + + def get_prev_evolution_by_monster(self, monster_id): + return self.graph.get_prev_evolution_by_monster(monster_id) + + def get_next_evolutions_by_monster(self, monster_id): + return self.graph.get_next_evolutions_by_monster(monster_id) + + def get_base_monster_by_id(self, monster_id): + return min(self.graph.get_alt_cards(monster_id)) + + def get_evolution_by_material(self, monster_id): + return self.database.query_many( + self.database.select_builder( + tables={DgEvolution.TABLE: DgEvolution.FIELDS}, + where=' OR '.join(['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in range(1, 6)]) + ), + (monster_id,) * 5, + DgEvolution, db_context=self) + + def get_evolution_tree_ids(self, base_monster_id): + # is not a tree i lied + base_id = base_monster_id + evolution_tree = [base_id] + n_evos = deque() + n_evos.append(base_id) + while len(n_evos) > 0: + n_evo_id = n_evos.popleft() + for e in self.get_next_evolutions_by_monster(n_evo_id): + n_evos.append(e) + evolution_tree.append(e) + return evolution_tree + + def get_monsters_where(self, f): + return [m for m in self.get_all_monsters() if f(m)] + + def get_first_monster_where(self, f): + ms = self.get_monsters_where(f) + if ms: + return min(ms, key=lambda m: m.monster_id) + + def get_monsters_by_series(self, series_id: int): + return self.get_monsters_where(lambda m: m.series_id == series_id) + + def get_monsters_by_active(self, active_skill_id: int): + return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id) + + def get_monster_evo_gem(self, name: str, region='ja'): + gem_suffix = { + 'ja': 'の希石', + 'en': '\'s Gem', + 'ko': ' 의 휘석' + } + if region not in gem_suffix: + return None + gem_name = name + gem_suffix.get(region) + return self.get_first_monster_where( + lambda m: getattr(m, 'name_{}'.format(region)) == gem_name and m.leader_skill_id == 10628) + + def _get_monster_query(self, monster_id: int): + monster = self.database.select_one_entry_by_pk(monster_id, DgMonster) + if monster is not None: + self.cachedmonsters[monster.monster_id] = monster + return monster + + def get_monster(self, monster_id: int): + self.refresh_monsters() + if monster_id in self.cachedmonsters: + return self.cachedmonsters.get(monster_id) + return self._get_monster_query(monster_id) + + def get_all_monster_ids_query(self, as_generator=True): + query = self.database.query_many(self.database.select_builder(tables={DgMonster.TABLE: ('monster_id',)}), (), + DictWithAttrAccess, + as_generator=as_generator, + db_context=self, graph=self.graph) + if as_generator: + return map(lambda m: m.monster_id, query) + return [m.monster_id for m in query] + + def refresh_monsters(self): + if self.expiry < datetime.now().timestamp(): + self.generate_all_monsters() + + def get_all_monsters(self, as_generator=True): + monsters = (self.get_monster(mid) for mid in self.get_all_monster_ids_query()) + if not as_generator: + return [*monsters] + return monsters + + def get_all_events(self, as_generator=True): + return self.database.query_many( + self.database.select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), + DgScheduledEvent, db_context=self, + as_generator=as_generator) + + def get_dungeon_by_id(self, dungeon_id: int): + return self.database.select_one_entry_by_pk(dungeon_id, DgDungeon) + + def get_base_monster_ids(self): + SELECT_BASE_MONSTER_ID = ''' + SELECT evolutions.from_id as monster_id FROM evolutions WHERE evolutions.from_id NOT IN (SELECT DISTINCT evolutions.to_id FROM evolutions) + UNION + SELECT monsters.monster_id FROM monsters WHERE monsters.monster_id NOT IN (SELECT evolutions.from_id FROM evolutions UNION SELECT evolutions.to_id FROM evolutions)''' + return self.database.query_many( + SELECT_BASE_MONSTER_ID, + (), + DictWithAttrAccess, db_context=self, + as_generator=True) + + def tokenize_monsters(self): + tokens = defaultdict(list) + for mid in self.get_all_monster_ids_query(): + monster = self.get_monster(mid) + for token in monster.name_en.split(): + tokens[token.lower()].append(monster) + return tokens + + def has_database(self): + return self.database.has_database() + + def close(self): + self.database.close() diff --git a/dadguide/database_loader.py b/dadguide/database_loader.py new file mode 100644 index 00000000..64536740 --- /dev/null +++ b/dadguide/database_loader.py @@ -0,0 +1,33 @@ +import shutil +import os + +from redbot.core import data_manager + +from .database_manager import DadguideDatabase +from .database_context import DbContext +from .monster_graph import MonsterGraph + + +def _data_file(file_name: str) -> str: + return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) + + +DB_DUMP_FILE = _data_file('dadguide.sqlite') +DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') + + +def load_database(existing_db): + # Release the handle to the database file if it has one + if existing_db: + existing_db.close() + # Overwrite the working copy so we can open a handle to it without affecting future downloads + if os.path.exists(DB_DUMP_FILE): + shutil.copy2(DB_DUMP_FILE, DB_DUMP_WORKING_FILE) + # Open the new working copy. + database = DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) + graph = MonsterGraph(database) + db_context = DbContext(database, graph) + graph.set_database(db_context) + graph.build_graph() + db_context.generate_all_monsters() + return db_context diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 2ff9f6a4..0dd84e16 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -4,12 +4,12 @@ import shutil import romkan import logging -import networkx as nx + import pytz import difflib import os import re -from discord.utils import find as find_first + from collections import OrderedDict, defaultdict, deque from redbot.core.utils import AsyncIter @@ -18,12 +18,6 @@ logger = logging.getLogger('red.padbot-cogs.dadguide.database_manager') -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) - -DB_DUMP_FILE = _data_file('dadguide.sqlite') -DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') - class Attribute(Enum): """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" @@ -75,37 +69,17 @@ class Server(Enum): NA = 1 KR = 2 -def get_edges(node, type): - return {mid for mid, edge in node.items() if edge.get('type') == type} class DadguideTableNotFound(Exception): def __init__(self, table_name): self.message = '{} not found'.format(table_name) -def load_database(existing_db): - # Release the handle to the database file if it has one - if existing_db: - existing_db.close() - # Overwrite the working copy so we can open a handle to it without affecting future downloads - if os.path.exists(DB_DUMP_FILE): - shutil.copy2(DB_DUMP_FILE, DB_DUMP_WORKING_FILE) - # Open the new working copy. - return DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) - - class DadguideDatabase(object): def __init__(self, data_file): self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) self._con.row_factory = lite.Row - self.graph = None - self.build_graph() - - self.cachedmonsters = None - self.expiry = 0 - self.generate_all_monsters() - def __del__(self): self.close() logger.info("Garbage Collecting Old Database") @@ -119,7 +93,7 @@ def close(self): self._con = None @staticmethod - def _select_builder(tables, key=None, where=None, order=None, distinct=False): + def select_builder(tables, key=None, where=None, order=None, distinct=False): if distinct: SELECT_FROM = 'SELECT DISTINCT {fields} FROM {first_table}' else: @@ -151,45 +125,47 @@ def _select_builder(tables, key=None, where=None, order=None, distinct=False): query.append(ORDER.format(order=order)) return ' '.join(query) - def _query_one(self, query, param, d_type): + def query_one(self, query, param, d_type, db_context=None, graph=None): cursor = self._con.cursor() cursor.execute(query, param) res = cursor.fetchone() if res is not None: if issubclass(d_type, DadguideItem): - return d_type(res, self) + return d_type(res, db_context, graph=graph) else: return d_type(res) return None - def _as_generator(self, cursor, d_type): + def as_generator(self, cursor, d_type, db_context=None, graph=None): res = cursor.fetchone() while res is not None: if issubclass(d_type, DadguideItem): - yield d_type(res, self) + yield d_type(res, db_context, graph=graph) else: yield d_type(res) res = cursor.fetchone() - def _query_many(self, query, param, d_type, idx_key=None, as_generator=False): + def query_many(self, query, param, d_type, idx_key=None, as_generator=False, + db_context=None, graph=None): cursor = self._con.cursor() cursor.execute(query, param) if cursor.rowcount == 0: return [] if as_generator: - return (d_type(res, self) - if issubclass(d_type, DadguideItem) - else d_type(res) - for res in cursor.fetchall()) + return (d_type(res, db_context, graph=graph) + if issubclass(d_type, DadguideItem) + else d_type(res) + for res in cursor.fetchall()) else: if idx_key is None: if issubclass(d_type, DadguideItem): - return [d_type(res, self) for res in cursor.fetchall()] + return [d_type(res, db_context, graph=graph) for res in cursor.fetchall()] else: return [d_type(res) for res in cursor.fetchall()] else: if issubclass(d_type, DadguideItem): - return DictWithAttrAccess({res[idx_key]: d_type(res, self) for res in cursor.fetchall()}) + return DictWithAttrAccess( + {res[idx_key]: d_type(res, db_context, graph=graph) for res in cursor.fetchall()}) else: return DictWithAttrAccess({res[idx_key]: d_type(res) for res in cursor.fetchall()}) @@ -198,17 +174,17 @@ def _max_id(self): cursor.execute("SELECT MAX(monster_id) FROM monsters") return cursor.fetchone()['MAX(monster_id)'] - def _select_one_entry_by_pk(self, pk, d_type): - return self._query_one( - self._select_builder( + def select_one_entry_by_pk(self, pk, d_type, db_context=None, graph=None): + return self.query_one( + self.select_builder( tables={d_type.TABLE: d_type.FIELDS}, where='{}.{}=?'.format(d_type.TABLE, d_type.PK)), (pk,), - d_type) + d_type, db_context=db_context, graph=graph) - def _get_table_fields(self, table_name: str): + def get_table_fields(self, table_name: str): # SQL inject vulnerable :v - table_info = self._query_many('PRAGMA table_info(' + table_name + ')', (), dict) + table_info = self.query_many('PRAGMA table_info(' + table_name + ')', (), dict) pk = None fields = [] for c in table_info: @@ -219,274 +195,6 @@ def _get_table_fields(self, table_name: str): raise DadguideTableNotFound(table_name) return fields, pk - def build_graph(self): - self.graph = nx.DiGraph() - - ms = self._query_many("SELECT * FROM monsters", (), DictWithAttrAccess) - es = self._query_many("SELECT * FROM evolutions", (), DictWithAttrAccess) - aws = self._query_many("SELECT * FROM awakenings", (), DgAwakening) - lss = self._query_many("SELECT * FROM leader_skills", (), DgLeaderSkill, idx_key='leader_skill_id') - ass = self._query_many("SELECT * FROM active_skills", (), DgActiveSkill, idx_key='active_skill_id') - ss = self._query_many("SELECT * FROM series", (), DgSeries, idx_key='series_id') - - mtoawo = defaultdict(list) - for a in aws: - mtoawo[a.monster_id].append(a) - - for m in ms: - self.graph.add_node(m.monster_id, - awakenings=mtoawo[m.monster_id], - leader_skill=lss.get(m.leader_skill_id), - active_skill=ass.get(m.active_skill_id), - series=ss.get(m.series_id)) - if m.linked_monster_id: - self.graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') - self.graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') - - for e in es: - self.graph.add_edge(e.from_id, e.to_id, type='evolution', **e) - self.graph.add_edge(e.to_id, e.from_id, type='back_evolution', **e) - - - def get_evo_tree(self, monster_id): - ids = set() - to_check = {monster_id} - while to_check: - mid = to_check.pop() - if mid in ids: - continue - n = self.graph[mid] - to_check.update(get_edges(n, 'evolution')) - to_check.update(get_edges(n, 'back_evolution')) - ids.add(mid) - return ids - - def get_transform_tree(self, monster_id): - ids = set() - to_check = {monster_id} - while to_check: - mid = to_check.pop() - if mid in ids: - continue - n = self.graph[mid] - to_check.update(get_edges(n, 'transformation')) - to_check.update(get_edges(n, 'back_transformation')) - ids.add(mid) - return ids - - def get_alt_cards(self, monster_id): - ids = set() - to_check = {monster_id} - while to_check: - mid = to_check.pop() - if mid in ids: - continue - n = self.graph[mid] - to_check.update(get_edges(n, 'evolution')) - to_check.update(get_edges(n, 'transformation')) - to_check.update(get_edges(n, 'back_evolution')) - to_check.update(get_edges(n, 'back_transformation')) - ids.add(mid) - return ids - - def get_active_skill_query(self, active_skill_id: int): - return self._select_one_entry_by_pk(active_skill_id, DgActiveSkill) - - def get_leader_skill_query(self, leader_skill_id: int): - return self._select_one_entry_by_pk(leader_skill_id, DgLeaderSkill) - - def get_awoken_skill(self, awoken_skill_id): - return self._select_one_entry_by_pk(awoken_skill_id, DgAwokenSkill) - - def get_awoken_skill_ids(self): - SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' - return [r.awoken_skill_id for r in - self._query_many(SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] - - def get_monsters_by_awakenings(self, awoken_skill_id: int): - # TODO: Make this not make monsters via query - return self._query_many( - self._select_builder( - tables=OrderedDict({ - DgMonster.TABLE: DgMonster.FIELDS, - DgAwakening.TABLE: None, - }), - where='{}.awoken_skill_id=?;'.format(DgAwakening.TABLE), - key=('monster_id',), - distinct=True - ), - (awoken_skill_id,), - DgMonster) - - def get_drop_dungeons(self, monster_id): - return self._query_many( - self._select_builder( - tables=OrderedDict({ - DgDungeon.TABLE: DgDungeon.FIELDS, - DgEncounter.TABLE: None, - DgDrop.TABLE: None, - }), - where='{0}.monster_id=?'.format(DgDrop.TABLE), - key=(DgDungeon.PK, DgEncounter.PK) - ), - (monster_id,), - DgDungeon) - - def monster_is_farmable(self, monster_id): - return self._query_one( - self._select_builder( - tables={DgDrop.TABLE: DgDrop.FIELDS}, - where='{0}.monster_id=?'.format(DgDrop.TABLE) - ), - (monster_id,), - DgDrop) is not None - - def monster_in_rem(self, monster_id): - m = self.get_monster(monster_id) - return m is not None and m.rem_egg == 1 - - def monster_in_pem(self, monster_id): - m = self.get_monster(monster_id) - return m is not None and m.pal_egg == 1 - - def monster_in_mp_shop(self, monster_id): - m = self.get_monster(monster_id) - return m is not None and m.buy_mp is not None - - def get_prev_evolution_by_monster(self, monster_id): - bes = get_edges(self.graph[monster_id], 'back_evolution') - if bes: return bes.pop() - - def get_next_evolutions_by_monster(self, monster_id): - return get_edges(self.graph[monster_id], 'evolution') - - def get_base_monster_by_id(self, monster_id): - return min(self.get_alt_cards(monster_id)) - - def get_evolution_by_material(self, monster_id): - return self._query_many( - self._select_builder( - tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where=' OR '.join(['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in range(1, 6)]) - ), - (monster_id,) * 5, - DgEvolution) - - def get_base_monster_ids(self): - SELECT_BASE_MONSTER_ID = ''' - SELECT evolutions.from_id as monster_id FROM evolutions WHERE evolutions.from_id NOT IN (SELECT DISTINCT evolutions.to_id FROM evolutions) - UNION - SELECT monsters.monster_id FROM monsters WHERE monsters.monster_id NOT IN (SELECT evolutions.from_id FROM evolutions UNION SELECT evolutions.to_id FROM evolutions)''' - return self._query_many( - SELECT_BASE_MONSTER_ID, - (), - DictWithAttrAccess, - as_generator=True) - - def get_evolution_tree_ids(self, base_monster_id): - # is not a tree i lied - base_id = base_monster_id - evolution_tree = [base_id] - n_evos = deque() - n_evos.append(base_id) - while len(n_evos) > 0: - n_evo_id = n_evos.popleft() - for e in self.get_next_evolutions_by_monster(n_evo_id): - n_evos.append(e) - evolution_tree.append(e) - return evolution_tree - - def monster_id_to_no(self, monster_id, region=Server.JP): - m = self.get_monster(monster_id) - if m: - return getattr(m, 'monster_no_{}'.format(region.name.lower())) - - def get_monsters_where(self, f): - return [m for m in self.get_all_monsters() if f(m)] - - def get_first_monster_where(self, f): - ms = self.get_monsters_where(f) - if ms: - return min(ms, key=lambda m: m.monster_id) - - def get_monsters_by_series(self, series_id: int): - return self.get_monsters_where(lambda m: m.series_id == series_id) - - def get_monsters_by_active(self, active_skill_id: int): - return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id) - - def get_monster_evo_gem(self, name: str, region='ja'): - gem_suffix = { - 'ja': 'の希石', - 'en': '\'s Gem', - 'ko': ' 의 휘석' - } - - if region not in gem_suffix: - return None - non_gem_name = name.replace(gem_suffix.get(region), '') - if non_gem_name == name: - return None - - return self.get_first_monster_where(lambda m: getattr(m, 'name_{}'.format(region)) == non_gem_name and m.leader_skill_id == 10628) - - - def get_na_only_monsters(self): - return self.get_monsters_where(lambda m: m.monster_id != m.monster_no_na and m.monster_no_jp == m.monster_no_na) - - def get_monster_query(self, monster_id: int): - monster = self._select_one_entry_by_pk(monster_id, DgMonster) - if monster is not None: - self.cachedmonsters[monster.monster_id] = monster - return monster - - def get_monster(self, monster_id: int): - self.refresh_monsters() - if monster_id in self.cachedmonsters: - return self.cachedmonsters.get(monster_id) - return self.get_monster_query(monster_id) - - def get_all_monster_ids_query(self, as_generator=True): - query = self._query_many(self._select_builder(tables={DgMonster.TABLE: ('monster_id',)}), (), DictWithAttrAccess, - as_generator=as_generator) - if as_generator: - return map(lambda m: m.monster_id, query) - return [m.monster_id for m in query] - - def refresh_monsters(self): - if self.expiry < datetime.now().timestamp(): - self.generate_all_monsters() - - def get_all_monsters(self, as_generator=True): - monsters = (self.get_monster(mid) for mid in self.get_all_monster_ids_query()) - if not as_generator: - return [*monsters] - return monsters - - def generate_all_monsters(self): - self.cachedmonsters = {m.monster_id: m for m in self._query_many( - self._select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), - (), - DgMonster, - as_generator=True)} - self.expiry = int(datetime.now().timestamp()) + 60*60 - - - def get_all_events(self, as_generator=True): - return self._query_many(self._select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), - DgScheduledEvent, - as_generator=as_generator) - - def get_dungeon_by_id(self, dungeon_id: int): - return self._select_one_entry_by_pk(dungeon_id, DgDungeon) - - def tokenize_monsters(self): - tokens = defaultdict(list) - for mid in self.get_all_monster_ids_query(): - monster = self.get_monster(mid) - for token in monster.name_en.split(): - tokens[token.lower()].append(monster) - return tokens def enum_or_none(enum, value, default=None): if value is not None: @@ -511,7 +219,7 @@ class DadguideItem(DictWithAttrAccess): PK = None AS_BOOL = () - def __init__(self, item, database): + def __init__(self, item, database, **kwargs): super(DadguideItem, self).__init__(item) self._database = database for k in self.AS_BOOL: @@ -564,7 +272,7 @@ class DgAwakening(DadguideItem): PK = 'awakening_id' AS_BOOL = ['is_super'] - def __init__(self, item, database): + def __init__(self, item, database, **kwargs): super(DgAwakening, self).__init__(item, database) @property @@ -589,7 +297,7 @@ class DgEvolution(DadguideItem): TABLE = 'evolutions' PK = 'evolution_id' - def __init__(self, item, database): + def __init__(self, item, database, **kwargs): super(DgEvolution, self).__init__(item, database) self.evolution_type = EvoType(self.evolution_type) @@ -652,9 +360,11 @@ class DgMonster(DadguideItem): PK = 'monster_id' AS_BOOL = ('on_jp', 'on_na', 'on_kr', 'has_animation', 'has_hqimage') - def __init__(self, item, database): + def __init__(self, item, database, graph): super(DgMonster, self).__init__(item, database) + self._graph = graph + self.roma_subname = None if self.name_en == self.name_ja: self.roma_subname = make_roma_subname(self.name_ja) @@ -680,20 +390,19 @@ def __init__(self, item, database): self.is_inheritable = bool(self.inheritable) - self.evo_from_id = self._database.get_prev_evolution_by_monster(self.monster_id) + self.evo_from_id = self._graph.get_prev_evolution_by_monster(self.monster_id) self.evo_from = None if self.evo_from_id: - self.evo_from = self._database.graph.edges[self.evo_from_id, self.monster_id] + self.evo_from = self._graph.edges[self.evo_from_id, self.monster_id] self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) - self._alt_version_id_list = sorted(self._database.get_alt_cards(self.monster_id)) + self._alt_version_id_list = sorted(self._graph.get_alt_cards(self.monster_id)) self._base_monster_id = self._alt_version_id_list[0] - self._alt_evo_id_list = sorted(self._database.get_evo_tree(self.monster_id)) + self._alt_evo_id_list = sorted(self._graph.get_evo_tree(self.monster_id)) self.search = MonsterSearchHelper(self) - @property def node(self): return self._database.graph.nodes[self.monster_id] @@ -1014,396 +723,6 @@ def make_roma_subname(name_ja): return adjusted_subname.strip() -class MonsterIndex(tsutils.aobject): - async def __init__(self, monster_database, nickname_overrides, basename_overrides, - panthname_overrides, accept_filter=None): - # Important not to hold onto anything except IDs here so we don't leak memory - base_monster_ids = monster_database.get_base_monster_ids() - - self.attr_short_prefix_map = { - Attribute.Fire: ['r'], - Attribute.Water: ['b'], - Attribute.Wood: ['g'], - Attribute.Light: ['l'], - Attribute.Dark: ['d'], - Attribute.Unknown: ['h'], - Attribute.Nil: ['x'], - } - self.attr_long_prefix_map = { - Attribute.Fire: ['red', 'fire'], - Attribute.Water: ['blue', 'water'], - Attribute.Wood: ['green', 'wood'], - Attribute.Light: ['light'], - Attribute.Dark: ['dark'], - Attribute.Unknown: ['unknown'], - Attribute.Nil: ['null', 'none'], - } - - self.series_to_prefix_map = { - 130: ['halloween', 'hw', 'h'], - 136: ['xmas', 'christmas'], - 125: ['summer', 'beach'], - 114: ['school', 'academy', 'gakuen'], - 139: ['new years', 'ny'], - 149: ['wedding', 'bride'], - 154: ['padr'], - 175: ['valentines', 'vday', 'v'], - 183: ['gh', 'gungho'], - 117: ['gh', 'gungho'], - } - - monster_id_to_nicknames = defaultdict(set) - for monster_id, nicknames in nickname_overrides.items(): - monster_id_to_nicknames[monster_id] = nicknames - - named_monsters = [] - async for base_mon in AsyncIter(base_monster_ids): - base_id = base_mon.monster_id - group_basename_overrides = basename_overrides.get(base_id, []) - evolution_tree = [monster_database.get_monster(m) for m in - monster_database.get_evolution_tree_ids(base_id)] - named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) - for monster in evolution_tree: - if accept_filter and not accept_filter(monster): - continue - prefixes = self.compute_prefixes(monster, evolution_tree) - extra_nicknames = monster_id_to_nicknames[monster.monster_id] - named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames) - named_monsters.append(named_monster) - - # Sort the NamedMonsters into the opposite order we want to accept their nicknames in - # This order is: - # 1) High priority first - # 2) Larger group sizes - # 3) Minimum ID size in the group - # 4) Monsters with higher ID values - def named_monsters_sort(nm: NamedMonster): - return (not nm.is_low_priority, nm.group_size, -1 * - nm.base_monster_no_na, nm.monster_no_na) - - named_monsters.sort(key=named_monsters_sort) - - # set up a set of all pantheon names, a set of all pantheon nicknames, and a dictionary of nickname -> full name - # later we will set up a dictionary of pantheon full name -> monsters - self.all_pantheon_names = set() - self.all_pantheon_names.update(panthname_overrides.values()) - - self.pantheon_nick_to_name = panthname_overrides - self.pantheon_nick_to_name.update(panthname_overrides) - - self.all_pantheon_nicknames = set() - self.all_pantheon_nicknames.update(panthname_overrides.keys()) - - self.all_prefixes = set() - self.pantheons = defaultdict(set) - self.all_entries = {} - self.two_word_entries = {} - for nm in named_monsters: - self.all_prefixes.update(nm.prefixes) - for nickname in nm.final_nicknames: - self.all_entries[nickname] = nm - for nickname in nm.final_two_word_nicknames: - self.two_word_entries[nickname] = nm - if nm.series: - for pantheon in self.all_pantheon_names: - if pantheon.lower() == nm.series.lower(): - self.pantheons[pantheon.lower()].add(nm) - - self.all_monsters = named_monsters - self.all_en_name_to_monsters = {m.name_en.lower(): m for m in named_monsters} - self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} - self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} - - for monster_id, nicknames in nickname_overrides.items(): - nm = self.monster_id_to_named_monster.get(monster_id) - if nm: - for nickname in nicknames: - self.all_entries[nickname] = nm - - def init_index(self): - pass - - def compute_prefixes(self, m: DgMonster, evotree: list): - prefixes = set() - - attr1_short_prefixes = self.attr_short_prefix_map[m.attr1] - attr1_long_prefixes = self.attr_long_prefix_map[m.attr1] - prefixes.update(attr1_short_prefixes) - prefixes.update(attr1_long_prefixes) - - # If no 2nd attribute, use x so we can look those monsters up easier - attr2_short_prefixes = self.attr_short_prefix_map.get(m.attr2, ['x']) - for a1 in attr1_short_prefixes: - for a2 in attr2_short_prefixes: - prefixes.add(a1 + a2) - prefixes.add(a1 + '/' + a2) - - # TODO: add prefixes based on type - - # Chibi monsters have the same NA name, except lowercased - lower_name = m.name_en.lower() - if m.name_en != m.name_ja: - if lower_name == m.name_en: - prefixes.add('chibi') - elif 'ミニ' in m.name_ja: - # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) - prefixes.add('chibi') - - awoken = lower_name.startswith('awoken') or '覚醒' in lower_name - revo = lower_name.startswith('reincarnated') or '転生' in lower_name - srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name - mega = lower_name.startswith('mega awoken') or '極醒' in lower_name - awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega - - # These clauses need to be separate to handle things like 'Awoken Thoth' which are - # actually Evos but have awoken in the name - if awoken: - prefixes.add('a') - prefixes.add('awoken') - - if revo: - prefixes.add('revo') - prefixes.add('reincarnated') - - if mega: - prefixes.add('mega') - prefixes.add('mega awoken') - prefixes.add('awoken') - prefixes.add('ma') - - if srevo: - prefixes.add('srevo') - prefixes.add('super reincarnated') - - # Prefixes for evo type - if m.cur_evo_type == EvoType.Base: - prefixes.add('base') - elif m.cur_evo_type == EvoType.Evo: - prefixes.add('evo') - elif m.cur_evo_type == EvoType.UvoAwoken and not awoken_or_revo_or_equip_or_mega: - prefixes.add('uvo') - prefixes.add('uevo') - elif m.cur_evo_type == EvoType.UuvoReincarnated and not awoken_or_revo_or_equip_or_mega: - prefixes.add('uuvo') - prefixes.add('uuevo') - - # True Evo Type Prefixes - if m.true_evo_type == InternalEvoType.Reincarnated: - prefixes.add('revo') - prefixes.add('reincarnated') - - # Other Prefixes - if m.farmable: - prefixes.add('farmable') - - # If any monster in the group is a pixel, add 'nonpixel' to all the versions - # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. - def is_pixel(n): - n = n.name_en.lower() - return n.startswith('pixel') or n.startswith('ドット') - - for gm in evotree: - if is_pixel(gm): - prefixes.update(['pixel'] if is_pixel(m) else ['np', 'nonpixel']) - break - - if m.is_equip: - prefixes.add('assist') - prefixes.add('equip') - - # Collab prefixes - prefixes.update(self.series_to_prefix_map.get(m.series.series_id, [])) - - return prefixes - - def find_monster(self, query): - query = tsutils.rmdiacritics(query).lower().strip() - - # id search - if query.isdigit(): - m = self.monster_no_na_to_named_monster.get(int(query)) - if m is None: - return None, 'Looks like a monster ID but was not found', None - else: - return m, None, "ID lookup" - # special handling for na/jp - - # TODO: need to handle na_only? - - # handle exact nickname match - if query in self.all_entries: - return self.all_entries[query], None, "Exact nickname" - - contains_ja = tsutils.containsJa(query) - if len(query) < 2 and contains_ja: - return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_ja: - return None, 'Your query must be at least 4 letters', None - - # TODO: this should be a length-limited priority queue - matches = set() - - # prefix search for ids, take max id - for nickname, m in self.all_entries.items(): - if query.endswith("base {}".format(m.monster_id)): - matches.add( - find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) - if len(matches): - return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() - - # prefix search for nicknames, space-preceeded, take max id - for nickname, m in self.all_entries.items(): - if nickname.startswith(query + ' '): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) - - # prefix search for nicknames, take max id - for nickname, m in self.all_entries.items(): - if nickname.startswith(query): - matches.add(m) - if len(matches): - all_names = ",".join(map(lambda x: x.name_en, matches)) - return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( - len(matches), all_names) - - # prefix search for full name, take max id - for nickname, m in self.all_entries.items(): - if (m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query)): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) - - # for nicknames with 2 names, prefix search 2nd word, take max id - if query in self.two_word_entries: - return self.two_word_entries[query], None, "Second-word nickname prefix, max of {}".format(len(matches)) - - # TODO: refactor 2nd search characteristcs for 2nd word - - # full name contains on nickname, take max id - for nickname, m in self.all_entries.items(): - if (query in m.name_en.lower() or query in m.name_ja.lower()): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( - len(matches)) - - # No decent matches. Try near hits on nickname instead - matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) - if len(matches): - match = matches[0] - return self.all_entries[match], None, 'Close nickname match ({})'.format(match) - - # Still no decent matches. Try near hits on full name instead - matches = difflib.get_close_matches( - query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) - if len(matches): - match = matches[0] - return self.all_en_name_to_monsters[match], None, 'Close name match ({})'.format(match) - - # About to give up, try matching all words - matches = set() - for nickname, m in self.all_entries.items(): - if (all(map(lambda x: x in m.name_en.lower(), query.split())) or - all(map(lambda x: x in m.name_ja.lower(), query.split()))): - matches.add(m) - if len(matches): - return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( - len(matches)) - - # couldn't find anything - return None, "Could not find a match for: " + query, None - - def find_monster2(self, query): - """Search with alternative method for resolving prefixes. - - Implements the lookup for id2, where you are allowed to specify multiple prefixes for a card. - All prefixes are required to be exactly matched by the card. - Follows a similar logic to the regular id but after each check, will remove any potential match that doesn't - contain every single specified prefix. - """ - query = tsutils.rmdiacritics(query).lower().strip() - # id search - if query.isdigit(): - m = self.monster_no_na_to_named_monster.get(int(query)) - if m is None: - return None, 'Looks like a monster ID but was not found', None - else: - return m, None, "ID lookup" - - # handle exact nickname match - if query in self.all_entries: - return self.all_entries[query], None, "Exact nickname" - - contains_ja = tsutils.containsJa(query) - if len(query) < 2 and contains_ja: - return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_ja: - return None, 'Your query must be at least 4 letters', None - - # we want to look up only the main part of the query, and then verify that each result has the prefixes - # so break up the query into an array of prefixes, and a string (new_query) that will be the lookup - query_prefixes = [] - parts_of_query = query.split() - new_query = '' - for i, part in enumerate(parts_of_query): - if part in self.all_prefixes: - query_prefixes.append(part) - else: - new_query = ' '.join(parts_of_query[i:]) - break - - # if we don't have any prefixes, then default to using the regular id lookup - if len(query_prefixes) < 1: - return self.find_monster(query) - - matches = PotentialMatches() - - # prefix search for ids, take max id - for nickname, m in self.all_entries.items(): - if query.endswith("base {}".format(m.monster_id)): - matches.add( - find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # first try to get matches from nicknames - for nickname, m in self.all_entries.items(): - if new_query in nickname: - matches.add(m) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # if we don't have any candidates yet, pick a new method - if not matches.length(): - # try matching on exact names next - for nickname, m in self.all_en_name_to_monsters.items(): - if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): - matches.add(m) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # check for exact match on pantheon name but only if needed - if not matches.length(): - for pantheon in self.all_pantheon_nicknames: - if new_query == pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, - self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - # check for any match on pantheon name, again but only if needed - if not matches.length(): - for pantheon in self.all_pantheon_nicknames: - if new_query in pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, - self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) - - if matches.length(): - return matches.pick_best_monster(), None, None - return None, "Could not find a match for: " + query, None - - def pickBestMonster(self, named_monster_list): - return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) - - class PotentialMatches(object): def __init__(self): self.match_list = set() @@ -1437,8 +756,8 @@ def pick_best_monster(self): class NamedMonsterGroup(object): def __init__(self, evolution_tree: list, basename_overrides: list): self.is_low_priority = ( - self._is_low_priority_monster(evolution_tree[0]) - or self._is_low_priority_group(evolution_tree)) + self._is_low_priority_monster(evolution_tree[0]) + or self._is_low_priority_group(evolution_tree)) base_monster = evolution_tree[0] self.group_size = len(evolution_tree) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py new file mode 100644 index 00000000..089440b0 --- /dev/null +++ b/dadguide/monster_graph.py @@ -0,0 +1,110 @@ +import networkx as nx +from collections import defaultdict +from .database_manager import DadguideDatabase +from .database_manager import DgActiveSkill +from .database_manager import DgLeaderSkill +from .database_manager import DgAwakening +from .database_manager import DictWithAttrAccess +from .database_manager import DgSeries + + +class MonsterGraph(object): + def __init__(self, database: DadguideDatabase): + self.database = database + self.db_context = None + self.graph = None + self.graph: nx.DiGraph + self.edges = None + self.nodes = None + + def set_database(self, db_context): + self.db_context = db_context + + def build_graph(self): + self.graph = nx.DiGraph() + + ms = self.database.query_many("SELECT * FROM monsters", (), DictWithAttrAccess, + db_context=self.db_context, graph=self) + es = self.database.query_many("SELECT * FROM evolutions", (), DictWithAttrAccess, + db_context=self.db_context, graph=self) + aws = self.database.query_many("SELECT * FROM awakenings", (), DgAwakening, + db_context=self.db_context, graph=self) + lss = self.database.query_many("SELECT * FROM leader_skills", (), DgLeaderSkill, idx_key='leader_skill_id', + db_context=self.db_context, graph=self) + ass = self.database.query_many("SELECT * FROM active_skills", (), DgActiveSkill, idx_key='active_skill_id', + db_context=self.db_context, graph=self) + ss = self.database.query_many("SELECT * FROM series", (), DgSeries, idx_key='series_id', + db_context=self.db_context, graph=self) + + mtoawo = defaultdict(list) + for a in aws: + mtoawo[a.monster_id].append(a) + + for m in ms: + self.graph.add_node(m.monster_id, + awakenings=mtoawo[m.monster_id], + leader_skill=lss.get(m.leader_skill_id), + active_skill=ass.get(m.active_skill_id), + series=ss.get(m.series_id)) + if m.linked_monster_id: + self.graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') + self.graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') + + for e in es: + self.graph.add_edge(e.from_id, e.to_id, type='evolution', **e) + self.graph.add_edge(e.to_id, e.from_id, type='back_evolution', **e) + + self.edges = self.graph.edges + self.nodes = self.graph.nodes + + @staticmethod + def _get_edges(node, etype): + return {mid for mid, edge in node.items() if edge.get('type') == etype} + + def get_evo_tree(self, monster_id): + ids = set() + to_check = {monster_id} + while to_check: + mid = to_check.pop() + if mid in ids: + continue + n = self.graph[mid] + to_check.update(self._get_edges(n, 'evolution')) + to_check.update(self._get_edges(n, 'back_evolution')) + ids.add(mid) + return ids + + def get_transform_tree(self, monster_id): + ids = set() + to_check = {monster_id} + while to_check: + mid = to_check.pop() + if mid in ids: + continue + n = self.graph[mid] + to_check.update(self._get_edges(n, 'transformation')) + to_check.update(self._get_edges(n, 'back_transformation')) + ids.add(mid) + return ids + + def get_alt_cards(self, monster_id): + ids = set() + to_check = {monster_id} + while to_check: + mid = to_check.pop() + if mid in ids: + continue + n = self.graph[mid] + to_check.update(self._get_edges(n, 'evolution')) + to_check.update(self._get_edges(n, 'transformation')) + to_check.update(self._get_edges(n, 'back_evolution')) + to_check.update(self._get_edges(n, 'back_transformation')) + ids.add(mid) + return ids + + def get_prev_evolution_by_monster(self, monster_id): + bes = self._get_edges(self.graph[monster_id], 'back_evolution') + if bes: return bes.pop() + + def get_next_evolutions_by_monster(self, monster_id): + return self._get_edges(self.graph[monster_id], 'evolution') diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py new file mode 100644 index 00000000..7fc37c69 --- /dev/null +++ b/dadguide/old_monster_index.py @@ -0,0 +1,406 @@ +import difflib + +from collections import defaultdict + +from redbot.core.utils import AsyncIter +from discord.utils import find as find_first +import tsutils + +from .database_manager import DgMonster +from .database_manager import Attribute +from .database_manager import EvoType +from .database_manager import InternalEvoType +from .database_manager import PotentialMatches +from .database_manager import NamedMonster +from .database_manager import NamedMonsterGroup +from .database_context import DbContext + + +class MonsterIndex(tsutils.aobject): + async def __init__(self, monster_database: DbContext, nickname_overrides, basename_overrides, + panthname_overrides, accept_filter=None): + # Important not to hold onto anything except IDs here so we don't leak memory + base_monster_ids = monster_database.get_base_monster_ids() + + self.attr_short_prefix_map = { + Attribute.Fire: ['r'], + Attribute.Water: ['b'], + Attribute.Wood: ['g'], + Attribute.Light: ['l'], + Attribute.Dark: ['d'], + Attribute.Unknown: ['h'], + Attribute.Nil: ['x'], + } + self.attr_long_prefix_map = { + Attribute.Fire: ['red', 'fire'], + Attribute.Water: ['blue', 'water'], + Attribute.Wood: ['green', 'wood'], + Attribute.Light: ['light'], + Attribute.Dark: ['dark'], + Attribute.Unknown: ['unknown'], + Attribute.Nil: ['null', 'none'], + } + + self.series_to_prefix_map = { + 130: ['halloween', 'hw', 'h'], + 136: ['xmas', 'christmas'], + 125: ['summer', 'beach'], + 114: ['school', 'academy', 'gakuen'], + 139: ['new years', 'ny'], + 149: ['wedding', 'bride'], + 154: ['padr'], + 175: ['valentines', 'vday', 'v'], + 183: ['gh', 'gungho'], + 117: ['gh', 'gungho'], + } + + monster_id_to_nicknames = defaultdict(set) + for monster_id, nicknames in nickname_overrides.items(): + monster_id_to_nicknames[monster_id] = nicknames + + named_monsters = [] + async for base_mon in AsyncIter(base_monster_ids): + base_id = base_mon.monster_id + group_basename_overrides = basename_overrides.get(base_id, []) + evolution_tree = [monster_database.get_monster(m) for m in + monster_database.get_evolution_tree_ids(base_id)] + named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) + for monster in evolution_tree: + if accept_filter and not accept_filter(monster): + continue + prefixes = self.compute_prefixes(monster, evolution_tree) + extra_nicknames = monster_id_to_nicknames[monster.monster_id] + named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames) + named_monsters.append(named_monster) + + # Sort the NamedMonsters into the opposite order we want to accept their nicknames in + # This order is: + # 1) High priority first + # 2) Larger group sizes + # 3) Minimum ID size in the group + # 4) Monsters with higher ID values + def named_monsters_sort(nm: NamedMonster): + return (not nm.is_low_priority, nm.group_size, -1 * + nm.base_monster_no_na, nm.monster_no_na) + + named_monsters.sort(key=named_monsters_sort) + + # set up a set of all pantheon names, a set of all pantheon nicknames, and a dictionary of nickname -> full name + # later we will set up a dictionary of pantheon full name -> monsters + self.all_pantheon_names = set() + self.all_pantheon_names.update(panthname_overrides.values()) + + self.pantheon_nick_to_name = panthname_overrides + self.pantheon_nick_to_name.update(panthname_overrides) + + self.all_pantheon_nicknames = set() + self.all_pantheon_nicknames.update(panthname_overrides.keys()) + + self.all_prefixes = set() + self.pantheons = defaultdict(set) + self.all_entries = {} + self.two_word_entries = {} + for nm in named_monsters: + self.all_prefixes.update(nm.prefixes) + for nickname in nm.final_nicknames: + self.all_entries[nickname] = nm + for nickname in nm.final_two_word_nicknames: + self.two_word_entries[nickname] = nm + if nm.series: + for pantheon in self.all_pantheon_names: + if pantheon.lower() == nm.series.lower(): + self.pantheons[pantheon.lower()].add(nm) + + self.all_monsters = named_monsters + self.all_en_name_to_monsters = {m.name_en.lower(): m for m in named_monsters} + self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} + self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} + + for monster_id, nicknames in nickname_overrides.items(): + nm = self.monster_id_to_named_monster.get(monster_id) + if nm: + for nickname in nicknames: + self.all_entries[nickname] = nm + + def init_index(self): + pass + + def compute_prefixes(self, m: DgMonster, evotree: list): + prefixes = set() + + attr1_short_prefixes = self.attr_short_prefix_map[m.attr1] + attr1_long_prefixes = self.attr_long_prefix_map[m.attr1] + prefixes.update(attr1_short_prefixes) + prefixes.update(attr1_long_prefixes) + + # If no 2nd attribute, use x so we can look those monsters up easier + attr2_short_prefixes = self.attr_short_prefix_map.get(m.attr2, ['x']) + for a1 in attr1_short_prefixes: + for a2 in attr2_short_prefixes: + prefixes.add(a1 + a2) + prefixes.add(a1 + '/' + a2) + + # TODO: add prefixes based on type + + # Chibi monsters have the same NA name, except lowercased + lower_name = m.name_en.lower() + if m.name_en != m.name_ja: + if lower_name == m.name_en: + prefixes.add('chibi') + elif 'ミニ' in m.name_ja: + # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) + prefixes.add('chibi') + + awoken = lower_name.startswith('awoken') or '覚醒' in lower_name + revo = lower_name.startswith('reincarnated') or '転生' in lower_name + srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name + mega = lower_name.startswith('mega awoken') or '極醒' in lower_name + awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega + + # These clauses need to be separate to handle things like 'Awoken Thoth' which are + # actually Evos but have awoken in the name + if awoken: + prefixes.add('a') + prefixes.add('awoken') + + if revo: + prefixes.add('revo') + prefixes.add('reincarnated') + + if mega: + prefixes.add('mega') + prefixes.add('mega awoken') + prefixes.add('awoken') + prefixes.add('ma') + + if srevo: + prefixes.add('srevo') + prefixes.add('super reincarnated') + + # Prefixes for evo type + if m.cur_evo_type == EvoType.Base: + prefixes.add('base') + elif m.cur_evo_type == EvoType.Evo: + prefixes.add('evo') + elif m.cur_evo_type == EvoType.UvoAwoken and not awoken_or_revo_or_equip_or_mega: + prefixes.add('uvo') + prefixes.add('uevo') + elif m.cur_evo_type == EvoType.UuvoReincarnated and not awoken_or_revo_or_equip_or_mega: + prefixes.add('uuvo') + prefixes.add('uuevo') + + # True Evo Type Prefixes + if m.true_evo_type == InternalEvoType.Reincarnated: + prefixes.add('revo') + prefixes.add('reincarnated') + + # Other Prefixes + if m.farmable: + prefixes.add('farmable') + + # If any monster in the group is a pixel, add 'nonpixel' to all the versions + # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. + def is_pixel(n): + n = n.name_en.lower() + return n.startswith('pixel') or n.startswith('ドット') + + for gm in evotree: + if is_pixel(gm): + prefixes.update(['pixel'] if is_pixel(m) else ['np', 'nonpixel']) + break + + if m.is_equip: + prefixes.add('assist') + prefixes.add('equip') + + # Collab prefixes + prefixes.update(self.series_to_prefix_map.get(m.series.series_id, [])) + + return prefixes + + def find_monster(self, query): + query = tsutils.rmdiacritics(query).lower().strip() + + # id search + if query.isdigit(): + m = self.monster_no_na_to_named_monster.get(int(query)) + if m is None: + return None, 'Looks like a monster ID but was not found', None + else: + return m, None, "ID lookup" + # special handling for na/jp + + # TODO: need to handle na_only? + + # handle exact nickname match + if query in self.all_entries: + return self.all_entries[query], None, "Exact nickname" + + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: + return None, 'Japanese queries must be at least 2 characters', None + elif len(query) < 4 and not contains_ja: + return None, 'Your query must be at least 4 letters', None + + # TODO: this should be a length-limited priority queue + matches = set() + + # prefix search for ids, take max id + for nickname, m in self.all_entries.items(): + if query.endswith("base {}".format(m.monster_id)): + matches.add( + find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) + if len(matches): + return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() + + # prefix search for nicknames, space-preceeded, take max id + for nickname, m in self.all_entries.items(): + if nickname.startswith(query + ' '): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) + + # prefix search for nicknames, take max id + for nickname, m in self.all_entries.items(): + if nickname.startswith(query): + matches.add(m) + if len(matches): + all_names = ",".join(map(lambda x: x.name_en, matches)) + return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( + len(matches), all_names) + + # prefix search for full name, take max id + for nickname, m in self.all_entries.items(): + if (m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query)): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) + + # for nicknames with 2 names, prefix search 2nd word, take max id + if query in self.two_word_entries: + return self.two_word_entries[query], None, "Second-word nickname prefix, max of {}".format(len(matches)) + + # TODO: refactor 2nd search characteristcs for 2nd word + + # full name contains on nickname, take max id + for nickname, m in self.all_entries.items(): + if (query in m.name_en.lower() or query in m.name_ja.lower()): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( + len(matches)) + + # No decent matches. Try near hits on nickname instead + matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) + if len(matches): + match = matches[0] + return self.all_entries[match], None, 'Close nickname match ({})'.format(match) + + # Still no decent matches. Try near hits on full name instead + matches = difflib.get_close_matches( + query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) + if len(matches): + match = matches[0] + return self.all_en_name_to_monsters[match], None, 'Close name match ({})'.format(match) + + # About to give up, try matching all words + matches = set() + for nickname, m in self.all_entries.items(): + if (all(map(lambda x: x in m.name_en.lower(), query.split())) or + all(map(lambda x: x in m.name_ja.lower(), query.split()))): + matches.add(m) + if len(matches): + return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( + len(matches)) + + # couldn't find anything + return None, "Could not find a match for: " + query, None + + def find_monster2(self, query): + """Search with alternative method for resolving prefixes. + + Implements the lookup for id2, where you are allowed to specify multiple prefixes for a card. + All prefixes are required to be exactly matched by the card. + Follows a similar logic to the regular id but after each check, will remove any potential match that doesn't + contain every single specified prefix. + """ + query = tsutils.rmdiacritics(query).lower().strip() + # id search + if query.isdigit(): + m = self.monster_no_na_to_named_monster.get(int(query)) + if m is None: + return None, 'Looks like a monster ID but was not found', None + else: + return m, None, "ID lookup" + + # handle exact nickname match + if query in self.all_entries: + return self.all_entries[query], None, "Exact nickname" + + contains_ja = tsutils.containsJa(query) + if len(query) < 2 and contains_ja: + return None, 'Japanese queries must be at least 2 characters', None + elif len(query) < 4 and not contains_ja: + return None, 'Your query must be at least 4 letters', None + + # we want to look up only the main part of the query, and then verify that each result has the prefixes + # so break up the query into an array of prefixes, and a string (new_query) that will be the lookup + query_prefixes = [] + parts_of_query = query.split() + new_query = '' + for i, part in enumerate(parts_of_query): + if part in self.all_prefixes: + query_prefixes.append(part) + else: + new_query = ' '.join(parts_of_query[i:]) + break + + # if we don't have any prefixes, then default to using the regular id lookup + if len(query_prefixes) < 1: + return self.find_monster(query) + + matches = PotentialMatches() + + # prefix search for ids, take max id + for nickname, m in self.all_entries.items(): + if query.endswith("base {}".format(m.monster_id)): + matches.add( + find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # first try to get matches from nicknames + for nickname, m in self.all_entries.items(): + if new_query in nickname: + matches.add(m) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # if we don't have any candidates yet, pick a new method + if not matches.length(): + # try matching on exact names next + for nickname, m in self.all_en_name_to_monsters.items(): + if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): + matches.add(m) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # check for exact match on pantheon name but only if needed + if not matches.length(): + for pantheon in self.all_pantheon_nicknames: + if new_query == pantheon.lower(): + matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, + self.pantheons) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + # check for any match on pantheon name, again but only if needed + if not matches.length(): + for pantheon in self.all_pantheon_nicknames: + if new_query in pantheon.lower(): + matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, + self.pantheons) + matches.remove_potential_matches_without_all_prefixes(query_prefixes) + + if matches.length(): + return matches.pick_best_monster(), None, None + return None, "Could not find a match for: " + query, None + + def pickBestMonster(self, named_monster_list): + return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py new file mode 100644 index 00000000..a0fca52c --- /dev/null +++ b/tests/dadguide/database_context_test.py @@ -0,0 +1,35 @@ +from dadguide.database_manager import DadguideDatabase +from dadguide.database_context import DbContext +from dadguide.monster_graph import MonsterGraph + +database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') +graph = MonsterGraph(database) +db_context = DbContext(database, graph) +graph.set_database(db_context) +graph.build_graph() +db_context.generate_all_monsters() + +assert db_context.get_active_skill_query(15)['name_en'] == 'Inferno Breath' +assert db_context.get_leader_skill_query(1202)['name_en'] == 'Fusion Soul' +assert db_context.get_awoken_skill(15)['name_en'] == 'Enhanced Water Orbs' +# print(ctx.get_awoken_skill_ids()) +assert db_context.get_monsters_by_awakenings(5)[0].name_en == 'Crystal Aurora Dragon' +assert db_context.get_drop_dungeons(4)[0]['name_en'] == 'Diagoldos Descended!' +assert db_context.monster_is_farmable(4) +assert not db_context.monster_is_farmable(5156) +print(db_context.monster_in_rem(1073)) # base pandora +print(db_context.monster_in_rem(2121)) +print(db_context.monster_in_pem(2120)) # arthur +assert db_context.monster_in_mp_shop(2256) # odindra +assert not db_context.monster_in_mp_shop(5156) # yusuke +assert db_context.get_prev_evolution_by_monster(1074) == 1073 # evo pandora +assert len(db_context.get_next_evolutions_by_monster(1074)) == 2 # evo pandora +assert db_context.get_base_monster_by_id(1074) == 1073 # evo pandora +assert len(db_context.get_evolution_by_material(5963)) == 3 # raziel gem +assert 6352 in db_context.get_evolution_tree_ids(1074) +assert db_context.get_monsters_by_series(1)[0].name_en == 'Tyrra' +assert db_context.get_monsters_by_active(1)[0].name_en == 'Tyrra' +assert db_context.get_monster_evo_gem( + 'Archangel of Knowledge, Raziel', region='en').name_en == 'Archangel of Knowledge, Raziel\'s Gem' + +assert db_context.get_monsters_by_awakenings(5)[0].evo_from_id == 26 # check the graph exists in DgMonster From 2362888333b163706e9e4379a9c5828363adc017 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 27 Oct 2020 11:37:40 -0700 Subject: [PATCH 0240/1220] monster_index --- dadguide/monster_index.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 dadguide/monster_index.py diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py new file mode 100644 index 00000000..084ec58e --- /dev/null +++ b/dadguide/monster_index.py @@ -0,0 +1,33 @@ +from token_mappings import * +from collections import defaultdict + + +class MonsterIndex2: + def __init__(self, monsters: 'List[DgMonster]', series: 'List[DgSeries]'): + self.monsters = monsters + self.index = {} + self._build_index() + + def _build_index(self): + self.index = {} + self.monster_name_index = self._build_monster_name_index(self.monsters) + self.series_name_index = self._build_series_name_index(self.series) + + def _build_monster_name_index(self, monsters): + tokens = defaultdict(list) + for monster in monsters: + for token in self._name_to_tokens(monster.name_en): + tokens[token.lower()].append(monster) + return tokens + + def _build_series_name_index(self, series): + tokens = defaultdict(list) + for s in series: + for token in self._name_to_tokens(s.name_en): + tokens[token.lower()].append(s) + return tokens + + def _name_to_tokens(self, name): + name = re.sub(r'[\-+]', ' ', name.lower()) + name = re.sub(r'[^a-z0-9 ]', '', name) + return [t.strip() for t in name.split() if t] From e111ed11caaa51d6db2787dda1288e120c5a631d Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 27 Oct 2020 12:24:26 -0700 Subject: [PATCH 0241/1220] padinfo fix --- dadguide/monster_index.py | 5 +++-- padinfo/padinfo.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 084ec58e..dc98f47b 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -5,11 +5,12 @@ class MonsterIndex2: def __init__(self, monsters: 'List[DgMonster]', series: 'List[DgSeries]'): self.monsters = monsters - self.index = {} + self.series = series + self.index = defaultdict(list) self._build_index() def _build_index(self): - self.index = {} + self.index = defaultdict(list) self.monster_name_index = self._build_monster_name_index(self.monsters) self.series_name_index = self._build_series_name_index(self.series) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index eca6faf4..ef43b40b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -337,8 +337,8 @@ async def sometimesperc(self, ctx, percent: int): async def checkbadness(self, ctx): """Check how good id is according to end users""" good = await self.config.good() - bad = await self.config.good() - await ctx.send(f"{bad}/{good+bad} ({bad/(good+bad) if good or bad else 'NaN'}%)") + bad = await self.config.bad() + await ctx.send(f"{bad}/{good+bad} ({int(round(bad/(good+bad)*100)) if good or bad else 'NaN'}%)") @commands.command() @checks.bot_has_permissions(embed_links=True) From 8941a7275910fd4e62c8e7025fb577f6c04639cd Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 28 Oct 2020 01:35:15 -0700 Subject: [PATCH 0242/1220] Precomputing max monster id in database manager init (#173) * DB Fix * max_id * its a function --- dadguide/database_context.py | 2 ++ dadguide/database_manager.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index dbb9de05..89b61f60 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -24,6 +24,8 @@ def __init__(self, database: DadguideDatabase, graph: MonsterGraph): self.graph.set_database(self) self.cachedmonsters = None self.expiry = 0 + + self.max_id = database._max_id() # self.generate_all_monsters() def generate_all_monsters(self): diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 0dd84e16..e2e2e129 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -334,7 +334,7 @@ class DgScheduledEvent(DadguideItem): TABLE = 'schedule' PK = 'event_id' - def __init__(self, item, database): + def __init__(self, item, database, **graph): super(DgScheduledEvent, self).__init__(item, database) self.dungeon = self._database.get_dungeon_by_id(self.dungeon_id) @@ -584,7 +584,7 @@ def history_us(self): def next_monster(self): next = None offset = 1 - while next is None and self.monster_no + offset <= self._database._max_id(): + while next is None and self.monster_no + offset <= self._database.max_id: next = self._database.get_monster(self.monster_no + offset) offset += 1 return next From e9180a4b861780281091e5c4ba33636bd9b74183 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 29 Oct 2020 02:55:04 -0700 Subject: [PATCH 0243/1220] Fix Awakenings and Reincarnated (#180) Fix order of awakenings & existence of reincarnated monsters --- dadguide/database_manager.py | 10 +++++----- padinfo/find_monster.py | 6 +++--- padinfo/padinfo.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index e2e2e129..3d57727d 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -55,9 +55,9 @@ class EvoType(Enum): class InternalEvoType(Enum): """Evo types unsupported by DadGuide.""" - Base = "" - Normal = "" - Ultimate = "" + Base = "Base" + Normal = "Normal" + Ultimate = "Ultimate" Reincarnated = "Reincarnated" Assist = "Assist" Pixel = "Pixel" @@ -385,7 +385,7 @@ def __init__(self, item, database, graph): self.in_pem = bool(self.pal_egg) self.in_rem = bool(self.rem_egg) - self.awakenings = self.node['awakenings'] + self.awakenings = sorted(self.node['awakenings'], key=lambda a: a.order_idx) self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) self.is_inheritable = bool(self.inheritable) @@ -452,7 +452,7 @@ def leader_skill(self): @property def cur_evo_type(self): - return self.evo_from['evolution_type'] if self.evo_from else EvoType.Base + return EvoType(self.evo_from['evolution_type']) if self.evo_from else EvoType.Base @property def true_evo_type(self): diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 6192e0c2..2816d275 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -5,9 +5,9 @@ for r in ('r','red','fire'): COLOR_MAP[r] = 'r' for b in ('b','blue','water'): COLOR_MAP[b] = 'b' for g in ('g','green','wood'): COLOR_MAP[g] = 'g' -for l in ('l','light','yellow', 'white'): COLOR_MAP[l] = 'l' -for d in ('d','dark','purple', 'black'): COLOR_MAP[d] = 'd' -for x in ('x','none','null', 'nil'): COLOR_MAP[x] = 'x' +for l in ('l','light','yellow','white'): COLOR_MAP[l] = 'l' +for d in ('d','dark','purple','black'): COLOR_MAP[d] = 'd' +for x in ('x','none','null','nil'): COLOR_MAP[x] = 'x' COLORS = 'rbgld?x' diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ef43b40b..87b0fa63 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -37,7 +37,7 @@ {0.prefix}id rd ares (select a specific evo for ares, the red/dark one) {0.prefix}id r/d ares (slashes, spaces work too) -computed nickname list and overrides: https://docs.google.com/spreadsheets/d/1EyzMjvf8ZCQ4K-gJYnNkiZlCEsT9YYI9dUd-T5qCirc/pubhtml +computed nickname list and overrides: https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/edit submit an override suggestion: https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" EMBED_NOT_GENERATED = -1 @@ -1222,7 +1222,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list): info_row_2 += '\n**Inheritable**' else: info_row_2 += '\n**Not inheritable**' - if tet_text: + if tet_text in ("Reincarnated", "Assist", "Pixel", "Super Reincarnated"): info_row_2 += '\n**{}**'.format(tet_text) embed.add_field(name=info_row_1, value=info_row_2) From c198f1eeea27a766e7c2014452b93683fbc67424 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 29 Oct 2020 03:20:26 -0700 Subject: [PATCH 0244/1220] Version Update (#182) * Version Update * I messed up dadguide's info.json --- azurelane/info.json | 2 +- channelmod/info.json | 2 +- chronomagia/info.json | 2 +- crud/info.json | 2 +- dadguide/info.json | 2 +- damagecalc/info.json | 2 +- feedback/info.json | 2 +- padboard/info.json | 2 +- padbuildimg/info.json | 2 +- padbuilds/info.json | 2 +- padevents/info.json | 2 +- padglobal/info.json | 2 +- padguidedb/info.json | 2 +- padinfo/info.json | 2 +- padmonitor/info.json | 2 +- padsearch/info.json | 2 +- padvision/info.json | 2 +- pricecheck/info.json | 2 +- profile/info.json | 2 +- schoolidol/info.json | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/azurelane/info.json b/azurelane/info.json index 7700c4c0..e97b1ee8 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]alid to get started. NOTE: This cog is no longer supported.", "short": "Shows Azur Lane data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/channelmod/info.json b/channelmod/info.json index d7eb3687..475bf9a4 100644 --- a/channelmod/info.json +++ b/channelmod/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]channelmod to get started", "short": "Mirror channels across servers.", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/chronomagia/info.json b/chronomagia/info.json index a7a23454..ba02c075 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]cmid to get started.", "short": "Shows Chrono Ma:Gia data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/crud/info.json b/crud/info.json index 86771d7f..c2ceef62 100644 --- a/crud/info.json +++ b/crud/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]crud to get started.", "short": "Pipeline CRUD", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/dadguide/info.json b/dadguide/info.json index 592cfab5..94a5452e 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]dadguide setdatafile if it's applicable to you.", "short": "Loads monster data (LOAD THIS FIRST)", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/damagecalc/info.json b/damagecalc/info.json index b2ba2304..f123f5ad 100644 --- a/damagecalc/info.json +++ b/damagecalc/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]damage to get started.", "short": "Calculates Puzzle & Dragons data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/feedback/info.json b/feedback/info.json index 3b0f7e7d..fc0f8044 100644 --- a/feedback/info.json +++ b/feedback/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]setfeedbackchannel and [p]setblogfeedbackchannel to set the feedback channels.", "short": "Feedback and credits", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padboard/info.json b/padboard/info.json index 577d542d..32c246b7 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]set_tflite_path to set up your tflite path. (You need to get this from The Tsubaki Bot Team)", "short": "Dawnglare utils", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padbuildimg/info.json b/padbuildimg/info.json index c2bd9a49..780d9b79 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -8,7 +8,7 @@ "install_msg": "Use [p]helpbuildimg for help!", "short": "PDChu cog", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padbuilds/info.json b/padbuilds/info.json index f3939156..a5d59afb 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]builds to get started.", "short": "Builds to help you through dungeons", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padevents/info.json b/padevents/info.json index c65bfe70..be9b88d0 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]padevents to get started.", "short": "PAD Event tracker", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padglobal/info.json b/padglobal/info.json index f837387c..1a520893 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]padglobal to get started.", "short": "Tsubaki guides and tips written by P&D professionals", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padguidedb/info.json b/padguidedb/info.json index 4a2946b8..f13de0fe 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]padguidedb to get started", "short": "Dungeon data scraper", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padinfo/info.json b/padinfo/info.json index aa578496..3457c96a 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]helpid for help", "short": "Serves monster data (LOAD THIS SECOND)", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padmonitor/info.json b/padmonitor/info.json index 2a392d7e..12ef9cef 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]addnewchannel to set up a monster monitor.", "short": "Announce new monsters", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padsearch/info.json b/padsearch/info.json index d4695ac6..c1471069 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]helpsearch for help.", "short": "Card search", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/padvision/info.json b/padvision/info.json index f49e6849..5ff98de2 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -7,7 +7,7 @@ "install_msg": "Make sure to use padboard with this cog!", "short": "Neural net helper", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/pricecheck/info.json b/pricecheck/info.json index d3084402..82b5d8b3 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]pcadmin to set up pricechecks.", "short": "Price Check", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/profile/info.json b/profile/info.json index 2fa3eabe..99094c26 100644 --- a/profile/info.json +++ b/profile/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]profile to get started", "short": "P&D Phonebook", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, diff --git a/schoolidol/info.json b/schoolidol/info.json index b678befe..1235d76d 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]sifid to get started.", "short": "Shows School Idol data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.0", + "max_bot_version": "3.4.2", "min_python_version": [ 3, 8, From 78e48408480821e3caa9f2c4b9bdd2e6caa58cc4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 29 Oct 2020 19:59:46 -0500 Subject: [PATCH 0245/1220] Remove _database attribute from DgItem classes (#183) * refactor DgSeries.series * remove self.dungeon from DgScheduledEvent * remove _database from ActiveSkill * DgAwokenSkill _database was alr unused * DgAwakening query has a join & kill DgAwokenSkill This lets us kill everything that uses self._database from DgAwakening for free because now name_ja/name_en come from the query immediately. The only use of DgAwokenSkill was in tests, so remove that test and delete the class completely. * remove already-unused properties from DgMonster * create models, add single farmable query * move farmable evo computation to graph part 1 * (padsearch) farmable evo to graph part 2 UNTESTED this needs to be tested on a unix system because it requires signal.SIGALRM and I'm on windows. * fix PR review comments so far * add pem/rem/mp evos to graph, rm from DgMons pem/rem aren't actually working, but they weren't working before, I don't think the data from the pipeline is right? or maybe it's just current JP? and idk what's in jp rem/pem right now... (I'm leaving the code in though, in case we can fix it because it would be nice if we can get it to work, so no reason to delete it) * half of moving base monster the other half depends on moving the entirety of the true_evo_type thing outside of the DgMonster class and i have no idea how half of this is determined so I'm gonna come back to this later.... * remove series from DgMonster * evo property * fix mp/farmable evos in padglobal * remove cross-cog imports * create leader skill model, join to query also remove DgLeaderSkill class since it's no longer used and get rid of one of the individual queries from the monster graph, now joining it to the monster query * DgActiveSkill -> active_skill_model, join into query * DgSeries -> SeriesModel, and join series into query * remove DgEncounter & Drop classes, now unused --- dadguide/dadguide.py | 1 + dadguide/database_context.py | 44 +------ dadguide/database_loader.py | 6 +- dadguide/database_manager.py | 150 ++------------------- dadguide/models/__init__.py | 0 dadguide/models/active_skill_model.py | 31 +++++ dadguide/models/base_model.py | 16 +++ dadguide/models/leader_skill_model.py | 36 ++++++ dadguide/models/monster_model.py | 53 ++++++++ dadguide/models/series_model.py | 20 +++ dadguide/monster_graph.py | 165 +++++++++++++++++++++--- dadguide/old_monster_index.py | 9 +- padevents/padevents.py | 19 ++- padglobal/padglobal.py | 14 +- padinfo/padinfo.py | 88 +++++++------ padsearch/padsearch.py | 9 +- tests/dadguide/database_context_test.py | 8 +- 17 files changed, 401 insertions(+), 268 deletions(-) create mode 100644 dadguide/models/__init__.py create mode 100644 dadguide/models/active_skill_model.py create mode 100644 dadguide/models/base_model.py create mode 100644 dadguide/models/leader_skill_model.py create mode 100644 dadguide/models/monster_model.py create mode 100644 dadguide/models/series_model.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 0dd6b3a0..5c0218c9 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -27,6 +27,7 @@ def _data_file(file_name: str) -> str: + # return 'S:\\Documents\\Games\\PAD\\dadguide.sqlite' return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 89b61f60..a16f11c1 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -3,15 +3,10 @@ from .database_manager import DadguideDatabase from .monster_graph import MonsterGraph -from .database_manager import DgActiveSkill -from .database_manager import DgLeaderSkill -from .database_manager import DgAwokenSkill from .database_manager import DadguideItem from .database_manager import DgMonster from .database_manager import DgAwakening from .database_manager import DgDungeon -from .database_manager import DgEncounter -from .database_manager import DgDrop from .database_manager import DgEvolution from .database_manager import DictWithAttrAccess from .database_manager import DgScheduledEvent @@ -36,18 +31,6 @@ def generate_all_monsters(self): as_generator=True, db_context=self, graph=self.graph)} self.expiry = int(datetime.now().timestamp()) + 60 * 60 - def get_active_skill_query(self, active_skill_id: int): - return self.database.select_one_entry_by_pk( - active_skill_id, DgActiveSkill, db_context=self) - - def get_leader_skill_query(self, leader_skill_id: int): - return self.database.select_one_entry_by_pk( - leader_skill_id, DgLeaderSkill, db_context=self) - - def get_awoken_skill(self, awoken_skill_id): - return self.database.select_one_entry_by_pk( - awoken_skill_id, DgAwokenSkill, db_context=self) - def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' return [r.awoken_skill_id for r in @@ -70,29 +53,6 @@ def get_monsters_by_awakenings(self, awoken_skill_id: int): (awoken_skill_id,), DgMonster, db_context=self, graph=self.graph) - def get_drop_dungeons(self, monster_id): - return self.database.query_many( - self.database.select_builder( - tables=OrderedDict({ - DgDungeon.TABLE: DgDungeon.FIELDS, - DgEncounter.TABLE: None, - DgDrop.TABLE: None, - }), - where='{0}.monster_id=?'.format(DgDrop.TABLE), - key=(DgDungeon.PK, DgEncounter.PK) - ), - (monster_id,), - DgDungeon, db_context=self) - - def monster_is_farmable(self, monster_id): - return self.database.query_one( - self.database.select_builder( - tables={DgDrop.TABLE: DgDrop.FIELDS}, - where='{0}.monster_id=?'.format(DgDrop.TABLE) - ), - (monster_id,), - DgDrop, db_context=self) is not None - def monster_in_rem(self, monster_id): m = self.get_monster(monster_id) return m is not None and m.rem_egg == 1 @@ -106,10 +66,10 @@ def monster_in_mp_shop(self, monster_id): return m is not None and m.buy_mp is not None def get_prev_evolution_by_monster(self, monster_id): - return self.graph.get_prev_evolution_by_monster(monster_id) + return self.graph.get_prev_evolution_by_monster_id(monster_id) def get_next_evolutions_by_monster(self, monster_id): - return self.graph.get_next_evolutions_by_monster(monster_id) + return self.graph.get_next_evolutions_by_monster_id(monster_id) def get_base_monster_by_id(self, monster_id): return min(self.graph.get_alt_cards(monster_id)) diff --git a/dadguide/database_loader.py b/dadguide/database_loader.py index 64536740..fcefef04 100644 --- a/dadguide/database_loader.py +++ b/dadguide/database_loader.py @@ -12,11 +12,9 @@ def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) -DB_DUMP_FILE = _data_file('dadguide.sqlite') -DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') - - def load_database(existing_db): + DB_DUMP_FILE = _data_file('dadguide.sqlite') + DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') # Release the handle to the database file if it has one if existing_db: existing_db.close() diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 3d57727d..791b8c56 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -229,44 +229,6 @@ def key(self): return self[self.PK] -class DgActiveSkill(DadguideItem): - TABLE = 'active_skills' - PK = 'active_skill_id' - - @property - def monsters(self): - return self._database.get_monsters_by_active(self.active_skill_id) - - @property - def skillups(self): - return list(filter(lambda x: x.farmable, self.monsters)) - - @property - def desc(self): - return self.desc_en or self.desc_ja - - @property - def name(self): - return self.name_en or self.name_ja - - -class DgLeaderSkill(DadguideItem): - TABLE = 'leader_skills' - PK = 'leader_skill_id' - - @property - def data(self): - return self.max_hp, self.max_atk, self.max_rcv, self.max_shield - - @property - def desc(self): - return self.desc_en or self.desc_ja - - @property - def name(self): - return self.name_en or self.name_ja - - class DgAwakening(DadguideItem): TABLE = 'awakenings' PK = 'awakening_id' @@ -275,22 +237,9 @@ class DgAwakening(DadguideItem): def __init__(self, item, database, **kwargs): super(DgAwakening, self).__init__(item, database) - @property - def skill(self): - return self._database.get_awoken_skill(self.awoken_skill_id) - @property def name(self): - return self.skill.name_en if self.skill.name_en is not None else self.skill.name_ja - - -class DgAwokenSkill(DadguideItem): - TABLE = 'awoken_skills' - PK = 'awoken_skill_id' - - @property - def monsters_with_awakening(self): - return self._database.get_monsters_by_awakenings(self.awoken_skill_id) + return self.name_en if self.name_en is not None else self.name_ja class DgEvolution(DadguideItem): @@ -302,41 +251,17 @@ def __init__(self, item, database, **kwargs): self.evolution_type = EvoType(self.evolution_type) -class DgSeries(DadguideItem): - TABLE = 'series' - PK = 'series_id' - - @property - def monsters(self): - return self._database.get_monsters_by_series(self.series_id) - - @property - def name(self): - return self.name_en if self.name_en is not None else self.name_ja - - class DgDungeon(DadguideItem): TABLE = 'dungeons' PK = 'dungeon_id' -class DgEncounter(DadguideItem): - TABLE = 'encounters' - PK = 'encounter_id' - - -class DgDrop(DadguideItem): - TABLE = 'drops' - PK = 'drop_id' - - class DgScheduledEvent(DadguideItem): TABLE = 'schedule' PK = 'event_id' def __init__(self, item, database, **graph): super(DgScheduledEvent, self).__init__(item, database) - self.dungeon = self._database.get_dungeon_by_id(self.dungeon_id) @property def open_datetime(self): @@ -385,12 +310,13 @@ def __init__(self, item, database, graph): self.in_pem = bool(self.pal_egg) self.in_rem = bool(self.rem_egg) - self.awakenings = sorted(self.node['awakenings'], key=lambda a: a.order_idx) + self.awakenings = sorted(self.node['model'].awakenings, key=lambda a: a.order_idx) + self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) self.is_inheritable = bool(self.inheritable) - self.evo_from_id = self._graph.get_prev_evolution_by_monster(self.monster_id) + self.evo_from_id = self._graph.get_prev_evolution_by_monster_id(self.monster_id) self.evo_from = None if self.evo_from_id: self.evo_from = self._graph.edges[self.evo_from_id, self.monster_id] @@ -444,11 +370,11 @@ def stats(self, lv=99, plus=0, inherit=False): @property def active_skill(self): - return self.node['active_skill'] + return self.node['model'].active_skill @property def leader_skill(self): - return self.node['leader_skill'] + return self.node['model'].leader_skill @property def cur_evo_type(self): @@ -487,66 +413,14 @@ def material_of(self): mat_of = self._database.get_evolution_by_material(self.monster_id) return [self._database.get_monster(x.to_id) for x in mat_of] - def _evolutions_to(self): - return self._database.get_next_evolutions_by_monster(self.monster_id) - - @property - def evo_to(self): - return [self._database.get_monster(x) for x in self._evolutions_to()] - @property def base_monster(self): return self._database.get_monster(self._base_monster_id) - @property - def alt_evos(self): - return [self._database.get_monster(a) for a in self._alt_evo_id_list] - @property def alt_versions(self): return [self._database.get_monster(a) for a in self._alt_version_id_list] - @property - def is_base_monster(self): - return self.evo_from is None - - @property - def series(self): - return self.node['series'] - - @property - def is_gfe(self): - return self.series_id == 34 - - @property - def drop_dungeons(self): - return self._database.get_drop_dungeons(self.monster_id) - - @property - def farmable(self): - return self._database.monster_is_farmable(self.monster_id) - - @property - def farmable_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_is_farmable(e_id): - return True - return False - - @property - def rem_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_in_rem(e_id): - return True - return False - - @property - def pem_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_in_pem(e_id): - return True - return False - @property def killers(self): type_to_killers_map = { @@ -569,13 +443,6 @@ def killers(self): def in_mpshop(self): return self.buy_mp is not None - @property - def mp_evo(self): - for e_id in self._alt_evo_id_list: - if self._database.monster_in_mp_shop(e_id): - return True - return False - @property def history_us(self): return '[{}] New Added'.format(self.reg_date) @@ -839,7 +706,7 @@ def _is_low_priority_group(self, mg: list): class NamedMonster(object): - def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set): + def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, db_context=None): # Must not hold onto monster or monster_group! # Hold on to the IDs instead @@ -856,7 +723,8 @@ def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixe self.prefixes = prefixes # Pantheon - self.series = monster.series.name if monster.series else None + series = db_context.graph.get_monster(monster.monster_no).series + self.series = series.name if series else None # Data used to determine how to rank the nicknames self.is_low_priority = monster_group.is_low_priority or monster.is_equip diff --git a/dadguide/models/__init__.py b/dadguide/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dadguide/models/active_skill_model.py b/dadguide/models/active_skill_model.py new file mode 100644 index 00000000..665a790d --- /dev/null +++ b/dadguide/models/active_skill_model.py @@ -0,0 +1,31 @@ +from .base_model import BaseModel + + +class ActiveSkillModel(BaseModel): + def __init__(self, **kwargs): + self.active_skill_id = kwargs['active_skill_id'] + self.name_ja = kwargs['name_ja'] + self.name_en = kwargs['name_en'] + self.name_ko = kwargs['name_ko'] + + self.desc_ja = kwargs['desc_ja'] + self.desc_en = kwargs['desc_en'] + self.desc_ko = kwargs['desc_ko'] + + self.turn_max = kwargs['turn_max'] + self.turn_min = kwargs['turn_min'] + + @property + def desc(self): + return self.desc_en or self.desc_ja + + @property + def name(self): + return self.name_en or self.name_ja + + def to_dict(self): + return { + 'monster_id': self.active_skill_id, + 'name_ja': self.name_ja, + 'name_en': self.name_en, + } diff --git a/dadguide/models/base_model.py b/dadguide/models/base_model.py new file mode 100644 index 00000000..0883ed30 --- /dev/null +++ b/dadguide/models/base_model.py @@ -0,0 +1,16 @@ +import json + + +class BaseModel(object): + + def to_dict(self): + raise NotImplementedError + + def to_string(self): + return json.dumps(self.to_dict()) + + def __repr__(self): + try: + print(self.to_string()) + except NotImplementedError: + super() diff --git a/dadguide/models/leader_skill_model.py b/dadguide/models/leader_skill_model.py new file mode 100644 index 00000000..ea5e753f --- /dev/null +++ b/dadguide/models/leader_skill_model.py @@ -0,0 +1,36 @@ +from .base_model import BaseModel + + +class LeaderSkillModel(BaseModel): + def __init__(self, **kwargs): + self.leader_skill_id = kwargs['leader_skill_id'] + self.name_ja = kwargs['name_ja'] + self.name_en = kwargs['name_en'] + self.name_ko = kwargs['name_ko'] + self.max_hp = kwargs['max_hp'] + self.max_atk = kwargs['max_atk'] + self.max_rcv = kwargs['max_rcv'] + self.max_shield = kwargs['max_shield'] + self.max_combos = kwargs['max_combos'] + self.desc_en = kwargs['desc_en'] + self.desc_ja = kwargs['desc_ja'] + self.desc_ko = kwargs['desc_ko'] + + @property + def data(self): + return self.max_hp, self.max_atk, self.max_rcv, self.max_shield + + @property + def desc(self): + return self.desc_en or self.desc_ja + + @property + def name(self): + return self.name_en or self.name_ja + + def to_dict(self): + return { + 'monster_id': self.leader_skill_id, + 'name_ja': self.name_ja, + 'name_en': self.name_en, + } diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py new file mode 100644 index 00000000..e7d6c49d --- /dev/null +++ b/dadguide/models/monster_model.py @@ -0,0 +1,53 @@ +from .base_model import BaseModel +from ..database_manager import Attribute +from ..database_manager import MonsterType +from ..database_manager import enum_or_none +from ..database_manager import make_roma_subname +import tsutils + + +class MonsterModel(BaseModel): + def __init__(self, **kwargs): + self.monster_id = kwargs['monster_id'] + self.monster_no = self.monster_id + self.monster_no_jp = kwargs['monster_no_jp'] + self.monster_no_na = kwargs['monster_no_na'] + self.monster_no_kr = kwargs['monster_no_kr'] + self.awakenings = kwargs['awakenings'] + self.leader_skill = kwargs['leader_skill'] + self.active_skill = kwargs['active_skill'] + self.series = kwargs['series'] + self.name_ja = kwargs['name_ja'] + self.name_ko = kwargs['name_ko'] + self.name_en = kwargs['name_en'] + self.roma_subname = None + if self.name_en == self.name_ja: + self.roma_subname = make_roma_subname(self.name_ja) + else: + # Remove annoying stuff from NA names, like Jörmungandr + self.name_en = tsutils.rmdiacritics(self.name_en) + self.name_en = kwargs['name_en_override'] or self.name_en + + self.type1 = enum_or_none(MonsterType, kwargs['type_1_id']) + self.type2 = enum_or_none(MonsterType, kwargs['type_2_id']) + self.type3 = enum_or_none(MonsterType, kwargs['type_3_id']) + self.types = list(filter(None, [self.type1, self.type2, self.type3])) + + self.rarity = kwargs['rarity'] + self.is_farmable = kwargs['is_farmable'] + self.in_rem = kwargs['in_rem'] + self.in_pem = kwargs['in_pem'] + self.in_mpshop = kwargs['in_mpshop'] + self.on_jp = kwargs['on_jp'] + self.on_na = kwargs['on_na'] + self.on_kr = kwargs['on_kr'] + self.attr1 = enum_or_none(Attribute, kwargs['attribute_1_id'], Attribute.Nil) + self.attr2 = enum_or_none(Attribute, kwargs['attribute_2_id'], Attribute.Nil) + self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) + + def to_dict(self): + return { + 'monster_id': self.monster_id, + 'name_ja': self.name_ja, + 'name_en': self.name_en, + } diff --git a/dadguide/models/series_model.py b/dadguide/models/series_model.py new file mode 100644 index 00000000..ee10780b --- /dev/null +++ b/dadguide/models/series_model.py @@ -0,0 +1,20 @@ +from .base_model import BaseModel + + +class SeriesModel(BaseModel): + def __init__(self, **kwargs): + self.series_id = kwargs['series_id'] + self.name_ja = kwargs['name_ja'] + self.name_en = kwargs['name_en'] + self.name_ko = kwargs['name_ko'] + + @property + def name(self): + return self.name_en if self.name_en is not None else self.name_ja + + def to_dict(self): + return { + 'monster_id': self.series_id, + 'name_ja': self.name_ja, + 'name_en': self.name_en, + } diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 089440b0..e9243a5d 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,11 +1,13 @@ import networkx as nx from collections import defaultdict +from .database_manager import DgMonster from .database_manager import DadguideDatabase -from .database_manager import DgActiveSkill -from .database_manager import DgLeaderSkill from .database_manager import DgAwakening from .database_manager import DictWithAttrAccess -from .database_manager import DgSeries +from .models.monster_model import MonsterModel +from .models.leader_skill_model import LeaderSkillModel +from .models.active_skill_model import ActiveSkillModel +from .models.series_model import SeriesModel class MonsterGraph(object): @@ -23,29 +25,80 @@ def set_database(self, db_context): def build_graph(self): self.graph = nx.DiGraph() - ms = self.database.query_many("SELECT * FROM monsters", (), DictWithAttrAccess, - db_context=self.db_context, graph=self) + ms = self.database.query_many( + "SELECT monsters.*, leader_skills.name_ja AS ls_name_ja, leader_skills.name_en AS ls_name_en, leader_skills.name_ko AS ls_name_ko, leader_skills.desc_ja AS ls_desc_ja, leader_skills.desc_en AS ls_desc_en, leader_skills.desc_ko AS ls_desc_ko, leader_skills.max_hp, leader_skills.max_atk, leader_skills.max_rcv, leader_skills.max_rcv, leader_skills.max_shield, leader_skills.max_combos, active_skills.name_ja AS as_name_ja, active_skills.name_en AS as_name_en, active_skills.name_ko AS as_name_ko, active_skills.desc_ja AS as_desc_ja, active_skills.desc_en AS as_desc_en, active_skills.desc_ko AS as_desc_ko, active_skills.turn_max, active_skills.turn_min, series.name_ja AS s_name_ja, series.name_en AS s_name_en, series.name_ko AS s_name_ko, drops.drop_id FROM monsters LEFT OUTER JOIN leader_skills ON monsters.leader_skill_id = leader_skills.leader_skill_id LEFT OUTER JOIN active_skills ON monsters.active_skill_id = active_skills.active_skill_id LEFT OUTER JOIN series ON monsters.series_id = series.series_id LEFT OUTER JOIN drops ON monsters.monster_id = drops.monster_id GROUP BY monsters.monster_id", (), DictWithAttrAccess, + db_context=self.db_context, graph=self) + es = self.database.query_many("SELECT * FROM evolutions", (), DictWithAttrAccess, db_context=self.db_context, graph=self) - aws = self.database.query_many("SELECT * FROM awakenings", (), DgAwakening, - db_context=self.db_context, graph=self) - lss = self.database.query_many("SELECT * FROM leader_skills", (), DgLeaderSkill, idx_key='leader_skill_id', - db_context=self.db_context, graph=self) - ass = self.database.query_many("SELECT * FROM active_skills", (), DgActiveSkill, idx_key='active_skill_id', + + aws = self.database.query_many("SELECT monster_id, awoken_skills.awoken_skill_id, is_super, order_idx, name_ja, name_en FROM awakenings JOIN awoken_skills ON awakenings.awoken_skill_id=awoken_skills.awoken_skill_id", (), DgAwakening, db_context=self.db_context, graph=self) - ss = self.database.query_many("SELECT * FROM series", (), DgSeries, idx_key='series_id', - db_context=self.db_context, graph=self) mtoawo = defaultdict(list) for a in aws: mtoawo[a.monster_id].append(a) for m in ms: - self.graph.add_node(m.monster_id, - awakenings=mtoawo[m.monster_id], - leader_skill=lss.get(m.leader_skill_id), - active_skill=ass.get(m.active_skill_id), - series=ss.get(m.series_id)) + ls_model = LeaderSkillModel(leader_skill_id=m.leader_skill_id, + name_ja=m.ls_name_ja, + name_en=m.ls_name_en, + name_ko=m.ls_name_ko, + desc_ja=m.ls_desc_ja, + desc_en=m.ls_desc_en, + desc_ko=m.ls_desc_ko, + max_hp=m.max_hp, + max_atk=m.max_atk, + max_rcv=m.max_rcv, + max_shield=m.max_shield, + max_combos=m.max_combos + ) if m.leader_skill_id != 0 else None + + as_model = ActiveSkillModel(active_skill_id=m.active_skill_id, + name_ja=m.as_name_ja, + name_en=m.as_name_en, + name_ko=m.as_name_ko, + desc_ja=m.as_name_ja, + desc_en=m.as_name_en, + desc_ko=m.as_name_ko, + turn_max=m.turn_max, + turn_min=m.turn_min + ) if m.active_skill_id != 0 else None + + s_model = SeriesModel(series_id=m.series_id, + name_ja=m.s_name_ja, + name_en=m.s_name_en, + name_ko=m.s_name_ko + ) + + m_model = MonsterModel(monster_id=m.monster_id, + monster_no_jp=m.monster_no_jp, + monster_no_na=m.monster_no_na, + monster_no_kr=m.monster_no_kr, + awakenings=mtoawo[m.monster_id], + leader_skill=ls_model, + active_skill=as_model, + series=s_model, + attribute_1_id=m.attribute_1_id, + attribute_2_id=m.attribute_2_id, + name_ja=m.name_ja, + name_en=m.name_en, + name_ko=m.name_ko, + name_en_override=m.name_en_override, + rarity=m.rarity, + is_farmable=m.drop_id is not None, + in_pem=m.pal_egg == 1, + in_rem=m.rem_egg == 1, + in_mpshop=m.buy_mp is not None, + on_jp=m.on_jp == 1, + on_na=m.on_na == 1, + on_kr=m.on_kr == 1, + type_1_id=m.type_1_id, + type_2_id=m.type_2_id, + type_3_id=m.type_3_id + ) + + self.graph.add_node(m.monster_id, model=m_model) if m.linked_monster_id: self.graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') self.graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') @@ -61,6 +114,11 @@ def build_graph(self): def _get_edges(node, etype): return {mid for mid, edge in node.items() if edge.get('type') == etype} + def get_monster(self, monster_id): + if monster_id not in self.graph.nodes: + return None + return self.graph.nodes[monster_id]['model'] + def get_evo_tree(self, monster_id): ids = set() to_check = {monster_id} @@ -102,9 +160,78 @@ def get_alt_cards(self, monster_id): ids.add(mid) return ids - def get_prev_evolution_by_monster(self, monster_id): + def get_alt_monsters_by_id(self, monster_id): + ids = self.get_alt_cards(monster_id) + return [self.get_monster(m_id) for m_id in ids] + + def get_base_id_by_id(self, monster_id): + alt_cards = self.get_alt_cards(monster_id) + if alt_cards is None: + return None + return sorted(alt_cards)[0] + + def get_base_monster_by_id(self, monster_id): + return self.get_monster(self.get_base_id_by_id(monster_id)) + + def get_prev_evolution_by_monster_id(self, monster_id): bes = self._get_edges(self.graph[monster_id], 'back_evolution') if bes: return bes.pop() - def get_next_evolutions_by_monster(self, monster_id): + def get_next_evolutions_by_monster_id(self, monster_id): return self._get_edges(self.graph[monster_id], 'evolution') + + # farmable + def monster_is_farmable_by_id(self, monster_id): + return self.graph.nodes[monster_id]['model'].is_farmable + + def monster_is_farmable(self, monster: DgMonster): + return self.monster_is_farmable_by_id(monster.monster_no) + + def monster_is_farmable_evo_by_id(self, monster_id): + return any( + m for m in self.get_evo_tree(monster_id) if self.monster_is_farmable_by_id(m)) + + def monster_is_farmable_evo(self, monster: DgMonster): + return self.monster_is_farmable_evo_by_id(monster.monster_no) + + # mp + def monster_is_mp_by_id(self, monster_id): + return self.graph.nodes[monster_id]['model'].in_mpshop + + def monster_is_mp(self, monster: DgMonster): + return self.monster_is_mp_by_id(monster.monster_no) + + def monster_is_mp_evo_by_id(self, monster_id): + return any( + m for m in self.get_evo_tree(monster_id) if self.monster_is_mp_by_id(m)) + + def monster_is_mp_evo(self, monster: DgMonster): + return self.monster_is_mp_evo_by_id(monster.monster_no) + + # pem + def monster_is_pem_by_id(self, monster_id): + return self.graph.nodes[monster_id]['model'].in_pem + + def monster_is_pem(self, monster: DgMonster): + return self.monster_is_pem_by_id(monster.monster_no) + + def monster_is_pem_evo_by_id(self, monster_id): + return any( + m for m in self.get_evo_tree(monster_id) if self.monster_is_pem_by_id(m)) + + def monster_is_pem_evo(self, monster: DgMonster): + return self.monster_is_pem_evo_by_id(monster.monster_no) + + # rem + def monster_is_rem_by_id(self, monster_id): + return self.graph.nodes[monster_id]['model'].in_rem + + def monster_is_rem(self, monster: DgMonster): + return self.monster_is_rem_by_id(monster.monster_no) + + def monster_is_rem_evo_by_id(self, monster_id): + return any( + m for m in self.get_evo_tree(monster_id) if self.monster_is_rem_by_id(m)) + + def monster_is_rem_evo(self, monster: DgMonster): + return self.monster_is_rem_evo_by_id(monster.monster_no) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 7fc37c69..38b9b601 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -20,6 +20,7 @@ class MonsterIndex(tsutils.aobject): async def __init__(self, monster_database: DbContext, nickname_overrides, basename_overrides, panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory + self.db_context = monster_database base_monster_ids = monster_database.get_base_monster_ids() self.attr_short_prefix_map = { @@ -70,7 +71,8 @@ async def __init__(self, monster_database: DbContext, nickname_overrides, basena continue prefixes = self.compute_prefixes(monster, evolution_tree) extra_nicknames = monster_id_to_nicknames[monster.monster_id] - named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames) + named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames, + db_context=self.db_context) named_monsters.append(named_monster) # Sort the NamedMonsters into the opposite order we want to accept their nicknames in @@ -195,7 +197,7 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('reincarnated') # Other Prefixes - if m.farmable: + if self.db_context.graph.monster_is_farmable_evo(m): prefixes.add('farmable') # If any monster in the group is a pixel, add 'nonpixel' to all the versions @@ -214,7 +216,8 @@ def is_pixel(n): prefixes.add('equip') # Collab prefixes - prefixes.update(self.series_to_prefix_map.get(m.series.series_id, [])) + prefixes.update(self.series_to_prefix_map.get( + self.db_context.graph.get_monster(m.monster_no).series.series_id, [])) return prefixes diff --git a/padevents/padevents.py b/padevents/padevents.py index 050d5ccd..f22e9c37 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -21,6 +21,7 @@ SUPPORTED_SERVERS = ["JP", "NA", "KR"] GROUPS = ['red', 'blue', 'green'] + class PadEvents(commands.Cog): """Pad Event Tracker""" def __init__(self, bot, *args, **kwargs): @@ -75,7 +76,8 @@ async def refresh_data(self): new_events = [] for se in scheduled_events: try: - new_events.append(Event(se)) + db_context = self.bot.get_cog("Dadguide").database + new_events.append(Event(se, db_context)) except Exception as ex: logger.exception("Refresh error:") @@ -213,7 +215,6 @@ class Dungeon: pass d = Dungeon() d.name_en = 'fake_dungeon_name' d.dungeon_type = DungeonType.Unknown7 - te.dungeon = d te.event_modifier = 'fake_event_modifier' self.events.append(Event(te)) @@ -403,11 +404,13 @@ async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping when event is red""" await self.aepc(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) await ctx.tick() + @aep_set.command(name="bluechannel") async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping when event is blue""" await self.aepc(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) await ctx.tick() + @aep_set.command(name="greenchannel") async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping when event is green""" @@ -425,11 +428,13 @@ async def aep_s_redrole(self, ctx, key, role: discord.Role): """Sets role to ping when event is red""" await self.aepc(ctx, key, 'roles', lambda x: [role.id, x[1], x[2]]) await ctx.tick() + @aep_set.command(name="bluerole") async def aep_s_bluerole(self, ctx, key, role: discord.Role): """Sets role to ping when event is blue""" await self.aepc(ctx, key, 'roles', lambda x: [x[0], role.id, x[2]]) await ctx.tick() + @aep_set.command(name="greenrole") async def aep_s_greenrole(self, ctx, key, role: discord.Role): """Sets role to ping when event is green""" @@ -912,13 +917,15 @@ def removeDailyReg(self, channel_id, server): class Event: - def __init__(self, scheduled_event: "DgScheduledEvent"): + def __init__(self, scheduled_event: "DgScheduledEvent", db_context): + self.db_context = db_context self.key = scheduled_event.key() self.server = SUPPORTED_SERVERS[scheduled_event.server_id] self.open_datetime = scheduled_event.open_datetime self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name - self.dungeon_name = scheduled_event.dungeon.name_en if scheduled_event.dungeon else 'unknown_dungeon' + self.dungeon = self.db_context.get_dungeon_by_id(scheduled_event.dungeon_id) + self.dungeon_name = self.dungeon.name_en if self.dungeon else 'unknown_dungeon' self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) @@ -929,8 +936,8 @@ def __init__(self, scheduled_event: "DgScheduledEvent"): self.name_and_modifier += ', ' + self.clean_event_name self.event_type = EventType(scheduled_event.event_type_id) - self.dungeon_type = DungeonType( - scheduled_event.dungeon.dungeon_type) if scheduled_event.dungeon else DungeonType.Unknown + + self.dungeon_type = DungeonType(self.dungeon.dungeon_type) if self.dungeon else DungeonType.Unknown def start_from_now_sec(self): now = datetime.datetime.now(pytz.utc) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 890c5101..e114c9d6 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -711,6 +711,10 @@ async def which(self, ctx, *, term: str = None): await ctx.send(self.emojify(definition)) async def _resolve_which(self, ctx, term): + + db_context = self.bot.get_cog('Dadguide').database + db_context: DbContext + term = term.lower().replace('?', '') nm, _, _ = await lookup_named_monster(term) if nm is None: @@ -730,11 +734,11 @@ async def _resolve_which(self, ctx, term): monster = monster_id_to_monster(monster_id) - if monster.mp_evo: + if db_context.graph.monster_is_mp_evo(monster): return name, MP_BUY_MSG.format(ctx.prefix), None, False - elif monster.farmable_evo: + elif db_context.graph.monster_is_farmable_evo(monster): return name, FARMABLE_MSG, None, False - elif check_simple_tree(monster): + elif check_simple_tree(monster, db_context): return name, SIMPLE_TREE_MSG, None, False else: await ctx.send(inline('No which info for {} (#{})'.format(name, monster_id))) @@ -1199,10 +1203,10 @@ def emojify(self, message): return tsutils.fix_emojis_for_server(emojis, message) -def check_simple_tree(monster): +def check_simple_tree(monster, db_context): attr1 = monster.attr1 active_skill = monster.active_skill - for m in monster.alt_versions: + for m in db_context.graph.get_alt_monsters_by_id(monster.monster_no): if m.attr1 != attr1 or m.active_skill != active_skill: return False if m.is_equip: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 87b0fa63..b2ed9e54 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -81,12 +81,14 @@ def _data_file(file_name: str) -> str: class IdEmojiUpdater(EmojiUpdater): def __init__(self, emoji_to_embed, m: "DgMonster" = None, - pad_info=None, selected_emoji=None, bot=None): + pad_info=None, selected_emoji=None, bot=None, + db_context: "DbContext" = None): self.emoji_dict = emoji_to_embed self.m = m self.pad_info = pad_info self.selected_emoji = selected_emoji self.bot = bot + self.db_context = db_context self.pad_info.settings.log_emoji("start_"+selected_emoji) @@ -94,7 +96,6 @@ def on_update(self, ctx, selected_emoji): evoID = self.pad_info.settings.checkEvoID(ctx.author.id) self.pad_info.settings.log_emoji(selected_emoji) if evoID: - DGCOG = self.bot.get_cog('Dadguide') evos = sorted({*self.m.alt_versions}, key=lambda m: m.monster_id) index = evos.index(self.m) if selected_emoji == self.pad_info.previous_monster_emoji: @@ -166,6 +167,9 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + self.DGCOG = self.bot.get_cog("Dadguide") + self.db_context = self.DGCOG.database + self.settings = PadInfoSettings("padinfo") self.index_all = None @@ -428,7 +432,8 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): ctx, starting_menu_emoji, IdEmojiUpdater(emoji_to_embed, pad_info=self, - m=m, selected_emoji=starting_menu_emoji, bot=self.bot) + m=m, selected_emoji=starting_menu_emoji, bot=self.bot, + db_context=self.db_context) ) async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): @@ -441,23 +446,23 @@ async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): ) def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): - id_embed = monsterToEmbed(m, self.get_emojis()) - evo_embed = monsterToEvoEmbed(m) + id_embed = monsterToEmbed(m, self.get_emojis(), self.db_context) + evo_embed = monsterToEvoEmbed(m, self.db_context) mats_embed = monsterToEvoMatsEmbed(m) animated = m.has_animation pic_embed = monsterToPicEmbed(m, animated=animated) - other_info_embed = monsterToOtherInfoEmbed(m) + other_info_embed = monsterToOtherInfoEmbed(m, self.db_context) emoji_to_embed = OrderedDict() emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed emoji_to_embed[self.mats_emoji] = mats_embed emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = monsterToPantheonEmbed(m) + pantheon_embed = monsterToPantheonEmbed(m, self.db_context) if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed - skillups_embed = monsterToSkillupsEmbed(m) + skillups_embed = monsterToSkillupsEmbed(m, self.db_context) if skillups_embed: emoji_to_embed[self.skillups_emoji] = skillups_embed @@ -487,7 +492,7 @@ async def _do_evolistmenu(self, ctx, sm): emoji_to_embed = OrderedDict() for idx, m in enumerate(monsters): emoji = char_to_emoji(str(idx)) - emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis()) + emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis(), self.db_context) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji @@ -650,8 +655,8 @@ async def leaderskill(self, ctx, *, whole_query): emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis()) - emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis()) + emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis(), self.db_context) + emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis(), self.db_context) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -664,7 +669,7 @@ async def leaderskillsingle(self, ctx, *, query): return emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = monstersToLssEmbed(m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(m, self.get_emojis()) + emoji_to_embed[self.left_emoji] = monsterToEmbed(m, self.get_emojis(), self.db_context) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -948,13 +953,6 @@ def monsterToLongHeader(m: "DgMonster", link=False): return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg -def monsterToEvoText(m: "DgMonster"): - output = monsterToLongHeader(m) - for ae in sorted({*m.alt_versions}, key=lambda x: int(x.monster_id)): - output += "\n\t- {}".format(monsterToLongHeader(ae)) - return output - - def monsterToThumbnailUrl(m: "DgMonster"): return get_portrait_url(m) @@ -980,14 +978,15 @@ def addEvoListFields(list_of_monsters, embed, name): embed.add_field(name=field_name, value=field_data) -def monsterToEvoEmbed(m: "DgMonster"): +def monsterToEvoEmbed(m: "DgMonster", db_context: "DbContext"): embed = monsterToBaseEmbed(m) + alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_no) - if not len(m.alt_versions) and not m.evo_gem: + if not len(alt_versions) and not m.evo_gem: embed.description = 'No alternate evos or evo gem' return embed - addEvoListFields(m.alt_versions, embed, '{} alternate evo(s)') + addEvoListFields(alt_versions, embed, '{} alternate evo(s)') if not m.evo_gem: return embed addEvoListFields([m.evo_gem], embed, '{} evo gem(s)') @@ -1029,15 +1028,15 @@ def monsterToEvoMatsEmbed(m: "DgMonster"): return embed -def monsterToPantheonEmbed(m: "DgMonster"): - full_pantheon = m.series.monsters +def monsterToPantheonEmbed(m: "DgMonster", db_context: "DbContext"): + full_pantheon = db_context.get_monsters_by_series(m.series_id) pantheon_list = list(filter(lambda x: x.evo_from is None, full_pantheon)) if len(pantheon_list) == 0 or len(pantheon_list) > 6: return None embed = monsterToBaseEmbed(m) - field_name = 'Pantheon: ' + m.series.name + field_name = 'Pantheon: ' + db_context.graph.get_monster(m.monster_no).series.name field_data = '' for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): field_data += '\n' + monsterToHeader(monster, link=True) @@ -1046,8 +1045,13 @@ def monsterToPantheonEmbed(m: "DgMonster"): return embed -def monsterToSkillupsEmbed(m: "DgMonster"): - skillups_list = m.active_skill.skillups if m.active_skill else [] +def monsterToSkillupsEmbed(m: "DgMonster", db_context: "DbContext"): + if m.active_skill is None: + return None + possible_skillups_list = db_context.get_monsters_by_active(m.active_skill.active_skill_id) + skillups_list = list(filter( + lambda x: db_context.graph.monster_is_farmable_evo(x), possible_skillups_list)) + if len(skillups_list) == 0: return None @@ -1177,24 +1181,24 @@ def monsterToTypeString(m: "DgMonster"): return '/'.join([t.name for t in m.types]) -def monsterToAcquireString(m: "DgMonster"): +def monsterToAcquireString(m: "DgMonster", db_context: "DbContext"): acquire_text = None - if m.farmable and not m.mp_evo: + if db_context.graph.monster_is_farmable(m) and not db_context.graph.monster_is_mp_evo(m): # Some MP shop monsters 'drop' in PADR acquire_text = 'Farmable' - elif m.farmable_evo and not m.mp_evo: + elif db_context.graph.monster_is_farmable_evo(m) and not db_context.graph.monster_is_mp_evo(m): acquire_text = 'Farmable Evo' elif m.in_pem: acquire_text = 'In PEM' - elif m.pem_evo: + elif db_context.graph.monster_is_pem_evo(m): acquire_text = 'PEM Evo' elif m.in_rem: acquire_text = 'In REM' - elif m.rem_evo: + elif db_context.graph.monster_is_rem_evo(m): acquire_text = 'REM Evo' elif m.in_mpshop: acquire_text = 'MP Shop' - elif m.mp_evo: + elif db_context.graph.monster_is_mp_evo(m): acquire_text = 'MP Shop Evo' return acquire_text @@ -1206,16 +1210,22 @@ def match_emoji(emoji_list, name): return name -def monsterToEmbed(m: "DgMonster", emoji_list): +def monsterToEmbed(m: "DgMonster", emoji_list, db_context: "DbContext"): embed = monsterToBaseEmbed(m) info_row_1 = monsterToTypeString(m) - acquire_text = monsterToAcquireString(m) + acquire_text = monsterToAcquireString(m, db_context) tet_text = m.true_evo_type.value os = "" if m.orb_skin_id is None else " (Orb Skin)" - info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format(m.rarity, m.base_monster.rarity, os, m.cost) + info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format( + m.rarity, + db_context.graph.get_base_monster_by_id(m.monster_no).rarity, + os, + m.cost + ) + if acquire_text: info_row_2 += '\n**{}**'.format(acquire_text) if m.is_inheritable: @@ -1282,13 +1292,13 @@ def monsterToEmbed(m: "DgMonster", emoji_list): evos_body = ", ".join(f"**{m2.monster_id}**" if m2.monster_id==m.monster_id else f"[{m2.monster_id}]({get_pdx_url(m2)})" - for m2 in sorted({*m.alt_versions}, key=lambda m: m.monster_id)) + for m2 in sorted({*db_context.graph.get_alt_monsters_by_id(m.monster_no)}, key=lambda m: m.monster_id)) embed.add_field(name=evos_header, value=evos_body, inline=False) return embed -def monsterToOtherInfoEmbed(m: "DgMonster"): +def monsterToOtherInfoEmbed(m: "DgMonster", db_context: "DbContext"): embed = monsterToBaseEmbed(m) # Clear the thumbnail, takes up too much space embed.set_thumbnail(url='') @@ -1321,7 +1331,7 @@ def monsterToOtherInfoEmbed(m: "DgMonster"): if m.history_us: body_text += '\n**History:** {}'.format(m.history_us) - body_text += '\n**Series:** {}'.format(m.series.name) + body_text += '\n**Series:** {}'.format(db_context.graph.get_monster(m.monster_no).series.name) body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) if m.buy_mp is not None: body_text += " **Buy MP:** {:,}".format(m.buy_mp) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index cedb8aac..ebe7adb1 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -8,6 +8,7 @@ from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify from tsutils import tsutils +from dadguide.database_context import DbContext logger = logging.getLogger('red.padbot-cogs.padsearch') @@ -417,7 +418,8 @@ def build(self, **kwargs): class SearchConfig(object): - def __init__(self, lexer): + def __init__(self, lexer, db_context: DbContext): + self.db_context = db_context self.all = False self.cd = None self.farmable = None @@ -517,7 +519,7 @@ def __init__(self, lexer): self.filters.append(lambda m: m.search.active_min and m.search.active_min <= self.cd) if self.farmable: - self.filters.append(lambda m: m.farmable_evo) + self.filters.append(lambda m: self.db_context.graph.is_monster_farmable_evo(m)) if self.haste: text = "charge allies' skill by {}".format(self.haste) @@ -806,7 +808,8 @@ async def search(self, ctx, *, filter_spec: str): def _make_search_config(self, input): lexer = PadSearchLexer().build() lexer.input(input) - return SearchConfig(lexer) + db_context = self.bot.get_cog("Dadguide").database + return SearchConfig(lexer, db_context) @commands.command() @checks.is_owner() diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index a0fca52c..727e02bd 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -9,14 +9,10 @@ graph.build_graph() db_context.generate_all_monsters() -assert db_context.get_active_skill_query(15)['name_en'] == 'Inferno Breath' -assert db_context.get_leader_skill_query(1202)['name_en'] == 'Fusion Soul' -assert db_context.get_awoken_skill(15)['name_en'] == 'Enhanced Water Orbs' # print(ctx.get_awoken_skill_ids()) assert db_context.get_monsters_by_awakenings(5)[0].name_en == 'Crystal Aurora Dragon' -assert db_context.get_drop_dungeons(4)[0]['name_en'] == 'Diagoldos Descended!' -assert db_context.monster_is_farmable(4) -assert not db_context.monster_is_farmable(5156) +assert db_context.graph.monster_is_farmable_by_id(4) +assert not db_context.graph.monster_is_farmable_by_id(5156) print(db_context.monster_in_rem(1073)) # base pandora print(db_context.monster_in_rem(2121)) print(db_context.monster_in_pem(2120)) # arthur From 10d2ca026cd59809366138661766f3ba3407b0ea Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 29 Oct 2020 22:39:01 -0700 Subject: [PATCH 0246/1220] Indent Crud Change (4 -> 2) (#184) * Indent Crud Change (4 -> 2) * Sort JSON keys --- crud/crud.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index e18dbfc2..3c6760f6 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -74,7 +74,7 @@ async def execute_read(self, ctx, sql, replacements): with await self.get_cursor() as cursor: cursor.execute(sql, replacements) results = list(cursor.fetchall()) - msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) + msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False, sort_keys=True) await ctx.send(inline(cursor._executed)) for page in pagify(msg): await ctx.send(box(page)) @@ -150,7 +150,7 @@ async def series_add(self, ctx, *elements): 'series_id': elements['series_id'] }) async with aiofiles.open(fn, 'w') as f: - await f.write(json.dumps(j, indent=4, ensure_ascii=False)) + await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) @series.command(name="edit") @@ -185,7 +185,7 @@ async def series_edit(self, ctx, series_id: int, *elements): if e['series_id'] == series_id: e.update(elements) async with aiofiles.open(fn, 'w') as f: - await f.write(json.dumps(j, indent=4, ensure_ascii=False)) + await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) @series.command(name="delete") @checks.is_owner() @@ -203,7 +203,7 @@ async def series_delete(self, ctx, series_id: int): if e['series_id'] == series_id: j.remove(e) async with aiofiles.open(fn, 'w') as f: - await f.write(json.dumps(j, indent=4, ensure_ascii=False)) + await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) @crud.command() async def editmonsname(self, ctx, monster_id: int, *, name): From bd91a9c75cf70258359a593c7b8e75b2b1ebd51e Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 29 Oct 2020 22:52:25 -0700 Subject: [PATCH 0247/1220] [PadSearch] Fix import issue for type annotation (#185) --- padsearch/padsearch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index ebe7adb1..3e22560f 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -8,7 +8,6 @@ from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify from tsutils import tsutils -from dadguide.database_context import DbContext logger = logging.getLogger('red.padbot-cogs.padsearch') @@ -418,7 +417,7 @@ def build(self, **kwargs): class SearchConfig(object): - def __init__(self, lexer, db_context: DbContext): + def __init__(self, lexer, db_context: "DbContext"): self.db_context = db_context self.all = False self.cd = None From 09c40c571cc42f19abc8604eae5fd4fb32793f88 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 30 Oct 2020 02:35:09 -0500 Subject: [PATCH 0248/1220] fix params to active skill (#186) --- dadguide/monster_graph.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index e9243a5d..68ba935d 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -58,9 +58,9 @@ def build_graph(self): name_ja=m.as_name_ja, name_en=m.as_name_en, name_ko=m.as_name_ko, - desc_ja=m.as_name_ja, - desc_en=m.as_name_en, - desc_ko=m.as_name_ko, + desc_ja=m.as_desc_ja, + desc_en=m.as_desc_en, + desc_ko=m.as_desc_ko, turn_max=m.turn_max, turn_min=m.turn_min ) if m.active_skill_id != 0 else None From 3cc63579e386d6c8d6e69dc6ce7891d6508b010c Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Oct 2020 06:13:38 -0700 Subject: [PATCH 0249/1220] [PadInfo] Stop storing dadguide.db_context in (#187) --- padinfo/padinfo.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b2ed9e54..7aaa5a15 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -167,9 +167,6 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.DGCOG = self.bot.get_cog("Dadguide") - self.db_context = self.DGCOG.database - self.settings = PadInfoSettings("padinfo") self.index_all = None @@ -428,12 +425,16 @@ async def skillups(self, ctx, *, query: str): async def _do_idmenu(self, ctx, m, starting_menu_emoji): emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted({*m.alt_versions}, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) + + DGCOG = self.bot.get_cog("Dadguide") + db_context = DGCOG.database + return await self._do_menu( ctx, starting_menu_emoji, IdEmojiUpdater(emoji_to_embed, pad_info=self, m=m, selected_emoji=starting_menu_emoji, bot=self.bot, - db_context=self.db_context) + db_context=db_context) ) async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): @@ -446,23 +447,26 @@ async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): ) def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): - id_embed = monsterToEmbed(m, self.get_emojis(), self.db_context) - evo_embed = monsterToEvoEmbed(m, self.db_context) + DGCOG = self.bot.get_cog("Dadguide") + db_context = DGCOG.database + + id_embed = monsterToEmbed(m, self.get_emojis(), db_context) + evo_embed = monsterToEvoEmbed(m, db_context) mats_embed = monsterToEvoMatsEmbed(m) animated = m.has_animation pic_embed = monsterToPicEmbed(m, animated=animated) - other_info_embed = monsterToOtherInfoEmbed(m, self.db_context) + other_info_embed = monsterToOtherInfoEmbed(m, db_context) emoji_to_embed = OrderedDict() emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed emoji_to_embed[self.mats_emoji] = mats_embed emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = monsterToPantheonEmbed(m, self.db_context) + pantheon_embed = monsterToPantheonEmbed(m, db_context) if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed - skillups_embed = monsterToSkillupsEmbed(m, self.db_context) + skillups_embed = monsterToSkillupsEmbed(m, db_context) if skillups_embed: emoji_to_embed[self.skillups_emoji] = skillups_embed @@ -486,13 +490,16 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): return emoji_to_embed async def _do_evolistmenu(self, ctx, sm): + DGCOG = self.bot.get_cog("Dadguide") + db_context = DGCOG.database + monsters = sm.alt_versions monsters.sort(key=lambda m: m.monster_id) emoji_to_embed = OrderedDict() for idx, m in enumerate(monsters): emoji = char_to_emoji(str(idx)) - emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis(), self.db_context) + emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis(), db_context) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji @@ -619,6 +626,9 @@ async def leaderskill(self, ctx, *, whole_query): [p]ls r sonia "revo lu bu" [p]ls sonia lubu """ + DGCOG = self.bot.get_cog("Dadguide") + db_context = DGCOG.database + # deliberate order in case of multiple different separators. for sep in ('"', '/', ',', ' '): if sep in whole_query: @@ -655,21 +665,24 @@ async def leaderskill(self, ctx, *, whole_query): emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis(), self.db_context) - emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis(), self.db_context) + emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis(), db_context) + emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis(), db_context) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @commands.command(aliases=['lssingle']) @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): + DGCOG = self.bot.get_cog("Dadguide") + db_context = DGCOG.database + m, err, _ = await self.findMonster(query) if err: await ctx.send(err) return emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = monstersToLssEmbed(m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(m, self.get_emojis(), self.db_context) + emoji_to_embed[self.left_emoji] = monsterToEmbed(m, self.get_emojis(), db_context) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) From 7e8effdc6c0aaf0892418ceae38575a42f488f73 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 30 Oct 2020 08:24:59 -0500 Subject: [PATCH 0250/1220] [padglobal] fix uncontroversial trees in ^which (#188) --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index e114c9d6..a7e3212f 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1207,7 +1207,7 @@ def check_simple_tree(monster, db_context): attr1 = monster.attr1 active_skill = monster.active_skill for m in db_context.graph.get_alt_monsters_by_id(monster.monster_no): - if m.attr1 != attr1 or m.active_skill != active_skill: + if m.attr1 != attr1 or m.active_skill.active_skill_id != active_skill.active_skill_id: return False if m.is_equip: return False From 23458be01d84b414197738da4f2c04460a1035cb Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 31 Oct 2020 03:01:27 -0500 Subject: [PATCH 0251/1220] [padglobal which] print name in uncontroversial result (#190) * [padglobal which] print name in uncontroversial result * [padglobal] fix uncontroversial trees in ^which (#188) * actually return top evo * rename top id to be unambiguous --- dadguide/monster_graph.py | 9 +++++++++ padglobal/padglobal.py | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 68ba935d..179eee51 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -173,6 +173,15 @@ def get_base_id_by_id(self, monster_id): def get_base_monster_by_id(self, monster_id): return self.get_monster(self.get_base_id_by_id(monster_id)) + def get_numerical_sort_top_id_by_id(self, monster_id): + alt_cards = self.get_alt_cards(monster_id) + if alt_cards is None: + return None + return sorted(alt_cards)[-1] + + def get_numerical_sort_top_monster_by_id(self, monster_id): + return self.get_monster(self.get_numerical_sort_top_id_by_id(monster_id)) + def get_prev_evolution_by_monster_id(self, monster_id): bes = self._get_edges(self.graph[monster_id], 'back_evolution') if bes: return bes.pop() diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index a7e3212f..689af9c3 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -48,7 +48,7 @@ def _data_file(file_name: str) -> str: FARMABLE_MSG = 'This monster is **farmable** so make as many copies of whichever evos you like.' MP_BUY_MSG = ('This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' ', check {}mpdra? for specific recommendations.') -SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution.' +SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution: No. {}, {}.' def mod_help(self, ctx, help_type): @@ -739,7 +739,8 @@ async def _resolve_which(self, ctx, term): elif db_context.graph.monster_is_farmable_evo(monster): return name, FARMABLE_MSG, None, False elif check_simple_tree(monster, db_context): - return name, SIMPLE_TREE_MSG, None, False + top_monster = db_context.graph.get_numerical_sort_top_monster_by_id(monster.monster_no) + return name, SIMPLE_TREE_MSG.format(top_monster.monster_no, top_monster.name_en), None, False else: await ctx.send(inline('No which info for {} (#{})'.format(name, monster_id))) return None, None, None, None From 5626f723362a773c48d8df73453b6fbb2527f43a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 31 Oct 2020 03:01:45 -0500 Subject: [PATCH 0252/1220] [padevents] fix testevent missing param (#189) --- padevents/padevents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index f22e9c37..ded4a0f8 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -216,7 +216,7 @@ class Dungeon: pass d.name_en = 'fake_dungeon_name' d.dungeon_type = DungeonType.Unknown7 te.event_modifier = 'fake_event_modifier' - self.events.append(Event(te)) + self.events.append(Event(te, self.bot.get_cog('Dadguide').database)) await ctx.send("Fake event injected.") From c9d090f3d5c07c63c2c8b18eb83dbb3d796bcfd6 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Mon, 2 Nov 2020 05:35:32 -0500 Subject: [PATCH 0253/1220] add x as a prefix for christmas cards (#194) --- dadguide/old_monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 38b9b601..14dc1ef4 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -44,7 +44,7 @@ async def __init__(self, monster_database: DbContext, nickname_overrides, basena self.series_to_prefix_map = { 130: ['halloween', 'hw', 'h'], - 136: ['xmas', 'christmas'], + 136: ['xmas', 'christmas', 'x'], 125: ['summer', 'beach'], 114: ['school', 'academy', 'gakuen'], 139: ['new years', 'ny'], From dafd463c74f6c463fbd0740fcc0d28e164ad7105 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 2 Nov 2020 13:25:26 -0600 Subject: [PATCH 0254/1220] Make MonsterModel ready to replace DgMonster fully (#193) * implement cur, true evo type in graph * rm true_evo_type from DGMonster, fix revo in MonsterIndex * move evo mats to graph this also allowed for removing evo_from and evo_from_id completely from DgMonster, so that's completely moved to monster_graph now yay * move prev/next into graph * remove evo list from DgMonster all of the queries using this are still very slow because they require DgMonsters to be constructed, which requires actual db queries and not just graph queries, so I just moved the slow part outside of DgMonster and to the DbContext. However removing this method from DgMonster will make it easier to later replace the entirety of padinfo with MonsterModels, and when that's done I can stop casting as DgMonsters and just use MonsterModels from the graph directly, which will be fast so this was worth it * add evos to MonsterModel, rm from DgMonster * move material_of to db_context (not graph though) * MonsterModels can generate id menus now! * remove some unused methods from dbcontext * fix handling of evo type checking this has two parts: (1) I fixed the way I was getting edges from the graph, since before I was ignoring edge type, which in practice wasn't an issue probably but in theory could be, and in the future could be, so it was worth fixing. (2) I also fixed an issue where old data such as changed evo trees, for example Typhon's, could be interfering with the evo data returned. I added tstamp as an attribute to EvolutionModel and am sorting by this when returning an edge in the above method. This should hopefully "just work" with no further effort needed. Also added a test case to cover this, and added more documentation about this to the Dadguide wiki page in the repo. * no more database!!!!!! * remove db_context passing from query_many etc * put constants decl in try/except to make running tests easier * add material_for to graph, rm from db_context (1) the numbers for some mats like keepr of gold & dub mythlit disagree from prod but I'm pretty sure prod is wrong due to having old data mixed in with current data. I updated the query in the graph to guarantee that for evos we only have the most recent data. I also tried to check against padx but padx's data is even more out of date (e.g. keeper of gold is still listed as a jorm ullr mat) so that wasn't useful at all. tests for recent monsters without any old data all agree. (2) this change added a LOT of extra edges incident to some nodes, e.g. dub mythlit has over 700 edges out of it now. but running id dub mythlit does not take noticeable time so this is probably not an issue at all. In the event that it does become an issue in the future we can split the mat_for data into a separate graph (3) I didn't create back edges for mat data because they aren't needed currently and also see (2) and this would double that effect (even if currently it seems to have no noticeable impact) but we could of course do that if it seems useful at some point. * move build_graph, generate_all_monsters back into init * fix evo gem query i think * reformat sql queries * change evo_model.mats to list(filter()) and add test --- dadguide/dadguide.py | 32 ++-- dadguide/database_context.py | 53 +---- dadguide/database_loader.py | 3 - dadguide/database_manager.py | 114 ++--------- dadguide/models/evolution_model.py | 24 +++ dadguide/models/monster_model.py | 138 +++++++++++--- dadguide/monster_graph.py | 244 ++++++++++++++++++++++-- dadguide/old_monster_index.py | 17 +- padinfo/padinfo.py | 62 +++--- tests/dadguide/database_context_test.py | 39 ++-- 10 files changed, 471 insertions(+), 255 deletions(-) create mode 100644 dadguide/models/evolution_model.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 5c0218c9..46180fd1 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -27,26 +27,30 @@ def _data_file(file_name: str) -> str: - # return 'S:\\Documents\\Games\\PAD\\dadguide.sqlite' return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) -CSV_FILE_PATTERN = '{}.csv' -NAMES_EXPORT_PATH = _data_file('computed_names.json') -BASENAMES_EXPORT_PATH = _data_file('base_names.json') -TRANSLATEDNAMES_EXPORT_PATH = _data_file('translated_names.json') +try: + # allow to run the tests locally without being inside a bot instance, which won't + # have any cog data path loaded + CSV_FILE_PATTERN = '{}.csv' + NAMES_EXPORT_PATH = _data_file('computed_names.json') + BASENAMES_EXPORT_PATH = _data_file('base_names.json') + TRANSLATEDNAMES_EXPORT_PATH = _data_file('translated_names.json') -SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/pub?gid={}&single=true&output=csv' -NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') -GROUP_BASENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') -PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') + SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/pub?gid={}&single=true&output=csv' + NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') + GROUP_BASENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') + PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') -NICKNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('nicknames')) -BASENAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('basenames')) -PANTHNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('panthnames')) + NICKNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('nicknames')) + BASENAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('basenames')) + PANTHNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('panthnames')) -DB_DUMP_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/db/dadguide.sqlite' -DB_DUMP_FILE = _data_file('dadguide.sqlite') + DB_DUMP_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/db/dadguide.sqlite' + DB_DUMP_FILE = _data_file('dadguide.sqlite') +except RuntimeError: + pass class Dadguide(commands.Cog): diff --git a/dadguide/database_context.py b/dadguide/database_context.py index a16f11c1..5d45a56f 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -16,27 +16,25 @@ class DbContext(object): def __init__(self, database: DadguideDatabase, graph: MonsterGraph): self.database = database self.graph = graph - self.graph.set_database(self) self.cachedmonsters = None self.expiry = 0 self.max_id = database._max_id() - # self.generate_all_monsters() + self.generate_all_monsters() def generate_all_monsters(self): self.cachedmonsters = {m.monster_id: m for m in self.database.query_many( self.database.select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), (), DgMonster, - as_generator=True, db_context=self, graph=self.graph)} + as_generator=True, graph=self.graph)} self.expiry = int(datetime.now().timestamp()) + 60 * 60 def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' return [r.awoken_skill_id for r in self.database.query_many( - SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True, - db_context=self, graph=self.graph)] + SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True, graph=self.graph)] def get_monsters_by_awakenings(self, awoken_skill_id: int): # TODO: Make this not make monsters via query @@ -51,22 +49,7 @@ def get_monsters_by_awakenings(self, awoken_skill_id: int): distinct=True ), (awoken_skill_id,), - DgMonster, db_context=self, graph=self.graph) - - def monster_in_rem(self, monster_id): - m = self.get_monster(monster_id) - return m is not None and m.rem_egg == 1 - - def monster_in_pem(self, monster_id): - m = self.get_monster(monster_id) - return m is not None and m.pal_egg == 1 - - def monster_in_mp_shop(self, monster_id): - m = self.get_monster(monster_id) - return m is not None and m.buy_mp is not None - - def get_prev_evolution_by_monster(self, monster_id): - return self.graph.get_prev_evolution_by_monster_id(monster_id) + DgMonster, graph=self.graph) def get_next_evolutions_by_monster(self, monster_id): return self.graph.get_next_evolutions_by_monster_id(monster_id) @@ -74,15 +57,6 @@ def get_next_evolutions_by_monster(self, monster_id): def get_base_monster_by_id(self, monster_id): return min(self.graph.get_alt_cards(monster_id)) - def get_evolution_by_material(self, monster_id): - return self.database.query_many( - self.database.select_builder( - tables={DgEvolution.TABLE: DgEvolution.FIELDS}, - where=' OR '.join(['{}.mat_{}_id=?'.format(DgEvolution.TABLE, i) for i in range(1, 6)]) - ), - (monster_id,) * 5, - DgEvolution, db_context=self) - def get_evolution_tree_ids(self, base_monster_id): # is not a tree i lied base_id = base_monster_id @@ -110,18 +84,6 @@ def get_monsters_by_series(self, series_id: int): def get_monsters_by_active(self, active_skill_id: int): return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id) - def get_monster_evo_gem(self, name: str, region='ja'): - gem_suffix = { - 'ja': 'の希石', - 'en': '\'s Gem', - 'ko': ' 의 휘석' - } - if region not in gem_suffix: - return None - gem_name = name + gem_suffix.get(region) - return self.get_first_monster_where( - lambda m: getattr(m, 'name_{}'.format(region)) == gem_name and m.leader_skill_id == 10628) - def _get_monster_query(self, monster_id: int): monster = self.database.select_one_entry_by_pk(monster_id, DgMonster) if monster is not None: @@ -137,8 +99,7 @@ def get_monster(self, monster_id: int): def get_all_monster_ids_query(self, as_generator=True): query = self.database.query_many(self.database.select_builder(tables={DgMonster.TABLE: ('monster_id',)}), (), DictWithAttrAccess, - as_generator=as_generator, - db_context=self, graph=self.graph) + as_generator=as_generator, graph=self.graph) if as_generator: return map(lambda m: m.monster_id, query) return [m.monster_id for m in query] @@ -156,7 +117,7 @@ def get_all_monsters(self, as_generator=True): def get_all_events(self, as_generator=True): return self.database.query_many( self.database.select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), - DgScheduledEvent, db_context=self, + DgScheduledEvent, as_generator=as_generator) def get_dungeon_by_id(self, dungeon_id: int): @@ -170,7 +131,7 @@ def get_base_monster_ids(self): return self.database.query_many( SELECT_BASE_MONSTER_ID, (), - DictWithAttrAccess, db_context=self, + DictWithAttrAccess, as_generator=True) def tokenize_monsters(self): diff --git a/dadguide/database_loader.py b/dadguide/database_loader.py index fcefef04..fbeb5a8c 100644 --- a/dadguide/database_loader.py +++ b/dadguide/database_loader.py @@ -25,7 +25,4 @@ def load_database(existing_db): database = DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) graph = MonsterGraph(database) db_context = DbContext(database, graph) - graph.set_database(db_context) - graph.build_graph() - db_context.generate_all_monsters() return db_context diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 791b8c56..9db029e6 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -125,47 +125,46 @@ def select_builder(tables, key=None, where=None, order=None, distinct=False): query.append(ORDER.format(order=order)) return ' '.join(query) - def query_one(self, query, param, d_type, db_context=None, graph=None): + def query_one(self, query, param, d_type, graph=None): cursor = self._con.cursor() cursor.execute(query, param) res = cursor.fetchone() if res is not None: if issubclass(d_type, DadguideItem): - return d_type(res, db_context, graph=graph) + return d_type(res, graph=graph) else: return d_type(res) return None - def as_generator(self, cursor, d_type, db_context=None, graph=None): + def as_generator(self, cursor, d_type, graph=None): res = cursor.fetchone() while res is not None: if issubclass(d_type, DadguideItem): - yield d_type(res, db_context, graph=graph) + yield d_type(res, graph=graph) else: yield d_type(res) res = cursor.fetchone() - def query_many(self, query, param, d_type, idx_key=None, as_generator=False, - db_context=None, graph=None): + def query_many(self, query, param, d_type, idx_key=None, as_generator=False, graph=None): cursor = self._con.cursor() cursor.execute(query, param) if cursor.rowcount == 0: return [] if as_generator: - return (d_type(res, db_context, graph=graph) + return (d_type(res, graph=graph) if issubclass(d_type, DadguideItem) else d_type(res) for res in cursor.fetchall()) else: if idx_key is None: if issubclass(d_type, DadguideItem): - return [d_type(res, db_context, graph=graph) for res in cursor.fetchall()] + return [d_type(res, graph=graph) for res in cursor.fetchall()] else: return [d_type(res) for res in cursor.fetchall()] else: if issubclass(d_type, DadguideItem): return DictWithAttrAccess( - {res[idx_key]: d_type(res, db_context, graph=graph) for res in cursor.fetchall()}) + {res[idx_key]: d_type(res, graph=graph) for res in cursor.fetchall()}) else: return DictWithAttrAccess({res[idx_key]: d_type(res) for res in cursor.fetchall()}) @@ -174,13 +173,13 @@ def _max_id(self): cursor.execute("SELECT MAX(monster_id) FROM monsters") return cursor.fetchone()['MAX(monster_id)'] - def select_one_entry_by_pk(self, pk, d_type, db_context=None, graph=None): + def select_one_entry_by_pk(self, pk, d_type, graph=None): return self.query_one( self.select_builder( tables={d_type.TABLE: d_type.FIELDS}, where='{}.{}=?'.format(d_type.TABLE, d_type.PK)), (pk,), - d_type, db_context=db_context, graph=graph) + d_type, graph=graph) def get_table_fields(self, table_name: str): # SQL inject vulnerable :v @@ -219,9 +218,8 @@ class DadguideItem(DictWithAttrAccess): PK = None AS_BOOL = () - def __init__(self, item, database, **kwargs): + def __init__(self, item, **kwargs): super(DadguideItem, self).__init__(item) - self._database = database for k in self.AS_BOOL: self[k] = bool(self[k]) @@ -234,8 +232,8 @@ class DgAwakening(DadguideItem): PK = 'awakening_id' AS_BOOL = ['is_super'] - def __init__(self, item, database, **kwargs): - super(DgAwakening, self).__init__(item, database) + def __init__(self, item, **kwargs): + super(DgAwakening, self).__init__(item) @property def name(self): @@ -246,8 +244,8 @@ class DgEvolution(DadguideItem): TABLE = 'evolutions' PK = 'evolution_id' - def __init__(self, item, database, **kwargs): - super(DgEvolution, self).__init__(item, database) + def __init__(self, item, **kwargs): + super(DgEvolution, self).__init__(item) self.evolution_type = EvoType(self.evolution_type) @@ -260,8 +258,8 @@ class DgScheduledEvent(DadguideItem): TABLE = 'schedule' PK = 'event_id' - def __init__(self, item, database, **graph): - super(DgScheduledEvent, self).__init__(item, database) + def __init__(self, item, **graph): + super(DgScheduledEvent, self).__init__(item) @property def open_datetime(self): @@ -285,8 +283,8 @@ class DgMonster(DadguideItem): PK = 'monster_id' AS_BOOL = ('on_jp', 'on_na', 'on_kr', 'has_animation', 'has_hqimage') - def __init__(self, item, database, graph): - super(DgMonster, self).__init__(item, database) + def __init__(self, item, graph): + super(DgMonster, self).__init__(item) self._graph = graph @@ -316,22 +314,13 @@ def __init__(self, item, database, graph): self.is_inheritable = bool(self.inheritable) - self.evo_from_id = self._graph.get_prev_evolution_by_monster_id(self.monster_id) - self.evo_from = None - if self.evo_from_id: - self.evo_from = self._graph.edges[self.evo_from_id, self.monster_id] - self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) - self._alt_version_id_list = sorted(self._graph.get_alt_cards(self.monster_id)) - self._base_monster_id = self._alt_version_id_list[0] - self._alt_evo_id_list = sorted(self._graph.get_evo_tree(self.monster_id)) - self.search = MonsterSearchHelper(self) @property def node(self): - return self._database.graph.nodes[self.monster_id] + return self._graph.nodes[self.monster_id] @property def monster_no(self): @@ -376,51 +365,6 @@ def active_skill(self): def leader_skill(self): return self.node['model'].leader_skill - @property - def cur_evo_type(self): - return EvoType(self.evo_from['evolution_type']) if self.evo_from else EvoType.Base - - @property - def true_evo_type(self): - if self == self.base_monster: - return InternalEvoType.Base - elif 5077 in [dgi.monster_id for dgi in self.mats_for_evo]: - return InternalEvoType.SuperReincarnated - elif 3826 in [dgi.monster_id for dgi in self.mats_for_evo]: - return InternalEvoType.Pixel - elif self.is_equip: - return InternalEvoType.Assist - elif self.cur_evo_type == EvoType.UuvoReincarnated: - return InternalEvoType.Reincarnated - elif self.cur_evo_type == EvoType.UvoAwoken: - return InternalEvoType.Ultimate - else: - return InternalEvoType.Normal - - @property - def mats_for_evo(self): - if self.evo_from is None: - return [] - return [self._database.get_monster(self.evo_from['mat_{}_id'.format(i)]) for i in range(1, 6) if - self.evo_from['mat_{}_id'.format(i)] is not None] - - @property - def evo_gem(self): - return self._database.get_monster_evo_gem(self.name_ja) - - @property - def material_of(self): - mat_of = self._database.get_evolution_by_material(self.monster_id) - return [self._database.get_monster(x.to_id) for x in mat_of] - - @property - def base_monster(self): - return self._database.get_monster(self._base_monster_id) - - @property - def alt_versions(self): - return [self._database.get_monster(a) for a in self._alt_version_id_list] - @property def killers(self): type_to_killers_map = { @@ -447,24 +391,6 @@ def in_mpshop(self): def history_us(self): return '[{}] New Added'.format(self.reg_date) - @property - def next_monster(self): - next = None - offset = 1 - while next is None and self.monster_no + offset <= self._database.max_id: - next = self._database.get_monster(self.monster_no + offset) - offset += 1 - return next - - @property - def prev_monster(self): - next = None - offset = 1 - while next is None and self.monster_no - offset >= 1: - next = self._database.get_monster(self.monster_no - offset) - offset += 1 - return next - def __repr__(self): return "DgMonster<{} ({})>".format(self.name_en, self.monster_no) diff --git a/dadguide/models/evolution_model.py b/dadguide/models/evolution_model.py new file mode 100644 index 00000000..fbb2cca1 --- /dev/null +++ b/dadguide/models/evolution_model.py @@ -0,0 +1,24 @@ +from .base_model import BaseModel + + +class EvolutionModel(BaseModel): + def __init__(self, **kwargs): + self.evolution_type = kwargs['evolution_type'] + self.from_id = kwargs['from_id'] + self.to_id = kwargs['to_id'] + self.mat_1_id = kwargs['mat_1_id'] + self.mat_2_id = kwargs['mat_2_id'] + self.mat_3_id = kwargs['mat_3_id'] + self.mat_4_id = kwargs['mat_4_id'] + self.mat_5_id = kwargs['mat_5_id'] + self.mats = list(filter(None, [self.mat_1_id, self.mat_2_id, self.mat_3_id, self.mat_4_id, self.mat_5_id])) + self.is_pixel = 3826 in self.mats + self.is_super_reincarnated = 5077 in self.mats + self.tstamp = kwargs['tstamp'] + + def to_dict(self): + return { + 'evolution_type': self.evolution_type, + 'from_id': self.from_id, + 'to_id': self.to_id, + } diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index e7d6c49d..7d445f8e 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -7,43 +7,129 @@ class MonsterModel(BaseModel): - def __init__(self, **kwargs): - self.monster_id = kwargs['monster_id'] + def __init__(self, **m): + self.monster_id = m['monster_id'] self.monster_no = self.monster_id - self.monster_no_jp = kwargs['monster_no_jp'] - self.monster_no_na = kwargs['monster_no_na'] - self.monster_no_kr = kwargs['monster_no_kr'] - self.awakenings = kwargs['awakenings'] - self.leader_skill = kwargs['leader_skill'] - self.active_skill = kwargs['active_skill'] - self.series = kwargs['series'] - self.name_ja = kwargs['name_ja'] - self.name_ko = kwargs['name_ko'] - self.name_en = kwargs['name_en'] + self.monster_no_jp = m['monster_no_jp'] + self.monster_no_na = m['monster_no_na'] + self.monster_no_kr = m['monster_no_kr'] + self.awakenings = m['awakenings'] + self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) + self.leader_skill = m['leader_skill'] + self.active_skill = m['active_skill'] + self.series = m['series'] + self.series_id = m['series_id'] + self.name_ja = m['name_ja'] + self.name_ko = m['name_ko'] + self.name_en = m['name_en'] self.roma_subname = None if self.name_en == self.name_ja: self.roma_subname = make_roma_subname(self.name_ja) else: # Remove annoying stuff from NA names, like Jörmungandr self.name_en = tsutils.rmdiacritics(self.name_en) - self.name_en = kwargs['name_en_override'] or self.name_en + self.name_en = m['name_en_override'] or self.name_en - self.type1 = enum_or_none(MonsterType, kwargs['type_1_id']) - self.type2 = enum_or_none(MonsterType, kwargs['type_2_id']) - self.type3 = enum_or_none(MonsterType, kwargs['type_3_id']) + self.type1 = enum_or_none(MonsterType, m['type_1_id']) + self.type2 = enum_or_none(MonsterType, m['type_2_id']) + self.type3 = enum_or_none(MonsterType, m['type_3_id']) self.types = list(filter(None, [self.type1, self.type2, self.type3])) - self.rarity = kwargs['rarity'] - self.is_farmable = kwargs['is_farmable'] - self.in_rem = kwargs['in_rem'] - self.in_pem = kwargs['in_pem'] - self.in_mpshop = kwargs['in_mpshop'] - self.on_jp = kwargs['on_jp'] - self.on_na = kwargs['on_na'] - self.on_kr = kwargs['on_kr'] - self.attr1 = enum_or_none(Attribute, kwargs['attribute_1_id'], Attribute.Nil) - self.attr2 = enum_or_none(Attribute, kwargs['attribute_2_id'], Attribute.Nil) + self.rarity = m['rarity'] + self.is_farmable = m['is_farmable'] + self.in_rem = m['in_rem'] + self.in_pem = m['in_pem'] + self.in_mpshop = m['buy_mp'] is not None + self.buy_mp = m['buy_mp'] + self.sell_gold= m['sell_gold'] + self.sell_mp=m['sell_mp'] + self.reg_date = m['reg_date'] + self.on_jp = m['on_jp'] + self.on_na = m['on_na'] + self.on_kr = m['on_kr'] + self.attr1 = enum_or_none(Attribute, m['attribute_1_id'], Attribute.Nil) + self.attr2 = enum_or_none(Attribute, m['attribute_2_id'], Attribute.Nil) self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) + self.is_inheritable = m['is_inheritable'] + self.evo_gem_id = m['evo_gem_id'] + self.orb_skin_id = m['orb_skin_id'] + self.cost = m['cost'] + self.exp = m['exp'] + self.level = m['level'] + self.limit_mult = m['limit_mult'] + self.latent_slots = m['latent_slots'] + + self.hp_max = m['hp_max'] + self.hp_min = m['hp_min'] + self.hp_scale = m['hp_scale'] + self.atk_max = m['atk_max'] + self.atk_min = m['atk_min'] + self.atk_scale = m['atk_scale'] + self.rcv_max = m['rcv_max'] + self.rcv_min = m['rcv_min'] + self.rcv_scale = m['rcv_scale'] + self.stat_values = { + 'hp': {'min': self.hp_min, 'max': self.hp_max, 'scale': self.hp_scale}, + 'atk': {'min': self.atk_min, 'max': self.atk_max, 'scale': self.atk_scale}, + 'rcv': {'min': self.rcv_min, 'max': self.rcv_max, 'scale': self.rcv_scale} + } + + self.pronunciation_ja = m['pronunciation_ja'] + self.has_animation = m['has_animation'] + self.has_hqimage = m['has_hqimage'] + + @property + def killers(self): + type_to_killers_map = { + MonsterType.God: ['Devil'], + MonsterType.Devil: ['God'], + MonsterType.Machine: ['God', 'Balance'], + MonsterType.Dragon: ['Machine', 'Healer'], + MonsterType.Physical: ['Machine', 'Healer'], + MonsterType.Attacker: ['Devil', 'Physical'], + MonsterType.Healer: ['Dragon', 'Attacker'], + } + if MonsterType.Balance in self.types: + return ['Any'] + killers = set() + for t in self.types: + killers.update(type_to_killers_map.get(t, [])) + return sorted(killers) + + @property + def history_us(self): + return '[{}] New Added'.format(self.reg_date) + + def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): + s_min = float(self.stat_values[key]['min']) + s_max = float(self.stat_values[key]['max']) + if self.level > 1: + s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** self.stat_values[key]['scale'] + else: + s_val = s_min + if lv > 99: + s_val *= 1 + (self.limit_mult / 11 * (lv - 99)) / 100 + plus_dict = {'hp': 10, 'atk': 5, 'rcv': 3} + s_val += plus_dict[key] * max(min(plus, 99), 0) + if inherit: + inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} + if not is_plus_297: + s_val -= plus_dict[key] * max(min(plus, 99), 0) + s_val *= inherit_dict[key] + return int(round(s_val)) + + def stats(self, lv=99, plus=0, inherit=False): + is_plus_297 = False + if plus == 297: + plus = (99, 99, 99) + is_plus_297 = True + elif plus == 0: + plus = (0, 0, 0) + hp = self.stat('hp', lv, plus[0], inherit, is_plus_297) + atk = self.stat('atk', lv, plus[1], inherit, is_plus_297) + rcv = self.stat('rcv', lv, plus[2], inherit, is_plus_297) + weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) + return hp, atk, rcv, weighted def to_dict(self): return { diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 179eee51..b853c0fc 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,39 +1,103 @@ import networkx as nx +from typing import Optional from collections import defaultdict from .database_manager import DgMonster from .database_manager import DadguideDatabase from .database_manager import DgAwakening from .database_manager import DictWithAttrAccess +from .database_manager import EvoType +from .database_manager import InternalEvoType from .models.monster_model import MonsterModel from .models.leader_skill_model import LeaderSkillModel from .models.active_skill_model import ActiveSkillModel from .models.series_model import SeriesModel +from .models.evolution_model import EvolutionModel + +MONSTER_QUERY = """SELECT + monsters.*, + leader_skills.name_ja AS ls_name_ja, + leader_skills.name_en AS ls_name_en, + leader_skills.name_ko AS ls_name_ko, + leader_skills.desc_ja AS ls_desc_ja, + leader_skills.desc_en AS ls_desc_en, + leader_skills.desc_ko AS ls_desc_ko, + leader_skills.max_hp, + leader_skills.max_atk, + leader_skills.max_rcv, + leader_skills.max_rcv, + leader_skills.max_shield, + leader_skills.max_combos, + active_skills.name_ja AS as_name_ja, + active_skills.name_en AS as_name_en, + active_skills.name_ko AS as_name_ko, + active_skills.desc_ja AS as_desc_ja, + active_skills.desc_en AS as_desc_en, + active_skills.desc_ko AS as_desc_ko, + active_skills.turn_max, + active_skills.turn_min, + series.name_ja AS s_name_ja, + series.name_en AS s_name_en, + series.name_ko AS s_name_ko, + exchanges.target_monster_id AS evo_gem_id, + drops.drop_id +FROM + monsters + LEFT OUTER JOIN leader_skills ON monsters.leader_skill_id = leader_skills.leader_skill_id + LEFT OUTER JOIN active_skills ON monsters.active_skill_id = active_skills.active_skill_id + LEFT OUTER JOIN series ON monsters.series_id = series.series_id + LEFT OUTER JOIN monsters AS target_monsters ON monsters.name_ja || 'の希石' == target_monsters.name_ja + LEFT OUTER JOIN exchanges on target_monsters.monster_id = exchanges.target_monster_id + LEFT OUTER JOIN drops ON monsters.monster_id = drops.monster_id +GROUP BY + monsters.monster_id""" + +# make sure we're only looking at the most recent row for any evolution +# since the database might have old data in it still +# group by `to_id` and not `evolution_id` because PAD monsters can only have 1 recipe, and +# the evolution_id changes when data changes so grouping by evolution_id is unhelpful +EVOS_QUERY = """SELECT + evolutions.* +FROM + ( + SELECT + evolution_id, + MAX(tstamp) AS tstamp + FROM + evolutions + GROUP BY + to_id + ) AS latest_evolutions + INNER JOIN evolutions ON evolutions.evolution_id = latest_evolutions.evolution_id + AND evolutions.tstamp = latest_evolutions.tstamp""" + +AWAKENINGS_QUERY = """SELECT + monster_id, + awoken_skills.awoken_skill_id, + is_super, + order_idx, + name_ja, + name_en +FROM + awakenings + JOIN awoken_skills ON awakenings.awoken_skill_id = awoken_skills.awoken_skill_id""" class MonsterGraph(object): def __init__(self, database: DadguideDatabase): self.database = database - self.db_context = None self.graph = None self.graph: nx.DiGraph self.edges = None self.nodes = None - - def set_database(self, db_context): - self.db_context = db_context + self.max_monster_id = -1 + self.build_graph() def build_graph(self): self.graph = nx.DiGraph() - ms = self.database.query_many( - "SELECT monsters.*, leader_skills.name_ja AS ls_name_ja, leader_skills.name_en AS ls_name_en, leader_skills.name_ko AS ls_name_ko, leader_skills.desc_ja AS ls_desc_ja, leader_skills.desc_en AS ls_desc_en, leader_skills.desc_ko AS ls_desc_ko, leader_skills.max_hp, leader_skills.max_atk, leader_skills.max_rcv, leader_skills.max_rcv, leader_skills.max_shield, leader_skills.max_combos, active_skills.name_ja AS as_name_ja, active_skills.name_en AS as_name_en, active_skills.name_ko AS as_name_ko, active_skills.desc_ja AS as_desc_ja, active_skills.desc_en AS as_desc_en, active_skills.desc_ko AS as_desc_ko, active_skills.turn_max, active_skills.turn_min, series.name_ja AS s_name_ja, series.name_en AS s_name_en, series.name_ko AS s_name_ko, drops.drop_id FROM monsters LEFT OUTER JOIN leader_skills ON monsters.leader_skill_id = leader_skills.leader_skill_id LEFT OUTER JOIN active_skills ON monsters.active_skill_id = active_skills.active_skill_id LEFT OUTER JOIN series ON monsters.series_id = series.series_id LEFT OUTER JOIN drops ON monsters.monster_id = drops.monster_id GROUP BY monsters.monster_id", (), DictWithAttrAccess, - db_context=self.db_context, graph=self) - - es = self.database.query_many("SELECT * FROM evolutions", (), DictWithAttrAccess, - db_context=self.db_context, graph=self) - - aws = self.database.query_many("SELECT monster_id, awoken_skills.awoken_skill_id, is_super, order_idx, name_ja, name_en FROM awakenings JOIN awoken_skills ON awakenings.awoken_skill_id=awoken_skills.awoken_skill_id", (), DgAwakening, - db_context=self.db_context, graph=self) + ms = self.database.query_many(MONSTER_QUERY, (), DictWithAttrAccess, graph=self) + es = self.database.query_many(EVOS_QUERY, (), DictWithAttrAccess, graph=self) + aws = self.database.query_many(AWAKENINGS_QUERY, (), DgAwakening, graph=self) mtoawo = defaultdict(list) for a in aws: @@ -79,6 +143,7 @@ def build_graph(self): leader_skill=ls_model, active_skill=as_model, series=s_model, + series_id=m.series_id, attribute_1_id=m.attribute_1_id, attribute_2_id=m.attribute_2_id, name_ja=m.name_ja, @@ -89,13 +154,36 @@ def build_graph(self): is_farmable=m.drop_id is not None, in_pem=m.pal_egg == 1, in_rem=m.rem_egg == 1, - in_mpshop=m.buy_mp is not None, + buy_mp=m.buy_mp, + sell_mp=m.sell_mp, + sell_gold=m.sell_gold, + reg_date=m.reg_date, on_jp=m.on_jp == 1, on_na=m.on_na == 1, on_kr=m.on_kr == 1, type_1_id=m.type_1_id, type_2_id=m.type_2_id, - type_3_id=m.type_3_id + type_3_id=m.type_3_id, + is_inheritable=m.inheritable == 1, + evo_gem_id=m.evo_gem_id, + orb_skin_id=m.orb_skin_id, + cost=m.cost, + level=m.level, + exp=m.exp, + limit_mult=m.limit_mult, + pronunciation_ja=m.pronunciation_ja, + hp_max=m.hp_max, + hp_min=m.hp_min, + hp_scale=m.hp_scale, + atk_max=m.atk_max, + atk_min=m.atk_min, + atk_scale=m.atk_scale, + rcv_max=m.rcv_max, + rcv_min=m.rcv_min, + rcv_scale=m.rcv_scale, + latent_slots=m.latent_slots, + has_animation=m.has_animation == 1, + has_hqimage=m.has_hqimage == 1 ) self.graph.add_node(m.monster_id, model=m_model) @@ -103,9 +191,24 @@ def build_graph(self): self.graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') self.graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') + self.max_monster_id = max(self.max_monster_id, m.monster_id) + for e in es: - self.graph.add_edge(e.from_id, e.to_id, type='evolution', **e) - self.graph.add_edge(e.to_id, e.from_id, type='back_evolution', **e) + evo_model = EvolutionModel(**e) + + self.graph.add_edge( + evo_model.from_id, evo_model.to_id, type='evolution', model=evo_model) + self.graph.add_edge( + evo_model.to_id, evo_model.from_id, type='back_evolution', model=evo_model) + + # for material_of queries + already_used_in_this_evo = [] # don't add same mat more than once per evo + for mat in evo_model.mats: + if mat in already_used_in_this_evo: + continue + self.graph.add_edge( + mat, evo_model.to_id, type="material_of", model=evo_model) + already_used_in_this_evo.append(mat) self.edges = self.graph.edges self.nodes = self.graph.nodes @@ -114,7 +217,17 @@ def build_graph(self): def _get_edges(node, etype): return {mid for mid, edge in node.items() if edge.get('type') == etype} - def get_monster(self, monster_id): + @staticmethod + def _get_edge_model(node, etype): + possible_results = set() + for _, edge in node.items(): + if edge.get('type') == etype: + possible_results.add(edge['model']) + if len(possible_results) == 0: + return None + return sorted(possible_results, key=lambda x: x.tstamp)[-1] + + def get_monster(self, monster_id) -> Optional[MonsterModel]: if monster_id not in self.graph.nodes: return None return self.graph.nodes[monster_id]['model'] @@ -173,6 +286,12 @@ def get_base_id_by_id(self, monster_id): def get_base_monster_by_id(self, monster_id): return self.get_monster(self.get_base_id_by_id(monster_id)) + def monster_is_base_by_id(self, monster_id: int) -> bool: + return self.get_base_id_by_id(monster_id) == monster_id + + def monster_is_base(self, monster: DgMonster) -> bool: + return self.monster_is_base_by_id(monster.monster_no) + def get_numerical_sort_top_id_by_id(self, monster_id): alt_cards = self.get_alt_cards(monster_id) if alt_cards is None: @@ -182,13 +301,65 @@ def get_numerical_sort_top_id_by_id(self, monster_id): def get_numerical_sort_top_monster_by_id(self, monster_id): return self.get_monster(self.get_numerical_sort_top_id_by_id(monster_id)) + def get_evo_by_monster_id(self, monster_id) -> Optional[EvolutionModel]: + return self._get_edge_model(self.graph[monster_id], 'back_evolution') + + def cur_evo_type_by_monster_id(self, monster_id: int) -> EvoType: + prev_evo = self.get_evo_by_monster_id(monster_id) + return EvoType(prev_evo.evolution_type) if prev_evo else EvoType.Base + + def cur_evo_type_by_monster(self, monster: DgMonster) -> EvoType: + return self.cur_evo_type_by_monster_id(monster.monster_no) + + def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: + if self.get_base_id_by_id(monster_id) == monster_id: + return InternalEvoType.Base + + evo = self.get_evo_by_monster_id(monster_id) + if evo is None: + # this is possible without being the above case for transforms + return InternalEvoType.Base + if evo.is_super_reincarnated: + return InternalEvoType.SuperReincarnated + elif evo.is_pixel: + return InternalEvoType.Pixel + + monster = self.get_monster(monster_id) + if monster.is_equip: + return InternalEvoType.Assist + + cur_evo_type = self.cur_evo_type_by_monster_id(monster_id) + if cur_evo_type == EvoType.UuvoReincarnated: + return InternalEvoType.Reincarnated + elif cur_evo_type == EvoType.UvoAwoken: + return InternalEvoType.Ultimate + + return InternalEvoType.Normal + + def true_evo_type_by_monster(self, monster: DgMonster) -> InternalEvoType: + return self.true_evo_type_by_monster_id(monster.monster_no) + def get_prev_evolution_by_monster_id(self, monster_id): bes = self._get_edges(self.graph[monster_id], 'back_evolution') - if bes: return bes.pop() + if bes: + return bes.pop() + return None + + def get_prev_evolution_by_monster(self, monster: DgMonster): + return self.get_prev_evolution_by_monster_id(monster.monster_no) def get_next_evolutions_by_monster_id(self, monster_id): return self._get_edges(self.graph[monster_id], 'evolution') + def evo_mats_by_monster_id(self, monster_id: int) -> list: + evo = self.get_evo_by_monster_id(monster_id) + if evo is None: + return [] + return [self.get_monster(mat) for mat in evo.mats] + + def evo_mats_by_monster(self, monster: DgMonster) -> list: + return self.evo_mats_by_monster_id(monster.monster_no) + # farmable def monster_is_farmable_by_id(self, monster_id): return self.graph.nodes[monster_id]['model'].is_farmable @@ -244,3 +415,38 @@ def monster_is_rem_evo_by_id(self, monster_id): def monster_is_rem_evo(self, monster: DgMonster): return self.monster_is_rem_evo_by_id(monster.monster_no) + + def next_monster_id_by_id(self, monster_id: int) -> Optional[int]: + next_monster = None + offset = 1 + while next_monster is None and monster_id + offset <= self.max_monster_id: + next_monster = self.get_monster(monster_id + offset) + offset += 1 + if next_monster is None: + return None + return next_monster.monster_id + + def prev_monster_id_by_id(self, monster_id) -> Optional[int]: + prev_monster = None + offset = 1 + while prev_monster is None and monster_id - offset >= 1: + prev_monster = self.get_monster(monster_id - offset) + offset += 1 + if prev_monster is None: + return None + return prev_monster.monster_id + + def evo_gem_monster_by_id(self, monster_id) -> Optional[MonsterModel]: + this_monster = self.get_monster(monster_id) + if this_monster.evo_gem_id is None: + return None + return self.get_monster(this_monster.evo_gem_id) + + def evo_gem_monster(self, monster: DgMonster) -> Optional[MonsterModel]: + return self.evo_gem_monster_by_id(monster.monster_no) + + def material_of_ids_by_id(self, monster_id: int) -> list: + return sorted(self._get_edges(self.graph[monster_id], 'material_of')) + + def material_of_ids(self, monster: DgMonster) -> list: + return self.material_of_ids_by_id(monster.monster_no) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 14dc1ef4..0bbef3f8 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -153,8 +153,9 @@ def compute_prefixes(self, m: DgMonster, evotree: list): # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) prefixes.add('chibi') + true_evo_type = self.db_context.graph.true_evo_type_by_monster(m) awoken = lower_name.startswith('awoken') or '覚醒' in lower_name - revo = lower_name.startswith('reincarnated') or '転生' in lower_name + revo = true_evo_type == InternalEvoType.Reincarnated srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name mega = lower_name.startswith('mega awoken') or '極醒' in lower_name awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega @@ -180,22 +181,18 @@ def compute_prefixes(self, m: DgMonster, evotree: list): prefixes.add('super reincarnated') # Prefixes for evo type - if m.cur_evo_type == EvoType.Base: + cur_evo_type = self.db_context.graph.cur_evo_type_by_monster(m) + if cur_evo_type == EvoType.Base: prefixes.add('base') - elif m.cur_evo_type == EvoType.Evo: + elif cur_evo_type == EvoType.Evo: prefixes.add('evo') - elif m.cur_evo_type == EvoType.UvoAwoken and not awoken_or_revo_or_equip_or_mega: + elif cur_evo_type == EvoType.UvoAwoken and not awoken_or_revo_or_equip_or_mega: prefixes.add('uvo') prefixes.add('uevo') - elif m.cur_evo_type == EvoType.UuvoReincarnated and not awoken_or_revo_or_equip_or_mega: + elif cur_evo_type == EvoType.UuvoReincarnated and not awoken_or_revo_or_equip_or_mega: prefixes.add('uuvo') prefixes.add('uuevo') - # True Evo Type Prefixes - if m.true_evo_type == InternalEvoType.Reincarnated: - prefixes.add('revo') - prefixes.add('reincarnated') - # Other Prefixes if self.db_context.graph.monster_is_farmable_evo(m): prefixes.add('farmable') diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7aaa5a15..31a3d85d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -96,21 +96,21 @@ def on_update(self, ctx, selected_emoji): evoID = self.pad_info.settings.checkEvoID(ctx.author.id) self.pad_info.settings.log_emoji(selected_emoji) if evoID: - evos = sorted({*self.m.alt_versions}, key=lambda m: m.monster_id) - index = evos.index(self.m) + evos = sorted({*self.db_context.graph.get_alt_cards(self.m.monster_id)}) + index = evos.index(self.m.monster_id) if selected_emoji == self.pad_info.previous_monster_emoji: - newm = evos[index - 1] + new_id = evos[index - 1] elif selected_emoji == self.pad_info.next_monster_emoji: if index == len(evos) - 1: - newm = evos[0] + new_id = evos[0] else: - newm = evos[index + 1] + new_id = evos[index + 1] else: self.selected_emoji = selected_emoji return True - if newm.monster_id == self.m.monster_id: + if new_id == self.m.monster_id: return False - self.m = newm + self.m = self.db_context.graph.get_monster(new_id) else: if selected_emoji == self.pad_info.previous_monster_emoji: if self.m.prev_monster is None: @@ -124,7 +124,9 @@ def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=sorted({*self.m.alt_versions}, key=lambda x: x.monster_id) if evoID else [], menu_type = 1) + self.emoji_dict = self.pad_info.get_id_emoji_options( + m=self.m, scroll=sorted( + {*self.db_context.graph.get_alt_cards(self.m.monster_id)}) if evoID else [], menu_type=1) return True @@ -424,11 +426,12 @@ async def skillups(self, ctx, *, query: str): await ctx.send(self.makeFailureMsg(err)) async def _do_idmenu(self, ctx, m, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted({*m.alt_versions}, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) - DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database + alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_id) + emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted({*alt_versions}, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) + return await self._do_menu( ctx, starting_menu_emoji, @@ -452,7 +455,7 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): id_embed = monsterToEmbed(m, self.get_emojis(), db_context) evo_embed = monsterToEvoEmbed(m, db_context) - mats_embed = monsterToEvoMatsEmbed(m) + mats_embed = monsterToEvoMatsEmbed(m, db_context) animated = m.has_animation pic_embed = monsterToPicEmbed(m, animated=animated) other_info_embed = monsterToOtherInfoEmbed(m, db_context) @@ -493,7 +496,7 @@ async def _do_evolistmenu(self, ctx, sm): DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database - monsters = sm.alt_versions + monsters = db_context.graph.get_alt_monsters_by_id(sm.monster_id) monsters.sort(key=lambda m: m.monster_id) emoji_to_embed = OrderedDict() @@ -995,55 +998,58 @@ def monsterToEvoEmbed(m: "DgMonster", db_context: "DbContext"): embed = monsterToBaseEmbed(m) alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_no) - if not len(alt_versions) and not m.evo_gem: + evo_gem = db_context.graph.evo_gem_monster(m) + if not len(alt_versions) and not evo_gem: embed.description = 'No alternate evos or evo gem' return embed addEvoListFields(alt_versions, embed, '{} alternate evo(s)') - if not m.evo_gem: + if not evo_gem: return embed - addEvoListFields([m.evo_gem], embed, '{} evo gem(s)') + addEvoListFields([evo_gem], embed, '{} evo gem(s)') return embed -def addMonsterEvoOfList(monster_list, embed, field_name): - if not len(monster_list): +def addMonsterEvoOfList(monster_id_list, embed, field_name, db_context=None): + if not len(monster_id_list): return field_data = '' - if len(monster_list) > 5: - field_data = '{} monsters'.format(len(monster_list)) + if len(monster_id_list) > 5: + field_data = '{} monsters'.format(len(monster_id_list)) else: - item_count = min(len(monster_list), 5) + item_count = min(len(monster_id_list), 5) + monster_list = [db_context.graph.get_monster(m) for m in monster_id_list] for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) embed.add_field(name=field_name, value=field_data) -def monsterToEvoMatsEmbed(m: "DgMonster"): +def monsterToEvoMatsEmbed(m: "DgMonster", db_context: "DbContext"): embed = monsterToBaseEmbed(m) - mats_for_evo = m.mats_for_evo + mats_for_evo = db_context.graph.evo_mats_by_monster(m) field_name = 'Evo materials' field_data = '' if len(mats_for_evo) > 0: - for ae in m.mats_for_evo: + for ae in mats_for_evo: field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) else: field_data = 'None' embed.add_field(name=field_name, value=field_data) - addMonsterEvoOfList(m.material_of, embed, 'Material for') - if not m.evo_gem: + addMonsterEvoOfList(db_context.graph.material_of_ids(m), embed, 'Material for', db_context=db_context) + evo_gem = db_context.graph.evo_gem_monster(m) + if not evo_gem: return embed - addMonsterEvoOfList(m.evo_gem.material_of, embed, "Tree's gem (may not be this evo) is mat for") + addMonsterEvoOfList(db_context.graph.material_of_ids(evo_gem), embed, "Evo gem is mat for", db_context=db_context) return embed def monsterToPantheonEmbed(m: "DgMonster", db_context: "DbContext"): full_pantheon = db_context.get_monsters_by_series(m.series_id) - pantheon_list = list(filter(lambda x: x.evo_from is None, full_pantheon)) + pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) if len(pantheon_list) == 0 or len(pantheon_list) > 6: return None @@ -1228,7 +1234,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list, db_context: "DbContext"): info_row_1 = monsterToTypeString(m) acquire_text = monsterToAcquireString(m, db_context) - tet_text = m.true_evo_type.value + tet_text = db_context.graph.true_evo_type_by_monster(m).value os = "" if m.orb_skin_id is None else " (Orb Skin)" diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index 727e02bd..7b944ba2 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -1,31 +1,40 @@ from dadguide.database_manager import DadguideDatabase from dadguide.database_context import DbContext from dadguide.monster_graph import MonsterGraph +from dadguide.database_manager import EvoType +from dadguide.database_manager import InternalEvoType database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) db_context = DbContext(database, graph) -graph.set_database(db_context) -graph.build_graph() -db_context.generate_all_monsters() # print(ctx.get_awoken_skill_ids()) assert db_context.get_monsters_by_awakenings(5)[0].name_en == 'Crystal Aurora Dragon' assert db_context.graph.monster_is_farmable_by_id(4) assert not db_context.graph.monster_is_farmable_by_id(5156) -print(db_context.monster_in_rem(1073)) # base pandora -print(db_context.monster_in_rem(2121)) -print(db_context.monster_in_pem(2120)) # arthur -assert db_context.monster_in_mp_shop(2256) # odindra -assert not db_context.monster_in_mp_shop(5156) # yusuke -assert db_context.get_prev_evolution_by_monster(1074) == 1073 # evo pandora -assert len(db_context.get_next_evolutions_by_monster(1074)) == 2 # evo pandora -assert db_context.get_base_monster_by_id(1074) == 1073 # evo pandora -assert len(db_context.get_evolution_by_material(5963)) == 3 # raziel gem +assert db_context.get_base_monster_by_id(1074) == 1073 # evo pandora assert 6352 in db_context.get_evolution_tree_ids(1074) assert db_context.get_monsters_by_series(1)[0].name_en == 'Tyrra' assert db_context.get_monsters_by_active(1)[0].name_en == 'Tyrra' -assert db_context.get_monster_evo_gem( - 'Archangel of Knowledge, Raziel', region='en').name_en == 'Archangel of Knowledge, Raziel\'s Gem' -assert db_context.get_monsters_by_awakenings(5)[0].evo_from_id == 26 # check the graph exists in DgMonster +# evo types +# 5392 is mega dkali +assert db_context.graph.get_prev_evolution_by_monster_id(5392) == 1588 +assert db_context.graph.cur_evo_type_by_monster_id(5392) == EvoType.UvoAwoken +assert db_context.graph.cur_evo_type_by_monster_id(1587) == EvoType.Base # base dkali +assert db_context.graph.cur_evo_type_by_monster_id(6238) == EvoType.UuvoReincarnated # b sam3 +assert db_context.graph.cur_evo_type_by_monster_id(3270) == EvoType.UuvoReincarnated # revo hades +assert db_context.graph.cur_evo_type_by_monster_id(3391) == EvoType.UuvoReincarnated # revo blodin +assert db_context.graph.cur_evo_type_by_monster_id(1748) == EvoType.UvoAwoken # awoken hades + +assert db_context.graph.true_evo_type_by_monster_id(5392) == InternalEvoType.Ultimate +assert db_context.graph.true_evo_type_by_monster_id(6337) == InternalEvoType.SuperReincarnated # sr hades +assert db_context.graph.true_evo_type_by_monster_id(5871) == InternalEvoType.Pixel # px sakuya +assert db_context.graph.true_evo_type_by_monster_id(1465) == InternalEvoType.Normal # awoken thoth +assert db_context.graph.true_evo_type_by_monster_id(1464) == InternalEvoType.Base # base thoth + +# check that we're using the most recent data +assert db_context.graph.true_evo_type_by_monster_id(3908) == InternalEvoType.Reincarnated # revo typhon + +assert len(db_context.graph.get_evo_by_monster_id(3).mats) == 2 +assert 153 in db_context.graph.get_evo_by_monster_id(3).mats From 8e626e047b5d855e81415fdcf02b08ae9a1e0cdc Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 2 Nov 2020 15:39:31 -0600 Subject: [PATCH 0255/1220] display numbers as [XXXX] instead of No. XXXX (#199) * display numbers as [XXXX] instead of No. XXXX * verbatim quote uncontroverisal which result --- azurelane/azurelane.py | 2 +- padglobal/padglobal.py | 2 +- padinfo/padinfo.py | 2 +- padmonitor/padmonitor.py | 2 +- padsearch/padsearch.py | 2 +- schoolidol/schoolidol.py | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index 793c881e..ab38d063 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -109,7 +109,7 @@ def make_card_embed(c, image_idx): url = image['url'] file_name = url[url.rfind("/") + 1:] - header = 'No. {} {} - {}'.format(cid, name, image_title) + header = '[{}] {} - {}'.format(cid, name, image_title) image_url = 'https://storage.googleapis.com/mirubot/alimages/raw/{}'.format(file_name) embed = discord.Embed() diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 689af9c3..2ca2c3a1 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -48,7 +48,7 @@ def _data_file(file_name: str) -> str: FARMABLE_MSG = 'This monster is **farmable** so make as many copies of whichever evos you like.' MP_BUY_MSG = ('This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' ', check {}mpdra? for specific recommendations.') -SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution: No. {}, {}.' +SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution: `[{}] {}`.' def mod_help(self, ctx, help_type): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 31a3d85d..50e387c9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -951,7 +951,7 @@ def log_emoji(self, emote): def monsterToHeader(m: "DgMonster", link=False): - msg = 'No. {} {}'.format(m.monster_no_na, m.name_en) + msg = '[{}] {}'.format(m.monster_no_na, m.name_en) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 15345894..f12b89cd 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -63,7 +63,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: - msg += '\n\tNo. {} {}'.format(m.monster_no, m.name_en) + msg += '\n\t[{}] {}'.format(m.monster_no, m.name_en) if tsutils.containsJa( m.name_en) and m.name_en_override != m.name_en and m.name_en_override is not None: msg += ' ({})'.format(m.name_en_override) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 3e22560f..e3ddcf50 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -794,7 +794,7 @@ async def search(self, ctx, *, filter_spec: str): matched_monsters = matched_monsters[0:200] for m in matched_monsters: - msg += '\n\tNo. {} {}'.format(m.monster_no_na, m.name_en) + msg += '\n\t[{}] {}'.format(m.monster_no_na, m.name_en) if dm_required: header += '\nList too long to display; sent via DM' diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index e57ecbc5..9e1a29ad 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -144,9 +144,9 @@ def toHeader(c): collection = c['translated_collection'] name = c['idol']['name'] if collection: - return 'No. {} {} {}'.format(cid, collection, name) + return '[{}] {} {}'.format(cid, collection, name) else: - return 'No. {} {}'.format(cid, name) + return '[{}] {}'.format(cid, name) def get_info_url(c): From ec40ce33c744acf9d04d7dcb1ee65ec373625644 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 2 Nov 2020 18:14:20 -0600 Subject: [PATCH 0256/1220] Delete DGMonster completely & replace with MonsterModel (#200) * remove some now-unused functions from db manager/context * move enum types to enum_types.py from database_manager * create awakening models and stop using DgAwakening there's still a use of DgAwakening in database_context in the one query monsters by awakening function that I didn't delete (yet?), so I didn't delete the class definition of DgAwakening (yet?). I added models for both awakening and awoken_skill, but one references the other since they're coupled pretty tightly * padsearch no longer uses DgMonster temporarily move the SearchHelper to monster_model, a later refactor should move this thing to the actual padsearch cog where it really belongs but for now just stick it into the monster_model file since it doesn't do any db stuff and this is easy * move more stuff to model & out of dbmanager * im just deleting the awakening query * dbcontext doesn't import DgMonster anymore! * bye dgmonster!!!!!!!!!!!!!!!!!!!!! * remove now-unused util functions from dg_manager --- dadguide/dadguide.py | 8 +- dadguide/database_context.py | 67 +---- dadguide/database_manager.py | 330 +----------------------- dadguide/models/awakening_model.py | 21 ++ dadguide/models/awoken_skill_model.py | 23 ++ dadguide/models/enum_types.py | 59 +++++ dadguide/models/monster_model.py | 140 +++++++++- dadguide/monster_graph.py | 54 ++-- dadguide/monster_index.py | 2 +- dadguide/old_monster_index.py | 12 +- padinfo/padinfo.py | 50 ++-- padsearch/padsearch.py | 2 +- tests/dadguide/database_context_test.py | 6 +- 13 files changed, 314 insertions(+), 460 deletions(-) create mode 100644 dadguide/models/awakening_model.py create mode 100644 dadguide/models/awoken_skill_model.py create mode 100644 dadguide/models/enum_types.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 46180fd1..571b1577 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -23,6 +23,8 @@ from .old_monster_index import MonsterIndex from .database_loader import load_database +from .models.monster_model import MonsterModel + logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -106,9 +108,9 @@ async def create_index(self, accept_filter=None): self.panthname_overrides, accept_filter=accept_filter) - def get_monster_by_id(self, monster_id: int): - """Exported function that allows a client cog to get a full DgMonster by monster_id""" - return self.database.get_monster(monster_id) + def get_monster_by_id(self, monster_id: int) -> MonsterModel: + """Exported function that allows a client cog to get a full MonsterModel by monster_id""" + return self.database.graph.get_monster(monster_id) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 5d45a56f..8c06e659 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -4,10 +4,7 @@ from .database_manager import DadguideDatabase from .monster_graph import MonsterGraph from .database_manager import DadguideItem -from .database_manager import DgMonster -from .database_manager import DgAwakening from .database_manager import DgDungeon -from .database_manager import DgEvolution from .database_manager import DictWithAttrAccess from .database_manager import DgScheduledEvent @@ -16,19 +13,6 @@ class DbContext(object): def __init__(self, database: DadguideDatabase, graph: MonsterGraph): self.database = database self.graph = graph - self.cachedmonsters = None - self.expiry = 0 - - self.max_id = database._max_id() - self.generate_all_monsters() - - def generate_all_monsters(self): - self.cachedmonsters = {m.monster_id: m for m in self.database.query_many( - self.database.select_builder(tables={DgMonster.TABLE: DgMonster.FIELDS}), - (), - DgMonster, - as_generator=True, graph=self.graph)} - self.expiry = int(datetime.now().timestamp()) + 60 * 60 def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' @@ -36,27 +20,9 @@ def get_awoken_skill_ids(self): self.database.query_many( SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True, graph=self.graph)] - def get_monsters_by_awakenings(self, awoken_skill_id: int): - # TODO: Make this not make monsters via query - return self.database.query_many( - self.database.select_builder( - tables=OrderedDict({ - DgMonster.TABLE: DgMonster.FIELDS, - DgAwakening.TABLE: None, - }), - where='{}.awoken_skill_id=?;'.format(DgAwakening.TABLE), - key=('monster_id',), - distinct=True - ), - (awoken_skill_id,), - DgMonster, graph=self.graph) - def get_next_evolutions_by_monster(self, monster_id): return self.graph.get_next_evolutions_by_monster_id(monster_id) - def get_base_monster_by_id(self, monster_id): - return min(self.graph.get_alt_cards(monster_id)) - def get_evolution_tree_ids(self, base_monster_id): # is not a tree i lied base_id = base_monster_id @@ -84,32 +50,17 @@ def get_monsters_by_series(self, series_id: int): def get_monsters_by_active(self, active_skill_id: int): return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id) - def _get_monster_query(self, monster_id: int): - monster = self.database.select_one_entry_by_pk(monster_id, DgMonster) - if monster is not None: - self.cachedmonsters[monster.monster_id] = monster - return monster - - def get_monster(self, monster_id: int): - self.refresh_monsters() - if monster_id in self.cachedmonsters: - return self.cachedmonsters.get(monster_id) - return self._get_monster_query(monster_id) - def get_all_monster_ids_query(self, as_generator=True): - query = self.database.query_many(self.database.select_builder(tables={DgMonster.TABLE: ('monster_id',)}), (), - DictWithAttrAccess, - as_generator=as_generator, graph=self.graph) + query = self.database.query_many( + self.database.select_builder(tables={'monsters': ('monster_id',)}), (), + DictWithAttrAccess, + as_generator=as_generator, graph=self.graph) if as_generator: return map(lambda m: m.monster_id, query) return [m.monster_id for m in query] - def refresh_monsters(self): - if self.expiry < datetime.now().timestamp(): - self.generate_all_monsters() - def get_all_monsters(self, as_generator=True): - monsters = (self.get_monster(mid) for mid in self.get_all_monster_ids_query()) + monsters = (self.graph.get_monster(mid) for mid in self.get_all_monster_ids_query()) if not as_generator: return [*monsters] return monsters @@ -134,14 +85,6 @@ def get_base_monster_ids(self): DictWithAttrAccess, as_generator=True) - def tokenize_monsters(self): - tokens = defaultdict(list) - for mid in self.get_all_monster_ids_query(): - monster = self.get_monster(mid) - for token in monster.name_en.split(): - tokens[token.lower()].append(monster) - return tokens - def has_database(self): return self.database.has_database() diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 9db029e6..8fd4cc32 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -1,75 +1,20 @@ -from enum import Enum from datetime import datetime import sqlite3 as lite -import shutil import romkan import logging import pytz -import difflib -import os import re -from collections import OrderedDict, defaultdict, deque +from collections import defaultdict -from redbot.core.utils import AsyncIter -from redbot.core import data_manager import tsutils +from dadguide.models.enum_types import Attribute, MonsterType +from dadguide.models.monster_model import MonsterModel logger = logging.getLogger('red.padbot-cogs.dadguide.database_manager') -class Attribute(Enum): - """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" - Fire = 0 - Water = 1 - Wood = 2 - Light = 3 - Dark = 4 - Unknown = 5 - Nil = 6 - - -class MonsterType(Enum): - Evolve = 0 - Balance = 1 - Physical = 2 - Healer = 3 - Dragon = 4 - God = 5 - Attacker = 6 - Devil = 7 - Machine = 8 - Awoken = 12 - Enhance = 14 - Vendor = 15 - - -class EvoType(Enum): - """Evo types supported by DadGuide. Numbers correspond to their id values.""" - Base = 0 # Represents monsters who didn't require evo - Evo = 1 - UvoAwoken = 2 - UuvoReincarnated = 3 - - -class InternalEvoType(Enum): - """Evo types unsupported by DadGuide.""" - Base = "Base" - Normal = "Normal" - Ultimate = "Ultimate" - Reincarnated = "Reincarnated" - Assist = "Assist" - Pixel = "Pixel" - SuperReincarnated = "Super Reincarnated" - - -class Server(Enum): - JP = 0 - NA = 1 - KR = 2 - - class DadguideTableNotFound(Exception): def __init__(self, table_name): self.message = '{} not found'.format(table_name) @@ -168,11 +113,6 @@ def query_many(self, query, param, d_type, idx_key=None, as_generator=False, gra else: return DictWithAttrAccess({res[idx_key]: d_type(res) for res in cursor.fetchall()}) - def _max_id(self): - cursor = self._con.cursor() - cursor.execute("SELECT MAX(monster_id) FROM monsters") - return cursor.fetchone()['MAX(monster_id)'] - def select_one_entry_by_pk(self, pk, d_type, graph=None): return self.query_one( self.select_builder( @@ -195,13 +135,6 @@ def get_table_fields(self, table_name: str): return fields, pk -def enum_or_none(enum, value, default=None): - if value is not None: - return enum(value) - else: - return default - - class DictWithAttrAccess(dict): def __init__(self, item): super(DictWithAttrAccess, self).__init__(item) @@ -227,19 +160,6 @@ def key(self): return self[self.PK] -class DgAwakening(DadguideItem): - TABLE = 'awakenings' - PK = 'awakening_id' - AS_BOOL = ['is_super'] - - def __init__(self, item, **kwargs): - super(DgAwakening, self).__init__(item) - - @property - def name(self): - return self.name_en if self.name_en is not None else self.name_ja - - class DgEvolution(DadguideItem): TABLE = 'evolutions' PK = 'evolution_id' @@ -278,244 +198,6 @@ def close_datetime(self, value): self.end_timestamp = int(value.timestamp()) -class DgMonster(DadguideItem): - TABLE = 'monsters' - PK = 'monster_id' - AS_BOOL = ('on_jp', 'on_na', 'on_kr', 'has_animation', 'has_hqimage') - - def __init__(self, item, graph): - super(DgMonster, self).__init__(item) - - self._graph = graph - - self.roma_subname = None - if self.name_en == self.name_ja: - self.roma_subname = make_roma_subname(self.name_ja) - else: - # Remove annoying stuff from NA names, like Jörmungandr - self.name_en = tsutils.rmdiacritics(self.name_en) - - self.name_en = self.name_en_override or self.name_en - - self.attr1 = enum_or_none(Attribute, self.attribute_1_id, Attribute.Nil) - self.attr2 = enum_or_none(Attribute, self.attribute_2_id, Attribute.Nil) - - self.type1 = enum_or_none(MonsterType, self.type_1_id) - self.type2 = enum_or_none(MonsterType, self.type_2_id) - self.type3 = enum_or_none(MonsterType, self.type_3_id) - self.types = list(filter(None, [self.type1, self.type2, self.type3])) - - self.in_pem = bool(self.pal_egg) - self.in_rem = bool(self.rem_egg) - - self.awakenings = sorted(self.node['model'].awakenings, key=lambda a: a.order_idx) - - self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) - - self.is_inheritable = bool(self.inheritable) - - self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) - - self.search = MonsterSearchHelper(self) - - @property - def node(self): - return self._graph.nodes[self.monster_id] - - @property - def monster_no(self): - return self.monster_id - - def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): - s_min = float(self[key + '_min']) - s_max = float(self[key + '_max']) - if self.level > 1: - s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** self[key + '_scale'] - else: - s_val = s_min - if lv > 99: - s_val *= 1 + (self.limit_mult / 11 * (lv - 99)) / 100 - plus_dict = {'hp': 10, 'atk': 5, 'rcv': 3} - s_val += plus_dict[key] * max(min(plus, 99), 0) - if inherit: - inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} - if not is_plus_297: - s_val -= plus_dict[key] * max(min(plus, 99), 0) - s_val *= inherit_dict[key] - return int(round(s_val)) - - def stats(self, lv=99, plus=0, inherit=False): - is_plus_297 = False - if plus == 297: - plus = (99, 99, 99) - is_plus_297 = True - elif plus == 0: - plus = (0, 0, 0) - hp = self.stat('hp', lv, plus[0], inherit, is_plus_297) - atk = self.stat('atk', lv, plus[1], inherit, is_plus_297) - rcv = self.stat('rcv', lv, plus[2], inherit, is_plus_297) - weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) - return hp, atk, rcv, weighted - - @property - def active_skill(self): - return self.node['model'].active_skill - - @property - def leader_skill(self): - return self.node['model'].leader_skill - - @property - def killers(self): - type_to_killers_map = { - MonsterType.God: ['Devil'], - MonsterType.Devil: ['God'], - MonsterType.Machine: ['God', 'Balance'], - MonsterType.Dragon: ['Machine', 'Healer'], - MonsterType.Physical: ['Machine', 'Healer'], - MonsterType.Attacker: ['Devil', 'Physical'], - MonsterType.Healer: ['Dragon', 'Attacker'], - } - if MonsterType.Balance in self.types: - return ['Any'] - killers = set() - for t in self.types: - killers.update(type_to_killers_map.get(t, [])) - return sorted(killers) - - @property - def in_mpshop(self): - return self.buy_mp is not None - - @property - def history_us(self): - return '[{}] New Added'.format(self.reg_date) - - def __repr__(self): - return "DgMonster<{} ({})>".format(self.name_en, self.monster_no) - - def __eq__(self, other): - return isinstance(other, DgMonster) and self.monster_id == other.monster_id - - def __hash__(self): - return hash(("DgMonster", self.monster_id)) - - -class MonsterSearchHelper(object): - def __init__(self, m: DgMonster): - - self.name = '{} {}'.format(m.name_en, m.name_ja).lower() - leader_skill = m.leader_skill - self.leader = leader_skill.desc.lower() if leader_skill else '' - active_skill = m.active_skill - self.active_name = active_skill.name.lower() if active_skill else '' - self.active_desc = active_skill.desc.lower() if active_skill else '' - self.active = '{} {}'.format(self.active_name, self.active_desc) - self.active_min = active_skill.turn_min if active_skill else None - self.active_max = active_skill.turn_max if active_skill else None - - self.color = [m.attr1.name.lower()] - self.hascolor = [c.name.lower() for c in [m.attr1, m.attr2] if c] - - self.hp, self.atk, self.rcv, self.weighted_stats = m.stats(lv=110) - - self.types = [t.name for t in m.types] - - def replace_colors(text: str): - return text.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') - - self.leader = replace_colors(self.leader) - self.active = replace_colors(self.active) - self.active_name = replace_colors(self.active_name) - self.active_desc = replace_colors(self.active_desc) - - self.board_change = [] - self.orb_convert = defaultdict(list) - self.row_convert = [] - self.column_convert = [] - - def color_txt_to_list(txt): - txt = txt.replace('and', ' ') - txt = txt.replace(',', ' ') - txt = txt.replace('orbs', ' ') - txt = txt.replace('orb', ' ') - txt = txt.replace('mortal poison', 'mortalpoison') - txt = txt.replace('jammers', 'jammer') - txt = txt.strip() - return txt.split() - - def strip_prev_clause(txt: str, sep: str): - prev_clause_start_idx = txt.find(sep) - if prev_clause_start_idx >= 0: - prev_clause_start_idx += len(sep) - txt = txt[prev_clause_start_idx:] - return txt - - def strip_next_clause(txt: str, sep: str): - next_clause_start_idx = txt.find(sep) - if next_clause_start_idx >= 0: - txt = txt[:next_clause_start_idx] - return txt - - active_desc = self.active_desc - active_desc = active_desc.replace(' rows ', ' row ') - active_desc = active_desc.replace(' columns ', ' column ') - active_desc = active_desc.replace(' into ', ' to ') - active_desc = active_desc.replace('changes orbs to', 'all orbs to') - - board_change_txt = 'all orbs to' - if board_change_txt in active_desc: - txt = strip_prev_clause(active_desc, board_change_txt) - txt = strip_next_clause(txt, 'orbs') - txt = strip_next_clause(txt, ';') - self.board_change = color_txt_to_list(txt) - - txt = active_desc - if 'row' in txt: - parts = re.split(r'\Wand\W|;\W', txt) - for i in range(0, len(parts)): - if 'row' in parts[i]: - self.row_convert.append(strip_next_clause( - strip_prev_clause(parts[i], 'to '), ' orbs')) - - txt = active_desc - if 'column' in txt: - parts = re.split(r'\Wand\W|;\W', txt) - for i in range(0, len(parts)): - if 'column' in parts[i]: - self.column_convert.append(strip_next_clause( - strip_prev_clause(parts[i], 'to '), ' orbs')) - - convert_done = self.board_change or self.row_convert or self.column_convert - - change_txt = 'change ' - if not convert_done and change_txt in active_desc and 'orb' in active_desc: - txt = active_desc - parts = re.split(r'\Wand\W|;\W', txt) - for i in range(0, len(parts)): - parts[i] = strip_prev_clause(parts[i], change_txt) if change_txt in parts[i] else '' - - for part in parts: - sub_parts = part.split(' to ') - if len(sub_parts) > 1: - source_orbs = color_txt_to_list(sub_parts[0]) - dest_orbs = color_txt_to_list(sub_parts[1]) - for so in source_orbs: - for do in dest_orbs: - self.orb_convert[so].append(do) - - -def make_roma_subname(name_ja): - subname = re.sub(r'[=]', '', name_ja) - subname = re.sub(r'[「」]', '・', subname) - adjusted_subname = '' - for part in subname.split('・'): - roma_part = romkan.to_roma(part) - if part != roma_part and not tsutils.containsJa(roma_part): - adjusted_subname += ' ' + roma_part.strip('-') - return adjusted_subname.strip() - - class PotentialMatches(object): def __init__(self): self.match_list = set() @@ -568,7 +250,7 @@ def __init__(self, evolution_tree: list, basename_overrides: list): self.basenames = basename_overrides or self.computed_basenames - def _compute_monster_basename(self, m: DgMonster): + def _compute_monster_basename(self, m: MonsterModel): basename = m.name_en.lower() if ',' in basename: name_parts = basename.split(',') @@ -611,7 +293,7 @@ def count_and_id(): entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] return max(entries)[2] - def _is_low_priority_monster(self, m: DgMonster): + def _is_low_priority_monster(self, m: MonsterModel): lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] lp_substrings = ['tamadra'] lp_min_rarity = 2 @@ -632,7 +314,7 @@ def _is_low_priority_group(self, mg: list): class NamedMonster(object): - def __init__(self, monster: DgMonster, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, db_context=None): + def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, db_context=None): # Must not hold onto monster or monster_group! # Hold on to the IDs instead diff --git a/dadguide/models/awakening_model.py b/dadguide/models/awakening_model.py new file mode 100644 index 00000000..5bba484d --- /dev/null +++ b/dadguide/models/awakening_model.py @@ -0,0 +1,21 @@ +from .base_model import BaseModel +from .awoken_skill_model import AwokenSkillModel + + +class AwakeningModel(BaseModel): + """ + This class represents an awakening belonging to a monster, in contrast to AwokenSkillModel, which represents an "abstract" awoken skill. + """ + def __init__(self, awoken_skill_model: AwokenSkillModel = None, **kwargs): + self.awakening_id = kwargs['awakening_id'] + self.monster_id = kwargs['monster_id'] + self.awoken_skill_id = kwargs['awoken_skill_id'] + self.is_super = kwargs['is_super'] + self.order_idx = kwargs['order_idx'] + self.awoken_skill = awoken_skill_model + self.name = self.awoken_skill.name + + def to_dict(self): + return { + 'awakening_id': self.awakening_id, + } diff --git a/dadguide/models/awoken_skill_model.py b/dadguide/models/awoken_skill_model.py new file mode 100644 index 00000000..30f6fa63 --- /dev/null +++ b/dadguide/models/awoken_skill_model.py @@ -0,0 +1,23 @@ +from .base_model import BaseModel + + +class AwokenSkillModel(BaseModel): + def __init__(self, **kwargs): + self.awoken_skill_id = kwargs['awoken_skill_id'] + self.name_ja = kwargs['name_ja'] + self.name_en = kwargs['name_en'] + self.name_ko = kwargs['name_ko'] + self.name = self.name_en if self.name_en is not None else self.name_ja + self.desc_ja = kwargs['name_ja'] + self.desc_en = kwargs['desc_en'] + self.desc_ja = kwargs['desc_ja'] + self.desc_ko = kwargs['desc_ko'] + self.adj_hp = kwargs['adj_hp'] + self.adj_atk = kwargs['adj_atk'] + self.adj_rcv = kwargs['adj_rcv'] + + def to_dict(self): + return { + 'awoken_skill_id': self.awoken_skill_id, + 'name_en': self.name_en, + } diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py new file mode 100644 index 00000000..0635913c --- /dev/null +++ b/dadguide/models/enum_types.py @@ -0,0 +1,59 @@ +from enum import Enum + + +class Attribute(Enum): + """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" + Fire = 0 + Water = 1 + Wood = 2 + Light = 3 + Dark = 4 + Unknown = 5 + Nil = 6 + + +class MonsterType(Enum): + Evolve = 0 + Balance = 1 + Physical = 2 + Healer = 3 + Dragon = 4 + God = 5 + Attacker = 6 + Devil = 7 + Machine = 8 + Awoken = 12 + Enhance = 14 + Vendor = 15 + + +class EvoType(Enum): + """Evo types supported by DadGuide. Numbers correspond to their id values.""" + Base = 0 # Represents monsters who didn't require evo + Evo = 1 + UvoAwoken = 2 + UuvoReincarnated = 3 + + +class InternalEvoType(Enum): + """Evo types unsupported by DadGuide.""" + Base = "Base" + Normal = "Normal" + Ultimate = "Ultimate" + Reincarnated = "Reincarnated" + Assist = "Assist" + Pixel = "Pixel" + SuperReincarnated = "Super Reincarnated" + + +class Server(Enum): + JP = 0 + NA = 1 + KR = 2 + + +def enum_or_none(enum, value, default=None): + if value is not None: + return enum(value) + else: + return default diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 7d445f8e..24433cd0 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -1,9 +1,13 @@ from .base_model import BaseModel -from ..database_manager import Attribute -from ..database_manager import MonsterType -from ..database_manager import enum_or_none -from ..database_manager import make_roma_subname +from .enum_types import Attribute +from .enum_types import MonsterType +from .enum_types import enum_or_none +from .active_skill_model import ActiveSkillModel +from .leader_skill_model import LeaderSkillModel import tsutils +import re +from collections import defaultdict +import romkan class MonsterModel(BaseModel): @@ -13,10 +17,15 @@ def __init__(self, **m): self.monster_no_jp = m['monster_no_jp'] self.monster_no_na = m['monster_no_na'] self.monster_no_kr = m['monster_no_kr'] + + # these things are literally named backwards atm self.awakenings = m['awakenings'] self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) - self.leader_skill = m['leader_skill'] - self.active_skill = m['active_skill'] + self.leader_skill: LeaderSkillModel = m['leader_skill'] + self.leader_skill_id = self.leader_skill.leader_skill_id if self.leader_skill else None + self.active_skill: ActiveSkillModel = m['active_skill'] + self.active_skill_id = self.active_skill.active_skill_id if self.active_skill else None + self.series = m['series'] self.series_id = m['series_id'] self.name_ja = m['name_ja'] @@ -24,7 +33,7 @@ def __init__(self, **m): self.name_en = m['name_en'] self.roma_subname = None if self.name_en == self.name_ja: - self.roma_subname = make_roma_subname(self.name_ja) + self.roma_subname = self.make_roma_subname(self.name_ja) else: # Remove annoying stuff from NA names, like Jörmungandr self.name_en = tsutils.rmdiacritics(self.name_en) @@ -78,6 +87,8 @@ def __init__(self, **m): self.has_animation = m['has_animation'] self.has_hqimage = m['has_hqimage'] + self.search = MonsterSearchHelper(self) + @property def killers(self): type_to_killers_map = { @@ -131,9 +142,124 @@ def stats(self, lv=99, plus=0, inherit=False): weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) return hp, atk, rcv, weighted + @staticmethod + def make_roma_subname(name_ja): + subname = re.sub(r'[=]', '', name_ja) + subname = re.sub(r'[「」]', '・', subname) + adjusted_subname = '' + for part in subname.split('・'): + roma_part = romkan.to_roma(part) + if part != roma_part and not tsutils.containsJa(roma_part): + adjusted_subname += ' ' + roma_part.strip('-') + return adjusted_subname.strip() + def to_dict(self): return { 'monster_id': self.monster_id, 'name_ja': self.name_ja, 'name_en': self.name_en, } + + +class MonsterSearchHelper(object): + def __init__(self, m: MonsterModel): + + self.name = '{} {}'.format(m.name_en, m.name_ja).lower() + leader_skill = m.leader_skill + self.leader = leader_skill.desc.lower() if leader_skill else '' + active_skill = m.active_skill + self.active_name = active_skill.name.lower() if active_skill else '' + self.active_desc = active_skill.desc.lower() if active_skill else '' + self.active = '{} {}'.format(self.active_name, self.active_desc) + self.active_min = active_skill.turn_min if active_skill else None + self.active_max = active_skill.turn_max if active_skill else None + + self.color = [m.attr1.name.lower()] + self.hascolor = [c.name.lower() for c in [m.attr1, m.attr2] if c] + + self.hp, self.atk, self.rcv, self.weighted_stats = m.stats(lv=110) + + self.types = [t.name for t in m.types] + + def replace_colors(text: str): + return text.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') + + self.leader = replace_colors(self.leader) + self.active = replace_colors(self.active) + self.active_name = replace_colors(self.active_name) + self.active_desc = replace_colors(self.active_desc) + + self.board_change = [] + self.orb_convert = defaultdict(list) + self.row_convert = [] + self.column_convert = [] + + def color_txt_to_list(txt): + txt = txt.replace('and', ' ') + txt = txt.replace(',', ' ') + txt = txt.replace('orbs', ' ') + txt = txt.replace('orb', ' ') + txt = txt.replace('mortal poison', 'mortalpoison') + txt = txt.replace('jammers', 'jammer') + txt = txt.strip() + return txt.split() + + def strip_prev_clause(txt: str, sep: str): + prev_clause_start_idx = txt.find(sep) + if prev_clause_start_idx >= 0: + prev_clause_start_idx += len(sep) + txt = txt[prev_clause_start_idx:] + return txt + + def strip_next_clause(txt: str, sep: str): + next_clause_start_idx = txt.find(sep) + if next_clause_start_idx >= 0: + txt = txt[:next_clause_start_idx] + return txt + + active_desc = self.active_desc + active_desc = active_desc.replace(' rows ', ' row ') + active_desc = active_desc.replace(' columns ', ' column ') + active_desc = active_desc.replace(' into ', ' to ') + active_desc = active_desc.replace('changes orbs to', 'all orbs to') + + board_change_txt = 'all orbs to' + if board_change_txt in active_desc: + text = strip_prev_clause(active_desc, board_change_txt) + text = strip_next_clause(text, 'orbs') + text = strip_next_clause(text, ';') + self.board_change = color_txt_to_list(text) + + text = active_desc + if 'row' in text: + parts = re.split(r'\Wand\W|;\W', text) + for i in range(0, len(parts)): + if 'row' in parts[i]: + self.row_convert.append(strip_next_clause( + strip_prev_clause(parts[i], 'to '), ' orbs')) + + text = active_desc + if 'column' in text: + parts = re.split(r'\Wand\W|;\W', text) + for i in range(0, len(parts)): + if 'column' in parts[i]: + self.column_convert.append(strip_next_clause( + strip_prev_clause(parts[i], 'to '), ' orbs')) + + convert_done = self.board_change or self.row_convert or self.column_convert + + change_txt = 'change ' + if not convert_done and change_txt in active_desc and 'orb' in active_desc: + text = active_desc + parts = re.split(r'\Wand\W|;\W', text) + for i in range(0, len(parts)): + parts[i] = strip_prev_clause(parts[i], change_txt) if change_txt in parts[i] else '' + + for part in parts: + sub_parts = part.split(' to ') + if len(sub_parts) > 1: + source_orbs = color_txt_to_list(sub_parts[0]) + dest_orbs = color_txt_to_list(sub_parts[1]) + for so in source_orbs: + for do in dest_orbs: + self.orb_convert[so].append(do) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index b853c0fc..259fbffe 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,17 +1,16 @@ import networkx as nx from typing import Optional from collections import defaultdict -from .database_manager import DgMonster from .database_manager import DadguideDatabase -from .database_manager import DgAwakening from .database_manager import DictWithAttrAccess -from .database_manager import EvoType -from .database_manager import InternalEvoType +from .models.enum_types import EvoType, InternalEvoType from .models.monster_model import MonsterModel from .models.leader_skill_model import LeaderSkillModel from .models.active_skill_model import ActiveSkillModel from .models.series_model import SeriesModel from .models.evolution_model import EvolutionModel +from .models.awoken_skill_model import AwokenSkillModel +from .models.awakening_model import AwakeningModel MONSTER_QUERY = """SELECT monsters.*, @@ -71,12 +70,11 @@ AND evolutions.tstamp = latest_evolutions.tstamp""" AWAKENINGS_QUERY = """SELECT - monster_id, - awoken_skills.awoken_skill_id, - is_super, - order_idx, - name_ja, - name_en + awakenings.awakening_id, + awakenings.monster_id, + awakenings.is_super, + awakenings.order_idx, + awoken_skills.* FROM awakenings JOIN awoken_skills ON awakenings.awoken_skill_id = awoken_skills.awoken_skill_id""" @@ -97,11 +95,13 @@ def build_graph(self): ms = self.database.query_many(MONSTER_QUERY, (), DictWithAttrAccess, graph=self) es = self.database.query_many(EVOS_QUERY, (), DictWithAttrAccess, graph=self) - aws = self.database.query_many(AWAKENINGS_QUERY, (), DgAwakening, graph=self) + aws = self.database.query_many(AWAKENINGS_QUERY, (), DictWithAttrAccess, graph=self) mtoawo = defaultdict(list) for a in aws: - mtoawo[a.monster_id].append(a) + awoken_skill_model = AwokenSkillModel(**a) + awakening_model = AwakeningModel(awoken_skill_model=awoken_skill_model, **a) + mtoawo[a.monster_id].append(awakening_model) for m in ms: ls_model = LeaderSkillModel(leader_skill_id=m.leader_skill_id, @@ -289,7 +289,7 @@ def get_base_monster_by_id(self, monster_id): def monster_is_base_by_id(self, monster_id: int) -> bool: return self.get_base_id_by_id(monster_id) == monster_id - def monster_is_base(self, monster: DgMonster) -> bool: + def monster_is_base(self, monster: MonsterModel) -> bool: return self.monster_is_base_by_id(monster.monster_no) def get_numerical_sort_top_id_by_id(self, monster_id): @@ -308,7 +308,7 @@ def cur_evo_type_by_monster_id(self, monster_id: int) -> EvoType: prev_evo = self.get_evo_by_monster_id(monster_id) return EvoType(prev_evo.evolution_type) if prev_evo else EvoType.Base - def cur_evo_type_by_monster(self, monster: DgMonster) -> EvoType: + def cur_evo_type_by_monster(self, monster: MonsterModel) -> EvoType: return self.cur_evo_type_by_monster_id(monster.monster_no) def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: @@ -336,7 +336,7 @@ def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: return InternalEvoType.Normal - def true_evo_type_by_monster(self, monster: DgMonster) -> InternalEvoType: + def true_evo_type_by_monster(self, monster: MonsterModel) -> InternalEvoType: return self.true_evo_type_by_monster_id(monster.monster_no) def get_prev_evolution_by_monster_id(self, monster_id): @@ -345,7 +345,7 @@ def get_prev_evolution_by_monster_id(self, monster_id): return bes.pop() return None - def get_prev_evolution_by_monster(self, monster: DgMonster): + def get_prev_evolution_by_monster(self, monster: MonsterModel): return self.get_prev_evolution_by_monster_id(monster.monster_no) def get_next_evolutions_by_monster_id(self, monster_id): @@ -357,63 +357,63 @@ def evo_mats_by_monster_id(self, monster_id: int) -> list: return [] return [self.get_monster(mat) for mat in evo.mats] - def evo_mats_by_monster(self, monster: DgMonster) -> list: + def evo_mats_by_monster(self, monster: MonsterModel) -> list: return self.evo_mats_by_monster_id(monster.monster_no) # farmable def monster_is_farmable_by_id(self, monster_id): return self.graph.nodes[monster_id]['model'].is_farmable - def monster_is_farmable(self, monster: DgMonster): + def monster_is_farmable(self, monster: MonsterModel): return self.monster_is_farmable_by_id(monster.monster_no) def monster_is_farmable_evo_by_id(self, monster_id): return any( m for m in self.get_evo_tree(monster_id) if self.monster_is_farmable_by_id(m)) - def monster_is_farmable_evo(self, monster: DgMonster): + def monster_is_farmable_evo(self, monster: MonsterModel): return self.monster_is_farmable_evo_by_id(monster.monster_no) # mp def monster_is_mp_by_id(self, monster_id): return self.graph.nodes[monster_id]['model'].in_mpshop - def monster_is_mp(self, monster: DgMonster): + def monster_is_mp(self, monster: MonsterModel): return self.monster_is_mp_by_id(monster.monster_no) def monster_is_mp_evo_by_id(self, monster_id): return any( m for m in self.get_evo_tree(monster_id) if self.monster_is_mp_by_id(m)) - def monster_is_mp_evo(self, monster: DgMonster): + def monster_is_mp_evo(self, monster: MonsterModel): return self.monster_is_mp_evo_by_id(monster.monster_no) # pem def monster_is_pem_by_id(self, monster_id): return self.graph.nodes[monster_id]['model'].in_pem - def monster_is_pem(self, monster: DgMonster): + def monster_is_pem(self, monster: MonsterModel): return self.monster_is_pem_by_id(monster.monster_no) def monster_is_pem_evo_by_id(self, monster_id): return any( m for m in self.get_evo_tree(monster_id) if self.monster_is_pem_by_id(m)) - def monster_is_pem_evo(self, monster: DgMonster): + def monster_is_pem_evo(self, monster: MonsterModel): return self.monster_is_pem_evo_by_id(monster.monster_no) # rem def monster_is_rem_by_id(self, monster_id): return self.graph.nodes[monster_id]['model'].in_rem - def monster_is_rem(self, monster: DgMonster): + def monster_is_rem(self, monster: MonsterModel): return self.monster_is_rem_by_id(monster.monster_no) def monster_is_rem_evo_by_id(self, monster_id): return any( m for m in self.get_evo_tree(monster_id) if self.monster_is_rem_by_id(m)) - def monster_is_rem_evo(self, monster: DgMonster): + def monster_is_rem_evo(self, monster: MonsterModel): return self.monster_is_rem_evo_by_id(monster.monster_no) def next_monster_id_by_id(self, monster_id: int) -> Optional[int]: @@ -442,11 +442,11 @@ def evo_gem_monster_by_id(self, monster_id) -> Optional[MonsterModel]: return None return self.get_monster(this_monster.evo_gem_id) - def evo_gem_monster(self, monster: DgMonster) -> Optional[MonsterModel]: + def evo_gem_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: return self.evo_gem_monster_by_id(monster.monster_no) def material_of_ids_by_id(self, monster_id: int) -> list: return sorted(self._get_edges(self.graph[monster_id], 'material_of')) - def material_of_ids(self, monster: DgMonster) -> list: + def material_of_ids(self, monster: MonsterModel) -> list: return self.material_of_ids_by_id(monster.monster_no) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index dc98f47b..f448ca97 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -3,7 +3,7 @@ class MonsterIndex2: - def __init__(self, monsters: 'List[DgMonster]', series: 'List[DgSeries]'): + def __init__(self, monsters: 'List[MonsterModel]', series: 'List[DgSeries]'): self.monsters = monsters self.series = series self.index = defaultdict(list) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 0bbef3f8..e6a4fa94 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -6,10 +6,10 @@ from discord.utils import find as find_first import tsutils -from .database_manager import DgMonster -from .database_manager import Attribute -from .database_manager import EvoType -from .database_manager import InternalEvoType +from .models.monster_model import MonsterModel +from .models.enum_types import Attribute +from .models.enum_types import EvoType +from .models.enum_types import InternalEvoType from .database_manager import PotentialMatches from .database_manager import NamedMonster from .database_manager import NamedMonsterGroup @@ -63,7 +63,7 @@ async def __init__(self, monster_database: DbContext, nickname_overrides, basena async for base_mon in AsyncIter(base_monster_ids): base_id = base_mon.monster_id group_basename_overrides = basename_overrides.get(base_id, []) - evolution_tree = [monster_database.get_monster(m) for m in + evolution_tree = [monster_database.graph.get_monster(m) for m in monster_database.get_evolution_tree_ids(base_id)] named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) for monster in evolution_tree: @@ -127,7 +127,7 @@ def named_monsters_sort(nm: NamedMonster): def init_index(self): pass - def compute_prefixes(self, m: DgMonster, evotree: list): + def compute_prefixes(self, m: MonsterModel, evotree: list): prefixes = set() attr1_short_prefixes = self.attr_short_prefix_map[m.attr1] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 50e387c9..39ef5758 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -80,7 +80,7 @@ def _data_file(file_name: str) -> str: class IdEmojiUpdater(EmojiUpdater): - def __init__(self, emoji_to_embed, m: "DgMonster" = None, + def __init__(self, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, selected_emoji=None, bot=None, db_context: "DbContext" = None): self.emoji_dict = emoji_to_embed @@ -131,7 +131,7 @@ def on_update(self, ctx, selected_emoji): class ScrollEmojiUpdater(EmojiUpdater): - def __init__(self, emoji_to_embed, m: "DgMonster" = None, + def __init__(self, emoji_to_embed, m: "MonsterModel" = None, ms: "List[int]" = None, selected_emoji=None, pad_info=None, bot=None): self.emoji_dict = emoji_to_embed self.m = m @@ -950,12 +950,12 @@ def log_emoji(self, emote): self.save_settings() -def monsterToHeader(m: "DgMonster", link=False): +def monsterToHeader(m: "MonsterModel", link=False): msg = '[{}] {}'.format(m.monster_no_na, m.name_en) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg -def monsterToJaSuffix(m: "DgMonster"): +def monsterToJaSuffix(m: "MonsterModel"): suffix = "" if m.roma_subname: suffix += ' [{}]'.format(m.roma_subname) @@ -964,16 +964,16 @@ def monsterToJaSuffix(m: "DgMonster"): return suffix -def monsterToLongHeader(m: "DgMonster", link=False): +def monsterToLongHeader(m: "MonsterModel", link=False): msg = monsterToHeader(m) + monsterToJaSuffix(m) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg -def monsterToThumbnailUrl(m: "DgMonster"): +def monsterToThumbnailUrl(m: "MonsterModel"): return get_portrait_url(m) -def monsterToBaseEmbed(m: "DgMonster"): +def monsterToBaseEmbed(m: "MonsterModel"): header = monsterToLongHeader(m) embed = discord.Embed() embed.set_thumbnail(url=monsterToThumbnailUrl(m)) @@ -994,7 +994,7 @@ def addEvoListFields(list_of_monsters, embed, name): embed.add_field(name=field_name, value=field_data) -def monsterToEvoEmbed(m: "DgMonster", db_context: "DbContext"): +def monsterToEvoEmbed(m: "MonsterModel", db_context: "DbContext"): embed = monsterToBaseEmbed(m) alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_no) @@ -1025,7 +1025,7 @@ def addMonsterEvoOfList(monster_id_list, embed, field_name, db_context=None): embed.add_field(name=field_name, value=field_data) -def monsterToEvoMatsEmbed(m: "DgMonster", db_context: "DbContext"): +def monsterToEvoMatsEmbed(m: "MonsterModel", db_context: "DbContext"): embed = monsterToBaseEmbed(m) mats_for_evo = db_context.graph.evo_mats_by_monster(m) @@ -1047,7 +1047,7 @@ def monsterToEvoMatsEmbed(m: "DgMonster", db_context: "DbContext"): return embed -def monsterToPantheonEmbed(m: "DgMonster", db_context: "DbContext"): +def monsterToPantheonEmbed(m: "MonsterModel", db_context: "DbContext"): full_pantheon = db_context.get_monsters_by_series(m.series_id) pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) if len(pantheon_list) == 0 or len(pantheon_list) > 6: @@ -1064,7 +1064,7 @@ def monsterToPantheonEmbed(m: "DgMonster", db_context: "DbContext"): return embed -def monsterToSkillupsEmbed(m: "DgMonster", db_context: "DbContext"): +def monsterToSkillupsEmbed(m: "MonsterModel", db_context: "DbContext"): if m.active_skill is None: return None possible_skillups_list = db_context.get_monsters_by_active(m.active_skill.active_skill_id) @@ -1093,11 +1093,11 @@ def monsterToSkillupsEmbed(m: "DgMonster", db_context: "DbContext"): return embed -def monsterToPicUrl(m: "DgMonster"): +def monsterToPicUrl(m: "MonsterModel"): return get_pic_url(m) -def monsterToPicEmbed(m: "DgMonster", animated=False): +def monsterToPicEmbed(m: "MonsterModel", animated=False): embed = monsterToBaseEmbed(m) url = monsterToPicUrl(m) embed.set_image(url=url) @@ -1114,23 +1114,23 @@ def monsterToPicEmbed(m: "DgMonster", animated=False): return embed -def monsterToVideoUrl(m: "DgMonster", link_text='(MP4)'): +def monsterToVideoUrl(m: "MonsterModel", link_text='(MP4)'): return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) -def monsterToGifUrl(m: "DgMonster", link_text='(GIF)'): +def monsterToGifUrl(m: "MonsterModel", link_text='(GIF)'): return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) -def monsterToOrbSkinUrl(m: "DgMonster", link_text='Regular'): +def monsterToOrbSkinUrl(m: "MonsterModel", link_text='Regular'): return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) -def monsterToOrbSkinCBUrl(m: "DgMonster", link_text='Color Blind'): +def monsterToOrbSkinCBUrl(m: "MonsterModel", link_text='Color Blind'): return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) -def monsterToGifEmbed(m: "DgMonster"): +def monsterToGifEmbed(m: "MonsterModel"): embed = monsterToBaseEmbed(m) url = monsterToGifUrl(m) embed.set_image(url=url) @@ -1139,7 +1139,7 @@ def monsterToGifEmbed(m: "DgMonster"): return embed -def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): +def monstersToLsEmbed(left_m: "MonsterModel", right_m: "MonsterModel"): lls = left_m.leader_skill rls = right_m.leader_skill @@ -1168,7 +1168,7 @@ def monstersToLsEmbed(left_m: "DgMonster", right_m: "DgMonster"): return embed -def monstersToLssEmbed(m: "DgMonster"): +def monstersToLssEmbed(m: "MonsterModel"): ls = m.leader_skill if ls: @@ -1189,18 +1189,18 @@ def monstersToLssEmbed(m: "DgMonster"): return embed -def monsterToHeaderEmbed(m: "DgMonster"): +def monsterToHeaderEmbed(m: "MonsterModel"): header = monsterToLongHeader(m, link=True) embed = discord.Embed() embed.description = header return embed -def monsterToTypeString(m: "DgMonster"): +def monsterToTypeString(m: "MonsterModel"): return '/'.join([t.name for t in m.types]) -def monsterToAcquireString(m: "DgMonster", db_context: "DbContext"): +def monsterToAcquireString(m: "MonsterModel", db_context: "DbContext"): acquire_text = None if db_context.graph.monster_is_farmable(m) and not db_context.graph.monster_is_mp_evo(m): # Some MP shop monsters 'drop' in PADR @@ -1229,7 +1229,7 @@ def match_emoji(emoji_list, name): return name -def monsterToEmbed(m: "DgMonster", emoji_list, db_context: "DbContext"): +def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): embed = monsterToBaseEmbed(m) info_row_1 = monsterToTypeString(m) @@ -1317,7 +1317,7 @@ def monsterToEmbed(m: "DgMonster", emoji_list, db_context: "DbContext"): return embed -def monsterToOtherInfoEmbed(m: "DgMonster", db_context: "DbContext"): +def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext"): embed = monsterToBaseEmbed(m) # Clear the thumbnail, takes up too much space embed.set_thumbnail(url='') diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index e3ddcf50..875c08f4 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -518,7 +518,7 @@ def __init__(self, lexer, db_context: "DbContext"): self.filters.append(lambda m: m.search.active_min and m.search.active_min <= self.cd) if self.farmable: - self.filters.append(lambda m: self.db_context.graph.is_monster_farmable_evo(m)) + self.filters.append(lambda m: self.db_context.graph.monster_is_farmable_evo(m)) if self.haste: text = "charge allies' skill by {}".format(self.haste) diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index 7b944ba2..acdcb4b8 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -1,18 +1,16 @@ from dadguide.database_manager import DadguideDatabase from dadguide.database_context import DbContext from dadguide.monster_graph import MonsterGraph -from dadguide.database_manager import EvoType -from dadguide.database_manager import InternalEvoType +from dadguide.models.enum_types import EvoType, InternalEvoType database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) db_context = DbContext(database, graph) # print(ctx.get_awoken_skill_ids()) -assert db_context.get_monsters_by_awakenings(5)[0].name_en == 'Crystal Aurora Dragon' assert db_context.graph.monster_is_farmable_by_id(4) assert not db_context.graph.monster_is_farmable_by_id(5156) -assert db_context.get_base_monster_by_id(1074) == 1073 # evo pandora +assert db_context.graph.get_base_monster_by_id(1074).monster_id == 1073 # evo pandora assert 6352 in db_context.get_evolution_tree_ids(1074) assert db_context.get_monsters_by_series(1)[0].name_en == 'Tyrra' assert db_context.get_monsters_by_active(1)[0].name_en == 'Tyrra' From ae03096856867169f0f0017aea6d984304ec117d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 2 Nov 2020 18:18:32 -0600 Subject: [PATCH 0257/1220] don't store self.db_context (#201) --- padsearch/padsearch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 875c08f4..0a571dbe 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -418,7 +418,6 @@ def build(self, **kwargs): class SearchConfig(object): def __init__(self, lexer, db_context: "DbContext"): - self.db_context = db_context self.all = False self.cd = None self.farmable = None @@ -518,7 +517,7 @@ def __init__(self, lexer, db_context: "DbContext"): self.filters.append(lambda m: m.search.active_min and m.search.active_min <= self.cd) if self.farmable: - self.filters.append(lambda m: self.db_context.graph.monster_is_farmable_evo(m)) + self.filters.append(lambda m: db_context.graph.monster_is_farmable_evo(m)) if self.haste: text = "charge allies' skill by {}".format(self.haste) From 3c909b19440f61e8b6e505af448e0d5a6ea24ec2 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 3 Nov 2020 10:10:19 -0600 Subject: [PATCH 0258/1220] fix idmode number scroll to use graph (#202) --- padinfo/padinfo.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 39ef5758..0eaa2c37 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -113,13 +113,15 @@ def on_update(self, ctx, selected_emoji): self.m = self.db_context.graph.get_monster(new_id) else: if selected_emoji == self.pad_info.previous_monster_emoji: - if self.m.prev_monster is None: + prev_monster_id = self.db_context.graph.prev_monster_id_by_id(self.m.monster_id) + if prev_monster_id is None: return False - self.m = self.m.prev_monster + self.m = self.db_context.graph.get_monster(prev_monster_id) elif selected_emoji == self.pad_info.next_monster_emoji: - if self.m.next_monster is None: + next_monster_id = self.db_context.graph.next_monster_id_by_id(self.m.monster_id) + if next_monster_id is None: return False - self.m = self.m.next_monster + self.m = self.db_context.graph.get_monster(next_monster_id) else: self.selected_emoji = selected_emoji return True From ba31c58e00f583b0c0e396b4e82dac9b341a6659 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 4 Nov 2020 08:04:07 -0700 Subject: [PATCH 0259/1220] New ChannelMod (#203) * New Channlemod * opt into multimod * Fix docstring * Ticket 12 and 49 --- channelmod/channelmod.py | 68 ++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 2f8ffb80..161153e2 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -4,10 +4,10 @@ import re import time import traceback +from typing import Optional from datetime import datetime from io import BytesIO -from redbot.core import checks -from redbot.core import commands +from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline from tsutils import CogSettings, auth_check, box, replace_emoji_names_with_code, fix_emojis_for_server @@ -29,6 +29,9 @@ def __init__(self, bot, *args, **kwargs): self.settings = ChannelModSettings("channelmod") self.channel_last_spoke = {} + self.config = Config.get_conf(self, identifier=3747737700) + self.config.register_channel(multiedit=False) + GACOG = self.bot.get_cog("GlobalAdmin") if GACOG: self.bot.get_cog("GlobalAdmin").register_perm("channelmod") @@ -60,24 +63,36 @@ async def addmirror(self, ctx, source_channel_id: int, dest_channel_id: int, doc @channelmod.command() @checks.is_owner() - async def rmmirror(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): + async def rmmirror(self, ctx, source_channel_id: int, dest_channel_id: int): """Remove mirroring between two channels.""" - if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): - await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) + success = self.settings.rm_mirrored_channel(source_channel_id, dest_channel_id) + if not success: + await ctx.send("That isn't an existing mirror.") return - self.settings.rm_mirrored_channel(source_channel_id, dest_channel_id) await ctx.tick() @channelmod.command() @checks.is_owner() - async def mirrorconfig(self, ctx): + async def multiedit(self, ctx, channel: Optional[discord.TextChannel], enable: bool): + """Opt in a channel to multi-edit mode.""" + if channel is None: + channel = ctx.channel + await self.config.channel(channel).multiedit.set(multiedit) + await ctx.tick() + + @channelmod.command() + @checks.is_owner() + async def mirrorconfig(self, ctx, server_id: int = None): """List mirror config.""" mirrored_channels = self.settings.mirrored_channels() msg = 'Mirrored channels\n' for mc_id, config in mirrored_channels.items(): + if server_id is not None and mc_id != server_id and server_id not in config['channels']: + continue channel = self.bot.get_channel(mc_id) - channel_name = channel.name if channel else 'unknown' - msg += '\n{} ({})'.format(mc_id, channel_name) + channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' + multiedit = await self.config.channel(discord.Object(id=mc_id)).multiedit() + msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) for channel_id in config['channels']: channel = self.bot.get_channel(channel_id) channel_name = channel.name if channel else 'unknown' @@ -148,9 +163,12 @@ async def catchup(self, ctx, channel, from_message, to_message=None): @commands.Cog.listener('on_message_without_command') async def mirror_msg(self, message): - if message.author.id == self.bot.user.id: + author = message.author + + if author.bot: return + channel = message.channel mirrored_channels = self.settings.get_mirrored_channels(channel.id) @@ -161,12 +179,19 @@ async def mirror_msg(self, message): now_time = datetime.utcnow() last_spoke_time = datetime.utcfromtimestamp( last_spoke_timestamp) if last_spoke_timestamp else now_time - attribution_required = last_spoke != message.author.id + attribution_required = last_spoke != author.id attribution_required |= (now_time - last_spoke_time).total_seconds() > ATTRIBUTION_TIME_SECONDS - self.settings.set_last_spoke(channel.id, message.author.id) + self.settings.set_last_spoke(channel.id, author.id) attachment_bytes = None filename = None + + if await self.config.channel(message.channel).multiedit(): + await message.delete() + idmess = await message.channel.send("Pending...") + message = await message.channel.send(message.content, files=[await a.to_file() for a in message.attachments]) + await idmess.edit(content=str(message.id)) + if message.attachments: # If we know we're copying a message and that message has an attachment, # pre download it and reuse it for every upload. @@ -230,14 +255,14 @@ async def mirror_msg_delete(self, message): @commands.Cog.listener('on_reaction_add') async def mirror_reaction_add(self, reaction, user): message = reaction.message - if message.author.id != user.id: + if message.author.id != user.id and not await self.config.channel(message.channel).multiedit(): return await self.mirror_msg_mod(message, new_message_reaction=reaction.emoji) @commands.Cog.listener('on_reaction_remove') async def mirror_reaction_remove(self, reaction, user): message = reaction.message - if message.author.id != user.id: + if message.author.id != user.id and not await self.config.channel(message.channel).multiedit(): return await self.mirror_msg_mod(message, delete_message_reaction=reaction.emoji) @@ -246,7 +271,7 @@ async def mirror_msg_mod(self, message, delete_message_content: bool = False, new_message_reaction=None, delete_message_reaction=None): - if message.author.id == self.bot.user.id or isinstance(message.channel, discord.abc.PrivateChannel): + if isinstance(message.channel, discord.abc.PrivateChannel): return channel = message.channel @@ -266,11 +291,17 @@ async def mirror_msg_mod(self, message, fcontent = await self.mformat(new_message_content, channel, dest_message.channel) await dest_message.edit(content=fcontent) elif new_message_reaction: - await dest_message.add_reaction(new_message_reaction) + try: + await dest_message.add_reaction(new_message_reaction) + except discord.HTTPException: + pass elif delete_message_content: await dest_message.delete() elif delete_message_reaction: - await dest_message.remove_reaction(delete_message_reaction, dest_message.guild.me) + try: + await dest_message.remove_reaction(delete_message_reaction, dest_message.guild.me) + except discord.HTTPException: + pass except Exception as ex: logger.exception('Failed to mirror message edit from {} to {}:'.format(channel.id, dest_channel_id)) @@ -410,6 +441,9 @@ def rm_mirrored_channel(self, source_channel: int, dest_channel: int): if not dest_channel_config: channels.pop(source_channel) self.save_settings() + return True + else: + return False def add_mirrored_message(self, source_channel: int, source_message: str, dest_channel: int, dest_message: str): channel_config = self.mirrored_channels()[source_channel] From 525c020cba4d3bf3e75d934f60ba9905763e0ddb Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 4 Nov 2020 08:28:30 -0700 Subject: [PATCH 0260/1220] [Channelmod] fix mirrorconfig from one server --- channelmod/channelmod.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 161153e2..29436359 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -85,9 +85,16 @@ async def multiedit(self, ctx, channel: Optional[discord.TextChannel], enable: b async def mirrorconfig(self, ctx, server_id: int = None): """List mirror config.""" mirrored_channels = self.settings.mirrored_channels() + gchs = set() + if server_id: + guild = self.bot.get_guild(server_id) + if guild is None: + await ctx.send("Invalid server id.") + return + gchs = {c.id for c in self.bot.get_guild(server_id).channels} msg = 'Mirrored channels\n' for mc_id, config in mirrored_channels.items(): - if server_id is not None and mc_id != server_id and server_id not in config['channels']: + if server_id is not None and mc_id not in gchs and not gchs.intersection(config['channels']): continue channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' From d2b7bd410b9264bc7f6554839e2a1a5694b6a034 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 4 Nov 2020 08:40:30 -0700 Subject: [PATCH 0261/1220] Channelmod Infinite Loop fix and edit credits (#206) --- channelmod/channelmod.py | 5 +++-- feedback/feedback.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 29436359..9b48f817 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -252,8 +252,9 @@ async def mirror_msg(self, message): attachment_bytes.close() @commands.Cog.listener('on_message_edit') - async def mirror_msg_edit(self, message, new_message): - await self.mirror_msg_mod(message, new_message_content=new_message.content) + async def mirror_msg_edit(self, before, after): + if before.content != after.content: + await self.mirror_msg_mod(message, new_message_content=after.content) @commands.Cog.listener('on_message_delete') async def mirror_msg_delete(self, message): diff --git a/feedback/feedback.py b/feedback/feedback.py index 54e33ea5..6a7ddd35 100644 --- a/feedback/feedback.py +++ b/feedback/feedback.py @@ -70,7 +70,6 @@ async def credits(self, ctx): "This is an instance of [the Red Discord bot]({}), " "use the '{}info' command for more info. " "The various PAD related cogs were created by Aradia Megido and tactical_retreat. " - "Massive overhaul of the bot to Red v3 by Aradia Megido. " "This bot was created for the [PAD Community Server Discord]({}) but " "is available for other servers on request." "".format(red_repo, ctx.prefix, rpad_invite)) From 8d5a4b5bdfe24a8cc08943e41cb63514eebeea8a Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 4 Nov 2020 09:14:16 -0700 Subject: [PATCH 0262/1220] guildmirrorconfig (#207) Get the channelmod config for a specific server/guild * channelmirrorconfig * remove unnecessary code * docstring typo --- channelmod/channelmod.py | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 9b48f817..a3d662c5 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -99,11 +99,57 @@ async def mirrorconfig(self, ctx, server_id: int = None): channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' multiedit = await self.config.channel(discord.Object(id=mc_id)).multiedit() + msg += '\n\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) + for channel_id in config['channels']: + if server_id is not None and mc_id not in gchs and not channel_id not in gchs: + continue + channel = self.bot.get_channel(channel_id) + channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' + msg += '\n\t{} ({})'.format(channel_id, channel_name) + msg += '\n\n* indicates multi-edit' + await ctx.send(box(msg)) + + + @channelmod.command() + @checks.is_owner() + async def guildmirrorconfig(self, ctx, server_id: int): + """List mirror config for a guild.""" + mirrored_channels = self.settings.mirrored_channels() + gchs = set() + if server_id: + guild = self.bot.get_guild(server_id) + if guild is None: + await ctx.send("Invalid server id.") + return + gchs = {c.id for c in self.bot.get_guild(server_id).channels} + msg = 'Mirrored channels\n\n' + msg += 'From:' + for mc_id, config in mirrored_channels.items(): + if mc_id not in gchs: + continue + channel = self.bot.get_channel(mc_id) + channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' + multiedit = await self.config.channel(discord.Object(id=mc_id)).multiedit() + msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) + for channel_id in config['channels']: + channel = self.bot.get_channel(channel_id) + channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' + msg += '\n\t{} ({})'.format(channel_id, channel_name) + msg += '\n\nTo: ' + for mc_id, config in mirrored_channels.items(): + if not gchs.intersection(config['channels']): + continue + channel = self.bot.get_channel(mc_id) + channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' + multiedit = await self.config.channel(discord.Object(id=mc_id)).multiedit() msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) for channel_id in config['channels']: + if channel_id not in gchs: + continue channel = self.bot.get_channel(channel_id) channel_name = channel.name if channel else 'unknown' msg += '\n\t{} ({})'.format(channel_id, channel_name) + msg += '\n\n* indicates multi-edit' await ctx.send(box(msg)) @channelmod.command() From bca29a0f3cde0f9e61637f8b82d4c85997ffd922 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 4 Nov 2020 10:50:05 -0600 Subject: [PATCH 0263/1220] add name_en_override to MonsterModel, fix padmonitor (#208) --- dadguide/models/monster_model.py | 3 ++- padmonitor/padmonitor.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 24433cd0..8b99ae0b 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -37,7 +37,8 @@ def __init__(self, **m): else: # Remove annoying stuff from NA names, like Jörmungandr self.name_en = tsutils.rmdiacritics(self.name_en) - self.name_en = m['name_en_override'] or self.name_en + self.name_en_override = m['name_en_override'] + self.name_en = self.name_en_override or self.name_en self.type1 = enum_or_none(MonsterType, m['type_1_id']) self.type2 = enum_or_none(MonsterType, m['type_2_id']) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index f12b89cd..892f8bf7 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -4,9 +4,13 @@ import discord from io import BytesIO from redbot.core import checks, commands -from redbot.core.utils.chat_formatting import box, inline, pagify +from redbot.core.utils.chat_formatting import box, pagify + +from typing import TYPE_CHECKING logger = logging.getLogger('red.padbot-cogs.padmonitor') +if TYPE_CHECKING: + from dadguide.database_context import DbContext class PadMonitor(commands.Cog): @@ -40,15 +44,16 @@ async def check_seen_loop(self): async def check_seen(self): """Refresh the monster indexes.""" - pg_cog = self.bot.get_cog('Dadguide') - await pg_cog.wait_until_ready() - all_monsters = pg_cog.database.get_all_monsters(as_generator=False) + DGCOG = self.bot.get_cog('Dadguide') + await DGCOG.wait_until_ready() + db_context: "DbContext" = DGCOG.database + all_monsters = db_context.get_all_monsters(as_generator=False) jp_monster_map = {m.monster_no: m for m in all_monsters if m.on_jp} na_monster_map = {m.monster_no: m for m in all_monsters if m.on_na} def process(existing, new_map, name): if not existing: - logger.info('preloading', len(new_map)) + logger.info('preloading %i', len(new_map)) existing.extend(new_map.keys()) self.settings.save_settings() return None From d4ffe7f17e0e1a2ffc3f4d682b37a23b21862753 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 4 Nov 2020 09:59:09 -0700 Subject: [PATCH 0264/1220] [channelmod] split guild config to/from into sep messages (#209) --- channelmod/channelmod.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index a3d662c5..e6611c3d 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -8,8 +8,8 @@ from datetime import datetime from io import BytesIO from redbot.core import checks, commands, Config -from redbot.core.utils.chat_formatting import inline -from tsutils import CogSettings, auth_check, box, replace_emoji_names_with_code, fix_emojis_for_server +from redbot.core.utils.chat_formatting import inline, pagify, box +from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server logger = logging.getLogger('red.misc-cogs.channelmod') @@ -122,8 +122,7 @@ async def guildmirrorconfig(self, ctx, server_id: int): await ctx.send("Invalid server id.") return gchs = {c.id for c in self.bot.get_guild(server_id).channels} - msg = 'Mirrored channels\n\n' - msg += 'From:' + msg = 'From:' for mc_id, config in mirrored_channels.items(): if mc_id not in gchs: continue @@ -135,7 +134,10 @@ async def guildmirrorconfig(self, ctx, server_id: int): channel = self.bot.get_channel(channel_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' msg += '\n\t{} ({})'.format(channel_id, channel_name) - msg += '\n\nTo: ' + for page in pagify(msg): + await ctx.send(box(page)) + + msg = 'To:' for mc_id, config in mirrored_channels.items(): if not gchs.intersection(config['channels']): continue @@ -149,8 +151,11 @@ async def guildmirrorconfig(self, ctx, server_id: int): channel = self.bot.get_channel(channel_id) channel_name = channel.name if channel else 'unknown' msg += '\n\t{} ({})'.format(channel_id, channel_name) - msg += '\n\n* indicates multi-edit' - await ctx.send(box(msg)) + for page in pagify(msg): + await ctx.send(box(page)) + + await ctx.send(inline('* indicates multi-edit')) + @channelmod.command() async def countreactions(self, ctx, message: discord.Message): @@ -300,7 +305,7 @@ async def mirror_msg(self, message): @commands.Cog.listener('on_message_edit') async def mirror_msg_edit(self, before, after): if before.content != after.content: - await self.mirror_msg_mod(message, new_message_content=after.content) + await self.mirror_msg_mod(before, new_message_content=after.content) @commands.Cog.listener('on_message_delete') async def mirror_msg_delete(self, message): From 4a1fb110e7a0b9e280e3d7a0c4004da81eacdef5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 4 Nov 2020 11:52:39 -0600 Subject: [PATCH 0265/1220] improve naming of numeric next/prev functions (#204) --- dadguide/monster_graph.py | 16 ++++++++++++++-- padinfo/padinfo.py | 12 ++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 259fbffe..226dfddd 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -416,7 +416,7 @@ def monster_is_rem_evo_by_id(self, monster_id): def monster_is_rem_evo(self, monster: MonsterModel): return self.monster_is_rem_evo_by_id(monster.monster_no) - def next_monster_id_by_id(self, monster_id: int) -> Optional[int]: + def numeric_next_monster_id_by_id(self, monster_id: int) -> Optional[int]: next_monster = None offset = 1 while next_monster is None and monster_id + offset <= self.max_monster_id: @@ -426,7 +426,13 @@ def next_monster_id_by_id(self, monster_id: int) -> Optional[int]: return None return next_monster.monster_id - def prev_monster_id_by_id(self, monster_id) -> Optional[int]: + def numeric_next_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: + next_monster_id = self.numeric_next_monster_id_by_id(monster.monster_no) + if next_monster_id is None: + return None + return self.get_monster(next_monster_id) + + def numeric_prev_monster_id_by_id(self, monster_id) -> Optional[int]: prev_monster = None offset = 1 while prev_monster is None and monster_id - offset >= 1: @@ -436,6 +442,12 @@ def prev_monster_id_by_id(self, monster_id) -> Optional[int]: return None return prev_monster.monster_id + def numeric_prev_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: + prev_monster_id = self.numeric_prev_monster_id_by_id(monster.monster_no) + if prev_monster_id is None: + return None + return self.get_monster(prev_monster_id) + def evo_gem_monster_by_id(self, monster_id) -> Optional[MonsterModel]: this_monster = self.get_monster(monster_id) if this_monster.evo_gem_id is None: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0eaa2c37..0ff21918 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -113,15 +113,15 @@ def on_update(self, ctx, selected_emoji): self.m = self.db_context.graph.get_monster(new_id) else: if selected_emoji == self.pad_info.previous_monster_emoji: - prev_monster_id = self.db_context.graph.prev_monster_id_by_id(self.m.monster_id) - if prev_monster_id is None: + prev_monster = self.db_context.graph.numeric_prev_monster(self.m) + if prev_monster is None: return False - self.m = self.db_context.graph.get_monster(prev_monster_id) + self.m = prev_monster elif selected_emoji == self.pad_info.next_monster_emoji: - next_monster_id = self.db_context.graph.next_monster_id_by_id(self.m.monster_id) - if next_monster_id is None: + next_monster = self.db_context.graph.numeric_next_monster(self.m) + if next_monster is None: return False - self.m = self.db_context.graph.get_monster(next_monster_id) + self.m = next_monster else: self.selected_emoji = selected_emoji return True From c2eae9a4df312a7b06c23098ef4a767034148b5d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 5 Nov 2020 14:11:10 -0600 Subject: [PATCH 0266/1220] clean up linting errors in padinfo (#210) --- padinfo/padinfo.py | 49 +++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0ff21918..5b6cd101 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -18,6 +18,12 @@ from .find_monster import prefix_to_filter +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dadguide.database_context import DbContext + from dadguide.models.monster_model import MonsterModel + logger = logging.getLogger('red.padbot-cogs.padinfo') HELP_MSG = """ @@ -90,7 +96,7 @@ def __init__(self, emoji_to_embed, m: "MonsterModel" = None, self.bot = bot self.db_context = db_context - self.pad_info.settings.log_emoji("start_"+selected_emoji) + self.pad_info.settings.log_emoji("start_" + selected_emoji) def on_update(self, ctx, selected_emoji): evoID = self.pad_info.settings.checkEvoID(ctx.author.id) @@ -167,6 +173,7 @@ def on_update(self, ctx, selected_emoji): class PadInfo(commands.Cog): """Info for PAD Cards""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -306,12 +313,13 @@ async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): async def send_survey_after(self, ctx, query, result): sm = await self.config.user(ctx.author).survey_mode() - sms = [1, await self.config.sometimes_perc()/100, 0][sm] + sms = [1, await self.config.sometimes_perc() / 100, 0][sm] if random.random() < sms: params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': result}) url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params await asyncio.sleep(1) - userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", yemoji=char_to_emoji('y'), nemoji=char_to_emoji('n')) + userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", + yemoji=char_to_emoji('y'), nemoji=char_to_emoji('n')) if userres is True: await self.config.good.set(await self.config.good() + 1) elif userres is False: @@ -343,7 +351,7 @@ async def checkbadness(self, ctx): """Check how good id is according to end users""" good = await self.config.good() bad = await self.config.bad() - await ctx.send(f"{bad}/{good+bad} ({int(round(bad/(good+bad)*100)) if good or bad else 'NaN'}%)") + await ctx.send(f"{bad}/{good + bad} ({int(round(bad / (good + bad) * 100)) if good or bad else 'NaN'}%)") @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -432,7 +440,8 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): db_context = DGCOG.database alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_id) - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=sorted({*alt_versions}, key=lambda m: m.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type = 1) + emoji_to_embed = self.get_id_emoji_options( + m=m, scroll=sorted({*alt_versions}, key=lambda x: x.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type=1) return await self._do_menu( ctx, @@ -481,7 +490,6 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): # IdEmojiUpdater won't allow it, however they have to be defined # so that the buttons display in the first place - if len(scroll) > 1 and menu_type != 1: emoji_to_embed[self.first_monster_emoji] = None if len(scroll) != 1: @@ -499,7 +507,7 @@ async def _do_evolistmenu(self, ctx, sm): db_context = DGCOG.database monsters = db_context.graph.get_alt_monsters_by_id(sm.monster_id) - monsters.sort(key=lambda m: m.monster_id) + monsters.sort(key=lambda x: x.monster_id) emoji_to_embed = OrderedDict() for idx, m in enumerate(monsters): @@ -594,7 +602,7 @@ async def seriesscroll(self, ctx, *, query: str): m, err, debug_info = await self.findMonster(query) ms = DGCOG.database.get_monsters_by_series(m.series.series_id) - ms.sort(key=lambda m: m.monster_id) + ms.sort(key=lambda x: x.monster_id) ms = [m for m in ms if m.sell_mp >= 100] if not ms: @@ -638,7 +646,8 @@ async def leaderskill(self, ctx, *, whole_query): for sep in ('"', '/', ',', ' '): if sep in whole_query: - left_query, *right_query = [x.strip() for x in whole_query.split(sep) if x.strip()] or ('', '') # or in case of ^ls [sep] which is empty list + left_query, *right_query = [x.strip() for x in whole_query.split(sep) if x.strip()] or ( + '', '') # or in case of ^ls [sep] which is empty list # split on first separator, with if x.strip() block to prevent null values from showing up, mainly for quotes support # right query is the rest of query but in list form because of how .strip() works. bring it back to string form with ' '.join right_query = ' '.join(q for q in right_query) @@ -691,7 +700,6 @@ async def leaderskillsingle(self, ctx, *, query): await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) - @commands.command(aliases=['helppic', 'helpimg']) @checks.bot_has_permissions(embed_links=True) async def helpid(self, ctx): @@ -820,7 +828,8 @@ def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] - def makeFailureMsg(self, err): + @staticmethod + def makeFailureMsg(err): msg = ('Lookup failed: {}.\n' 'Try one of , , [argbld]/[rgbld] . ' 'Unexpected results? Use ^helpid for more info.').format(err) @@ -1170,6 +1179,7 @@ def monstersToLsEmbed(left_m: "MonsterModel", right_m: "MonsterModel"): return embed + def monstersToLssEmbed(m: "MonsterModel"): ls = m.leader_skill @@ -1238,12 +1248,12 @@ def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): acquire_text = monsterToAcquireString(m, db_context) tet_text = db_context.graph.true_evo_type_by_monster(m).value - os = "" if m.orb_skin_id is None else " (Orb Skin)" + orb_skin = "" if m.orb_skin_id is None else " (Orb Skin)" info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format( m.rarity, db_context.graph.get_base_monster_by_id(m.monster_no).rarity, - os, + orb_skin, m.cost ) @@ -1311,9 +1321,10 @@ def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): evos_header = "Alternate Evos" evos_body = ", ".join(f"**{m2.monster_id}**" - if m2.monster_id==m.monster_id + if m2.monster_id == m.monster_id else f"[{m2.monster_id}]({get_pdx_url(m2)})" - for m2 in sorted({*db_context.graph.get_alt_monsters_by_id(m.monster_no)}, key=lambda m: m.monster_id)) + for m2 in + sorted({*db_context.graph.get_alt_monsters_by_id(m.monster_no)}, key=lambda x: x.monster_id)) embed.add_field(name=evos_header, value=evos_body, inline=False) return embed @@ -1451,17 +1462,19 @@ def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=Non hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 def fmtNum(val): - return ('{:.2f}').format(val).strip('0').rstrip('.') + return '{:.2f}'.format(val).strip('0').rstrip('.') text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) if resist1 > 0 or resist2 > 0: text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) return text + def createSingleMultiplierText(hp, atk, rcv, resist): def fmtNum(val): - return ('{:.2f}').format(val).strip('0').rstrip('.') + return '{:.2f}'.format(val).strip('0').rstrip('.') + text = "{}/{}/{}".format(fmtNum(hp), fmtNum(atk), fmtNum(rcv)) if resist > 0: - text += ' Resist {}%'.format(fmtNum(100 * (resist))) + text += ' Resist {}%'.format(fmtNum(100 * resist)) return text From e8109dbc805d64b38f86d7019902a7cdf060661c Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 5 Nov 2020 13:44:59 -0700 Subject: [PATCH 0267/1220] [evolist] Fix trees with 11 entries & don't crash on more This fixes Ney, who currently has 11 entries, and anticipates trees eventually having more than 11 entries in them. The fallback for trees with more than 11 entries is using the ^evoscroll command. --- padinfo/padinfo.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5b6cd101..b7ef83ad 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -511,7 +511,14 @@ async def _do_evolistmenu(self, ctx, sm): emoji_to_embed = OrderedDict() for idx, m in enumerate(monsters): - emoji = char_to_emoji(str(idx)) + # FIXME: This is not sustainable! Figure out a fix + if idx == 10: + emoji = "\N{KEYCAP TEN}" + elif idx > 10: + await ctx.send("There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.prefix)) + return + else: + emoji = char_to_emoji(str(idx)) emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis(), db_context) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji From dce020135757cdfa0b843995c773bc0f24dce72d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 5 Nov 2020 15:01:49 -0600 Subject: [PATCH 0268/1220] fix evoscroll not working after MonsterModel change (#212) --- dadguide/monster_graph.py | 3 +++ padinfo/padinfo.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 226dfddd..e05c04e8 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -277,6 +277,9 @@ def get_alt_monsters_by_id(self, monster_id): ids = self.get_alt_cards(monster_id) return [self.get_monster(m_id) for m_id in ids] + def get_alt_monsters(self, monster: MonsterModel): + return self.get_alt_monsters_by_id(monster.monster_id) + def get_base_id_by_id(self, monster_id): alt_cards = self.get_alt_cards(monster_id) if alt_cards is None: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b7ef83ad..370eaf37 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -628,10 +628,12 @@ async def seriesscroll(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evoscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" + DGCOG = self.bot.get_cog("Dadguide") + db_context: "DbContext" = DGCOG.database m, err, debug_info = await self.findMonster(query) if m is not None: - await self._do_scrollmenu(ctx, m, sorted({*m.alt_versions}, key=lambda x: x.monster_id), self.id_emoji) + await self._do_scrollmenu(ctx, m, sorted(db_context.graph.get_alt_monsters(m), key=lambda x: x.monster_id), self.id_emoji) else: await ctx.send(self.makeFailureMsg(err)) From 60b7c0965d3c9879a558a17f3360e344ff3dbade Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 5 Nov 2020 17:04:12 -0700 Subject: [PATCH 0269/1220] Fix Ney even more (#214) * Fix Padinfo * Fix Ney even more --- padinfo/padinfo.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 370eaf37..695f72ab 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -511,14 +511,12 @@ async def _do_evolistmenu(self, ctx, sm): emoji_to_embed = OrderedDict() for idx, m in enumerate(monsters): - # FIXME: This is not sustainable! Figure out a fix - if idx == 10: - emoji = "\N{KEYCAP TEN}" - elif idx > 10: + chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" + if idx > 19: await ctx.send("There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.prefix)) return else: - emoji = char_to_emoji(str(idx)) + emoji = char_to_emoji(chars[idx]) emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis(), db_context) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji From b94e21c6394b43198c31c051dce9cd267fb99403 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 6 Nov 2020 09:41:18 -0600 Subject: [PATCH 0270/1220] [dadguide] fix BaseModel __repr__ (#215) --- dadguide/models/base_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/models/base_model.py b/dadguide/models/base_model.py index 0883ed30..4df9c912 100644 --- a/dadguide/models/base_model.py +++ b/dadguide/models/base_model.py @@ -11,6 +11,6 @@ def to_string(self): def __repr__(self): try: - print(self.to_string()) + return self.to_string() except NotImplementedError: - super() + return super().__repr__() From 501f60fd038cded5a4c47ea0e4ee2694046d15e0 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 6 Nov 2020 13:00:39 -0700 Subject: [PATCH 0271/1220] Use MultiDiGraph for Dadguide (#216) * Fix Padinfo * Fix Ney even more * MultiGraph --- dadguide/monster_graph.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index e05c04e8..12843be2 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -91,7 +91,7 @@ def __init__(self, database: DadguideDatabase): self.build_graph() def build_graph(self): - self.graph = nx.DiGraph() + self.graph = nx.MultiDiGraph() ms = self.database.query_many(MONSTER_QUERY, (), DictWithAttrAccess, graph=self) es = self.database.query_many(EVOS_QUERY, (), DictWithAttrAccess, graph=self) @@ -215,14 +215,15 @@ def build_graph(self): @staticmethod def _get_edges(node, etype): - return {mid for mid, edge in node.items() if edge.get('type') == etype} + return {mid for mid, atlas in node.items() for edge in atlas.values() if edge.get('type') == etype} @staticmethod def _get_edge_model(node, etype): possible_results = set() - for _, edge in node.items(): - if edge.get('type') == etype: - possible_results.add(edge['model']) + for atlas in node.values(): + for edge in atlas.values(): + if edge.get('type') == etype: + possible_results.add(edge['model']) if len(possible_results) == 0: return None return sorted(possible_results, key=lambda x: x.tstamp)[-1] From 3d5ed5d207295995a5a27a10c583e4db5b21ab92 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 7 Nov 2020 17:41:52 -0700 Subject: [PATCH 0272/1220] Add Voice ID to MonsterModel and Fix Awakenings Order (#217) --- dadguide/models/monster_model.py | 5 ++++- dadguide/monster_graph.py | 6 ++++-- padinfo/padinfo.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 8b99ae0b..f46f15ea 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -19,7 +19,7 @@ def __init__(self, **m): self.monster_no_kr = m['monster_no_kr'] # these things are literally named backwards atm - self.awakenings = m['awakenings'] + self.awakenings = sorted(m['awakenings'], key=lambda a: a.order_idx) self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) self.leader_skill: LeaderSkillModel = m['leader_skill'] self.leader_skill_id = self.leader_skill.leader_skill_id if self.leader_skill else None @@ -84,6 +84,9 @@ def __init__(self, **m): 'rcv': {'min': self.rcv_min, 'max': self.rcv_max, 'scale': self.rcv_scale} } + self.voice_id_jp = m['voice_id_jp'] + self.voice_id_na = m['voice_id_na'] + self.pronunciation_ja = m['pronunciation_ja'] self.has_animation = m['has_animation'] self.has_hqimage = m['has_hqimage'] diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 12843be2..f3a37bd1 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -172,6 +172,8 @@ def build_graph(self): exp=m.exp, limit_mult=m.limit_mult, pronunciation_ja=m.pronunciation_ja, + voice_id_jp=m.voice_id_jp, + voice_id_na=m.voice_id_na, hp_max=m.hp_max, hp_min=m.hp_min, hp_scale=m.hp_scale, @@ -183,8 +185,8 @@ def build_graph(self): rcv_scale=m.rcv_scale, latent_slots=m.latent_slots, has_animation=m.has_animation == 1, - has_hqimage=m.has_hqimage == 1 - ) + has_hqimage=m.has_hqimage == 1, + ) self.graph.add_node(m.monster_id, model=m_model) if m.linked_monster_id: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 695f72ab..b623902d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -736,7 +736,7 @@ async def padsay(self, ctx, server, *, query: str = None): if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: - await ctx.send(inline("No voice file found for " + m.name)) + await ctx.send(inline("No voice file found for " + m.name_en)) return base_dir = self.settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) From b1b191816f4e45d3eade857f45910a64a86a63b0 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 8 Nov 2020 22:54:42 -0700 Subject: [PATCH 0273/1220] Improve setemojiserver usage (#222) --- padglobal/padglobal.py | 39 ++++++++++++++++++++++++++++++++------- padinfo/padinfo.py | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 2ca2c3a1..c0ec543f 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -893,14 +893,39 @@ async def rmadmin(self, ctx, user): return await ctx.tick() - @padglobal.command() + @padglobal.group() + @checks.is_owner() + async def emojiservers(self, ctx): + """Emoji server subcommand""" + + @emojiservers.command(name="add") + @checks.is_owner() + async def es_add(self, ctx, server_id: int): + """Add the emoji server by ID""" + ess = self.settings.emojiServers() + if server_id not in ess: + ess.append(server_id) + self.settings.save_settings() + await ctx.tick() + + @emojiservers.command(name="remove", aliases=['rm', 'del']) + @checks.is_owner() + async def es_rm(self, ctx, server_id: int): + """Remove the emoji server by ID""" + ess = self.settings.emojiServers() + if server_id not in ess: + await ctx.send("That emoji server is not set.") + return + ess.remove(server_id) + self.settings.save_settings() + await ctx.tick() + + @emojiservers.command(name="list", aliases=['show']) @checks.is_owner() - async def setemojiservers(self, ctx, *, emoji_servers=''): - """Set the emoji servers by ID (csv)""" - self.settings.emojiServers().clear() - if emoji_servers: - self.settings.setEmojiServers(re.findall(r'\d+', emoji_servers)) - await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) + async def es_show(self, ctx): + """List the emoji servers by ID""" + ess = self.settings.emojiServers() + await ctx.send(box("\n".join(str(s) for s in ess))) def _get_emojis(self): emojis = list() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b623902d..e5be9f2e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -787,14 +787,39 @@ async def survey(self, ctx, value): async def padinfo(self, ctx): """PAD info management""" - @padinfo.command() + @padinfo.group() + @checks.is_owner() + async def emojiservers(self, ctx): + """Emoji server subcommand""" + + @emojiservers.command(name="add") + @checks.is_owner() + async def es_add(self, ctx, server_id: int): + """Add the emoji server by ID""" + ess = self.settings.emojiServers() + if server_id not in ess: + ess.append(server_id) + self.settings.save_settings() + await ctx.tick() + + @emojiservers.command(name="remove", aliases=['rm', 'del']) + @checks.is_owner() + async def es_rm(self, ctx, server_id: int): + """Remove the emoji server by ID""" + ess = self.settings.emojiServers() + if server_id not in ess: + await ctx.send("That emoji server is not set.") + return + ess.remove(server_id) + self.settings.save_settings() + await ctx.tick() + + @emojiservers.command(name="list", aliases=['show']) @checks.is_owner() - async def setemojiservers(self, ctx, *, emoji_servers=''): - """Set the emoji servers by ID (csv)""" - self.settings.emojiServers().clear() - if emoji_servers: - self.settings.setEmojiServers(emoji_servers.split(',')) - await ctx.send(inline('Set {} servers'.format(len(self.settings.emojiServers())))) + async def es_show(self, ctx): + """List the emoji servers by ID""" + ess = self.settings.emojiServers() + await ctx.send(box("\n".join(str(s) for s in ess))) @padinfo.command() @checks.is_owner() From 6230868d7d1f28fb3b11e483a6ca8abe6f57c06d Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 8 Nov 2020 23:14:47 -0700 Subject: [PATCH 0274/1220] Add fodder EXP in other info (#218) --- dadguide/models/monster_model.py | 1 + dadguide/monster_graph.py | 1 + padinfo/padinfo.py | 1 + 3 files changed, 3 insertions(+) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index f46f15ea..259bd00a 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -65,6 +65,7 @@ def __init__(self, **m): self.orb_skin_id = m['orb_skin_id'] self.cost = m['cost'] self.exp = m['exp'] + self.fodder_exp = m['fodder_exp'] self.level = m['level'] self.limit_mult = m['limit_mult'] self.latent_slots = m['latent_slots'] diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index f3a37bd1..ad5c994c 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -170,6 +170,7 @@ def build_graph(self): cost=m.cost, level=m.level, exp=m.exp, + fodder_exp=m.fodder_exp, limit_mult=m.limit_mult, pronunciation_ja=m.pronunciation_ja, voice_id_jp=m.voice_id_jp, diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e5be9f2e..e31f8c80 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1406,6 +1406,7 @@ def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext"): xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' body_text += '\n**XP to Max:** {}'.format(xp_text) body_text += ' **Max Level:**: {}'.format(m.level) + body_text += '\n**Fodder EXP:** {:,}'.format(m.fodder_exp) body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) embed.description = body_text From 89d1ac3261deaf78fbaab7db0e920046675fea91 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 9 Nov 2020 23:48:50 -0700 Subject: [PATCH 0275/1220] Add rem and pem data to monsters (#220) --- dadguide/monster_graph.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index ad5c994c..4844257d 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,4 +1,5 @@ -import networkx as nx +import networkx +import json from typing import Optional from collections import defaultdict from .database_manager import DadguideDatabase @@ -44,8 +45,8 @@ LEFT OUTER JOIN leader_skills ON monsters.leader_skill_id = leader_skills.leader_skill_id LEFT OUTER JOIN active_skills ON monsters.active_skill_id = active_skills.active_skill_id LEFT OUTER JOIN series ON monsters.series_id = series.series_id - LEFT OUTER JOIN monsters AS target_monsters ON monsters.name_ja || 'の希石' == target_monsters.name_ja - LEFT OUTER JOIN exchanges on target_monsters.monster_id = exchanges.target_monster_id + LEFT OUTER JOIN monsters AS target_monsters ON monsters.name_ja || 'の希石' = target_monsters.name_ja + LEFT OUTER JOIN exchanges ON target_monsters.monster_id = exchanges.target_monster_id LEFT OUTER JOIN drops ON monsters.monster_id = drops.monster_id GROUP BY monsters.monster_id""" @@ -79,23 +80,31 @@ awakenings JOIN awoken_skills ON awakenings.awoken_skill_id = awoken_skills.awoken_skill_id""" +EGG_QUERY = """SELECT + d_egg_machine_types.name AS type, + egg_machines.* +FROM + egg_machines + JOIN d_egg_machine_types ON d_egg_machine_types.egg_machine_type_id = egg_machines.egg_machine_type_id +""" class MonsterGraph(object): def __init__(self, database: DadguideDatabase): self.database = database self.graph = None - self.graph: nx.DiGraph + self.graph: networkx.MultiDiGraph self.edges = None self.nodes = None self.max_monster_id = -1 self.build_graph() def build_graph(self): - self.graph = nx.MultiDiGraph() + self.graph = networkx.MultiDiGraph() ms = self.database.query_many(MONSTER_QUERY, (), DictWithAttrAccess, graph=self) es = self.database.query_many(EVOS_QUERY, (), DictWithAttrAccess, graph=self) aws = self.database.query_many(AWAKENINGS_QUERY, (), DictWithAttrAccess, graph=self) + ems = self.database.query_many(EGG_QUERY, (), DictWithAttrAccess, graph=self) mtoawo = defaultdict(list) for a in aws: @@ -103,6 +112,14 @@ def build_graph(self): awakening_model = AwakeningModel(awoken_skill_model=awoken_skill_model, **a) mtoawo[a.monster_id].append(awakening_model) + mtoegg = defaultdict(lambda: {'pem': False, 'rem': False}) + for e in ems: + data = json.loads(e.contents) + type = 'pem' if e.type == "PEM" else 'rem' + for m in data: + id = int(m[1:-1]) # Remove parentheses + mtoegg[id][type] = True + for m in ms: ls_model = LeaderSkillModel(leader_skill_id=m.leader_skill_id, name_ja=m.ls_name_ja, @@ -152,8 +169,8 @@ def build_graph(self): name_en_override=m.name_en_override, rarity=m.rarity, is_farmable=m.drop_id is not None, - in_pem=m.pal_egg == 1, - in_rem=m.rem_egg == 1, + in_pem=mtoegg[m.monster_id]['pem'], + in_rem=mtoegg[m.monster_id]['rem'], buy_mp=m.buy_mp, sell_mp=m.sell_mp, sell_gold=m.sell_gold, From 405290aaa97d6f712ab15f3e515d6de6134a5ce4 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 10 Nov 2020 17:19:51 -0700 Subject: [PATCH 0276/1220] Fix evos and also make it look super nice (#225) --- padinfo/padinfo.py | 57 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e31f8c80..6ef2672a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -465,7 +465,7 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): db_context = DGCOG.database id_embed = monsterToEmbed(m, self.get_emojis(), db_context) - evo_embed = monsterToEvoEmbed(m, db_context) + evo_embed = monsterToEvoEmbed(m, self.get_emojis(), db_context) mats_embed = monsterToEvoMatsEmbed(m, db_context) animated = m.has_animation pic_embed = monsterToPicEmbed(m, animated=animated) @@ -998,9 +998,9 @@ def monsterToHeader(m: "MonsterModel", link=False): return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg -def monsterToJaSuffix(m: "MonsterModel"): +def monsterToJaSuffix(m: "MonsterModel", subname_on_override=True): suffix = "" - if m.roma_subname: + if m.roma_subname and (subname_on_override or m.name_en_override is None): suffix += ' [{}]'.format(m.roma_subname) if not m.on_na: suffix += ' (JP only)' @@ -1012,6 +1012,13 @@ def monsterToLongHeader(m: "MonsterModel", link=False): return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg +def monsterToEvoHeader(m: "MonsterModel", emoji_list, link=True): + prefix = f" {monster_attr_emoji(emoji_list, m)} " + msg = f"{m.monster_no_na} - {m.name_en}" + suffix = monsterToJaSuffix(m, False) + return prefix + ("[{}]({})".format(msg, get_pdx_url(m)) if link else msg) + suffix + + def monsterToThumbnailUrl(m: "MonsterModel"): return get_portrait_url(m) @@ -1026,30 +1033,52 @@ def monsterToBaseEmbed(m: "MonsterModel"): return embed -def addEvoListFields(list_of_monsters, embed, name): - if not len(list_of_monsters): +def addEvoListFields(monsters, current_monster, emoji_list): + if not len(monsters): return - field_name = name.format(len(list_of_monsters)) field_data = '' - for ae in sorted(list_of_monsters, key=lambda x: int(x.monster_id)): - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) + field_values = [] + for ae in sorted(monsters, key=lambda x: int(x.monster_id)): + monster_header = monsterToEvoHeader(ae, emoji_list, link=ae.monster_id != current_monster.monster_id) + '\n' + if len(field_data+monster_header) > 1024: + field_values.append(field_data) + field_data = "" + field_data += monster_header + field_values.append(field_data) + return field_values - embed.add_field(name=field_name, value=field_data) + +def monster_attr_emoji(emoji_list, monster: "MonsterModel"): + attr1 = monster.attr1.name.lower() + attr2 = monster.attr2.name.lower() + emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) + return match_emoji(emoji_list, emoji) -def monsterToEvoEmbed(m: "MonsterModel", db_context: "DbContext"): +def monsterToEvoEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): embed = monsterToBaseEmbed(m) alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_no) + gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) - evo_gem = db_context.graph.evo_gem_monster(m) if not len(alt_versions) and not evo_gem: embed.description = 'No alternate evos or evo gem' return embed - addEvoListFields(alt_versions, embed, '{} alternate evo(s)') - if not evo_gem: + evos = addEvoListFields(alt_versions, m, emoji_list) + if not gem_versions: + embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) + for f in evos[1:]: + embed.add_field(name="\u200b", value=f) return embed - addEvoListFields([evo_gem], embed, '{} evo gem(s)') + gems = addEvoListFields(gem_versions, m, emoji_list) + + embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) + for e in evos[1:]: + embed.add_field(name="\u200b", value=e, inline=False) + + embed.add_field(name="{} evolve gem(s)".format(len(gem_versions)), value=gems[0], inline=False) + for e in gems[1:]: + embed.add_field(name="\u200b", value=g, inline=False) return embed From 3d9e4fcdff96b429fe5d62f9371c1675a8921081 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 10 Nov 2020 21:19:06 -0700 Subject: [PATCH 0277/1220] Make channelmod ignore aliases (#229) --- channelmod/channelmod.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index e6611c3d..7954f34e 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -226,6 +226,9 @@ async def mirror_msg(self, message): if author.bot: return + if (await bot.get_context(message)).prefix is not None: + return + channel = message.channel mirrored_channels = self.settings.get_mirrored_channels(channel.id) From c0e47fab91d32a014cb6e47938668a970ed41f4a Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 10 Nov 2020 21:36:03 -0700 Subject: [PATCH 0278/1220] [channelmod] Use self.bot which actually works (#230) * Update channelmod.py * i'm not testing this one either but it like, totally should work --- channelmod/channelmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 7954f34e..e98b1430 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -226,7 +226,7 @@ async def mirror_msg(self, message): if author.bot: return - if (await bot.get_context(message)).prefix is not None: + if (await self.bot.get_context(message)).prefix is not None: return From 8c6f990cae13f3c19bb5f86138a6d5aab029a49e Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 11 Nov 2020 23:45:18 -0700 Subject: [PATCH 0279/1220] Requirements Update (#228) --- requirements.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4d3df173..3d8d4e36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,15 +3,13 @@ setuptools wheel Red-DiscordBot[postgres] prettytable -romkan -google-api -google-api-python-client -google-cloud -google-cloud-texttospeech +git+git://github.com/TsubakiBotPad/python-romkan pymysql ply opencv-python Pillow pytz aiohttp -oauth2client +aiofiles +aioodbc +networkx From 3d46fc2120161ab943b18097bd84e4237789d917 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 14 Nov 2020 01:47:41 -0700 Subject: [PATCH 0280/1220] Transform Killers (#232) --- dadguide/monster_graph.py | 45 +++++++++++++++++++++++++++++++++++++++ padinfo/padinfo.py | 7 +++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 4844257d..e6af04a6 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -316,6 +316,33 @@ def monster_is_base_by_id(self, monster_id: int) -> bool: def monster_is_base(self, monster: MonsterModel) -> bool: return self.monster_is_base_by_id(monster.monster_no) + def get_transform_base_id_by_id(self, monster_id): + # NOTE: This assumes that no two monsters will transform to the same monster. This + # also doesn't work for monsters like DMG which are transforms but also base + # cards. This also assumes that the "base" monster will be the lowest id in + # the case of a cyclical transform. + seen = set() + curr = monster_id + while curr not in seen: + seen.add(curr) + next_ids = self.get_prev_transforms_by_monster_id(monster_id) + if next_ids: + curr = next_ids.pop() + else: + break + else: + curr = min(seen) + return curr + + def get_transform_base_by_id(self, monster_id): + return self.get_monster(self.get_transform_base_id_by_id(monster_id)) + + def monster_is_transform_base_by_id(self, monster_id: int) -> bool: + return self.get_transform_base_id_by_id(monster_id) == monster_id + + def monster_is_transform_base(self, monster: MonsterModel) -> bool: + return self.monster_is_transform_base_by_id(monster.monster_no) + def get_numerical_sort_top_id_by_id(self, monster_id): alt_cards = self.get_alt_cards(monster_id) if alt_cards is None: @@ -375,6 +402,24 @@ def get_prev_evolution_by_monster(self, monster: MonsterModel): def get_next_evolutions_by_monster_id(self, monster_id): return self._get_edges(self.graph[monster_id], 'evolution') + def get_next_evolutions_by_monster(self, monster: MonsterModel): + return self.get_next_evolutions_by_monster_id(monster.monster_no) + + def get_prev_transforms_by_monster_id(self, monster_id): + return self._get_edges(self.graph[monster_id], 'back_transformation') + + def get_prev_transforms_by_monster(self, monster: MonsterModel): + return self.get_prev_evolution_by_monster_id(monster.monster_no) + + def get_next_transform_by_monster_id(self, monster_id): + bes = self._get_edges(self.graph[monster_id], 'transformation') + if bes: + return bes.pop() + return None + + def get_next_transform_by_monster(self, monster: MonsterModel): + return self.get_next_evolutions_by_monster_id(monster.monster_no) + def evo_mats_by_monster_id(self, monster_id: int) -> list: evo = self.get_evo_by_monster_id(monster_id) if evo is None: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6ef2672a..12df90d3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1358,7 +1358,12 @@ def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): if not len(awakenings_row): awakenings_row = 'No Awakenings' - killers_row = '**Available Killers:** [{} slots] {}'.format(m.latent_slots, ' '.join(m.killers)) + if db_context.graph.monster_is_transform_base(m): + killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, ' '.join(m.killers)) + else: + base_transform = db_context.graph.get_transform_base_by_id(m.monster_id) + killers_row = '**Avail. killers (pre-transform):** [{} slots] {}'.format(base_transform.latent_slots, ' '.join(base_transform.killers)) + embed.description = '{}\n{}'.format(awakenings_row, killers_row) From 73ac61ced6f41e9f676668a4aec89d8c9977d7ce Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 16 Nov 2020 00:10:54 -0600 Subject: [PATCH 0281/1220] Cleanup leftover Dadguide refactor stuff (#237) --- dadguide/dadguide.py | 2 +- dadguide/database_context.py | 4 ++-- dadguide/database_manager.py | 33 ++++++++++++--------------------- dadguide/monster_graph.py | 8 ++++---- padglobal/padglobal.py | 2 +- padinfo/padinfo.py | 8 ++++---- 6 files changed, 24 insertions(+), 33 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 571b1577..f1f5f732 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -108,7 +108,7 @@ async def create_index(self, accept_filter=None): self.panthname_overrides, accept_filter=accept_filter) - def get_monster_by_id(self, monster_id: int) -> MonsterModel: + def get_monster(self, monster_id: int) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" return self.database.graph.get_monster(monster_id) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 8c06e659..2352c047 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -18,7 +18,7 @@ def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' return [r.awoken_skill_id for r in self.database.query_many( - SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True, graph=self.graph)] + SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] def get_next_evolutions_by_monster(self, monster_id): return self.graph.get_next_evolutions_by_monster_id(monster_id) @@ -54,7 +54,7 @@ def get_all_monster_ids_query(self, as_generator=True): query = self.database.query_many( self.database.select_builder(tables={'monsters': ('monster_id',)}), (), DictWithAttrAccess, - as_generator=as_generator, graph=self.graph) + as_generator=as_generator) if as_generator: return map(lambda m: m.monster_id, query) return [m.monster_id for m in query] diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 8fd4cc32..e3bb1693 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -70,56 +70,56 @@ def select_builder(tables, key=None, where=None, order=None, distinct=False): query.append(ORDER.format(order=order)) return ' '.join(query) - def query_one(self, query, param, d_type, graph=None): + def query_one(self, query, param, d_type): cursor = self._con.cursor() cursor.execute(query, param) res = cursor.fetchone() if res is not None: if issubclass(d_type, DadguideItem): - return d_type(res, graph=graph) + return d_type(res) else: return d_type(res) return None - def as_generator(self, cursor, d_type, graph=None): + def as_generator(self, cursor, d_type): res = cursor.fetchone() while res is not None: if issubclass(d_type, DadguideItem): - yield d_type(res, graph=graph) + yield d_type(res) else: yield d_type(res) res = cursor.fetchone() - def query_many(self, query, param, d_type, idx_key=None, as_generator=False, graph=None): + def query_many(self, query, param, d_type, idx_key=None, as_generator=False): cursor = self._con.cursor() cursor.execute(query, param) if cursor.rowcount == 0: return [] if as_generator: - return (d_type(res, graph=graph) + return (d_type(res) if issubclass(d_type, DadguideItem) else d_type(res) for res in cursor.fetchall()) else: if idx_key is None: if issubclass(d_type, DadguideItem): - return [d_type(res, graph=graph) for res in cursor.fetchall()] + return [d_type(res) for res in cursor.fetchall()] else: return [d_type(res) for res in cursor.fetchall()] else: if issubclass(d_type, DadguideItem): return DictWithAttrAccess( - {res[idx_key]: d_type(res, graph=graph) for res in cursor.fetchall()}) + {res[idx_key]: d_type(res) for res in cursor.fetchall()}) else: return DictWithAttrAccess({res[idx_key]: d_type(res) for res in cursor.fetchall()}) - def select_one_entry_by_pk(self, pk, d_type, graph=None): + def select_one_entry_by_pk(self, pk, d_type): return self.query_one( self.select_builder( tables={d_type.TABLE: d_type.FIELDS}, where='{}.{}=?'.format(d_type.TABLE, d_type.PK)), (pk,), - d_type, graph=graph) + d_type) def get_table_fields(self, table_name: str): # SQL inject vulnerable :v @@ -151,7 +151,7 @@ class DadguideItem(DictWithAttrAccess): PK = None AS_BOOL = () - def __init__(self, item, **kwargs): + def __init__(self, item): super(DadguideItem, self).__init__(item) for k in self.AS_BOOL: self[k] = bool(self[k]) @@ -160,15 +160,6 @@ def key(self): return self[self.PK] -class DgEvolution(DadguideItem): - TABLE = 'evolutions' - PK = 'evolution_id' - - def __init__(self, item, **kwargs): - super(DgEvolution, self).__init__(item) - self.evolution_type = EvoType(self.evolution_type) - - class DgDungeon(DadguideItem): TABLE = 'dungeons' PK = 'dungeon_id' @@ -178,7 +169,7 @@ class DgScheduledEvent(DadguideItem): TABLE = 'schedule' PK = 'event_id' - def __init__(self, item, **graph): + def __init__(self, item): super(DgScheduledEvent, self).__init__(item) @property diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index e6af04a6..73f0fcac 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -101,10 +101,10 @@ def __init__(self, database: DadguideDatabase): def build_graph(self): self.graph = networkx.MultiDiGraph() - ms = self.database.query_many(MONSTER_QUERY, (), DictWithAttrAccess, graph=self) - es = self.database.query_many(EVOS_QUERY, (), DictWithAttrAccess, graph=self) - aws = self.database.query_many(AWAKENINGS_QUERY, (), DictWithAttrAccess, graph=self) - ems = self.database.query_many(EGG_QUERY, (), DictWithAttrAccess, graph=self) + ms = self.database.query_many(MONSTER_QUERY, (), DictWithAttrAccess) + es = self.database.query_many(EVOS_QUERY, (), DictWithAttrAccess) + aws = self.database.query_many(AWAKENINGS_QUERY, (), DictWithAttrAccess) + ems = self.database.query_many(EGG_QUERY, (), DictWithAttrAccess) mtoawo = defaultdict(list) for a in aws: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index c0ec543f..0c2e75dc 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -80,7 +80,7 @@ def monster_id_to_monster(monster_id): dg_cog = PADGLOBAL_COG.bot.get_cog('Dadguide') if dg_cog is None: return None - return dg_cog.get_monster_by_id(monster_id) + return dg_cog.get_monster(monster_id) def monster_id_to_named_monster(monster_id): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 12df90d3..916f5114 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -267,9 +267,9 @@ async def refresh_index(self): logger.info('Done refreshing indexes') - def get_monster_by_id(self, monster_id: int): + def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') - return dg_cog.get_monster_by_id(monster_id) + return dg_cog.get_monster(monster_id) @commands.command() async def jpname(self, ctx, *, query: str): @@ -875,7 +875,7 @@ async def findMonster(self, query, server_filter=ServerFilter.any): self.historic_lookups[query] = monster_no json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) - m = self.get_monster_by_id(nm.monster_id) if nm else None + m = self.get_monster(nm.monster_id) if nm else None return m, err, debug_info @@ -901,7 +901,7 @@ async def findMonster2(self, query, server_filter=ServerFilter.any): self.historic_lookups_id2[query] = monster_no json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) - m = self.get_monster_by_id(nm.monster_id) if nm else None + m = self.get_monster(nm.monster_id) if nm else None return m, err, debug_info From 4753c1b1fa22131b8878d1fcfefad90c968c143f Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 17 Nov 2020 15:09:00 -0700 Subject: [PATCH 0282/1220] Add LS combo and FUA text (#233) --- dadguide/models/leader_skill_model.py | 12 ++- dadguide/monster_graph.py | 8 +- padinfo/padinfo.py | 101 ++++++++++++++++---------- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/dadguide/models/leader_skill_model.py b/dadguide/models/leader_skill_model.py index ea5e753f..535e411d 100644 --- a/dadguide/models/leader_skill_model.py +++ b/dadguide/models/leader_skill_model.py @@ -12,13 +12,23 @@ def __init__(self, **kwargs): self.max_rcv = kwargs['max_rcv'] self.max_shield = kwargs['max_shield'] self.max_combos = kwargs['max_combos'] + self.bonus_damage = kwargs['bonus_damage'] + self.mult_bonus_damage = kwargs['mult_bonus_damage'] + self.extra_time = kwargs['extra_time'] self.desc_en = kwargs['desc_en'] self.desc_ja = kwargs['desc_ja'] self.desc_ko = kwargs['desc_ko'] @property def data(self): - return self.max_hp, self.max_atk, self.max_rcv, self.max_shield + return (self.max_hp, + self.max_atk, + self.max_rcv, + self.max_shield, + self.max_combos, + self.bonus_damage, + self.mult_bonus_damage, + self.extra_time) @property def desc(self): diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 73f0fcac..51124aa0 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -27,6 +27,9 @@ leader_skills.max_rcv, leader_skills.max_shield, leader_skills.max_combos, + leader_skills.bonus_damage, + leader_skills.mult_bonus_damage, + leader_skills.extra_time, active_skills.name_ja AS as_name_ja, active_skills.name_en AS as_name_en, active_skills.name_ko AS as_name_ko, @@ -132,7 +135,10 @@ def build_graph(self): max_atk=m.max_atk, max_rcv=m.max_rcv, max_shield=m.max_shield, - max_combos=m.max_combos + max_combos=m.max_combos, + bonus_damage=m.bonus_damage, + mult_bonus_damage=m.mult_bonus_damage, + extra_time=m.extra_time, ) if m.leader_skill_id != 0 else None as_model = ActiveSkillModel(active_skill_id=m.active_skill_id, diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 916f5114..7476d621 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1215,44 +1215,27 @@ def monstersToLsEmbed(left_m: "MonsterModel", right_m: "MonsterModel"): lls = left_m.leader_skill rls = right_m.leader_skill - if lls: - lhp, latk, lrcv, lresist = lls.data - else: - lhp, latk, lrcv, lresist = 1, 1, 1, 0 - - if rls: - rhp, ratk, rrcv, rresist = rls.data - else: - rhp, ratk, rrcv, rresist = 1, 1, 1, 0 - - multiplier_text = createMultiplierText(lhp, latk, lrcv, lresist, rhp, ratk, rrcv, rresist) + multiplier_text = createMultiplierText(lls, rls) embed = discord.Embed() - embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) + embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( monsterToHeader(left_m, link=True), - left_m.leader_skill.desc if left_m.leader_skill else 'None') + lls.desc if lls else 'None') description += '\n**{}**\n{}'.format( monsterToHeader(right_m, link=True), - right_m.leader_skill.desc if right_m.leader_skill else 'None') + rls.desc if rls else 'None') embed.description = description return embed def monstersToLssEmbed(m: "MonsterModel"): - ls = m.leader_skill - - if ls: - hp, atk, rcv, resist = ls.data - else: - hp, atk, rcv, resist = 1, 1, 1, 0 - - multiplier_text = createSingleMultiplierText(hp, atk, rcv, resist) + multiplier_text = createSingleMultiplierText(m.leader_skill) embed = discord.Embed() - embed.title = 'Multiplier [{}]\n\n'.format(multiplier_text) + embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( monsterToHeader(m, link=True), @@ -1380,9 +1363,8 @@ def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): ls_row = m.leader_skill.desc if leader_skill else 'None' ls_header = 'Leader Skill' if leader_skill: - hp, atk, rcv, resist = m.leader_skill.data - multiplier_text = createMultiplierText(hp, atk, rcv, resist) - ls_header += " [ {} ]".format(multiplier_text) + multiplier_text = createMultiplierText(leader_skill) + ls_header += " {}".format(multiplier_text) embed.add_field(name=ls_header, value=ls_row, inline=False) evos_header = "Alternate Evos" @@ -1523,25 +1505,68 @@ def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext"): 72: 'misc_poisonboost', } +def humanize_number(number, sigfigs=2): + n = float("{0:.{1}g}".format(number, sigfigs)) + if n >= 1e9: + return str(int(n//1e9))+"B" + elif n >= 1e6: + return str(int(n//1e6))+"M" + elif n >= 1e3: + return str(int(n//1e3))+"k" + else: + return str(int(n)) -def createMultiplierText(hp1, atk1, rcv1, resist1, hp2=None, atk2=None, rcv2=None, resist2=None): - if all([x is None for x in (hp2, atk2, rcv2, resist2)]): - hp2, atk2, rcv2, resist2 = hp1, atk1, rcv1, resist1 +def createMultiplierText(ls1, ls2=None): + if ls2 and not ls1: + ls1, ls2 = ls2, ls1 - def fmtNum(val): - return '{:.2f}'.format(val).strip('0').rstrip('.') + if ls1: + hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = ls1.data + else: + hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = 1, 1, 1, 0, 0, 0, 0, 0 + + if ls2: + hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = ls2.data + else: + hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 + + return format_ls_text( + hp1*hp2, + atk1*atk2, + rcv1*rcv2, + 100 * (1 - (1 - resist1) * (1 - resist2)), + combo1+combo2, + fua1+fua2, + mfua1+mfua2, + te1+te2 + ) - text = "{}/{}/{}".format(fmtNum(hp1 * hp2), fmtNum(atk1 * atk2), fmtNum(rcv1 * rcv2)) - if resist1 > 0 or resist2 > 0: - text += ' Resist {}%'.format(fmtNum(100 * (1 - (1 - resist1) * (1 - resist2)))) - return text +def createSingleMultiplierText(ls=None): + if ls: + hp, atk, rcv, resist, combo, fua, mfua, te = ls.data + else: + hp, atk, rcv, resist, combo, fua, mfua, te = 1, 1, 1, 0, 0, 0, 0, 0 + + return format_ls_text(hp, atk, rcv, resist, combo, fua, mfua, te) -def createSingleMultiplierText(hp, atk, rcv, resist): + +def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): def fmtNum(val): return '{:.2f}'.format(val).strip('0').rstrip('.') text = "{}/{}/{}".format(fmtNum(hp), fmtNum(atk), fmtNum(rcv)) - if resist > 0: + if resist != 0: text += ' Resist {}%'.format(fmtNum(100 * resist)) - return text + + extras = [] + if combo: + extras.append('+{}c'.format(combo)) + if fua: + extras.append('{} fua'.format(humanize_number(fua, 2))) + elif mfua: + textextras.append('fua') + + if extras: + return '[{}] [{}]'.format(text, ' '.join(extras)) + return '[{}]'.format(text) From 82529791e99d23c7a7fcd3dae0867a34e1b7e85d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 17 Nov 2020 20:33:39 -0600 Subject: [PATCH 0283/1220] [padinfo] Fix typo in ls fua format (#239) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7476d621..60d78075 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1565,7 +1565,7 @@ def fmtNum(val): if fua: extras.append('{} fua'.format(humanize_number(fua, 2))) elif mfua: - textextras.append('fua') + extras.append('fua') if extras: return '[{}] [{}]'.format(text, ' '.join(extras)) From 2579302b4bc773141aec2a22da5c0419bde40eb2 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 17 Nov 2020 20:42:38 -0600 Subject: [PATCH 0284/1220] [padinfo] Fix shield multiplier issue (#240) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 60d78075..7f83dcc8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1534,7 +1534,7 @@ def createMultiplierText(ls1, ls2=None): hp1*hp2, atk1*atk2, rcv1*rcv2, - 100 * (1 - (1 - resist1) * (1 - resist2)), + 1 - (1 - resist1) * (1 - resist2), combo1+combo2, fua1+fua2, mfua1+mfua2, From c4d7074f310042eef772dfff78040dfa8b1e1f95 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 17 Nov 2020 22:54:46 -0700 Subject: [PATCH 0285/1220] Remove Max Version Requirement (#245) --- azurelane/info.json | 2 +- channelmod/info.json | 2 +- chronomagia/info.json | 2 +- crud/info.json | 2 +- dadguide/info.json | 2 +- damagecalc/info.json | 2 +- feedback/info.json | 2 +- padboard/info.json | 2 +- padbuildimg/info.json | 2 +- padbuilds/info.json | 2 +- padevents/info.json | 2 +- padglobal/info.json | 2 +- padguidedb/info.json | 2 +- padinfo/info.json | 2 +- padmonitor/info.json | 2 +- padsearch/info.json | 2 +- padvision/info.json | 2 +- pricecheck/info.json | 2 +- profile/info.json | 2 +- schoolidol/info.json | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/azurelane/info.json b/azurelane/info.json index e97b1ee8..fa131c3e 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]alid to get started. NOTE: This cog is no longer supported.", "short": "Shows Azur Lane data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/channelmod/info.json b/channelmod/info.json index 475bf9a4..f91e1ec1 100644 --- a/channelmod/info.json +++ b/channelmod/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]channelmod to get started", "short": "Mirror channels across servers.", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/chronomagia/info.json b/chronomagia/info.json index ba02c075..6327b539 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]cmid to get started.", "short": "Shows Chrono Ma:Gia data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/crud/info.json b/crud/info.json index c2ceef62..76399d25 100644 --- a/crud/info.json +++ b/crud/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]crud to get started.", "short": "Pipeline CRUD", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/dadguide/info.json b/dadguide/info.json index 94a5452e..78e190ba 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]dadguide setdatafile if it's applicable to you.", "short": "Loads monster data (LOAD THIS FIRST)", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/damagecalc/info.json b/damagecalc/info.json index f123f5ad..0cad2163 100644 --- a/damagecalc/info.json +++ b/damagecalc/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]damage to get started.", "short": "Calculates Puzzle & Dragons data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/feedback/info.json b/feedback/info.json index fc0f8044..f218d55c 100644 --- a/feedback/info.json +++ b/feedback/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]setfeedbackchannel and [p]setblogfeedbackchannel to set the feedback channels.", "short": "Feedback and credits", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padboard/info.json b/padboard/info.json index 32c246b7..9ee3f780 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]set_tflite_path to set up your tflite path. (You need to get this from The Tsubaki Bot Team)", "short": "Dawnglare utils", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padbuildimg/info.json b/padbuildimg/info.json index 780d9b79..91176506 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -8,7 +8,7 @@ "install_msg": "Use [p]helpbuildimg for help!", "short": "PDChu cog", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padbuilds/info.json b/padbuilds/info.json index a5d59afb..04a71dbd 100644 --- a/padbuilds/info.json +++ b/padbuilds/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]builds to get started.", "short": "Builds to help you through dungeons", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padevents/info.json b/padevents/info.json index be9b88d0..be7f15e6 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]padevents to get started.", "short": "PAD Event tracker", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padglobal/info.json b/padglobal/info.json index 1a520893..036cbb7e 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]padglobal to get started.", "short": "Tsubaki guides and tips written by P&D professionals", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padguidedb/info.json b/padguidedb/info.json index f13de0fe..f259baaa 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]padguidedb to get started", "short": "Dungeon data scraper", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padinfo/info.json b/padinfo/info.json index 3457c96a..0cdcab43 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]helpid for help", "short": "Serves monster data (LOAD THIS SECOND)", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padmonitor/info.json b/padmonitor/info.json index 12ef9cef..bbec0c4a 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]addnewchannel to set up a monster monitor.", "short": "Announce new monsters", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padsearch/info.json b/padsearch/info.json index c1471069..1a11f28b 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]helpsearch for help.", "short": "Card search", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/padvision/info.json b/padvision/info.json index 5ff98de2..d161fb62 100644 --- a/padvision/info.json +++ b/padvision/info.json @@ -7,7 +7,7 @@ "install_msg": "Make sure to use padboard with this cog!", "short": "Neural net helper", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/pricecheck/info.json b/pricecheck/info.json index 82b5d8b3..faa006b2 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]pcadmin to set up pricechecks.", "short": "Price Check", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/profile/info.json b/profile/info.json index 99094c26..1083e2c3 100644 --- a/profile/info.json +++ b/profile/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]profile to get started", "short": "P&D Phonebook", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, diff --git a/schoolidol/info.json b/schoolidol/info.json index 1235d76d..669bccfb 100644 --- a/schoolidol/info.json +++ b/schoolidol/info.json @@ -7,7 +7,7 @@ "install_msg": "Use [p]sifid to get started.", "short": "Shows School Idol data", "min_bot_version": "3.4.0", - "max_bot_version": "3.4.2", + "max_bot_version": "0.0.0", "min_python_version": [ 3, 8, From 0330c531f645e1fa4b2d6e0c2c49c2f189993e4b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 18 Nov 2020 23:37:46 -0600 Subject: [PATCH 0286/1220] Remove last DadguideItem classes, including DadguideItem itself (#238) --- dadguide/database_context.py | 37 ++++++---- dadguide/database_manager.py | 88 +++--------------------- dadguide/models/dungeon_model.py | 57 +++++++++++++++ dadguide/models/scheduled_event_model.py | 38 ++++++++++ dadguide/monster_graph.py | 9 ++- padevents/padevents.py | 51 +++----------- 6 files changed, 140 insertions(+), 140 deletions(-) create mode 100644 dadguide/models/dungeon_model.py create mode 100644 dadguide/models/scheduled_event_model.py diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 2352c047..d0e558c8 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -1,12 +1,21 @@ from datetime import datetime from collections import OrderedDict, defaultdict, deque +from typing import Optional from .database_manager import DadguideDatabase from .monster_graph import MonsterGraph -from .database_manager import DadguideItem -from .database_manager import DgDungeon -from .database_manager import DictWithAttrAccess -from .database_manager import DgScheduledEvent +from .models.scheduled_event_model import ScheduledEventModel +from .models.dungeon_model import DungeonModel + + +SCHEDULED_EVENT_QUERY = """SELECT + schedule.*, + dungeons.name_ja AS d_name_ja, + dungeons.name_en AS d_name_en, + dungeons.name_ko AS d_name_ko, + dungeons.dungeon_type AS dungeon_type +FROM + schedule LEFT OUTER JOIN dungeons ON schedule.dungeon_id = dungeons.dungeon_id""" class DbContext(object): @@ -18,7 +27,7 @@ def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' return [r.awoken_skill_id for r in self.database.query_many( - SELECT_AWOKEN_SKILL_IDS, (), DadguideItem, as_generator=True)] + SELECT_AWOKEN_SKILL_IDS, (), as_generator=True)] def get_next_evolutions_by_monster(self, monster_id): return self.graph.get_next_evolutions_by_monster_id(monster_id) @@ -53,7 +62,6 @@ def get_monsters_by_active(self, active_skill_id: int): def get_all_monster_ids_query(self, as_generator=True): query = self.database.query_many( self.database.select_builder(tables={'monsters': ('monster_id',)}), (), - DictWithAttrAccess, as_generator=as_generator) if as_generator: return map(lambda m: m.monster_id, query) @@ -65,14 +73,14 @@ def get_all_monsters(self, as_generator=True): return [*monsters] return monsters - def get_all_events(self, as_generator=True): - return self.database.query_many( - self.database.select_builder(tables={DgScheduledEvent.TABLE: DgScheduledEvent.FIELDS}), (), - DgScheduledEvent, - as_generator=as_generator) - - def get_dungeon_by_id(self, dungeon_id: int): - return self.database.select_one_entry_by_pk(dungeon_id, DgDungeon) + def get_all_events(self) -> ScheduledEventModel: + result = self.database.query_many(SCHEDULED_EVENT_QUERY, ()) + for se in result: + se['dungeon_model'] = DungeonModel(name_ja=se['d_name_ja'], + name_en=se['d_name_en'], + name_ko=se['d_name_ko'], + **se) + yield ScheduledEventModel(**se) def get_base_monster_ids(self): SELECT_BASE_MONSTER_ID = ''' @@ -82,7 +90,6 @@ def get_base_monster_ids(self): return self.database.query_many( SELECT_BASE_MONSTER_ID, (), - DictWithAttrAccess, as_generator=True) def has_database(self): diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index e3bb1693..09f6ae47 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -70,56 +70,34 @@ def select_builder(tables, key=None, where=None, order=None, distinct=False): query.append(ORDER.format(order=order)) return ' '.join(query) - def query_one(self, query, param, d_type): + def query_one(self, query, param): cursor = self._con.cursor() cursor.execute(query, param) res = cursor.fetchone() if res is not None: - if issubclass(d_type, DadguideItem): - return d_type(res) - else: - return d_type(res) + return DictWithAttrAccess(res) return None - def as_generator(self, cursor, d_type): + @staticmethod + def as_generator(cursor): res = cursor.fetchone() while res is not None: - if issubclass(d_type, DadguideItem): - yield d_type(res) - else: - yield d_type(res) + yield DictWithAttrAccess(res) res = cursor.fetchone() - def query_many(self, query, param, d_type, idx_key=None, as_generator=False): + def query_many(self, query, param, idx_key=None, as_generator=False): cursor = self._con.cursor() cursor.execute(query, param) if cursor.rowcount == 0: return [] if as_generator: - return (d_type(res) - if issubclass(d_type, DadguideItem) - else d_type(res) + return (DictWithAttrAccess(res) for res in cursor.fetchall()) else: if idx_key is None: - if issubclass(d_type, DadguideItem): - return [d_type(res) for res in cursor.fetchall()] - else: - return [d_type(res) for res in cursor.fetchall()] + return [DictWithAttrAccess(res) for res in cursor.fetchall()] else: - if issubclass(d_type, DadguideItem): - return DictWithAttrAccess( - {res[idx_key]: d_type(res) for res in cursor.fetchall()}) - else: - return DictWithAttrAccess({res[idx_key]: d_type(res) for res in cursor.fetchall()}) - - def select_one_entry_by_pk(self, pk, d_type): - return self.query_one( - self.select_builder( - tables={d_type.TABLE: d_type.FIELDS}, - where='{}.{}=?'.format(d_type.TABLE, d_type.PK)), - (pk,), - d_type) + return DictWithAttrAccess({res[idx_key]: DictWithAttrAccess(res) for res in cursor.fetchall()}) def get_table_fields(self, table_name: str): # SQL inject vulnerable :v @@ -141,54 +119,6 @@ def __init__(self, item): self.__dict__ = self -class DadguideItem(DictWithAttrAccess): - """ - Base class for all items loaded from DadGuide. - Is a dict with attr access. - """ - TABLE = None - FIELDS = '*' - PK = None - AS_BOOL = () - - def __init__(self, item): - super(DadguideItem, self).__init__(item) - for k in self.AS_BOOL: - self[k] = bool(self[k]) - - def key(self): - return self[self.PK] - - -class DgDungeon(DadguideItem): - TABLE = 'dungeons' - PK = 'dungeon_id' - - -class DgScheduledEvent(DadguideItem): - TABLE = 'schedule' - PK = 'event_id' - - def __init__(self, item): - super(DgScheduledEvent, self).__init__(item) - - @property - def open_datetime(self): - return datetime.utcfromtimestamp(self.start_timestamp).replace(tzinfo=pytz.UTC) - - @open_datetime.setter - def open_datetime(self, value): - self.start_timestamp = int(value.timestamp()) - - @property - def close_datetime(self): - return datetime.utcfromtimestamp(self.end_timestamp).replace(tzinfo=pytz.UTC) - - @close_datetime.setter - def close_datetime(self, value): - self.end_timestamp = int(value.timestamp()) - - class PotentialMatches(object): def __init__(self): self.match_list = set() diff --git a/dadguide/models/dungeon_model.py b/dadguide/models/dungeon_model.py new file mode 100644 index 00000000..2ef1d301 --- /dev/null +++ b/dadguide/models/dungeon_model.py @@ -0,0 +1,57 @@ +from .base_model import BaseModel + + +class DungeonModel(BaseModel): + def __init__(self, **kwargs): + self.dungeon_id = kwargs['dungeon_id'] + self.name_ja = kwargs['name_ja'] + self.name_en = kwargs['name_en'] + self.clean_name_en = self._make_clean_name_en(self.name_en) + self.name_ko = kwargs['name_ko'] + + self.dungeon_type = kwargs['dungeon_type'] + + @staticmethod + def _make_clean_name_en(name): + if 'tamadra invades in some tech' in name.lower(): + return 'Latents invades some Techs & 20x +Eggs' + if '1.5x Bonus Pal Point in multiplay' in name: + name = '[Descends] 1.5x Pal Points in multiplay' + clean_name_map = { + 'No Continues': 'No Cont', + 'No Continue': 'No Cont', + 'Some Limited Time Dungeons': 'Some Guerrillas', + 'are added in': 'in', + '!': '', + 'Dragon Infestation': 'Dragons', + ' Infestation': 's', + 'Daily Descended Dungeon': 'Daily Descends', + 'Chance for ': '', + 'Jewel of the Spirit': 'Spirit Jewel', + ' & ': '/', + ' / ': '/', + 'PAD Radar': 'PADR', + 'in normal dungeons': 'in normals', + 'Selected ': 'Some ', + 'Enhanced ': 'Enh ', + 'All Att. Req.': 'All Att.', + 'Extreme King Metal Dragon': 'Extreme KMD', + 'Golden Mound-Tricolor [Fr/Wt/Wd Only]': 'Golden Mound', + 'Gods-Awakening Materials Descended': "Awoken Mats", + 'Orb move time 4 sec': '4s move time', + 'Awakening Materials Descended': 'Awkn Mats', + 'Awakening Materials': 'Awkn Mats', + "Star Treasure Thieves' Den": 'STTD', + 'Ruins of the Star Vault': 'Star Vault', + '-★6 or lower Enhanced': '', + } + for find, replace in clean_name_map.items(): + name = name.replace(find, replace) + return name + + def to_dict(self): + return { + 'dungeon_id': self.dungeon_id, + 'name_ja': self.name_ja, + 'name_en': self.name_en + } diff --git a/dadguide/models/scheduled_event_model.py b/dadguide/models/scheduled_event_model.py new file mode 100644 index 00000000..712b25da --- /dev/null +++ b/dadguide/models/scheduled_event_model.py @@ -0,0 +1,38 @@ +from .base_model import BaseModel +from .dungeon_model import DungeonModel +from datetime import datetime +import pytz + + +class ScheduledEventModel(BaseModel): + def __init__(self, **kwargs): + self.event_id = kwargs["event_id"] + self.server_id = kwargs.get("server_id") + self.event_type_id = kwargs.get("event_type_id") + self.start_timestamp = kwargs.get("start_timestamp") + self.end_timestamp = kwargs.get("end_timestamp") + self.group_name = kwargs.get("group_name") + + self.dungeon: DungeonModel = kwargs.get("dungeon_model") + self.dungeon_id = self.dungeon.dungeon_id if self.dungeon else None + + @property + def open_datetime(self): + return datetime.utcfromtimestamp(self.start_timestamp).replace(tzinfo=pytz.UTC) + + @open_datetime.setter + def open_datetime(self, value): + self.start_timestamp = int(value.timestamp()) + + @property + def close_datetime(self): + return datetime.utcfromtimestamp(self.end_timestamp).replace(tzinfo=pytz.UTC) + + @close_datetime.setter + def close_datetime(self, value): + self.end_timestamp = int(value.timestamp()) + + def to_dict(self): + return { + 'dungeon_id': self.event_id, + } diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 51124aa0..cb57bcb4 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -3,7 +3,6 @@ from typing import Optional from collections import defaultdict from .database_manager import DadguideDatabase -from .database_manager import DictWithAttrAccess from .models.enum_types import EvoType, InternalEvoType from .models.monster_model import MonsterModel from .models.leader_skill_model import LeaderSkillModel @@ -104,10 +103,10 @@ def __init__(self, database: DadguideDatabase): def build_graph(self): self.graph = networkx.MultiDiGraph() - ms = self.database.query_many(MONSTER_QUERY, (), DictWithAttrAccess) - es = self.database.query_many(EVOS_QUERY, (), DictWithAttrAccess) - aws = self.database.query_many(AWAKENINGS_QUERY, (), DictWithAttrAccess) - ems = self.database.query_many(EGG_QUERY, (), DictWithAttrAccess) + ms = self.database.query_many(MONSTER_QUERY, ()) + es = self.database.query_many(EVOS_QUERY, ()) + aws = self.database.query_many(AWAKENINGS_QUERY, ()) + ems = self.database.query_many(EGG_QUERY, ()) mtoawo = defaultdict(list) for a in aws: diff --git a/padevents/padevents.py b/padevents/padevents.py index ded4a0f8..70a8249a 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -16,6 +16,11 @@ from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings, confirm_message +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from dadguide.database_context import DbContext + from dadguide.models.scheduled_event_model import ScheduledEventModel + logger = logging.getLogger('red.padbot-cogs.padevents') SUPPORTED_SERVERS = ["JP", "NA", "KR"] @@ -198,7 +203,7 @@ async def testevent(self, ctx, server, seconds: int = 0): await dg_cog.wait_until_ready() dg_module = __import__(dg_cog.__module__) - te = dg_module.DgScheduledEvent({'dungeon_id': 1}, dg_cog.database) + te = dg_module.ScheduledEventModule({'dungeon_id': 1}) te.server = server te.server_id = SUPPORTED_SERVERS.index(server) @@ -917,18 +922,18 @@ def removeDailyReg(self, channel_id, server): class Event: - def __init__(self, scheduled_event: "DgScheduledEvent", db_context): + def __init__(self, scheduled_event: "ScheduledEventModel", db_context): self.db_context = db_context - self.key = scheduled_event.key() + self.key = scheduled_event.event_id self.server = SUPPORTED_SERVERS[scheduled_event.server_id] self.open_datetime = scheduled_event.open_datetime self.close_datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name - self.dungeon = self.db_context.get_dungeon_by_id(scheduled_event.dungeon_id) + self.dungeon = scheduled_event.dungeon self.dungeon_name = self.dungeon.name_en if self.dungeon else 'unknown_dungeon' self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' - self.clean_dungeon_name = cleanDungeonNames(self.dungeon_name) + self.clean_dungeon_name = self.dungeon.clean_name_en if self.dungeon else 'unknown_dungeon' self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') self.name_and_modifier = self.clean_dungeon_name @@ -1128,39 +1133,3 @@ def isEventWanted(event): return False return True - - -def cleanDungeonNames(name): - # TODO: Make this info internally stored - if 'tamadra invades in some tech' in name.lower(): - return 'Latents invades some Techs & 20x +Eggs' - if '1.5x Bonus Pal Point in multiplay' in name: - name = '[Descends] 1.5x Pal Points in multiplay' - name = name.replace('No Continues', 'No Cont') - name = name.replace('No Continue', 'No Cont') - name = name.replace('Some Limited Time Dungeons', 'Some Guerrillas') - name = name.replace('are added in', 'in') - name = name.replace('!', '') - name = name.replace('Dragon Infestation', 'Dragons') - name = name.replace(' Infestation', 's') - name = name.replace('Daily Descended Dungeon', 'Daily Descends') - name = name.replace('Chance for ', '') - name = name.replace('Jewel of the Spirit', 'Spirit Jewel') - name = name.replace(' & ', '/') - name = name.replace(' / ', '/') - name = name.replace('PAD Radar', 'PADR') - name = name.replace('in normal dungeons', 'in normals') - name = name.replace('Selected ', 'Some ') - name = name.replace('Enhanced ', 'Enh ') - name = name.replace('All Att. Req.', 'All Att.') - name = name.replace('Extreme King Metal Dragon', 'Extreme KMD') - name = name.replace('Golden Mound-Tricolor [Fr/Wt/Wd Only]', 'Golden Mound') - name = name.replace('Gods-Awakening Materials Descended', "Awoken Mats") - name = name.replace('Orb move time 4 sec', '4s move time') - name = name.replace('Awakening Materials Descended', 'Awkn Mats') - name = name.replace('Awakening Materials', 'Awkn Mats') - name = name.replace("Star Treasure Thieves' Den", 'STTD') - name = name.replace('Ruins of the Star Vault', 'Star Vault') - name = name.replace('-★6 or lower Enhanced', '') - - return name From 2f33db54b71e0c95f927a11905885f76b8bd1fd4 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 Nov 2020 12:13:23 -0700 Subject: [PATCH 0287/1220] Add reload to pdm (#250) * Add reload to pdm * add permissions check --- padmonitor/padmonitor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 892f8bf7..a3fdfe3c 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -94,11 +94,16 @@ async def announce(self, channel_id, message): logger.exception('failed to send message to {}:'.format(channel_id)) @commands.group(aliases=['pdm']) - @checks.mod_or_permissions(manage_guild=True) async def padmonitor(self, ctx): """PAD info monitoring""" pass + @padmonitor.command() + @checks.is_owner() + async def reload(self, ctx): + """Update PDM channels""" + await self.check_seen() + @padmonitor.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) From b661d90b90e6e9bf8716949a02d40dc8a2ef729e Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 21 Nov 2020 20:09:32 -0700 Subject: [PATCH 0288/1220] PadMonitor typing and padglobal addwhich fix (#252) --- dadguide/monster_graph.py | 5 ++++- padglobal/padglobal.py | 20 ++++++++++++-------- padmonitor/padmonitor.py | 4 +++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index cb57bcb4..7628bf09 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -315,11 +315,14 @@ def get_base_id_by_id(self, monster_id): def get_base_monster_by_id(self, monster_id): return self.get_monster(self.get_base_id_by_id(monster_id)) + def get_base_monster(self, monster: MonsterModel): + return self.get_monster(self.get_base_id_by_id(monster.monster_id)) + def monster_is_base_by_id(self, monster_id: int) -> bool: return self.get_base_id_by_id(monster_id) == monster_id def monster_is_base(self, monster: MonsterModel) -> bool: - return self.monster_is_base_by_id(monster.monster_no) + return self.monster_is_base_by_id(monster.monster_id) def get_transform_base_id_by_id(self, monster_id): # NOTE: This assumes that no two monsters will transform to the same monster. This diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 0c2e75dc..08ef96d2 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -800,8 +800,9 @@ async def addwhich(self, ctx, monster_id: int, *, definition): e.x. [p]padglobal addwhich 3818 take the pixel one """ m = monster_id_to_monster(monster_id) - if m != m.base_monster: - m = m.base_monster + base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + if m != base_monster: + m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id @@ -813,8 +814,9 @@ async def addwhich(self, ctx, monster_id: int, *, definition): async def rmwhich(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" m = monster_id_to_monster(monster_id) - if m != m.base_monster: - m = m.base_monster + base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + if m != base_monster: + m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format( m.name_en)): @@ -1186,8 +1188,9 @@ async def rmdungeonguide(self, ctx, term: str): async def addleaderguide(self, ctx, monster_id: int, *, definition: str): """Adds a leader guide to the [p]guide command""" m = monster_id_to_monster(monster_id) - if m != m.base_monster: - m = m.base_monster + base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + if m != base_monster: + m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id @@ -1199,8 +1202,9 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): async def rmleaderguide(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" m = monster_id_to_monster(monster_id) - if m != m.base_monster: - m = m.base_monster + base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + if m != base_monster: + m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format( diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index a3fdfe3c..40a109ba 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -102,7 +102,9 @@ async def padmonitor(self, ctx): @checks.is_owner() async def reload(self, ctx): """Update PDM channels""" - await self.check_seen() + async with ctx.typing(): + await self.check_seen() + await ctx.tick() @padmonitor.command() @commands.guild_only() From dc27c521c79c91e900decd19ef92eff28a284961 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 21 Nov 2020 20:15:38 -0700 Subject: [PATCH 0289/1220] [padglobal] Fix ^addwhich - Bot isn't global (#253) --- padglobal/padglobal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 08ef96d2..1fcdbcd4 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -800,7 +800,7 @@ async def addwhich(self, ctx, monster_id: int, *, definition): e.x. [p]padglobal addwhich 3818 take the pixel one """ m = monster_id_to_monster(monster_id) - base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -814,7 +814,7 @@ async def addwhich(self, ctx, monster_id: int, *, definition): async def rmwhich(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" m = monster_id_to_monster(monster_id) - base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1188,7 +1188,7 @@ async def rmdungeonguide(self, ctx, term: str): async def addleaderguide(self, ctx, monster_id: int, *, definition: str): """Adds a leader guide to the [p]guide command""" m = monster_id_to_monster(monster_id) - base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1202,7 +1202,7 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): async def rmleaderguide(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" m = monster_id_to_monster(monster_id) - base_monster = bot.get_cog("Dadguide").database.graph.get_base_monster(m) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) From 20c1adc91302b498c68875aaaedc1dab140271f4 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 22 Nov 2020 13:34:23 -0700 Subject: [PATCH 0290/1220] Fix guideto (#254) --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 1fcdbcd4..a748912d 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1158,7 +1158,7 @@ async def guideto(self, ctx, to_user: discord.Member, *, term: str): result_output = '**Guide for {}**\n{}'.format(term, text) result = "{} asked me to send you this:\n{}".format( ctx.author.name, result_output) - await to_user.send_message(result) + await to_user.send(result) msg = "Sent guide for {} to {}".format(term, to_user.name) await ctx.send(inline(msg)) From 58574fa78986e52df92e5672e75a240f9ff6be30 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 23 Nov 2020 01:00:37 -0600 Subject: [PATCH 0291/1220] [padinfo] Several updates to improve user clarity (#246) --- padinfo/padinfo.py | 181 ++++++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7f83dcc8..2cf42079 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -466,21 +466,21 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): id_embed = monsterToEmbed(m, self.get_emojis(), db_context) evo_embed = monsterToEvoEmbed(m, self.get_emojis(), db_context) - mats_embed = monsterToEvoMatsEmbed(m, db_context) + mats_embed = monsterToEvoMatsEmbed(m, db_context, self.get_emojis()) animated = m.has_animation - pic_embed = monsterToPicEmbed(m, animated=animated) - other_info_embed = monsterToOtherInfoEmbed(m, db_context) + pic_embed = monsterToPicEmbed(m, self.get_emojis(), animated=animated) + other_info_embed = monsterToOtherInfoEmbed(m, db_context, self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed emoji_to_embed[self.mats_emoji] = mats_embed emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = monsterToPantheonEmbed(m, db_context) + pantheon_embed = monsterToPantheonEmbed(m, db_context, self.get_emojis()) if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed - skillups_embed = monsterToSkillupsEmbed(m, db_context) + skillups_embed = monsterToSkillupsEmbed(m, db_context, self.get_emojis()) if skillups_embed: emoji_to_embed[self.skillups_emoji] = skillups_embed @@ -584,7 +584,7 @@ async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" m, err, debug_info = await self.findMonster(query) if m is not None: - embed = monsterToHeaderEmbed(m) + embed = monsterToHeaderEmbed(m, self.get_emojis()) await ctx.send(embed=embed) else: await ctx.send(self.makeFailureMsg(err)) @@ -993,8 +993,9 @@ def log_emoji(self, emote): self.save_settings() -def monsterToHeader(m: "MonsterModel", link=False): - msg = '[{}] {}'.format(m.monster_no_na, m.name_en) +def monsterToHeader(m: "MonsterModel", link=False, show_types=False, allowed_emojis=None): + type_emojis = '{} '.format(''.join([str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))) for t in m.types])) if show_types else '' + msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg @@ -1007,13 +1008,13 @@ def monsterToJaSuffix(m: "MonsterModel", subname_on_override=True): return suffix -def monsterToLongHeader(m: "MonsterModel", link=False): - msg = monsterToHeader(m) + monsterToJaSuffix(m) +def monsterToLongHeader(m: "MonsterModel", link=False, show_types=False, allowed_emojis=None): + msg = monsterToHeader(m, show_types=show_types, allowed_emojis=allowed_emojis) + monsterToJaSuffix(m) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg -def monsterToEvoHeader(m: "MonsterModel", emoji_list, link=True): - prefix = f" {monster_attr_emoji(emoji_list, m)} " +def monsterToEvoHeader(m: "MonsterModel", allowed_emojis, link=True): + prefix = f" {monster_attr_emoji(allowed_emojis, m)} " msg = f"{m.monster_no_na} - {m.name_en}" suffix = monsterToJaSuffix(m, False) return prefix + ("[{}]({})".format(msg, get_pdx_url(m)) if link else msg) + suffix @@ -1023,8 +1024,8 @@ def monsterToThumbnailUrl(m: "MonsterModel"): return get_portrait_url(m) -def monsterToBaseEmbed(m: "MonsterModel"): - header = monsterToLongHeader(m) +def monsterToBaseEmbed(m: "MonsterModel", allowed_emojis): + header = monsterToLongHeader(m, allowed_emojis=allowed_emojis) embed = discord.Embed() embed.set_thumbnail(url=monsterToThumbnailUrl(m)) embed.title = header @@ -1033,13 +1034,13 @@ def monsterToBaseEmbed(m: "MonsterModel"): return embed -def addEvoListFields(monsters, current_monster, emoji_list): +def addEvoListFields(monsters, current_monster, allowed_emojis): if not len(monsters): return field_data = '' field_values = [] for ae in sorted(monsters, key=lambda x: int(x.monster_id)): - monster_header = monsterToEvoHeader(ae, emoji_list, link=ae.monster_id != current_monster.monster_id) + '\n' + monster_header = monsterToEvoHeader(ae, allowed_emojis, link=ae.monster_id != current_monster.monster_id) + '\n' if len(field_data+monster_header) > 1024: field_values.append(field_data) field_data = "" @@ -1048,29 +1049,29 @@ def addEvoListFields(monsters, current_monster, emoji_list): return field_values -def monster_attr_emoji(emoji_list, monster: "MonsterModel"): +def monster_attr_emoji(allowed_emojis, monster: "MonsterModel"): attr1 = monster.attr1.name.lower() attr2 = monster.attr2.name.lower() emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) - return match_emoji(emoji_list, emoji) + return match_emoji(allowed_emojis, emoji) -def monsterToEvoEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): - embed = monsterToBaseEmbed(m) +def monsterToEvoEmbed(m: "MonsterModel", allowed_emojis, db_context: "DbContext"): + embed = monsterToBaseEmbed(m, allowed_emojis) alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_no) gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) - if not len(alt_versions) and not evo_gem: + if not len(alt_versions): embed.description = 'No alternate evos or evo gem' return embed - evos = addEvoListFields(alt_versions, m, emoji_list) + evos = addEvoListFields(alt_versions, m, allowed_emojis) if not gem_versions: embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) for f in evos[1:]: embed.add_field(name="\u200b", value=f) return embed - gems = addEvoListFields(gem_versions, m, emoji_list) + gems = addEvoListFields(gem_versions, m, allowed_emojis) embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) for e in evos[1:]: @@ -1097,8 +1098,8 @@ def addMonsterEvoOfList(monster_id_list, embed, field_name, db_context=None): embed.add_field(name=field_name, value=field_data) -def monsterToEvoMatsEmbed(m: "MonsterModel", db_context: "DbContext"): - embed = monsterToBaseEmbed(m) +def monsterToEvoMatsEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): + embed = monsterToBaseEmbed(m, allowed_emojis) mats_for_evo = db_context.graph.evo_mats_by_monster(m) @@ -1119,13 +1120,13 @@ def monsterToEvoMatsEmbed(m: "MonsterModel", db_context: "DbContext"): return embed -def monsterToPantheonEmbed(m: "MonsterModel", db_context: "DbContext"): +def monsterToPantheonEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): full_pantheon = db_context.get_monsters_by_series(m.series_id) pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) if len(pantheon_list) == 0 or len(pantheon_list) > 6: return None - embed = monsterToBaseEmbed(m) + embed = monsterToBaseEmbed(m, allowed_emojis) field_name = 'Pantheon: ' + db_context.graph.get_monster(m.monster_no).series.name field_data = '' @@ -1136,7 +1137,7 @@ def monsterToPantheonEmbed(m: "MonsterModel", db_context: "DbContext"): return embed -def monsterToSkillupsEmbed(m: "MonsterModel", db_context: "DbContext"): +def monsterToSkillupsEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): if m.active_skill is None: return None possible_skillups_list = db_context.get_monsters_by_active(m.active_skill.active_skill_id) @@ -1146,7 +1147,7 @@ def monsterToSkillupsEmbed(m: "MonsterModel", db_context: "DbContext"): if len(skillups_list) == 0: return None - embed = monsterToBaseEmbed(m) + embed = monsterToBaseEmbed(m, allowed_emojis) field_name = 'Skillups' field_data = '' @@ -1169,8 +1170,8 @@ def monsterToPicUrl(m: "MonsterModel"): return get_pic_url(m) -def monsterToPicEmbed(m: "MonsterModel", animated=False): - embed = monsterToBaseEmbed(m) +def monsterToPicEmbed(m: "MonsterModel", allowed_emojis, animated=False): + embed = monsterToBaseEmbed(m, allowed_emojis) url = monsterToPicUrl(m) embed.set_image(url=url) # Clear the thumbnail, don't need it on pic @@ -1202,15 +1203,6 @@ def monsterToOrbSkinCBUrl(m: "MonsterModel", link_text='Color Blind'): return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) -def monsterToGifEmbed(m: "MonsterModel"): - embed = monsterToBaseEmbed(m) - url = monsterToGifUrl(m) - embed.set_image(url=url) - # Clear the thumbnail, don't need it on pic - embed.set_thumbnail(url='') - return embed - - def monstersToLsEmbed(left_m: "MonsterModel", right_m: "MonsterModel"): lls = left_m.leader_skill rls = right_m.leader_skill @@ -1245,17 +1237,13 @@ def monstersToLssEmbed(m: "MonsterModel"): return embed -def monsterToHeaderEmbed(m: "MonsterModel"): - header = monsterToLongHeader(m, link=True) +def monsterToHeaderEmbed(m: "MonsterModel", allowed_emojis): + header = monsterToLongHeader(m, link=True, allowed_emojis=allowed_emojis) embed = discord.Embed() embed.description = header return embed -def monsterToTypeString(m: "MonsterModel"): - return '/'.join([t.name for t in m.types]) - - def monsterToAcquireString(m: "MonsterModel", db_context: "DbContext"): acquire_text = None if db_context.graph.monster_is_farmable(m) and not db_context.graph.monster_is_mp_evo(m): @@ -1278,17 +1266,51 @@ def monsterToAcquireString(m: "MonsterModel", db_context: "DbContext"): return acquire_text -def match_emoji(emoji_list, name): - for e in emoji_list: +def match_emoji(allowed_emojis, name): + for e in allowed_emojis: if e.name == name: return e return name -def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): - embed = monsterToBaseEmbed(m) +def monsterToEmbed(m: "MonsterModel", allowed_emojis, db_context: "DbContext"): + embed = monsterToBaseEmbed(m, allowed_emojis) + + # in case we want to readd the type emojis later + # types_row = ' '.join(['{} {}'.format(str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))), t.name) for t in m.types]) + types_row = '/'.join(['{}'.format(t.name) for t in m.types]) + + awakenings_row = '' + for idx, a in enumerate(m.awakenings): + as_id = a.awoken_skill_id + as_name = a.name + mapped_awakening = AWAKENING_MAP.get(as_id, as_name) + mapped_awakening = match_emoji(allowed_emojis, mapped_awakening) + + # Wrap superawakenings to the next line + if len(m.awakenings) - idx == m.superawakening_count: + awakenings_row += '\n{}'.format(match_emoji(allowed_emojis, 'sa_questionmark')) + awakenings_row += ' {}'.format(mapped_awakening) + else: + awakenings_row += ' {}'.format(mapped_awakening) + + is_transform_base = db_context.graph.monster_is_transform_base(m) + transform_base = db_context.graph.get_transform_base_by_id(m.monster_id) if not is_transform_base else None + + awakenings_row = awakenings_row.strip() + + if not len(awakenings_row): + awakenings_row = 'No Awakenings' + + if is_transform_base: + + killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, get_killers_text(m, allowed_emojis)) + else: + killers_row = '**Avail. killers (pre-transform):** [{} slots] {}'.format(transform_base.latent_slots, get_killers_text(transform_base, allowed_emojis)) + + embed.add_field(name=types_row, value='{}\n{}'.format(awakenings_row, killers_row), inline=False) - info_row_1 = monsterToTypeString(m) + info_row_1 = 'Inheritable' if m.is_inheritable else 'Not inheritable' acquire_text = monsterToAcquireString(m, db_context) tet_text = db_context.graph.true_evo_type_by_monster(m).value @@ -1303,10 +1325,6 @@ def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): if acquire_text: info_row_2 += '\n**{}**'.format(acquire_text) - if m.is_inheritable: - info_row_2 += '\n**Inheritable**' - else: - info_row_2 += '\n**Not inheritable**' if tet_text in ("Reincarnated", "Assist", "Pixel", "Super Reincarnated"): info_row_2 += '\n**{}**'.format(tet_text) @@ -1315,41 +1333,16 @@ def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): hp, atk, rcv, weighted = m.stats() if m.limit_mult > 0: lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_row_1 = 'Weighted {} | LB {} (+{}%)'.format(weighted, lb_weighted, m.limit_mult) + stats_row_1 = 'Stats (LB, +{}%)'.format(m.limit_mult) stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( hp, lb_hp, atk, lb_atk, rcv, lb_rcv) else: - stats_row_1 = 'Weighted {}'.format(weighted) + stats_row_1 = 'Stats' stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) + if any(x if x.name == 'Enhance' else None for x in m.types): + stats_row_2 += '\n**Fodder EXP** {:,}'.format(m.fodder_exp) embed.add_field(name=stats_row_1, value=stats_row_2) - awakenings_row = '' - for idx, a in enumerate(m.awakenings): - as_id = a.awoken_skill_id - as_name = a.name - mapped_awakening = AWAKENING_MAP.get(as_id, as_name) - mapped_awakening = match_emoji(emoji_list, mapped_awakening) - - # Wrap superawakenings to the next line - if len(m.awakenings) - idx == m.superawakening_count: - awakenings_row += '\n{}'.format(mapped_awakening) - else: - awakenings_row += ' {}'.format(mapped_awakening) - - awakenings_row = awakenings_row.strip() - - if not len(awakenings_row): - awakenings_row = 'No Awakenings' - - if db_context.graph.monster_is_transform_base(m): - killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, ' '.join(m.killers)) - else: - base_transform = db_context.graph.get_transform_base_by_id(m.monster_id) - killers_row = '**Avail. killers (pre-transform):** [{} slots] {}'.format(base_transform.latent_slots, ' '.join(base_transform.killers)) - - - embed.description = '{}\n{}'.format(awakenings_row, killers_row) - active_header = 'Active Skill' active_body = 'None' active_skill = m.active_skill @@ -1378,8 +1371,14 @@ def monsterToEmbed(m: "MonsterModel", emoji_list, db_context: "DbContext"): return embed -def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext"): - embed = monsterToBaseEmbed(m) +def get_killers_text(m: "MonsterModel", allowed_emojis): + if 'Any' in m.killers: + return 'Any' + return ' '.join([str(match_emoji(allowed_emojis, 'latent_killer_{}'.format(k.lower()))) for k in m.killers]) + + +def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): + embed = monsterToBaseEmbed(m, allowed_emojis) # Clear the thumbnail, takes up too much space embed.set_thumbnail(url='') @@ -1422,6 +1421,14 @@ def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext"): xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' body_text += '\n**XP to Max:** {}'.format(xp_text) body_text += ' **Max Level:**: {}'.format(m.level) + + # weighted stat calculation & display + hp, atk, rcv, weighted = m.stats() + body_text += '\n**Weighted stats:** {}'.format(weighted) + if m.limit_mult > 0: + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) + body_text += ' | LB {} (+{}%)'.format(lb_weighted, m.limit_mult) + body_text += '\n**Fodder EXP:** {:,}'.format(m.fodder_exp) body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) @@ -1505,6 +1512,7 @@ def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext"): 72: 'misc_poisonboost', } + def humanize_number(number, sigfigs=2): n = float("{0:.{1}g}".format(number, sigfigs)) if n >= 1e9: @@ -1516,6 +1524,7 @@ def humanize_number(number, sigfigs=2): else: return str(int(n)) + def createMultiplierText(ls1, ls2=None): if ls2 and not ls1: ls1, ls2 = ls2, ls1 From 781e3f18d867e4a7b26dc7c8417fd61b94ee06ec Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 24 Nov 2020 00:55:15 -0600 Subject: [PATCH 0292/1220] Add entire tree discovery to id2 lookups (#255) Previously there was a note saying not to add any NamedMonster or NamedMonsterGroup attributes to NamedMonster but I think that's no longer relevant because MonsterModel doesn't hold a db reference? So this should be ok? This specifically fixes a bug that ^id2 dg equip diablos didn't work because the nickname of diablos was overwritten by ivanus due to the name change on the specific card, now the entire tree is auto added to discovery if any single evo in the tree is discovered via the nickname match. --- dadguide/database_manager.py | 200 +--------------------------- dadguide/old_monster_index.py | 238 ++++++++++++++++++++++++++++++++-- 2 files changed, 227 insertions(+), 211 deletions(-) diff --git a/dadguide/database_manager.py b/dadguide/database_manager.py index 09f6ae47..bb773986 100644 --- a/dadguide/database_manager.py +++ b/dadguide/database_manager.py @@ -1,16 +1,6 @@ -from datetime import datetime import sqlite3 as lite -import romkan -import logging - -import pytz -import re - -from collections import defaultdict -import tsutils -from dadguide.models.enum_types import Attribute, MonsterType -from dadguide.models.monster_model import MonsterModel +import logging logger = logging.getLogger('red.padbot-cogs.dadguide.database_manager') @@ -117,191 +107,3 @@ class DictWithAttrAccess(dict): def __init__(self, item): super(DictWithAttrAccess, self).__init__(item) self.__dict__ = self - - -class PotentialMatches(object): - def __init__(self): - self.match_list = set() - - def add(self, m): - self.match_list.add(m) - - def update(self, monster_list): - self.match_list.update(monster_list) - - def length(self): - return len(self.match_list) - - def remove_potential_matches_without_all_prefixes(self, query_prefixes): - to_remove = set() - for m in self.match_list: - for prefix in query_prefixes: - if prefix not in m.prefixes: - to_remove.add(m) - break - self.match_list.difference_update(to_remove) - - def get_monsters_from_potential_pantheon_match(self, pantheon, pantheon_nick_to_name, pantheons): - full_name = pantheon_nick_to_name[pantheon] - self.update(pantheons[full_name]) - - def pick_best_monster(self): - return max(self.match_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) - - -class NamedMonsterGroup(object): - def __init__(self, evolution_tree: list, basename_overrides: list): - self.is_low_priority = ( - self._is_low_priority_monster(evolution_tree[0]) - or self._is_low_priority_group(evolution_tree)) - - base_monster = evolution_tree[0] - self.group_size = len(evolution_tree) - self.base_monster_no = base_monster.monster_id - self.base_monster_no_na = base_monster.monster_no_na - - self.monster_no_to_basename = { - m.monster_id: self._compute_monster_basename(m) for m in evolution_tree - } - - self.computed_basename = self._compute_group_basename(evolution_tree) - self.computed_basenames = set([self.computed_basename]) - if '-' in self.computed_basename: - self.computed_basenames.add(self.computed_basename.replace('-', ' ')) - - self.basenames = basename_overrides or self.computed_basenames - - def _compute_monster_basename(self, m: MonsterModel): - basename = m.name_en.lower() - if ',' in basename: - name_parts = basename.split(',') - if name_parts[1].strip().startswith('the '): - # handle names like 'xxx, the yyy' where xxx is the name - basename = name_parts[0] - else: - # otherwise, grab the chunk after the last comma - basename = name_parts[-1] - - for x in ['awoken', 'reincarnated']: - if basename.startswith(x): - basename = basename.replace(x, '') - - # Fix for DC collab garbage - basename = basename.replace('(comics)', '') - basename = basename.replace('(film)', '') - - return basename.strip() - - def _compute_group_basename(self, monsters): - """Computes the basename for a group of monsters. - - Prefer a basename with the largest count across the group. If all the - groups have equal size, prefer the lowest monster number basename. - This monster in general has better names, particularly when all the - names are unique, e.g. for male/female hunters.""" - - def count_and_id(): - return [0, 0] - - basename_to_info = defaultdict(count_and_id) - - for m in monsters: - basename = self.monster_no_to_basename[m.monster_id] - entry = basename_to_info[basename] - entry[0] += 1 - entry[1] = max(entry[1], m.monster_id) - - entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] - return max(entries)[2] - - def _is_low_priority_monster(self, m: MonsterModel): - lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] - lp_substrings = ['tamadra'] - lp_min_rarity = 2 - name = m.name_en.lower() - - failed_type = m.type1 in lp_types - failed_ss = any([x in name for x in lp_substrings]) - failed_rarity = m.rarity < lp_min_rarity - failed_chibi = name == m.name_en and m.name_en != m.name_ja - failed_equip = m.is_equip - return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip - - def _is_low_priority_group(self, mg: list): - lp_grp_min_rarity = 5 - max_rarity = max(m.rarity for m in mg) - failed_max_rarity = max_rarity < lp_grp_min_rarity - return failed_max_rarity - - -class NamedMonster(object): - def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, db_context=None): - # Must not hold onto monster or monster_group! - - # Hold on to the IDs instead - self.monster_id = monster.monster_id - self.monster_no_na = monster.monster_no_na - self.monster_no_jp = monster.monster_no_jp - - # ID of the root of the tree for this monster - self.base_monster_no = monster_group.base_monster_no - self.base_monster_no_na = monster_group.base_monster_no_na - - # This stuff is important for nickname generation - self.group_basenames = monster_group.basenames - self.prefixes = prefixes - - # Pantheon - series = db_context.graph.get_monster(monster.monster_no).series - self.series = series.name if series else None - - # Data used to determine how to rank the nicknames - self.is_low_priority = monster_group.is_low_priority or monster.is_equip - self.group_size = monster_group.group_size - self.rarity = monster.rarity - - # Used in fallback searches - self.name_en = monster.name_en - self.name_ja = monster.name_ja - - # These are just extra metadata - self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] - self.group_computed_basename = monster_group.computed_basename - self.extra_nicknames = extra_nicknames - - # Compute any extra prefixes - if self.monster_basename in ('ana', 'ace'): - self.prefixes.add(self.monster_basename) - - # Compute extra basenames by checking for two-word basenames and using the second half - self.two_word_basenames = set() - for basename in self.group_basenames: - basename_words = basename.split(' ') - if len(basename_words) == 2: - self.two_word_basenames.add(basename_words[1]) - - # The primary result nicknames - self.final_nicknames = set() - # Set the configured override nicknames - self.final_nicknames.update(self.extra_nicknames) - # Set the roma subname for JP monsters - if monster.roma_subname: - self.final_nicknames.add(monster.roma_subname) - - # For each basename, add nicknames - for basename in self.group_basenames: - # Add the basename directly - self.final_nicknames.add(basename) - # Add the prefix plus basename, and the prefix with a space between basename - for prefix in self.prefixes: - self.final_nicknames.add(prefix + basename) - self.final_nicknames.add(prefix + ' ' + basename) - - self.final_two_word_nicknames = set() - # Slightly different process for two-word basenames. Does this make sense? Who knows. - for basename in self.two_word_basenames: - self.final_two_word_nicknames.add(basename) - # Add the prefix plus basename, and the prefix with a space between basename - for prefix in self.prefixes: - self.final_two_word_nicknames.add(prefix + basename) - self.final_two_word_nicknames.add(prefix + ' ' + basename) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index e6a4fa94..8e37435d 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -1,18 +1,18 @@ import difflib -from collections import defaultdict - from redbot.core.utils import AsyncIter from discord.utils import find as find_first import tsutils +from collections import defaultdict + from .models.monster_model import MonsterModel from .models.enum_types import Attribute from .models.enum_types import EvoType from .models.enum_types import InternalEvoType -from .database_manager import PotentialMatches -from .database_manager import NamedMonster -from .database_manager import NamedMonsterGroup +from dadguide.models.enum_types import Attribute, MonsterType +from dadguide.models.monster_model import MonsterModel +from dadguide.models.series_model import SeriesModel from .database_context import DbContext @@ -62,18 +62,23 @@ async def __init__(self, monster_database: DbContext, nickname_overrides, basena named_monsters = [] async for base_mon in AsyncIter(base_monster_ids): base_id = base_mon.monster_id + series = monster_database.graph.get_monster(base_id).series group_basename_overrides = basename_overrides.get(base_id, []) evolution_tree = [monster_database.graph.get_monster(m) for m in monster_database.get_evolution_tree_ids(base_id)] named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) + named_evolution_tree = [] for monster in evolution_tree: if accept_filter and not accept_filter(monster): continue prefixes = self.compute_prefixes(monster, evolution_tree) extra_nicknames = monster_id_to_nicknames[monster.monster_id] - named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames, - db_context=self.db_context) + named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames, series) named_monsters.append(named_monster) + named_evolution_tree.append(named_monster) + for named_monster in named_evolution_tree: + named_monster.set_evolution_tree(named_evolution_tree) + # Sort the NamedMonsters into the opposite order we want to accept their nicknames in # This order is: @@ -366,13 +371,13 @@ def find_monster2(self, query): if query.endswith("base {}".format(m.monster_id)): matches.add( find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.update_list(query_prefixes) # first try to get matches from nicknames for nickname, m in self.all_entries.items(): if new_query in nickname: matches.add(m) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.update_list(query_prefixes) # if we don't have any candidates yet, pick a new method if not matches.length(): @@ -380,7 +385,7 @@ def find_monster2(self, query): for nickname, m in self.all_en_name_to_monsters.items(): if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): matches.add(m) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.update_list(query_prefixes) # check for exact match on pantheon name but only if needed if not matches.length(): @@ -388,7 +393,7 @@ def find_monster2(self, query): if new_query == pantheon.lower(): matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.update_list(query_prefixes) # check for any match on pantheon name, again but only if needed if not matches.length(): @@ -396,7 +401,7 @@ def find_monster2(self, query): if new_query in pantheon.lower(): matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, self.pantheons) - matches.remove_potential_matches_without_all_prefixes(query_prefixes) + matches.update_list(query_prefixes) if matches.length(): return matches.pick_best_monster(), None, None @@ -404,3 +409,212 @@ def find_monster2(self, query): def pickBestMonster(self, named_monster_list): return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) + + +class PotentialMatches(object): + def __init__(self): + self.match_list = set() + + def add(self, m): + self.match_list.add(m) + + def update(self, monster_list): + self.match_list.update(monster_list) + + def length(self): + return len(self.match_list) + + def update_list(self, query_prefixes): + self._add_trees() + self._remove_any_without_all_prefixes(query_prefixes) + + def _add_trees(self): + to_add = set() + for m in self.match_list: + for evo in m.evolution_tree: + to_add.add(evo) + self.match_list.update(to_add) + + def _remove_any_without_all_prefixes(self, query_prefixes): + to_remove = set() + for m in self.match_list: + for prefix in query_prefixes: + if prefix not in m.prefixes: + to_remove.add(m) + break + self.match_list.difference_update(to_remove) + + def get_monsters_from_potential_pantheon_match(self, pantheon, pantheon_nick_to_name, pantheons): + full_name = pantheon_nick_to_name[pantheon] + self.update(pantheons[full_name]) + + def pick_best_monster(self): + return max(self.match_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) + + +class NamedMonsterGroup(object): + def __init__(self, evolution_tree: list, basename_overrides: list): + self.is_low_priority = ( + self._is_low_priority_monster(evolution_tree[0]) + or self._is_low_priority_group(evolution_tree)) + + base_monster = evolution_tree[0] + + self.group_size = len(evolution_tree) + self.base_monster_no = base_monster.monster_id + self.base_monster_no_na = base_monster.monster_no_na + + self.monster_no_to_basename = { + m.monster_id: self._compute_monster_basename(m) for m in evolution_tree + } + + self.computed_basename = self._compute_group_basename(evolution_tree) + self.computed_basenames = set([self.computed_basename]) + if '-' in self.computed_basename: + self.computed_basenames.add(self.computed_basename.replace('-', ' ')) + + self.basenames = basename_overrides or self.computed_basenames + + def _compute_monster_basename(self, m: MonsterModel): + basename = m.name_en.lower() + if ',' in basename: + name_parts = basename.split(',') + if name_parts[1].strip().startswith('the '): + # handle names like 'xxx, the yyy' where xxx is the name + basename = name_parts[0] + else: + # otherwise, grab the chunk after the last comma + basename = name_parts[-1] + + for x in ['awoken', 'reincarnated']: + if basename.startswith(x): + basename = basename.replace(x, '') + + # Fix for DC collab garbage + basename = basename.replace('(comics)', '') + basename = basename.replace('(film)', '') + + return basename.strip() + + def _compute_group_basename(self, monsters): + """Computes the basename for a group of monsters. + + Prefer a basename with the largest count across the group. If all the + groups have equal size, prefer the lowest monster number basename. + This monster in general has better names, particularly when all the + names are unique, e.g. for male/female hunters.""" + + def count_and_id(): + return [0, 0] + + basename_to_info = defaultdict(count_and_id) + + for m in monsters: + basename = self.monster_no_to_basename[m.monster_id] + entry = basename_to_info[basename] + entry[0] += 1 + entry[1] = max(entry[1], m.monster_id) + + entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] + return max(entries)[2] + + def _is_low_priority_monster(self, m: MonsterModel): + lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] + lp_substrings = ['tamadra'] + lp_min_rarity = 2 + name = m.name_en.lower() + + failed_type = m.type1 in lp_types + failed_ss = any([x in name for x in lp_substrings]) + failed_rarity = m.rarity < lp_min_rarity + failed_chibi = name == m.name_en and m.name_en != m.name_ja + failed_equip = m.is_equip + return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip + + def _is_low_priority_group(self, mg: list): + lp_grp_min_rarity = 5 + max_rarity = max(m.rarity for m in mg) + failed_max_rarity = max_rarity < lp_grp_min_rarity + return failed_max_rarity + + +class NamedMonster(object): + def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, series: SeriesModel): + + self.evolution_tree = None + + # Hold on to the IDs instead + self.monster_id = monster.monster_id + self.monster_no_na = monster.monster_no_na + self.monster_no_jp = monster.monster_no_jp + + # ID of the root of the tree for this monster + self.base_monster_no = monster_group.base_monster_no + self.base_monster_no_na = monster_group.base_monster_no_na + + # This stuff is important for nickname generation + self.group_basenames = monster_group.basenames + self.prefixes = prefixes + + # Pantheon + series = series + self.series = series.name if series else None + + # Data used to determine how to rank the nicknames + self.is_low_priority = monster_group.is_low_priority or monster.is_equip + self.group_size = monster_group.group_size + self.rarity = monster.rarity + + # Used in fallback searches + self.name_en = monster.name_en + self.name_ja = monster.name_ja + + # These are just extra metadata + self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] + self.group_computed_basename = monster_group.computed_basename + self.extra_nicknames = extra_nicknames + + # Compute any extra prefixes + if self.monster_basename in ('ana', 'ace'): + self.prefixes.add(self.monster_basename) + + # Compute extra basenames by checking for two-word basenames and using the second half + self.two_word_basenames = set() + for basename in self.group_basenames: + basename_words = basename.split(' ') + if len(basename_words) == 2: + self.two_word_basenames.add(basename_words[1]) + + # The primary result nicknames + self.final_nicknames = set() + # Set the configured override nicknames + self.final_nicknames.update(self.extra_nicknames) + # Set the roma subname for JP monsters + if monster.roma_subname: + self.final_nicknames.add(monster.roma_subname) + + # For each basename, add nicknames + for basename in self.group_basenames: + # Add the basename directly + self.final_nicknames.add(basename) + # Add the prefix plus basename, and the prefix with a space between basename + for prefix in self.prefixes: + self.final_nicknames.add(prefix + basename) + self.final_nicknames.add(prefix + ' ' + basename) + + self.final_two_word_nicknames = set() + # Slightly different process for two-word basenames. Does this make sense? Who knows. + for basename in self.two_word_basenames: + self.final_two_word_nicknames.add(basename) + # Add the prefix plus basename, and the prefix with a space between basename + for prefix in self.prefixes: + self.final_two_word_nicknames.add(prefix + basename) + self.final_two_word_nicknames.add(prefix + ' ' + basename) + + def set_evolution_tree(self, evolution_tree): + """ + Set the evolution tree to a list of NamedMonsters so that we can have + nice things like prefix lookups on the entire tree in id2 and not cry + about Diablos equip + """ + self.evolution_tree = evolution_tree \ No newline at end of file From cefdb80152dba59e3dd8dbfd986b9004d32a4ee5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 24 Nov 2020 00:55:33 -0600 Subject: [PATCH 0293/1220] [dadguide] Remove unneeded graph query (#256) --- dadguide/old_monster_index.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 8e37435d..76bd36a1 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -218,8 +218,7 @@ def is_pixel(n): prefixes.add('equip') # Collab prefixes - prefixes.update(self.series_to_prefix_map.get( - self.db_context.graph.get_monster(m.monster_no).series.series_id, [])) + prefixes.update(self.series_to_prefix_map.get(m.series.series_id, [])) return prefixes From 842222c897318b66dae1148a2dd310d07d62ff08 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 24 Nov 2020 00:56:15 -0600 Subject: [PATCH 0294/1220] Refactor id menu (#257) --- padinfo/id_menu.py | 565 ++++++++++++++++++++++++++++++++ padinfo/leader_skills.py | 66 ++++ padinfo/padinfo.py | 675 ++------------------------------------- 3 files changed, 664 insertions(+), 642 deletions(-) create mode 100644 padinfo/id_menu.py create mode 100644 padinfo/leader_skills.py diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py new file mode 100644 index 00000000..a40f7e86 --- /dev/null +++ b/padinfo/id_menu.py @@ -0,0 +1,565 @@ +import discord +import prettytable + +import tsutils +import urllib.parse + +from redbot.core.utils.chat_formatting import box, inline + +from .leader_skills import createMultiplierText +from .leader_skills import createSingleMultiplierText + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dadguide.database_context import DbContext + from dadguide.models.monster_model import MonsterModel + +INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' + +MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' +RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' +RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' +VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' +GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' +ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' +ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' + +YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' +SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' +ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' + + +def get_pdx_url(m: "MonsterModel"): + return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) + + +def get_portrait_url(m: "MonsterModel"): + return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) + + +def get_pic_url(m: "MonsterModel"): + return RPAD_PIC_TEMPLATE.format(m.monster_id) + + +class IdMenu(object): + def __init__(self, db_context: "DbContext" = None, allowed_emojis: list = None): + self.db_context = db_context + self.allowed_emojis = allowed_emojis + + def match_emoji(self, name): + for e in self.allowed_emojis: + if e.name == name: + return e + return name + + @staticmethod + def monsterToHeader(m: "MonsterModel", link=False): + # type_emojis = '{} '.format(''.join( + # [str(self.match_emoji('mons_type_{}'.format(t.name.lower()))) for t in m.types])) if show_types else '' + type_emojis = '' + msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) + return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg + + @staticmethod + def monsterToJaSuffix(m: "MonsterModel", subname_on_override=True): + suffix = "" + if m.roma_subname and (subname_on_override or m.name_en_override is None): + suffix += ' [{}]'.format(m.roma_subname) + if not m.on_na: + suffix += ' (JP only)' + return suffix + + def monsterToLongHeader(self, m: "MonsterModel", link=False): + msg = self.monsterToHeader(m) + self.monsterToJaSuffix(m) + return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg + + def monsterToEvoHeader(self, m: "MonsterModel", link=True): + prefix = f" {self.monster_attr_emoji(m)} " + msg = f"{m.monster_no_na} - {m.name_en}" + suffix = self.monsterToJaSuffix(m, False) + return prefix + ("[{}]({})".format(msg, get_pdx_url(m)) if link else msg) + suffix + + @staticmethod + def monsterToThumbnailUrl(m: "MonsterModel"): + return get_portrait_url(m) + + def monsterToBaseEmbed(self, m: "MonsterModel"): + header = self.monsterToLongHeader(m) + embed = discord.Embed() + embed.set_thumbnail(url=self.monsterToThumbnailUrl(m)) + embed.title = header + embed.url = get_pdx_url(m) + embed.set_footer(text='Requester may click the reactions below to switch tabs') + return embed + + def addEvoListFields(self, monsters, current_monster): + if not len(monsters): + return + field_data = '' + field_values = [] + for ae in sorted(monsters, key=lambda x: int(x.monster_id)): + monster_header = self.monsterToEvoHeader( + ae, link=ae.monster_id != current_monster.monster_id) + '\n' + if len(field_data + monster_header) > 1024: + field_values.append(field_data) + field_data = "" + field_data += monster_header + field_values.append(field_data) + return field_values + + def monster_attr_emoji(self, monster: "MonsterModel"): + attr1 = monster.attr1.name.lower() + attr2 = monster.attr2.name.lower() + emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) + return self.match_emoji(emoji) + + def monsterToEvoEmbed(self, m: "MonsterModel"): + embed = self.monsterToBaseEmbed(m) + alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) + gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) + + if not len(alt_versions): + embed.description = 'No alternate evos or evo gem' + return embed + + evos = self.addEvoListFields(alt_versions, m) + if not gem_versions: + embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) + for f in evos[1:]: + embed.add_field(name="\u200b", value=f) + return embed + gems = self.addEvoListFields(gem_versions, m) + + embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) + for e in evos[1:]: + embed.add_field(name="\u200b", value=e, inline=False) + + embed.add_field(name="{} evolve gem(s)".format(len(gem_versions)), value=gems[0], inline=False) + for e in gems[1:]: + embed.add_field(name="\u200b", value=e, inline=False) + + return embed + + def addMonsterEvoOfList(self, monster_id_list, embed, field_name): + if not len(monster_id_list): + return + field_data = '' + if len(monster_id_list) > 5: + field_data = '{} monsters'.format(len(monster_id_list)) + else: + item_count = min(len(monster_id_list), 5) + monster_list = [self.db_context.graph.get_monster(m) for m in monster_id_list] + for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: + field_data += "{}\n".format(self.monsterToLongHeader(ae, link=True)) + embed.add_field(name=field_name, value=field_data) + + def monsterToEvoMatsEmbed(self, m: "MonsterModel"): + embed = self.monsterToBaseEmbed(m) + + mats_for_evo = self.db_context.graph.evo_mats_by_monster(m) + + field_name = 'Evo materials' + field_data = '' + if len(mats_for_evo) > 0: + for ae in mats_for_evo: + field_data += "{}\n".format(self.monsterToLongHeader(ae, link=True)) + else: + field_data = 'None' + embed.add_field(name=field_name, value=field_data) + + self.addMonsterEvoOfList(self.db_context.graph.material_of_ids(m), embed, 'Material for') + evo_gem = self.db_context.graph.evo_gem_monster(m) + if not evo_gem: + return embed + self.addMonsterEvoOfList(self.db_context.graph.material_of_ids(evo_gem), embed, "Evo gem is mat for") + return embed + + def monsterToPantheonEmbed(self, m: "MonsterModel"): + full_pantheon = self.db_context.get_monsters_by_series(m.series_id) + pantheon_list = list(filter(lambda x: self.db_context.graph.monster_is_base(x), full_pantheon)) + if len(pantheon_list) == 0 or len(pantheon_list) > 6: + return None + + embed = self.monsterToBaseEmbed(m) + + field_name = 'Pantheon: ' + self.db_context.graph.get_monster(m.monster_no).series.name + field_data = '' + for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): + field_data += '\n' + self.monsterToHeader(monster, link=True) + embed.add_field(name=field_name, value=field_data) + + return embed + + def monsterToSkillupsEmbed(self, m: "MonsterModel"): + if m.active_skill is None: + return None + possible_skillups_list = self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) + skillups_list = list(filter( + lambda x: self.db_context.graph.monster_is_farmable_evo(x), possible_skillups_list)) + + if len(skillups_list) == 0: + return None + + embed = self.monsterToBaseEmbed(m) + + field_name = 'Skillups' + field_data = '' + + # Prevent huge skillup lists + if len(skillups_list) > 8: + field_data = '({} skillups omitted)'.format(len(skillups_list) - 8) + skillups_list = skillups_list[0:8] + + for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): + field_data += '\n' + self.monsterToHeader(monster, link=True) + + if len(field_data.strip()): + embed.add_field(name=field_name, value=field_data) + + return embed + + @staticmethod + def monsterToPicUrl(m: "MonsterModel"): + return get_pic_url(m) + + def monsterToPicEmbed(self, m: "MonsterModel", animated=False): + embed = self.monsterToBaseEmbed(m) + url = self.monsterToPicUrl(m) + embed.set_image(url=url) + # Clear the thumbnail, don't need it on pic + embed.set_thumbnail(url='') + extra_links = [] + if animated: + extra_links.append('Animation: {} -- {}'.format(self.monsterToVideoUrl(m), self.monsterToGifUrl(m))) + if m.orb_skin_id is not None: + extra_links.append('Orb Skin: {} -- {}'.format(self.monsterToOrbSkinUrl(m), self.monsterToOrbSkinCBUrl(m))) + if len(extra_links) > 0: + embed.add_field(name='Extra Links', value='\n'.join(extra_links)) + + return embed + + @staticmethod + def monsterToVideoUrl(m: "MonsterModel", link_text='(MP4)'): + return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) + + @staticmethod + def monsterToGifUrl(m: "MonsterModel", link_text='(GIF)'): + return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) + + @staticmethod + def monsterToOrbSkinUrl(m: "MonsterModel", link_text='Regular'): + return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) + + @staticmethod + def monsterToOrbSkinCBUrl(m: "MonsterModel", link_text='Color Blind'): + return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) + + def monstersToLsEmbed(self, left_m: "MonsterModel", right_m: "MonsterModel"): + lls = left_m.leader_skill + rls = right_m.leader_skill + + multiplier_text = createMultiplierText(lls, rls) + + embed = discord.Embed() + embed.title = '{}\n\n'.format(multiplier_text) + description = '' + description += '\n**{}**\n{}'.format( + self.monsterToHeader(left_m, link=True), + lls.desc if lls else 'None') + description += '\n**{}**\n{}'.format( + self.monsterToHeader(right_m, link=True), + rls.desc if rls else 'None') + embed.description = description + + return embed + + def monsterToHeaderEmbed(self, m: "MonsterModel"): + header = self.monsterToLongHeader(m, link=True) + embed = discord.Embed() + embed.description = header + return embed + + def monsterToAcquireString(self, m: "MonsterModel"): + acquire_text = None + if self.db_context.graph.monster_is_farmable(m) and not self.db_context.graph.monster_is_mp_evo(m): + # Some MP shop monsters 'drop' in PADR + acquire_text = 'Farmable' + elif self.db_context.graph.monster_is_farmable_evo(m) and not self.db_context.graph.monster_is_mp_evo(m): + acquire_text = 'Farmable Evo' + elif m.in_pem: + acquire_text = 'In PEM' + elif self.db_context.graph.monster_is_pem_evo(m): + acquire_text = 'PEM Evo' + elif m.in_rem: + acquire_text = 'In REM' + elif self.db_context.graph.monster_is_rem_evo(m): + acquire_text = 'REM Evo' + elif m.in_mpshop: + acquire_text = 'MP Shop' + elif self.db_context.graph.monster_is_mp_evo(m): + acquire_text = 'MP Shop Evo' + return acquire_text + + def monsterToEmbed(self, m: "MonsterModel"): + embed = self.monsterToBaseEmbed(m) + + # in case we want to readd the type emojis later + # types_row = ' '.join(['{} {}'.format(str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))), t.name) for t in m.types]) + types_row = '/'.join(['{}'.format(t.name) for t in m.types]) + + awakenings_row = '' + for idx, a in enumerate(m.awakenings): + as_id = a.awoken_skill_id + as_name = a.name + mapped_awakening = AWAKENING_MAP.get(as_id, as_name) + mapped_awakening = self.match_emoji(mapped_awakening) + + # Wrap superawakenings to the next line + if len(m.awakenings) - idx == m.superawakening_count: + awakenings_row += '\n{}'.format(self.match_emoji('sa_questionmark')) + awakenings_row += ' {}'.format(mapped_awakening) + else: + awakenings_row += ' {}'.format(mapped_awakening) + + is_transform_base = self.db_context.graph.monster_is_transform_base(m) + transform_base = self.db_context.graph.get_transform_base_by_id(m.monster_id) if not is_transform_base else None + + awakenings_row = awakenings_row.strip() + + if not len(awakenings_row): + awakenings_row = 'No Awakenings' + + if is_transform_base: + + killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, + self.get_killers_text(m)) + else: + killers_row = '**Avail. killers (pre-transform):** [{} slots] {}'.format( + transform_base.latent_slots, + self.get_killers_text(transform_base)) + + embed.add_field(name=types_row, value='{}\n{}'.format(awakenings_row, killers_row), inline=False) + + info_row_1 = 'Inheritable' if m.is_inheritable else 'Not inheritable' + acquire_text = self.monsterToAcquireString(m) + tet_text = self.db_context.graph.true_evo_type_by_monster(m).value + + orb_skin = "" if m.orb_skin_id is None else " (Orb Skin)" + + info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format( + m.rarity, + self.db_context.graph.get_base_monster_by_id(m.monster_no).rarity, + orb_skin, + m.cost + ) + + if acquire_text: + info_row_2 += '\n**{}**'.format(acquire_text) + if tet_text in ("Reincarnated", "Assist", "Pixel", "Super Reincarnated"): + info_row_2 += '\n**{}**'.format(tet_text) + + embed.add_field(name=info_row_1, value=info_row_2) + + hp, atk, rcv, weighted = m.stats() + if m.limit_mult > 0: + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) + stats_row_1 = 'Stats (LB, +{}%)'.format(m.limit_mult) + stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( + hp, lb_hp, atk, lb_atk, rcv, lb_rcv) + else: + stats_row_1 = 'Stats' + stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) + if any(x if x.name == 'Enhance' else None for x in m.types): + stats_row_2 += '\n**Fodder EXP** {:,}'.format(m.fodder_exp) + embed.add_field(name=stats_row_1, value=stats_row_2) + + active_header = 'Active Skill' + active_body = 'None' + active_skill = m.active_skill + if active_skill: + active_header = 'Active Skill ({} -> {})'.format(active_skill.turn_max, + active_skill.turn_min) + active_body = active_skill.desc + embed.add_field(name=active_header, value=active_body, inline=False) + + leader_skill = m.leader_skill + ls_row = m.leader_skill.desc if leader_skill else 'None' + ls_header = 'Leader Skill' + if leader_skill: + multiplier_text = createMultiplierText(leader_skill) + ls_header += " {}".format(multiplier_text) + embed.add_field(name=ls_header, value=ls_row, inline=False) + + evos_header = "Alternate Evos" + evos_body = ", ".join(f"**{m2.monster_id}**" + if m2.monster_id == m.monster_id + else f"[{m2.monster_id}]({get_pdx_url(m2)})" + for m2 in + sorted({*self.db_context.graph.get_alt_monsters_by_id(m.monster_no)}, + key=lambda x: x.monster_id)) + embed.add_field(name=evos_header, value=evos_body, inline=False) + + return embed + + def get_killers_text(self, m: "MonsterModel"): + if 'Any' in m.killers: + return 'Any' + return ' '.join([str(self.match_emoji('latent_killer_{}'.format(k.lower()))) for k in m.killers]) + + def monsterToOtherInfoEmbed(self, m: "MonsterModel"): + embed = self.monsterToBaseEmbed(m) + # Clear the thumbnail, takes up too much space + embed.set_thumbnail(url='') + + body_text = '\n' + stat_cols = ['', 'HP', 'ATK', 'RCV'] + for plus in (0, 297): + body_text += '**Stats at +{}:**'.format(plus) + tbl = prettytable.PrettyTable(stat_cols) + tbl.hrules = prettytable.NONE + tbl.vrules = prettytable.NONE + tbl.align = "l" + levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) + for lv in levels: + for inh in (False, True): + hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) + row_name = 'Lv{}'.format(lv) + if inh: + row_name = '(Inh)' + tbl.add_row([row_name.format(plus), hp, atk, rcv]) + body_text += box(tbl.get_string()) + + body_text += "\n**JP Name**: {}".format(m.name_ja) + body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + + if m.history_us: + body_text += '\n**History:** {}'.format(m.history_us) + + body_text += '\n**Series:** {}'.format(self.db_context.graph.get_monster(m.monster_no).series.name) + body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) + if m.buy_mp is not None: + body_text += " **Buy MP:** {:,}".format(m.buy_mp) + + if m.exp < 1000000: + xp_text = '{:,}'.format(m.exp) + else: + xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' + body_text += '\n**XP to Max:** {}'.format(xp_text) + body_text += ' **Max Level:**: {}'.format(m.level) + + # weighted stat calculation & display + hp, atk, rcv, weighted = m.stats() + body_text += '\n**Weighted stats:** {}'.format(weighted) + if m.limit_mult > 0: + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) + body_text += ' | LB {} (+{}%)'.format(lb_weighted, m.limit_mult) + + body_text += '\n**Fodder EXP:** {:,}'.format(m.fodder_exp) + body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) + + embed.description = body_text + + return embed + + def monsterToLinksEmbed(self, m: "MonsterModel"): + embed = self.monsterToBaseEmbed(m) + embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( + YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), + SKYOZORA_TEMPLATE.format(m.monster_no_jp), + INFO_PDX_TEMPLATE.format(m.monster_no_jp), + ILMINA_TEMPLATE.format(m.monster_no_jp)) + embed.set_footer(text='') + + def monstersToLssEmbed(self, m: "MonsterModel"): + multiplier_text = createSingleMultiplierText(m.leader_skill) + + embed = discord.Embed() + embed.title = '{}\n\n'.format(multiplier_text) + description = '' + description += '\n**{}**\n{}'.format( + self.monsterToHeader(m, link=True), + m.leader_skill.desc if m.leader_skill else 'None') + embed.description = description + + return embed + + +AWAKENING_MAP = { + 1: 'boost_hp', + 2: 'boost_atk', + 3: 'boost_rcv', + 4: 'reduce_fire', + 5: 'reduce_water', + 6: 'reduce_wood', + 7: 'reduce_light', + 8: 'reduce_dark', + 9: 'misc_autoheal', + 10: 'res_bind', + 11: 'res_blind', + 12: 'res_jammer', + 13: 'res_poison', + 14: 'oe_fire', + 15: 'oe_water', + 16: 'oe_wood', + 17: 'oe_light', + 18: 'oe_dark', + 19: 'misc_te', + 20: 'misc_bindclear', + 21: 'misc_sb', + 22: 'row_fire', + 23: 'row_water', + 24: 'row_wood', + 25: 'row_light', + 26: 'row_dark', + 27: 'misc_tpa', + 28: 'res_skillbind', + 29: 'oe_heart', + 30: 'misc_multiboost', + 31: 'killer_dragon', + 32: 'killer_god', + 33: 'killer_devil', + 34: 'killer_machine', + 35: 'killer_balance', + 36: 'killer_attacker', + 37: 'killer_physical', + 38: 'killer_healer', + 39: 'killer_evomat', + 40: 'killer_awoken', + 41: 'killer_enhancemat', + 42: 'killer_vendor', + 43: 'misc_comboboost', + 44: 'misc_guardbreak', + 45: 'misc_extraattack', + 46: 'teamboost_hp', + 47: 'teamboost_rcv', + 48: 'misc_voidshield', + 49: 'misc_assist', + 50: 'misc_super_extraattack', + 51: 'misc_skillcharge', + 52: 'res_bind_super', + 53: 'misc_te_super', + 54: 'res_cloud', + 55: 'res_seal', + 56: 'misc_sb_super', + 57: 'attack_boost_high', + 58: 'attack_boost_low', + 59: 'l_shield', + 60: 'l_attack', + 61: 'misc_super_comboboost', + 62: 'orb_combo', + 63: 'misc_voice', + 64: 'misc_dungeonbonus', + 65: 'reduce_hp', + 66: 'reduce_atk', + 67: 'reduce_rcv', + 68: 'res_blind_super', + 69: 'res_jammer_super', + 70: 'res_poison_super', + 71: 'misc_jammerboost', + 72: 'misc_poisonboost', +} diff --git a/padinfo/leader_skills.py b/padinfo/leader_skills.py new file mode 100644 index 00000000..ee5b2000 --- /dev/null +++ b/padinfo/leader_skills.py @@ -0,0 +1,66 @@ +def humanize_number(number, sigfigs=2): + n = float("{0:.{1}g}".format(number, sigfigs)) + if n >= 1e9: + return str(int(n//1e9))+"B" + elif n >= 1e6: + return str(int(n//1e6))+"M" + elif n >= 1e3: + return str(int(n//1e3))+"k" + else: + return str(int(n)) + + +def createMultiplierText(ls1, ls2=None): + if ls2 and not ls1: + ls1, ls2 = ls2, ls1 + + if ls1: + hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = ls1.data + else: + hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = 1, 1, 1, 0, 0, 0, 0, 0 + + if ls2: + hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = ls2.data + else: + hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 + + return format_ls_text( + hp1*hp2, + atk1*atk2, + rcv1*rcv2, + 1 - (1 - resist1) * (1 - resist2), + combo1+combo2, + fua1+fua2, + mfua1+mfua2, + te1+te2 + ) + + +def createSingleMultiplierText(ls=None): + if ls: + hp, atk, rcv, resist, combo, fua, mfua, te = ls.data + else: + hp, atk, rcv, resist, combo, fua, mfua, te = 1, 1, 1, 0, 0, 0, 0, 0 + + return format_ls_text(hp, atk, rcv, resist, combo, fua, mfua, te) + + +def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): + def fmtNum(val): + return '{:.2f}'.format(val).strip('0').rstrip('.') + + text = "{}/{}/{}".format(fmtNum(hp), fmtNum(atk), fmtNum(rcv)) + if resist != 0: + text += ' Resist {}%'.format(fmtNum(100 * resist)) + + extras = [] + if combo: + extras.append('+{}c'.format(combo)) + if fua: + extras.append('{} fua'.format(humanize_number(fua, 2))) + elif mfua: + extras.append('fua') + + if extras: + return '[{}] [{}]'.format(text, ' '.join(extras)) + return '[{}]'.format(text) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2cf42079..2497ea62 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -17,6 +17,7 @@ from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, confirm_message from .find_monster import prefix_to_filter +from .id_menu import IdMenu from typing import TYPE_CHECKING @@ -48,20 +49,6 @@ EMBED_NOT_GENERATED = -1 -INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' - -MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' -RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' -RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' -VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' -GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' - -YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' -SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' -ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' - class ServerFilter(Enum): any = 0 @@ -69,18 +56,6 @@ class ServerFilter(Enum): jp = 2 -def get_pdx_url(m): - return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) - - -def get_portrait_url(m): - return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) - - -def get_pic_url(m): - return RPAD_PIC_TEMPLATE.format(m.monster_id) - - def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) @@ -140,7 +115,7 @@ def on_update(self, ctx, selected_emoji): class ScrollEmojiUpdater(EmojiUpdater): def __init__(self, emoji_to_embed, m: "MonsterModel" = None, - ms: "List[int]" = None, selected_emoji=None, pad_info=None, bot=None): + ms: "list[int]" = None, selected_emoji=None, pad_info=None, bot=None): self.emoji_dict = emoji_to_embed self.m = m self.ms = ms @@ -149,7 +124,6 @@ def __init__(self, emoji_to_embed, m: "MonsterModel" = None, self.bot = bot def on_update(self, ctx, selected_emoji): - DGCOG = self.bot.get_cog('Dadguide') index = self.ms.index(self.m) if selected_emoji == self.pad_info.first_monster_emoji: @@ -276,7 +250,7 @@ async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" m, err, debug_info = await self.findMonster(query) if m is not None: - await ctx.send(monsterToHeader(m)) + await ctx.send(IdMenu.monsterToHeader(m)) await ctx.send(box(m.name_ja)) else: await ctx.send(self.makeFailureMsg(err)) @@ -441,7 +415,8 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_id) emoji_to_embed = self.get_id_emoji_options( - m=m, scroll=sorted({*alt_versions}, key=lambda x: x.monster_id) if self.settings.checkEvoID(ctx.author.id) else [], menu_type=1) + m=m, scroll=sorted({*alt_versions}, key=lambda x: x.monster_id) if self.settings.checkEvoID( + ctx.author.id) else [], menu_type=1) return await self._do_menu( ctx, @@ -464,23 +439,25 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database - id_embed = monsterToEmbed(m, self.get_emojis(), db_context) - evo_embed = monsterToEvoEmbed(m, self.get_emojis(), db_context) - mats_embed = monsterToEvoMatsEmbed(m, db_context, self.get_emojis()) + menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) + + id_embed = menu.monsterToEmbed(m) + evo_embed = menu.monsterToEvoEmbed(m) + mats_embed = menu.monsterToEvoMatsEmbed(m) animated = m.has_animation - pic_embed = monsterToPicEmbed(m, self.get_emojis(), animated=animated) - other_info_embed = monsterToOtherInfoEmbed(m, db_context, self.get_emojis()) + pic_embed = menu.monsterToPicEmbed(m, animated=animated) + other_info_embed = menu.monsterToOtherInfoEmbed(m) emoji_to_embed = OrderedDict() emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed emoji_to_embed[self.mats_emoji] = mats_embed emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = monsterToPantheonEmbed(m, db_context, self.get_emojis()) + pantheon_embed = menu.monsterToPantheonEmbed(m) if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed - skillups_embed = monsterToSkillupsEmbed(m, db_context, self.get_emojis()) + skillups_embed = menu.monsterToSkillupsEmbed(m) if skillups_embed: emoji_to_embed[self.skillups_emoji] = skillups_embed @@ -510,14 +487,17 @@ async def _do_evolistmenu(self, ctx, sm): monsters.sort(key=lambda x: x.monster_id) emoji_to_embed = OrderedDict() + menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) + starting_menu_emoji = None for idx, m in enumerate(monsters): chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" if idx > 19: - await ctx.send("There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.prefix)) + await ctx.send( + "There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.prefix)) return else: emoji = char_to_emoji(chars[idx]) - emoji_to_embed[emoji] = monsterToEmbed(m, self.get_emojis(), db_context) + emoji_to_embed[emoji] = menu.monsterToEmbed(m) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji @@ -556,13 +536,8 @@ async def links(self, ctx, *, query: str): """Monster links""" m, err, debug_info = await self.findMonster(query) if m is not None: - embed = monsterToBaseEmbed(m) - embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - embed.set_footer(text='') + menu = IdMenu() + embed = menu.monsterToLinksEmbed(m) await ctx.send(embed=embed) else: @@ -584,7 +559,8 @@ async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" m, err, debug_info = await self.findMonster(query) if m is not None: - embed = monsterToHeaderEmbed(m, self.get_emojis()) + menu = IdMenu(allowed_emojis=self.get_emojis()) + embed = menu.monsterToHeaderEmbed(m) await ctx.send(embed=embed) else: await ctx.send(self.makeFailureMsg(err)) @@ -631,7 +607,8 @@ async def evoscroll(self, ctx, *, query: str): m, err, debug_info = await self.findMonster(query) if m is not None: - await self._do_scrollmenu(ctx, m, sorted(db_context.graph.get_alt_monsters(m), key=lambda x: x.monster_id), self.id_emoji) + await self._do_scrollmenu(ctx, m, sorted(db_context.graph.get_alt_monsters(m), key=lambda x: x.monster_id), + self.id_emoji) else: await ctx.send(self.makeFailureMsg(err)) @@ -684,10 +661,11 @@ async def leaderskill(self, ctx, *, whole_query): await ctx.send(inline(err_msg.format('Right', right_query))) return + menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(left_m, self.get_emojis(), db_context) - emoji_to_embed[self.right_emoji] = monsterToEmbed(right_m, self.get_emojis(), db_context) + emoji_to_embed[self.ls_emoji] = menu.monstersToLsEmbed(left_m, right_m) + emoji_to_embed[self.left_emoji] = menu.monsterToEmbed(left_m) + emoji_to_embed[self.right_emoji] = menu.monsterToEmbed(right_m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -701,9 +679,10 @@ async def leaderskillsingle(self, ctx, *, query): if err: await ctx.send(err) return + menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = monstersToLssEmbed(m) - emoji_to_embed[self.left_emoji] = monsterToEmbed(m, self.get_emojis(), db_context) + emoji_to_embed[self.ls_emoji] = menu.monstersToLssEmbed(m) + emoji_to_embed[self.left_emoji] = menu.monsterToEmbed(m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -740,7 +719,7 @@ async def padsay(self, ctx, server, *, query: str = None): return base_dir = self.settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) - header = '{} ({})'.format(monsterToHeader(m), server) + header = '{} ({})'.format(IdMenu.monsterToHeader(m), server) if not os.path.exists(voice_file): await ctx.send(inline('Could not find voice for ' + header)) return @@ -991,591 +970,3 @@ def voiceDir(self): def log_emoji(self, emote): self.bot_settings['emoji_use'][emote] = self.bot_settings['emoji_use'].get(emote, 0) + 1 self.save_settings() - - -def monsterToHeader(m: "MonsterModel", link=False, show_types=False, allowed_emojis=None): - type_emojis = '{} '.format(''.join([str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))) for t in m.types])) if show_types else '' - msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - -def monsterToJaSuffix(m: "MonsterModel", subname_on_override=True): - suffix = "" - if m.roma_subname and (subname_on_override or m.name_en_override is None): - suffix += ' [{}]'.format(m.roma_subname) - if not m.on_na: - suffix += ' (JP only)' - return suffix - - -def monsterToLongHeader(m: "MonsterModel", link=False, show_types=False, allowed_emojis=None): - msg = monsterToHeader(m, show_types=show_types, allowed_emojis=allowed_emojis) + monsterToJaSuffix(m) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - -def monsterToEvoHeader(m: "MonsterModel", allowed_emojis, link=True): - prefix = f" {monster_attr_emoji(allowed_emojis, m)} " - msg = f"{m.monster_no_na} - {m.name_en}" - suffix = monsterToJaSuffix(m, False) - return prefix + ("[{}]({})".format(msg, get_pdx_url(m)) if link else msg) + suffix - - -def monsterToThumbnailUrl(m: "MonsterModel"): - return get_portrait_url(m) - - -def monsterToBaseEmbed(m: "MonsterModel", allowed_emojis): - header = monsterToLongHeader(m, allowed_emojis=allowed_emojis) - embed = discord.Embed() - embed.set_thumbnail(url=monsterToThumbnailUrl(m)) - embed.title = header - embed.url = get_pdx_url(m) - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed - - -def addEvoListFields(monsters, current_monster, allowed_emojis): - if not len(monsters): - return - field_data = '' - field_values = [] - for ae in sorted(monsters, key=lambda x: int(x.monster_id)): - monster_header = monsterToEvoHeader(ae, allowed_emojis, link=ae.monster_id != current_monster.monster_id) + '\n' - if len(field_data+monster_header) > 1024: - field_values.append(field_data) - field_data = "" - field_data += monster_header - field_values.append(field_data) - return field_values - - -def monster_attr_emoji(allowed_emojis, monster: "MonsterModel"): - attr1 = monster.attr1.name.lower() - attr2 = monster.attr2.name.lower() - emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) - return match_emoji(allowed_emojis, emoji) - - -def monsterToEvoEmbed(m: "MonsterModel", allowed_emojis, db_context: "DbContext"): - embed = monsterToBaseEmbed(m, allowed_emojis) - alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_no) - gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) - - if not len(alt_versions): - embed.description = 'No alternate evos or evo gem' - return embed - - evos = addEvoListFields(alt_versions, m, allowed_emojis) - if not gem_versions: - embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) - for f in evos[1:]: - embed.add_field(name="\u200b", value=f) - return embed - gems = addEvoListFields(gem_versions, m, allowed_emojis) - - embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) - for e in evos[1:]: - embed.add_field(name="\u200b", value=e, inline=False) - - embed.add_field(name="{} evolve gem(s)".format(len(gem_versions)), value=gems[0], inline=False) - for e in gems[1:]: - embed.add_field(name="\u200b", value=g, inline=False) - - return embed - - -def addMonsterEvoOfList(monster_id_list, embed, field_name, db_context=None): - if not len(monster_id_list): - return - field_data = '' - if len(monster_id_list) > 5: - field_data = '{} monsters'.format(len(monster_id_list)) - else: - item_count = min(len(monster_id_list), 5) - monster_list = [db_context.graph.get_monster(m) for m in monster_id_list] - for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - embed.add_field(name=field_name, value=field_data) - - -def monsterToEvoMatsEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): - embed = monsterToBaseEmbed(m, allowed_emojis) - - mats_for_evo = db_context.graph.evo_mats_by_monster(m) - - field_name = 'Evo materials' - field_data = '' - if len(mats_for_evo) > 0: - for ae in mats_for_evo: - field_data += "{}\n".format(monsterToLongHeader(ae, link=True)) - else: - field_data = 'None' - embed.add_field(name=field_name, value=field_data) - - addMonsterEvoOfList(db_context.graph.material_of_ids(m), embed, 'Material for', db_context=db_context) - evo_gem = db_context.graph.evo_gem_monster(m) - if not evo_gem: - return embed - addMonsterEvoOfList(db_context.graph.material_of_ids(evo_gem), embed, "Evo gem is mat for", db_context=db_context) - return embed - - -def monsterToPantheonEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): - full_pantheon = db_context.get_monsters_by_series(m.series_id) - pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 6: - return None - - embed = monsterToBaseEmbed(m, allowed_emojis) - - field_name = 'Pantheon: ' + db_context.graph.get_monster(m.monster_no).series.name - field_data = '' - for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): - field_data += '\n' + monsterToHeader(monster, link=True) - embed.add_field(name=field_name, value=field_data) - - return embed - - -def monsterToSkillupsEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): - if m.active_skill is None: - return None - possible_skillups_list = db_context.get_monsters_by_active(m.active_skill.active_skill_id) - skillups_list = list(filter( - lambda x: db_context.graph.monster_is_farmable_evo(x), possible_skillups_list)) - - if len(skillups_list) == 0: - return None - - embed = monsterToBaseEmbed(m, allowed_emojis) - - field_name = 'Skillups' - field_data = '' - - # Prevent huge skillup lists - if len(skillups_list) > 8: - field_data = '({} skillups omitted)'.format(len(skillups_list) - 8) - skillups_list = skillups_list[0:8] - - for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): - field_data += '\n' + monsterToHeader(monster, link=True) - - if len(field_data.strip()): - embed.add_field(name=field_name, value=field_data) - - return embed - - -def monsterToPicUrl(m: "MonsterModel"): - return get_pic_url(m) - - -def monsterToPicEmbed(m: "MonsterModel", allowed_emojis, animated=False): - embed = monsterToBaseEmbed(m, allowed_emojis) - url = monsterToPicUrl(m) - embed.set_image(url=url) - # Clear the thumbnail, don't need it on pic - embed.set_thumbnail(url='') - extra_links = [] - if animated: - extra_links.append('Animation: {} -- {}'.format(monsterToVideoUrl(m), monsterToGifUrl(m))) - if m.orb_skin_id is not None: - extra_links.append('Orb Skin: {} -- {}'.format(monsterToOrbSkinUrl(m), monsterToOrbSkinCBUrl(m))) - if len(extra_links) > 0: - embed.add_field(name='Extra Links', value='\n'.join(extra_links)) - - return embed - - -def monsterToVideoUrl(m: "MonsterModel", link_text='(MP4)'): - return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) - - -def monsterToGifUrl(m: "MonsterModel", link_text='(GIF)'): - return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) - - -def monsterToOrbSkinUrl(m: "MonsterModel", link_text='Regular'): - return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) - - -def monsterToOrbSkinCBUrl(m: "MonsterModel", link_text='Color Blind'): - return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) - - -def monstersToLsEmbed(left_m: "MonsterModel", right_m: "MonsterModel"): - lls = left_m.leader_skill - rls = right_m.leader_skill - - multiplier_text = createMultiplierText(lls, rls) - - embed = discord.Embed() - embed.title = '{}\n\n'.format(multiplier_text) - description = '' - description += '\n**{}**\n{}'.format( - monsterToHeader(left_m, link=True), - lls.desc if lls else 'None') - description += '\n**{}**\n{}'.format( - monsterToHeader(right_m, link=True), - rls.desc if rls else 'None') - embed.description = description - - return embed - - -def monstersToLssEmbed(m: "MonsterModel"): - multiplier_text = createSingleMultiplierText(m.leader_skill) - - embed = discord.Embed() - embed.title = '{}\n\n'.format(multiplier_text) - description = '' - description += '\n**{}**\n{}'.format( - monsterToHeader(m, link=True), - m.leader_skill.desc if m.leader_skill else 'None') - embed.description = description - - return embed - - -def monsterToHeaderEmbed(m: "MonsterModel", allowed_emojis): - header = monsterToLongHeader(m, link=True, allowed_emojis=allowed_emojis) - embed = discord.Embed() - embed.description = header - return embed - - -def monsterToAcquireString(m: "MonsterModel", db_context: "DbContext"): - acquire_text = None - if db_context.graph.monster_is_farmable(m) and not db_context.graph.monster_is_mp_evo(m): - # Some MP shop monsters 'drop' in PADR - acquire_text = 'Farmable' - elif db_context.graph.monster_is_farmable_evo(m) and not db_context.graph.monster_is_mp_evo(m): - acquire_text = 'Farmable Evo' - elif m.in_pem: - acquire_text = 'In PEM' - elif db_context.graph.monster_is_pem_evo(m): - acquire_text = 'PEM Evo' - elif m.in_rem: - acquire_text = 'In REM' - elif db_context.graph.monster_is_rem_evo(m): - acquire_text = 'REM Evo' - elif m.in_mpshop: - acquire_text = 'MP Shop' - elif db_context.graph.monster_is_mp_evo(m): - acquire_text = 'MP Shop Evo' - return acquire_text - - -def match_emoji(allowed_emojis, name): - for e in allowed_emojis: - if e.name == name: - return e - return name - - -def monsterToEmbed(m: "MonsterModel", allowed_emojis, db_context: "DbContext"): - embed = monsterToBaseEmbed(m, allowed_emojis) - - # in case we want to readd the type emojis later - # types_row = ' '.join(['{} {}'.format(str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))), t.name) for t in m.types]) - types_row = '/'.join(['{}'.format(t.name) for t in m.types]) - - awakenings_row = '' - for idx, a in enumerate(m.awakenings): - as_id = a.awoken_skill_id - as_name = a.name - mapped_awakening = AWAKENING_MAP.get(as_id, as_name) - mapped_awakening = match_emoji(allowed_emojis, mapped_awakening) - - # Wrap superawakenings to the next line - if len(m.awakenings) - idx == m.superawakening_count: - awakenings_row += '\n{}'.format(match_emoji(allowed_emojis, 'sa_questionmark')) - awakenings_row += ' {}'.format(mapped_awakening) - else: - awakenings_row += ' {}'.format(mapped_awakening) - - is_transform_base = db_context.graph.monster_is_transform_base(m) - transform_base = db_context.graph.get_transform_base_by_id(m.monster_id) if not is_transform_base else None - - awakenings_row = awakenings_row.strip() - - if not len(awakenings_row): - awakenings_row = 'No Awakenings' - - if is_transform_base: - - killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, get_killers_text(m, allowed_emojis)) - else: - killers_row = '**Avail. killers (pre-transform):** [{} slots] {}'.format(transform_base.latent_slots, get_killers_text(transform_base, allowed_emojis)) - - embed.add_field(name=types_row, value='{}\n{}'.format(awakenings_row, killers_row), inline=False) - - info_row_1 = 'Inheritable' if m.is_inheritable else 'Not inheritable' - acquire_text = monsterToAcquireString(m, db_context) - tet_text = db_context.graph.true_evo_type_by_monster(m).value - - orb_skin = "" if m.orb_skin_id is None else " (Orb Skin)" - - info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format( - m.rarity, - db_context.graph.get_base_monster_by_id(m.monster_no).rarity, - orb_skin, - m.cost - ) - - if acquire_text: - info_row_2 += '\n**{}**'.format(acquire_text) - if tet_text in ("Reincarnated", "Assist", "Pixel", "Super Reincarnated"): - info_row_2 += '\n**{}**'.format(tet_text) - - embed.add_field(name=info_row_1, value=info_row_2) - - hp, atk, rcv, weighted = m.stats() - if m.limit_mult > 0: - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_row_1 = 'Stats (LB, +{}%)'.format(m.limit_mult) - stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( - hp, lb_hp, atk, lb_atk, rcv, lb_rcv) - else: - stats_row_1 = 'Stats' - stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) - if any(x if x.name == 'Enhance' else None for x in m.types): - stats_row_2 += '\n**Fodder EXP** {:,}'.format(m.fodder_exp) - embed.add_field(name=stats_row_1, value=stats_row_2) - - active_header = 'Active Skill' - active_body = 'None' - active_skill = m.active_skill - if active_skill: - active_header = 'Active Skill ({} -> {})'.format(active_skill.turn_max, - active_skill.turn_min) - active_body = active_skill.desc - embed.add_field(name=active_header, value=active_body, inline=False) - - leader_skill = m.leader_skill - ls_row = m.leader_skill.desc if leader_skill else 'None' - ls_header = 'Leader Skill' - if leader_skill: - multiplier_text = createMultiplierText(leader_skill) - ls_header += " {}".format(multiplier_text) - embed.add_field(name=ls_header, value=ls_row, inline=False) - - evos_header = "Alternate Evos" - evos_body = ", ".join(f"**{m2.monster_id}**" - if m2.monster_id == m.monster_id - else f"[{m2.monster_id}]({get_pdx_url(m2)})" - for m2 in - sorted({*db_context.graph.get_alt_monsters_by_id(m.monster_no)}, key=lambda x: x.monster_id)) - embed.add_field(name=evos_header, value=evos_body, inline=False) - - return embed - - -def get_killers_text(m: "MonsterModel", allowed_emojis): - if 'Any' in m.killers: - return 'Any' - return ' '.join([str(match_emoji(allowed_emojis, 'latent_killer_{}'.format(k.lower()))) for k in m.killers]) - - -def monsterToOtherInfoEmbed(m: "MonsterModel", db_context: "DbContext", allowed_emojis): - embed = monsterToBaseEmbed(m, allowed_emojis) - # Clear the thumbnail, takes up too much space - embed.set_thumbnail(url='') - - body_text = '\n' - stat_cols = ['', 'HP', 'ATK', 'RCV'] - for plus in (0, 297): - body_text += '**Stats at +{}:**'.format(plus) - tbl = prettytable.PrettyTable(stat_cols) - tbl.hrules = prettytable.NONE - tbl.vrules = prettytable.NONE - tbl.align = "l" - levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) - for lv in levels: - for inh in (False, True): - hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) - row_name = 'Lv{}'.format(lv) - if inh: - row_name = '(Inh)' - tbl.add_row([row_name.format(plus), hp, atk, rcv]) - body_text += box(tbl.get_string()) - - body_text += "\n**JP Name**: {}".format(m.name_ja) - body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - - if m.history_us: - body_text += '\n**History:** {}'.format(m.history_us) - - body_text += '\n**Series:** {}'.format(db_context.graph.get_monster(m.monster_no).series.name) - body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) - if m.buy_mp is not None: - body_text += " **Buy MP:** {:,}".format(m.buy_mp) - - if m.exp < 1000000: - xp_text = '{:,}'.format(m.exp) - else: - xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' - body_text += '\n**XP to Max:** {}'.format(xp_text) - body_text += ' **Max Level:**: {}'.format(m.level) - - # weighted stat calculation & display - hp, atk, rcv, weighted = m.stats() - body_text += '\n**Weighted stats:** {}'.format(weighted) - if m.limit_mult > 0: - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - body_text += ' | LB {} (+{}%)'.format(lb_weighted, m.limit_mult) - - body_text += '\n**Fodder EXP:** {:,}'.format(m.fodder_exp) - body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) - - embed.description = body_text - - return embed - - -AWAKENING_MAP = { - 1: 'boost_hp', - 2: 'boost_atk', - 3: 'boost_rcv', - 4: 'reduce_fire', - 5: 'reduce_water', - 6: 'reduce_wood', - 7: 'reduce_light', - 8: 'reduce_dark', - 9: 'misc_autoheal', - 10: 'res_bind', - 11: 'res_blind', - 12: 'res_jammer', - 13: 'res_poison', - 14: 'oe_fire', - 15: 'oe_water', - 16: 'oe_wood', - 17: 'oe_light', - 18: 'oe_dark', - 19: 'misc_te', - 20: 'misc_bindclear', - 21: 'misc_sb', - 22: 'row_fire', - 23: 'row_water', - 24: 'row_wood', - 25: 'row_light', - 26: 'row_dark', - 27: 'misc_tpa', - 28: 'res_skillbind', - 29: 'oe_heart', - 30: 'misc_multiboost', - 31: 'killer_dragon', - 32: 'killer_god', - 33: 'killer_devil', - 34: 'killer_machine', - 35: 'killer_balance', - 36: 'killer_attacker', - 37: 'killer_physical', - 38: 'killer_healer', - 39: 'killer_evomat', - 40: 'killer_awoken', - 41: 'killer_enhancemat', - 42: 'killer_vendor', - 43: 'misc_comboboost', - 44: 'misc_guardbreak', - 45: 'misc_extraattack', - 46: 'teamboost_hp', - 47: 'teamboost_rcv', - 48: 'misc_voidshield', - 49: 'misc_assist', - 50: 'misc_super_extraattack', - 51: 'misc_skillcharge', - 52: 'res_bind_super', - 53: 'misc_te_super', - 54: 'res_cloud', - 55: 'res_seal', - 56: 'misc_sb_super', - 57: 'attack_boost_high', - 58: 'attack_boost_low', - 59: 'l_shield', - 60: 'l_attack', - 61: 'misc_super_comboboost', - 62: 'orb_combo', - 63: 'misc_voice', - 64: 'misc_dungeonbonus', - 65: 'reduce_hp', - 66: 'reduce_atk', - 67: 'reduce_rcv', - 68: 'res_blind_super', - 69: 'res_jammer_super', - 70: 'res_poison_super', - 71: 'misc_jammerboost', - 72: 'misc_poisonboost', -} - - -def humanize_number(number, sigfigs=2): - n = float("{0:.{1}g}".format(number, sigfigs)) - if n >= 1e9: - return str(int(n//1e9))+"B" - elif n >= 1e6: - return str(int(n//1e6))+"M" - elif n >= 1e3: - return str(int(n//1e3))+"k" - else: - return str(int(n)) - - -def createMultiplierText(ls1, ls2=None): - if ls2 and not ls1: - ls1, ls2 = ls2, ls1 - - if ls1: - hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = ls1.data - else: - hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = 1, 1, 1, 0, 0, 0, 0, 0 - - if ls2: - hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = ls2.data - else: - hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 - - return format_ls_text( - hp1*hp2, - atk1*atk2, - rcv1*rcv2, - 1 - (1 - resist1) * (1 - resist2), - combo1+combo2, - fua1+fua2, - mfua1+mfua2, - te1+te2 - ) - - -def createSingleMultiplierText(ls=None): - if ls: - hp, atk, rcv, resist, combo, fua, mfua, te = ls.data - else: - hp, atk, rcv, resist, combo, fua, mfua, te = 1, 1, 1, 0, 0, 0, 0, 0 - - return format_ls_text(hp, atk, rcv, resist, combo, fua, mfua, te) - - -def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): - def fmtNum(val): - return '{:.2f}'.format(val).strip('0').rstrip('.') - - text = "{}/{}/{}".format(fmtNum(hp), fmtNum(atk), fmtNum(rcv)) - if resist != 0: - text += ' Resist {}%'.format(fmtNum(100 * resist)) - - extras = [] - if combo: - extras.append('+{}c'.format(combo)) - if fua: - extras.append('{} fua'.format(humanize_number(fua, 2))) - elif mfua: - extras.append('fua') - - if extras: - return '[{}] [{}]'.format(text, ' '.join(extras)) - return '[{}]'.format(text) From 328b66ac2cd8bcf0db4c7ac18df0466d7e7faf79 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 24 Nov 2020 02:12:36 -0600 Subject: [PATCH 0295/1220] Rename idmenu methods (#258) --- padinfo/id_menu.py | 115 ++++++++++++++++++++++----------------------- padinfo/padinfo.py | 34 +++++++------- 2 files changed, 73 insertions(+), 76 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index a40f7e86..91960d4d 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -54,7 +54,7 @@ def match_emoji(self, name): return name @staticmethod - def monsterToHeader(m: "MonsterModel", link=False): + def monster_header(m: "MonsterModel", link=False): # type_emojis = '{} '.format(''.join( # [str(self.match_emoji('mons_type_{}'.format(t.name.lower()))) for t in m.types])) if show_types else '' type_emojis = '' @@ -62,7 +62,7 @@ def monsterToHeader(m: "MonsterModel", link=False): return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg @staticmethod - def monsterToJaSuffix(m: "MonsterModel", subname_on_override=True): + def monster_ja_suffix(m: "MonsterModel", subname_on_override=True): suffix = "" if m.roma_subname and (subname_on_override or m.name_en_override is None): suffix += ' [{}]'.format(m.roma_subname) @@ -70,36 +70,36 @@ def monsterToJaSuffix(m: "MonsterModel", subname_on_override=True): suffix += ' (JP only)' return suffix - def monsterToLongHeader(self, m: "MonsterModel", link=False): - msg = self.monsterToHeader(m) + self.monsterToJaSuffix(m) + def monster_long_header(self, m: "MonsterModel", link=False): + msg = self.monster_header(m) + self.monster_ja_suffix(m) return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - def monsterToEvoHeader(self, m: "MonsterModel", link=True): - prefix = f" {self.monster_attr_emoji(m)} " + def monster_evo_header(self, m: "MonsterModel", link=True): + prefix = f" {self._get_monster_attr_emoji(m)} " msg = f"{m.monster_no_na} - {m.name_en}" - suffix = self.monsterToJaSuffix(m, False) + suffix = self.monster_ja_suffix(m, False) return prefix + ("[{}]({})".format(msg, get_pdx_url(m)) if link else msg) + suffix @staticmethod - def monsterToThumbnailUrl(m: "MonsterModel"): + def make_thumbnail_url(m: "MonsterModel"): return get_portrait_url(m) - def monsterToBaseEmbed(self, m: "MonsterModel"): - header = self.monsterToLongHeader(m) + def make_base_embed(self, m: "MonsterModel"): + header = self.monster_long_header(m) embed = discord.Embed() - embed.set_thumbnail(url=self.monsterToThumbnailUrl(m)) + embed.set_thumbnail(url=self.make_thumbnail_url(m)) embed.title = header embed.url = get_pdx_url(m) embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed - def addEvoListFields(self, monsters, current_monster): + def _get_evo_list_fields(self, monsters, current_monster): if not len(monsters): return field_data = '' field_values = [] for ae in sorted(monsters, key=lambda x: int(x.monster_id)): - monster_header = self.monsterToEvoHeader( + monster_header = self.monster_evo_header( ae, link=ae.monster_id != current_monster.monster_id) + '\n' if len(field_data + monster_header) > 1024: field_values.append(field_data) @@ -108,14 +108,14 @@ def addEvoListFields(self, monsters, current_monster): field_values.append(field_data) return field_values - def monster_attr_emoji(self, monster: "MonsterModel"): + def _get_monster_attr_emoji(self, monster: "MonsterModel"): attr1 = monster.attr1.name.lower() attr2 = monster.attr2.name.lower() emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) return self.match_emoji(emoji) - def monsterToEvoEmbed(self, m: "MonsterModel"): - embed = self.monsterToBaseEmbed(m) + def make_evo_embed(self, m: "MonsterModel"): + embed = self.make_base_embed(m) alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) @@ -123,13 +123,13 @@ def monsterToEvoEmbed(self, m: "MonsterModel"): embed.description = 'No alternate evos or evo gem' return embed - evos = self.addEvoListFields(alt_versions, m) + evos = self._get_evo_list_fields(alt_versions, m) if not gem_versions: embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) for f in evos[1:]: embed.add_field(name="\u200b", value=f) return embed - gems = self.addEvoListFields(gem_versions, m) + gems = self._get_evo_list_fields(gem_versions, m) embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) for e in evos[1:]: @@ -141,7 +141,7 @@ def monsterToEvoEmbed(self, m: "MonsterModel"): return embed - def addMonsterEvoOfList(self, monster_id_list, embed, field_name): + def _add_mats_of_list(self, embed, monster_id_list, field_name): if not len(monster_id_list): return field_data = '' @@ -151,11 +151,11 @@ def addMonsterEvoOfList(self, monster_id_list, embed, field_name): item_count = min(len(monster_id_list), 5) monster_list = [self.db_context.graph.get_monster(m) for m in monster_id_list] for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: - field_data += "{}\n".format(self.monsterToLongHeader(ae, link=True)) + field_data += "{}\n".format(self.monster_long_header(ae, link=True)) embed.add_field(name=field_name, value=field_data) - def monsterToEvoMatsEmbed(self, m: "MonsterModel"): - embed = self.monsterToBaseEmbed(m) + def make_evo_mats_embed(self, m: "MonsterModel"): + embed = self.make_base_embed(m) mats_for_evo = self.db_context.graph.evo_mats_by_monster(m) @@ -163,35 +163,35 @@ def monsterToEvoMatsEmbed(self, m: "MonsterModel"): field_data = '' if len(mats_for_evo) > 0: for ae in mats_for_evo: - field_data += "{}\n".format(self.monsterToLongHeader(ae, link=True)) + field_data += "{}\n".format(self.monster_long_header(ae, link=True)) else: field_data = 'None' embed.add_field(name=field_name, value=field_data) - self.addMonsterEvoOfList(self.db_context.graph.material_of_ids(m), embed, 'Material for') + self._add_mats_of_list(embed, self.db_context.graph.material_of_ids(m), 'Material for') evo_gem = self.db_context.graph.evo_gem_monster(m) if not evo_gem: return embed - self.addMonsterEvoOfList(self.db_context.graph.material_of_ids(evo_gem), embed, "Evo gem is mat for") + self._add_mats_of_list(embed, self.db_context.graph.material_of_ids(evo_gem), "Evo gem is mat for") return embed - def monsterToPantheonEmbed(self, m: "MonsterModel"): + def make_pantheon_embed(self, m: "MonsterModel"): full_pantheon = self.db_context.get_monsters_by_series(m.series_id) pantheon_list = list(filter(lambda x: self.db_context.graph.monster_is_base(x), full_pantheon)) if len(pantheon_list) == 0 or len(pantheon_list) > 6: return None - embed = self.monsterToBaseEmbed(m) + embed = self.make_base_embed(m) field_name = 'Pantheon: ' + self.db_context.graph.get_monster(m.monster_no).series.name field_data = '' for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): - field_data += '\n' + self.monsterToHeader(monster, link=True) + field_data += '\n' + self.monster_header(monster, link=True) embed.add_field(name=field_name, value=field_data) return embed - def monsterToSkillupsEmbed(self, m: "MonsterModel"): + def make_skillups_embed(self, m: "MonsterModel"): if m.active_skill is None: return None possible_skillups_list = self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) @@ -201,7 +201,7 @@ def monsterToSkillupsEmbed(self, m: "MonsterModel"): if len(skillups_list) == 0: return None - embed = self.monsterToBaseEmbed(m) + embed = self.make_base_embed(m) field_name = 'Skillups' field_data = '' @@ -212,50 +212,46 @@ def monsterToSkillupsEmbed(self, m: "MonsterModel"): skillups_list = skillups_list[0:8] for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): - field_data += '\n' + self.monsterToHeader(monster, link=True) + field_data += '\n' + self.monster_header(monster, link=True) if len(field_data.strip()): embed.add_field(name=field_name, value=field_data) return embed - @staticmethod - def monsterToPicUrl(m: "MonsterModel"): - return get_pic_url(m) - - def monsterToPicEmbed(self, m: "MonsterModel", animated=False): - embed = self.monsterToBaseEmbed(m) - url = self.monsterToPicUrl(m) + def make_picture_embed(self, m: "MonsterModel", animated=False): + embed = self.make_base_embed(m) + url = get_pic_url(m) embed.set_image(url=url) # Clear the thumbnail, don't need it on pic embed.set_thumbnail(url='') extra_links = [] if animated: - extra_links.append('Animation: {} -- {}'.format(self.monsterToVideoUrl(m), self.monsterToGifUrl(m))) + extra_links.append('Animation: {} -- {}'.format(self.monster_video_url(m), self.monster_gif_url(m))) if m.orb_skin_id is not None: - extra_links.append('Orb Skin: {} -- {}'.format(self.monsterToOrbSkinUrl(m), self.monsterToOrbSkinCBUrl(m))) + extra_links.append('Orb Skin: {} -- {}'.format(self.monster_orb_skin_url(m), self.monster_orb_skin_cb_url(m))) if len(extra_links) > 0: embed.add_field(name='Extra Links', value='\n'.join(extra_links)) return embed @staticmethod - def monsterToVideoUrl(m: "MonsterModel", link_text='(MP4)'): + def monster_video_url(m: "MonsterModel", link_text='(MP4)'): return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) @staticmethod - def monsterToGifUrl(m: "MonsterModel", link_text='(GIF)'): + def monster_gif_url(m: "MonsterModel", link_text='(GIF)'): return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) @staticmethod - def monsterToOrbSkinUrl(m: "MonsterModel", link_text='Regular'): + def monster_orb_skin_url(m: "MonsterModel", link_text='Regular'): return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) @staticmethod - def monsterToOrbSkinCBUrl(m: "MonsterModel", link_text='Color Blind'): + def monster_orb_skin_cb_url(m: "MonsterModel", link_text='Color Blind'): return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) - def monstersToLsEmbed(self, left_m: "MonsterModel", right_m: "MonsterModel"): + def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): lls = left_m.leader_skill rls = right_m.leader_skill @@ -265,22 +261,22 @@ def monstersToLsEmbed(self, left_m: "MonsterModel", right_m: "MonsterModel"): embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( - self.monsterToHeader(left_m, link=True), + self.monster_header(left_m, link=True), lls.desc if lls else 'None') description += '\n**{}**\n{}'.format( - self.monsterToHeader(right_m, link=True), + self.monster_header(right_m, link=True), rls.desc if rls else 'None') embed.description = description return embed - def monsterToHeaderEmbed(self, m: "MonsterModel"): - header = self.monsterToLongHeader(m, link=True) + def make_header_embed(self, m: "MonsterModel"): + header = self.monster_long_header(m, link=True) embed = discord.Embed() embed.description = header return embed - def monsterToAcquireString(self, m: "MonsterModel"): + def _monster_acquisition_string(self, m: "MonsterModel"): acquire_text = None if self.db_context.graph.monster_is_farmable(m) and not self.db_context.graph.monster_is_mp_evo(m): # Some MP shop monsters 'drop' in PADR @@ -301,8 +297,8 @@ def monsterToAcquireString(self, m: "MonsterModel"): acquire_text = 'MP Shop Evo' return acquire_text - def monsterToEmbed(self, m: "MonsterModel"): - embed = self.monsterToBaseEmbed(m) + def make_embed(self, m: "MonsterModel"): + embed = self.make_base_embed(m) # in case we want to readd the type emojis later # types_row = ' '.join(['{} {}'.format(str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))), t.name) for t in m.types]) @@ -342,7 +338,7 @@ def monsterToEmbed(self, m: "MonsterModel"): embed.add_field(name=types_row, value='{}\n{}'.format(awakenings_row, killers_row), inline=False) info_row_1 = 'Inheritable' if m.is_inheritable else 'Not inheritable' - acquire_text = self.monsterToAcquireString(m) + acquire_text = self._monster_acquisition_string(m) tet_text = self.db_context.graph.true_evo_type_by_monster(m).value orb_skin = "" if m.orb_skin_id is None else " (Orb Skin)" @@ -407,8 +403,8 @@ def get_killers_text(self, m: "MonsterModel"): return 'Any' return ' '.join([str(self.match_emoji('latent_killer_{}'.format(k.lower()))) for k in m.killers]) - def monsterToOtherInfoEmbed(self, m: "MonsterModel"): - embed = self.monsterToBaseEmbed(m) + def make_otherinfo_embed(self, m: "MonsterModel"): + embed = self.make_base_embed(m) # Clear the thumbnail, takes up too much space embed.set_thumbnail(url='') @@ -466,23 +462,24 @@ def monsterToOtherInfoEmbed(self, m: "MonsterModel"): return embed - def monsterToLinksEmbed(self, m: "MonsterModel"): - embed = self.monsterToBaseEmbed(m) + def make_links_embed(self, m: "MonsterModel"): + embed = self.make_base_embed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), SKYOZORA_TEMPLATE.format(m.monster_no_jp), INFO_PDX_TEMPLATE.format(m.monster_no_jp), ILMINA_TEMPLATE.format(m.monster_no_jp)) embed.set_footer(text='') + return embed - def monstersToLssEmbed(self, m: "MonsterModel"): + def make_lssingle_embed(self, m: "MonsterModel"): multiplier_text = createSingleMultiplierText(m.leader_skill) embed = discord.Embed() embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( - self.monsterToHeader(m, link=True), + self.monster_header(m, link=True), m.leader_skill.desc if m.leader_skill else 'None') embed.description = description diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2497ea62..b4b06ac4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -250,7 +250,7 @@ async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" m, err, debug_info = await self.findMonster(query) if m is not None: - await ctx.send(IdMenu.monsterToHeader(m)) + await ctx.send(IdMenu.monster_header(m)) await ctx.send(box(m.name_ja)) else: await ctx.send(self.makeFailureMsg(err)) @@ -441,23 +441,23 @@ def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) - id_embed = menu.monsterToEmbed(m) - evo_embed = menu.monsterToEvoEmbed(m) - mats_embed = menu.monsterToEvoMatsEmbed(m) + id_embed = menu.make_embed(m) + evo_embed = menu.make_evo_embed(m) + mats_embed = menu.make_evo_mats_embed(m) animated = m.has_animation - pic_embed = menu.monsterToPicEmbed(m, animated=animated) - other_info_embed = menu.monsterToOtherInfoEmbed(m) + pic_embed = menu.make_picture_embed(m, animated=animated) + other_info_embed = menu.make_otherinfo_embed(m) emoji_to_embed = OrderedDict() emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed emoji_to_embed[self.mats_emoji] = mats_embed emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = menu.monsterToPantheonEmbed(m) + pantheon_embed = menu.make_pantheon_embed(m) if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed - skillups_embed = menu.monsterToSkillupsEmbed(m) + skillups_embed = menu.make_skillups_embed(m) if skillups_embed: emoji_to_embed[self.skillups_emoji] = skillups_embed @@ -497,7 +497,7 @@ async def _do_evolistmenu(self, ctx, sm): return else: emoji = char_to_emoji(chars[idx]) - emoji_to_embed[emoji] = menu.monsterToEmbed(m) + emoji_to_embed[emoji] = menu.make_embed(m) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji @@ -537,7 +537,7 @@ async def links(self, ctx, *, query: str): m, err, debug_info = await self.findMonster(query) if m is not None: menu = IdMenu() - embed = menu.monsterToLinksEmbed(m) + embed = menu.make_links_embed(m) await ctx.send(embed=embed) else: @@ -560,7 +560,7 @@ async def lookup(self, ctx, *, query: str): m, err, debug_info = await self.findMonster(query) if m is not None: menu = IdMenu(allowed_emojis=self.get_emojis()) - embed = menu.monsterToHeaderEmbed(m) + embed = menu.make_header_embed(m) await ctx.send(embed=embed) else: await ctx.send(self.makeFailureMsg(err)) @@ -663,9 +663,9 @@ async def leaderskill(self, ctx, *, whole_query): menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = menu.monstersToLsEmbed(left_m, right_m) - emoji_to_embed[self.left_emoji] = menu.monsterToEmbed(left_m) - emoji_to_embed[self.right_emoji] = menu.monsterToEmbed(right_m) + emoji_to_embed[self.ls_emoji] = menu.make_ls_embed(left_m, right_m) + emoji_to_embed[self.left_emoji] = menu.make_embed(left_m) + emoji_to_embed[self.right_emoji] = menu.make_embed(right_m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -681,8 +681,8 @@ async def leaderskillsingle(self, ctx, *, query): return menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = menu.monstersToLssEmbed(m) - emoji_to_embed[self.left_emoji] = menu.monsterToEmbed(m) + emoji_to_embed[self.ls_emoji] = menu.make_lssingle_embed(m) + emoji_to_embed[self.left_emoji] = menu.make_embed(m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -719,7 +719,7 @@ async def padsay(self, ctx, server, *, query: str = None): return base_dir = self.settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) - header = '{} ({})'.format(IdMenu.monsterToHeader(m), server) + header = '{} ({})'.format(IdMenu.monster_header(m), server) if not os.path.exists(voice_file): await ctx.send(inline('Could not find voice for ' + header)) return From e3b545301e5aca21df46bbf53d51cd2cbb9ba2f6 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:36:39 -0600 Subject: [PATCH 0296/1220] Add tsutils to requirements (#259) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 3d8d4e36..0b6d3995 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ aiohttp aiofiles aioodbc networkx +tsutils From a6f411f158f7f00bdd0430897e1ed10f0ddefe05 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 24 Nov 2020 22:20:17 -0800 Subject: [PATCH 0297/1220] [channelmod] Fix misnamed variable (#261) --- channelmod/channelmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index e98b1430..66197180 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -77,7 +77,7 @@ async def multiedit(self, ctx, channel: Optional[discord.TextChannel], enable: b """Opt in a channel to multi-edit mode.""" if channel is None: channel = ctx.channel - await self.config.channel(channel).multiedit.set(multiedit) + await self.config.channel(channel).multiedit.set(enable) await ctx.tick() @channelmod.command() From fda173ac042f5798c78768b33f9c73631defbf72 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 24 Nov 2020 22:31:44 -0800 Subject: [PATCH 0298/1220] [channelmod] Correct author text (#262) * Fix misnamed variable * Correct author * .name --- channelmod/channelmod.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 66197180..ca050d90 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -268,7 +268,7 @@ async def mirror_msg(self, message): continue try: if attribution_required: - msg = self.makeheader(message) + msg = self.makeheader(message, author) await dest_channel.send(msg) fmessage = await self.mformat(message.content, message.channel, dest_channel) @@ -367,8 +367,8 @@ async def mirror_msg_mod(self, message, except Exception as ex: logger.exception('Failed to mirror message edit from {} to {}:'.format(channel.id, dest_channel_id)) - def makeheader(self, message): - return 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name, + def makeheader(self, message, author=None): + return 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name if author is None else author.name, message.guild.name, message.channel.name, message.jump_url) From 18b3a069bdc37c452c09945f5bdf76c8f3a75fd6 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 24 Nov 2020 22:38:14 -0800 Subject: [PATCH 0299/1220] [channelmod] Fix attatchments (#264) --- channelmod/channelmod.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index ca050d90..b8050670 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -247,12 +247,6 @@ async def mirror_msg(self, message): attachment_bytes = None filename = None - if await self.config.channel(message.channel).multiedit(): - await message.delete() - idmess = await message.channel.send("Pending...") - message = await message.channel.send(message.content, files=[await a.to_file() for a in message.attachments]) - await idmess.edit(content=str(message.id)) - if message.attachments: # If we know we're copying a message and that message has an attachment, # pre download it and reuse it for every upload. @@ -262,6 +256,13 @@ async def mirror_msg(self, message): filename = attachment.filename attachment_bytes = BytesIO(await attachment.read()) + if await self.config.channel(message.channel).multiedit(): + await message.delete() + idmess = await message.channel.send("Pending...") + message = await message.channel.send(message.content, files=[await a.to_file() for a in message.attachments]) + await idmess.edit(content=str(message.id)) + + for dest_channel_id in mirrored_channels: dest_channel = self.bot.get_channel(dest_channel_id) if not dest_channel: From 49b7621392f16760a0fbafab0166a9cb7af6e0ef Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 25 Nov 2020 01:53:24 -0600 Subject: [PATCH 0300/1220] [padsearch] Fix allowed attr from null to nil (#265) --- padsearch/padsearch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 0a571dbe..fd12af4a 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -68,7 +68,7 @@ def filt_timeout(filts, ms): 'wood', 'light', 'dark', - 'null', + 'nil', ] TYPES = [ From f3d671a54e452690607084e4df94fe3db0b40152 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 25 Nov 2020 01:56:04 -0600 Subject: [PATCH 0301/1220] [padsearch] Allow aliases for nil (#266) --- padsearch/padsearch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index fd12af4a..d69b23a5 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -135,7 +135,8 @@ def split_csv_orbcolors(value): 'g': 'wood', 'l': 'light', 'd': 'dark', - 'x': 'null', + 'x': 'nil', + 'null': 'nil', 'heart': 'heal', 'h': 'heal', 'p': 'poison', From 1dee27b10b07508cf07f82a25b986d3b126286c0 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 25 Nov 2020 15:40:59 -0600 Subject: [PATCH 0302/1220] Delete temporary id3 (#268) --- padinfo/padinfo.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b4b06ac4..95ad5e6a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -352,19 +352,6 @@ async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): else: await ctx.send(self.makeFailureMsg(err)) - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def id3(self, ctx, *, query: str): - """Monster info (main tab)""" - await self._do_id3(ctx, query) - - async def _do_id3(self, ctx, query: str): - m = await self.findMonster3(query) - if m is not None: - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await ctx.send("whoops") - @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): @@ -898,28 +885,6 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster2(query) - async def findMonster3(self, query): - return await self._findMonster3(query) - - async def _findMonster3(self, query): - DGCOG = self.bot.get_cog("Dadguide") - if DGCOG is None: - raise ValueError("Dadguide cog is not loaded") - - query = rmdiacritics(query).split() - monstergen = DGCOG.database.get_all_monsters() - for c, token in enumerate(query): - try: - filt = prefix_to_filter(token) - except: - print(token) - raise - if filt is None: - monster_name = " ".join(query[c:]) - break - monstergen = filter(filt, monstergen) - return max(monstergen, key=lambda x: (not x.is_equip, x.rarity, x.monster_no_na)) - class PadInfoSettings(CogSettings): def make_default_settings(self): From 43fb46c2fc9baa72957c1ac4cadbf146f4d7d227 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 26 Nov 2020 21:11:09 -0800 Subject: [PATCH 0303/1220] Bold Leader and Active Skill headers (#273) --- padinfo/id_menu.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 91960d4d..35a716b2 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -370,21 +370,21 @@ def make_embed(self, m: "MonsterModel"): stats_row_2 += '\n**Fodder EXP** {:,}'.format(m.fodder_exp) embed.add_field(name=stats_row_1, value=stats_row_2) - active_header = 'Active Skill' + active_header = '**Active Skill**' active_body = 'None' active_skill = m.active_skill if active_skill: - active_header = 'Active Skill ({} -> {})'.format(active_skill.turn_max, + active_header = '**Active Skill ({} -> {})**'.format(active_skill.turn_max, active_skill.turn_min) active_body = active_skill.desc embed.add_field(name=active_header, value=active_body, inline=False) leader_skill = m.leader_skill ls_row = m.leader_skill.desc if leader_skill else 'None' - ls_header = 'Leader Skill' + ls_header = '**Leader Skill**' if leader_skill: multiplier_text = createMultiplierText(leader_skill) - ls_header += " {}".format(multiplier_text) + ls_header += " **{}**".format(multiplier_text) embed.add_field(name=ls_header, value=ls_row, inline=False) evos_header = "Alternate Evos" From 32c540f0ec7f159993a9bcaac0a4d890a78633c6 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 27 Nov 2020 01:43:10 -0600 Subject: [PATCH 0304/1220] [Dadguide] Pycharm autoformat & linting errors (#269) --- dadguide/database_context.py | 1 - dadguide/models/awakening_model.py | 1 + dadguide/models/monster_model.py | 7 ++--- dadguide/monster_graph.py | 9 ++++--- dadguide/old_monster_index.py | 42 ++++++++++++++++-------------- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index d0e558c8..5659ed5a 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -7,7 +7,6 @@ from .models.scheduled_event_model import ScheduledEventModel from .models.dungeon_model import DungeonModel - SCHEDULED_EVENT_QUERY = """SELECT schedule.*, dungeons.name_ja AS d_name_ja, diff --git a/dadguide/models/awakening_model.py b/dadguide/models/awakening_model.py index 5bba484d..bed8ee48 100644 --- a/dadguide/models/awakening_model.py +++ b/dadguide/models/awakening_model.py @@ -6,6 +6,7 @@ class AwakeningModel(BaseModel): """ This class represents an awakening belonging to a monster, in contrast to AwokenSkillModel, which represents an "abstract" awoken skill. """ + def __init__(self, awoken_skill_model: AwokenSkillModel = None, **kwargs): self.awakening_id = kwargs['awakening_id'] self.monster_id = kwargs['monster_id'] diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 259bd00a..38aae1d1 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -51,8 +51,8 @@ def __init__(self, **m): self.in_pem = m['in_pem'] self.in_mpshop = m['buy_mp'] is not None self.buy_mp = m['buy_mp'] - self.sell_gold= m['sell_gold'] - self.sell_mp=m['sell_mp'] + self.sell_gold = m['sell_gold'] + self.sell_mp = m['sell_mp'] self.reg_date = m['reg_date'] self.on_jp = m['on_jp'] self.on_na = m['on_na'] @@ -120,7 +120,8 @@ def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): s_min = float(self.stat_values[key]['min']) s_max = float(self.stat_values[key]['max']) if self.level > 1: - s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** self.stat_values[key]['scale'] + scale = self.stat_values[key]['scale'] + s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** scale else: s_val = s_min if lv > 99: diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 7628bf09..5b0b2505 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -90,6 +90,7 @@ JOIN d_egg_machine_types ON d_egg_machine_types.egg_machine_type_id = egg_machines.egg_machine_type_id """ + class MonsterGraph(object): def __init__(self, database: DadguideDatabase): self.database = database @@ -117,10 +118,10 @@ def build_graph(self): mtoegg = defaultdict(lambda: {'pem': False, 'rem': False}) for e in ems: data = json.loads(e.contents) - type = 'pem' if e.type == "PEM" else 'rem' + e_type = 'pem' if e.type == "PEM" else 'rem' for m in data: - id = int(m[1:-1]) # Remove parentheses - mtoegg[id][type] = True + idx = int(m[1:-1]) # Remove parentheses + mtoegg[idx][e_type] = True for m in ms: ls_model = LeaderSkillModel(leader_skill_id=m.leader_skill_id, @@ -209,7 +210,7 @@ def build_graph(self): latent_slots=m.latent_slots, has_animation=m.has_animation == 1, has_hqimage=m.has_hqimage == 1, - ) + ) self.graph.add_node(m.monster_id, model=m_model) if m.linked_monster_id: diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 76bd36a1..508baf95 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -79,16 +79,15 @@ async def __init__(self, monster_database: DbContext, nickname_overrides, basena for named_monster in named_evolution_tree: named_monster.set_evolution_tree(named_evolution_tree) - # Sort the NamedMonsters into the opposite order we want to accept their nicknames in # This order is: # 1) High priority first # 2) Larger group sizes # 3) Minimum ID size in the group # 4) Monsters with higher ID values - def named_monsters_sort(nm: NamedMonster): - return (not nm.is_low_priority, nm.group_size, -1 * - nm.base_monster_no_na, nm.monster_no_na) + def named_monsters_sort(named_mon: NamedMonster): + return (not named_mon.is_low_priority, named_mon.group_size, -1 * + named_mon.base_monster_no_na, named_mon.monster_no_na) named_monsters.sort(key=named_monsters_sort) @@ -255,14 +254,14 @@ def find_monster(self, query): matches.add( find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) if len(matches): - return self.pickBestMonster(matches), None, "Base ID match, max of 1".format() + return self.pick_best_monster(matches), None, "Base ID match, max of 1".format() # prefix search for nicknames, space-preceeded, take max id for nickname, m in self.all_entries.items(): if nickname.startswith(query + ' '): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) + return self.pick_best_monster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) # prefix search for nicknames, take max id for nickname, m in self.all_entries.items(): @@ -270,15 +269,15 @@ def find_monster(self, query): matches.add(m) if len(matches): all_names = ",".join(map(lambda x: x.name_en, matches)) - return self.pickBestMonster(matches), None, "Nickname prefix, max of {}, matches=({})".format( + return self.pick_best_monster(matches), None, "Nickname prefix, max of {}, matches=({})".format( len(matches), all_names) # prefix search for full name, take max id for nickname, m in self.all_entries.items(): - if (m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query)): + if m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, "Full name, max of {}".format(len(matches)) + return self.pick_best_monster(matches), None, "Full name, max of {}".format(len(matches)) # for nicknames with 2 names, prefix search 2nd word, take max id if query in self.two_word_entries: @@ -288,10 +287,10 @@ def find_monster(self, query): # full name contains on nickname, take max id for nickname, m in self.all_entries.items(): - if (query in m.name_en.lower() or query in m.name_ja.lower()): + if query in m.name_en.lower() or query in m.name_ja.lower(): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, 'Nickname contains nickname match ({})'.format( + return self.pick_best_monster(matches), None, 'Nickname contains nickname match ({})'.format( len(matches)) # No decent matches. Try near hits on nickname instead @@ -314,7 +313,7 @@ def find_monster(self, query): all(map(lambda x: x in m.name_ja.lower(), query.split()))): matches.add(m) if len(matches): - return self.pickBestMonster(matches), None, 'All word match on full name, max of {}'.format( + return self.pick_best_monster(matches), None, 'All word match on full name, max of {}'.format( len(matches)) # couldn't find anything @@ -406,7 +405,8 @@ def find_monster2(self, query): return matches.pick_best_monster(), None, None return None, "Could not find a match for: " + query, None - def pickBestMonster(self, named_monster_list): + @staticmethod + def pick_best_monster(named_monster_list): return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) @@ -468,13 +468,14 @@ def __init__(self, evolution_tree: list, basename_overrides: list): } self.computed_basename = self._compute_group_basename(evolution_tree) - self.computed_basenames = set([self.computed_basename]) + self.computed_basenames = {self.computed_basename} if '-' in self.computed_basename: self.computed_basenames.add(self.computed_basename.replace('-', ' ')) self.basenames = basename_overrides or self.computed_basenames - def _compute_monster_basename(self, m: MonsterModel): + @staticmethod + def _compute_monster_basename(m: MonsterModel): basename = m.name_en.lower() if ',' in basename: name_parts = basename.split(',') @@ -517,7 +518,8 @@ def count_and_id(): entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] return max(entries)[2] - def _is_low_priority_monster(self, m: MonsterModel): + @staticmethod + def _is_low_priority_monster(m: MonsterModel): lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] lp_substrings = ['tamadra'] lp_min_rarity = 2 @@ -530,7 +532,8 @@ def _is_low_priority_monster(self, m: MonsterModel): failed_equip = m.is_equip return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip - def _is_low_priority_group(self, mg: list): + @staticmethod + def _is_low_priority_group(mg: list): lp_grp_min_rarity = 5 max_rarity = max(m.rarity for m in mg) failed_max_rarity = max_rarity < lp_grp_min_rarity @@ -538,7 +541,8 @@ def _is_low_priority_group(self, mg: list): class NamedMonster(object): - def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, series: SeriesModel): + def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, + series: SeriesModel): self.evolution_tree = None @@ -616,4 +620,4 @@ def set_evolution_tree(self, evolution_tree): nice things like prefix lookups on the entire tree in id2 and not cry about Diablos equip """ - self.evolution_tree = evolution_tree \ No newline at end of file + self.evolution_tree = evolution_tree From fdf3f1a406de895485253e35e6f0c9edfa873867 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Dec 2020 02:16:47 -0800 Subject: [PATCH 0305/1220] [channelmod] Pagify report (#274) --- channelmod/channelmod.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index b8050670..6038b10a 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -107,7 +107,8 @@ async def mirrorconfig(self, ctx, server_id: int = None): channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' msg += '\n\t{} ({})'.format(channel_id, channel_name) msg += '\n\n* indicates multi-edit' - await ctx.send(box(msg)) + for page in pagify(msg): + await ctx.send(box(page)) @channelmod.command() From f90f0a1e5d131fbfc81f04522a345f1523480157 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 2 Dec 2020 23:36:43 -0600 Subject: [PATCH 0306/1220] Add channelmod mirroredit and supporting commands (#275) * Add mirroredit and supporting commands * Review updates * Update mirroredit docstring * Add comment about discord.Forbidden errors * Docstring whitespace --- channelmod/channelmod.py | 46 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 6038b10a..2ed7af01 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -30,7 +30,7 @@ def __init__(self, bot, *args, **kwargs): self.channel_last_spoke = {} self.config = Config.get_conf(self, identifier=3747737700) - self.config.register_channel(multiedit=False) + self.config.register_channel(multiedit=False, mirroredit_target=None) GACOG = self.bot.get_cog("GlobalAdmin") if GACOG: self.bot.get_cog("GlobalAdmin").register_perm("channelmod") @@ -432,6 +432,50 @@ def emojify(self, message): message = replace_emoji_names_with_code(emojis, message) return fix_emojis_for_server(emojis, message) + @channelmod.command() + @checks.is_owner() + async def setmirroreditchannel(self, ctx, channelid): + """Sets the mirroredit_target for the current channel to `channelid`""" + await self.config.channel(ctx.channel).mirroredit_target.set(channelid) + await ctx.tick() + + @channelmod.command() + @checks.is_owner() + async def rmmirroreditchannel(self, ctx): + """Removes the mirroredit_target for the current channel""" + await self.config.channel(ctx.channel).mirroredit_target.set(None) + await ctx.tick() + + @commands.command(aliases=['medit']) + @checks.mod_or_permissions(manage_messages=True) + async def mirroredit(self, ctx, message, *, content): + """Given a message ID from the mirroredit-configured channel, replaces it. + This is a TEMPORARY command until the alias command is fixed to respect newlines. + At that point, we will just use aliases instead of configuring channel IDs here. + """ + try: + message = await commands.MessageConverter().convert(ctx, message) + except commands.MessageNotFound as e: + channel_id = await self.config.channel(ctx.channel).mirroredit_target() + if channel_id is None: + await ctx.send("Please configure a mirroredit channel here") + return + channel = self.bot.get_channel(channel_id) + try: + message = await channel.fetch_message(int(message)) + except (discord.NotFound, ValueError): + raise e + if message.author != ctx.me: + await ctx.send("I can't edit a message that's not my own") + return + # Even if permissions change in the target channel in between the bot sending and + # editing the message, the bot will still be able to edit the message, so testing + # for discord.Forbidden is not actually required here (this is in contrast to + # human users lacking an edit button when channels are read-only but they previously) + # had sent a message in them. Tested 2020-12-02. + await message.edit(content=content) + await ctx.tick() + class ChannelModSettings(CogSettings): def make_default_settings(self): From a2448807190f4b97945d37d251f94fceaf4295e3 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 2 Dec 2020 21:49:50 -0800 Subject: [PATCH 0307/1220] Channelmod setmirroreditchannel fix (#276) --- channelmod/channelmod.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 2ed7af01..52a65c30 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -434,9 +434,9 @@ def emojify(self, message): @channelmod.command() @checks.is_owner() - async def setmirroreditchannel(self, ctx, channelid): + async def setmirroreditchannel(self, ctx, channel: discord.TextChannel): """Sets the mirroredit_target for the current channel to `channelid`""" - await self.config.channel(ctx.channel).mirroredit_target.set(channelid) + await self.config.channel(ctx.channel).mirroredit_target.set(channel.id) await ctx.tick() @channelmod.command() @@ -458,9 +458,12 @@ async def mirroredit(self, ctx, message, *, content): except commands.MessageNotFound as e: channel_id = await self.config.channel(ctx.channel).mirroredit_target() if channel_id is None: - await ctx.send("Please configure a mirroredit channel here") + await ctx.send("Please configure a mirroredit channel here. Please add one with `{0.prefix}setmirroreditchannel`".format(ctx)) return channel = self.bot.get_channel(channel_id) + if channel is None: + await ctx.send("Invalid mirroredit channel. Please add one with `{0.prefix}setmirroreditchannel`".format(ctx)) + return try: message = await channel.fetch_message(int(message)) except (discord.NotFound, ValueError): From 93c9d9f81d24752eba125eb82afcafab1d95cb7a Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Dec 2020 02:49:36 -0800 Subject: [PATCH 0308/1220] Fix medit by removing edit requirements (#278) --- channelmod/channelmod.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/channelmod/channelmod.py b/channelmod/channelmod.py index 52a65c30..c45e3b30 100644 --- a/channelmod/channelmod.py +++ b/channelmod/channelmod.py @@ -309,8 +309,7 @@ async def mirror_msg(self, message): @commands.Cog.listener('on_message_edit') async def mirror_msg_edit(self, before, after): - if before.content != after.content: - await self.mirror_msg_mod(before, new_message_content=after.content) + await self.mirror_msg_mod(before, new_message_content=after.content) @commands.Cog.listener('on_message_delete') async def mirror_msg_delete(self, message): @@ -457,9 +456,6 @@ async def mirroredit(self, ctx, message, *, content): message = await commands.MessageConverter().convert(ctx, message) except commands.MessageNotFound as e: channel_id = await self.config.channel(ctx.channel).mirroredit_target() - if channel_id is None: - await ctx.send("Please configure a mirroredit channel here. Please add one with `{0.prefix}setmirroreditchannel`".format(ctx)) - return channel = self.bot.get_channel(channel_id) if channel is None: await ctx.send("Invalid mirroredit channel. Please add one with `{0.prefix}setmirroreditchannel`".format(ctx)) From 4db767352200b3c47d3e5e4c87a47bc46aae01c7 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 9 Dec 2020 20:55:40 -0600 Subject: [PATCH 0309/1220] Fix old index's handling of base transform monsters (#283) Fixes an issue with ^which not working for transform monsters, which was reported specifically for Grandis. A more robust fix would be to improve the query in DbContext but this is good enough and we're replacing this entire thing soon anyway. Also added type hinting in padinfo. --- dadguide/old_monster_index.py | 17 ++++++++++++----- padinfo/padinfo.py | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 508baf95..4845af55 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -62,7 +62,8 @@ async def __init__(self, monster_database: DbContext, nickname_overrides, basena named_monsters = [] async for base_mon in AsyncIter(base_monster_ids): base_id = base_mon.monster_id - series = monster_database.graph.get_monster(base_id).series + base_monster = monster_database.graph.get_monster(base_id) + series = base_monster.series group_basename_overrides = basename_overrides.get(base_id, []) evolution_tree = [monster_database.graph.get_monster(m) for m in monster_database.get_evolution_tree_ids(base_id)] @@ -73,7 +74,13 @@ async def __init__(self, monster_database: DbContext, nickname_overrides, basena continue prefixes = self.compute_prefixes(monster, evolution_tree) extra_nicknames = monster_id_to_nicknames[monster.monster_id] - named_monster = NamedMonster(monster, named_mg, prefixes, extra_nicknames, series) + + # The query mis-handles transforms so we have to fetch base monsters + # from the graph properly ourselves instead of just listening to whatever + # the query says the base monster is above + named_monster = NamedMonster( + monster, named_mg, prefixes, extra_nicknames, series, + base_monster=monster_database.graph.get_base_monster(monster)) named_monsters.append(named_monster) named_evolution_tree.append(named_monster) for named_monster in named_evolution_tree: @@ -542,7 +549,7 @@ def _is_low_priority_group(mg: list): class NamedMonster(object): def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, - series: SeriesModel): + series: SeriesModel, base_monster: MonsterModel = None): self.evolution_tree = None @@ -552,8 +559,8 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref self.monster_no_jp = monster.monster_no_jp # ID of the root of the tree for this monster - self.base_monster_no = monster_group.base_monster_no - self.base_monster_no_na = monster_group.base_monster_no_na + self.base_monster_no = base_monster.monster_id + self.base_monster_no_na = base_monster.monster_no_na # This stuff is important for nickname generation self.group_basenames = monster_group.basenames diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 95ad5e6a..4de53fe4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from dadguide.database_context import DbContext from dadguide.models.monster_model import MonsterModel + from dadguide.old_monster_index import NamedMonster logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -845,7 +846,7 @@ async def findMonster(self, query, server_filter=ServerFilter.any): return m, err, debug_info - async def _findMonster(self, query, server_filter=ServerFilter.any): + async def _findMonster(self, query, server_filter=ServerFilter.any) -> "NamedMonster": while self.index_lock.locked(): await asyncio.sleep(1) From ca1d13b4dc417ccf6d9bbda5f1434b18ff74e793 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 11 Dec 2020 23:41:21 -0800 Subject: [PATCH 0310/1220] [PadEvents] Don't crash on invalid channel id (#284) This is standard to do as get_channel can always return None. --- padevents/padevents.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 70a8249a..ade17c34 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -153,7 +153,9 @@ async def check_started(self): if e.server == gr['server']: try: channel = self.bot.get_channel(int(gr['channel_id'])) - + if channel is None: + continue + role_name = '{}_group_{}'.format(e.server, e.groupLongName()) role = channel.guild.get_role(role_name) if role and role.mentionable: From ac4c65ea42d2ea16525caa9d26323c4a228bb85a Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 13 Dec 2020 18:27:19 -0800 Subject: [PATCH 0311/1220] Rename channelmod and add nodeletion mode (#288) --- {channelmod => channelmirror}/__init__.py | 4 +- .../channelmirror.py | 68 +++++++++++-------- {channelmod => channelmirror}/info.json | 0 3 files changed, 41 insertions(+), 31 deletions(-) rename {channelmod => channelmirror}/__init__.py (60%) rename channelmod/channelmod.py => channelmirror/channelmirror.py (92%) rename {channelmod => channelmirror}/info.json (100%) diff --git a/channelmod/__init__.py b/channelmirror/__init__.py similarity index 60% rename from channelmod/__init__.py rename to channelmirror/__init__.py index 532573f1..6b346512 100644 --- a/channelmod/__init__.py +++ b/channelmirror/__init__.py @@ -1,8 +1,8 @@ -from .channelmod import ChannelMod +from .channelmirror import ChannelMirror __red_end_user_data_statement__ = "No personal data is stored." def setup(bot): - n = ChannelMod(bot) + n = ChannelMirror(bot) bot.add_cog(n) diff --git a/channelmod/channelmod.py b/channelmirror/channelmirror.py similarity index 92% rename from channelmod/channelmod.py rename to channelmirror/channelmirror.py index c45e3b30..009055f2 100644 --- a/channelmod/channelmod.py +++ b/channelmirror/channelmirror.py @@ -11,7 +11,7 @@ from redbot.core.utils.chat_formatting import inline, pagify, box from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server -logger = logging.getLogger('red.misc-cogs.channelmod') +logger = logging.getLogger('red.misc-cogs.channelmirror') # Three hour cooldown ATTRIBUTION_TIME_SECONDS = 60 * 60 * 3 @@ -20,20 +20,20 @@ MESSAGE_LINK = 'https://discordapp.com/channels/{0.guild.id}/{0.channel.id}/{0.id}' -class ChannelMod(commands.Cog): - """Channel moderation tools.""" +class ChannelMirror(commands.Cog): + """Channel mirroring tools.""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.settings = ChannelModSettings("channelmod") + self.settings = ChannelMirrorSettings("channelmod") self.channel_last_spoke = {} self.config = Config.get_conf(self, identifier=3747737700) - self.config.register_channel(multiedit=False, mirroredit_target=None) + self.config.register_channel(multiedit=False, mirroredit_target=None, nodeletion=False) GACOG = self.bot.get_cog("GlobalAdmin") - if GACOG: self.bot.get_cog("GlobalAdmin").register_perm("channelmod") + if GACOG: GACOG.register_perm("channelmirror") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -47,13 +47,13 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return - @commands.group() - async def channelmod(self, ctx): - """Manage channel moderation settings""" + @commands.group(aliases=['channelmod']) + async def channelmirror(self, ctx): + """Manage channel mirroring settings""" - @channelmod.command() + @channelmirror.command(aliases=['addmirror']) @checks.is_owner() - async def addmirror(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): + async def add(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): """Set mirroring between two channels.""" if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) @@ -61,9 +61,9 @@ async def addmirror(self, ctx, source_channel_id: int, dest_channel_id: int, doc self.settings.add_mirrored_channel(source_channel_id, dest_channel_id) await ctx.tick() - @channelmod.command() + @channelmirror.command(aliases=['rmmirror', 'rm', 'delete']) @checks.is_owner() - async def rmmirror(self, ctx, source_channel_id: int, dest_channel_id: int): + async def remove(self, ctx, source_channel_id: int, dest_channel_id: int): """Remove mirroring between two channels.""" success = self.settings.rm_mirrored_channel(source_channel_id, dest_channel_id) if not success: @@ -71,18 +71,27 @@ async def rmmirror(self, ctx, source_channel_id: int, dest_channel_id: int): return await ctx.tick() - @channelmod.command() + @channelmirror.command() @checks.is_owner() - async def multiedit(self, ctx, channel: Optional[discord.TextChannel], enable: bool): + async def multiedit(self, ctx, channel: Optional[discord.TextChannel], enable: bool = True): """Opt in a channel to multi-edit mode.""" if channel is None: channel = ctx.channel await self.config.channel(channel).multiedit.set(enable) await ctx.tick() - @channelmod.command() + @channelmirror.command() @checks.is_owner() - async def mirrorconfig(self, ctx, server_id: int = None): + async def nodeletion(self, ctx, channel: Optional[discord.TextChannel], enable: bool = True): + """Opt in a channel to no-deletion mode.""" + if channel is None: + channel = ctx.channel + await self.config.channel(channel).nodeletion.set(enable) + await ctx.tick() + + @channelmirror.command(aliases=['mirrorconfig']) + @checks.is_owner() + async def config(self, ctx, server_id: int = None): """List mirror config.""" mirrored_channels = self.settings.mirrored_channels() gchs = set() @@ -111,9 +120,9 @@ async def mirrorconfig(self, ctx, server_id: int = None): await ctx.send(box(page)) - @channelmod.command() + @channelmirror.command(aliases=['guildmirrorconfig']) @checks.is_owner() - async def guildmirrorconfig(self, ctx, server_id: int): + async def guildconfig(self, ctx, server_id: int): """List mirror config for a guild.""" mirrored_channels = self.settings.mirrored_channels() gchs = set() @@ -158,7 +167,7 @@ async def guildmirrorconfig(self, ctx, server_id: int): await ctx.send(inline('* indicates multi-edit')) - @channelmod.command() + @channelmirror.command() async def countreactions(self, ctx, message: discord.Message): """Count reactions on a message and all of its mirrors. @@ -191,8 +200,8 @@ async def countreactions(self, ctx, message: discord.Message): o += "{{}}: {{:{}}}\n".format(maxlen).format(r, c) await ctx.send(o) - @channelmod.command() - @auth_check('channelmod') + @channelmirror.command() + @auth_check('channelmirror') async def catchup(self, ctx, channel, from_message, to_message=None): """Catch up a mirror for all messages after from_message (inclusive)""" if channel.isdigit(): @@ -313,7 +322,8 @@ async def mirror_msg_edit(self, before, after): @commands.Cog.listener('on_message_delete') async def mirror_msg_delete(self, message): - await self.mirror_msg_mod(message, delete_message_content=True) + if not await self.config.channel(message.channel).nodeletion(): + await self.mirror_msg_mod(message, delete_message_content=True) @commands.Cog.listener('on_reaction_add') async def mirror_reaction_add(self, reaction, user): @@ -431,16 +441,16 @@ def emojify(self, message): message = replace_emoji_names_with_code(emojis, message) return fix_emojis_for_server(emojis, message) - @channelmod.command() + @channelmirror.command(aliases=['setmirroreditchannel']) @checks.is_owner() - async def setmirroreditchannel(self, ctx, channel: discord.TextChannel): - """Sets the mirroredit_target for the current channel to `channelid`""" + async def seteditchannel(self, ctx, channel: discord.TextChannel): + """Sets the edit_target for the current channel to `channelid`""" await self.config.channel(ctx.channel).mirroredit_target.set(channel.id) await ctx.tick() - @channelmod.command() + @channelmirror.command(aliases=['rmmirroreditchannel']) @checks.is_owner() - async def rmmirroreditchannel(self, ctx): + async def rmeditchannel(self, ctx): """Removes the mirroredit_target for the current channel""" await self.config.channel(ctx.channel).mirroredit_target.set(None) await ctx.tick() @@ -476,7 +486,7 @@ async def mirroredit(self, ctx, message, *, content): await ctx.tick() -class ChannelModSettings(CogSettings): +class ChannelMirrorSettings(CogSettings): def make_default_settings(self): config = { 'servers': {}, diff --git a/channelmod/info.json b/channelmirror/info.json similarity index 100% rename from channelmod/info.json rename to channelmirror/info.json From 5f9d3d16070e2741a4acdaf18ded344d135850c8 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 13 Dec 2020 18:38:11 -0800 Subject: [PATCH 0312/1220] channelmod -> channelmirror for settings file name (#289) --- channelmirror/channelmirror.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index 009055f2..b8822df6 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -26,7 +26,7 @@ class ChannelMirror(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.settings = ChannelMirrorSettings("channelmod") + self.settings = ChannelMirrorSettings("channelmirror") self.channel_last_spoke = {} self.config = Config.get_conf(self, identifier=3747737700) From 4deb361b447f0521920424e02f8ab6742f7f6efc Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 13 Dec 2020 21:17:47 -0600 Subject: [PATCH 0313/1220] Add new awakening-restricted latents display (#287) * Add new awakening-restricted latents display * Improve map variable names --- dadguide/models/enum_types.py | 7 ++++++ dadguide/models/monster_model.py | 17 ++++++++++++++ padinfo/id_menu.py | 39 +++++++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 0635913c..8a4b9606 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -52,6 +52,13 @@ class Server(Enum): KR = 2 +class AwakeningRestrictedLatent(Enum): + """Latent awakenings with availability gated by having an awakening""" + AbsorbPierce = 606 + SpinnerClear = 607 + UnmatchableClear = 608 + + def enum_or_none(enum, value, default=None): if value is not None: return enum(value) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 38aae1d1..30fea7cb 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -1,6 +1,7 @@ from .base_model import BaseModel from .enum_types import Attribute from .enum_types import MonsterType +from .enum_types import AwakeningRestrictedLatent from .enum_types import enum_or_none from .active_skill_model import ActiveSkillModel from .leader_skill_model import LeaderSkillModel @@ -112,6 +113,22 @@ def killers(self): killers.update(type_to_killers_map.get(t, [])) return sorted(killers) + @property + def awakening_restricted_latents(self): + monster_awakening_to_allowed_latent_map = [ + {27: AwakeningRestrictedLatent.UnmatchableClear}, # TPA + {62: AwakeningRestrictedLatent.AbsorbPierce}, # Combo orb + {20: AwakeningRestrictedLatent.SpinnerClear}, # Bind clear + ] + latents = [] + for row in monster_awakening_to_allowed_latent_map: + # iterate over one thing + for awakening in row.keys(): + if any([x.awoken_skill_id == awakening and not x.is_super for x in self.awakenings]): + latents.append(row[awakening]) + + return latents + @property def history_us(self): return '[{}] New Added'.format(self.reg_date) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 35a716b2..e27e8c20 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from dadguide.database_context import DbContext from dadguide.models.monster_model import MonsterModel + from dadguide.models.enum_types import AwakeningRestrictedLatent INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' @@ -308,7 +309,7 @@ def make_embed(self, m: "MonsterModel"): for idx, a in enumerate(m.awakenings): as_id = a.awoken_skill_id as_name = a.name - mapped_awakening = AWAKENING_MAP.get(as_id, as_name) + mapped_awakening = AWAKENING_ID_TO_EMOJI_NAME_MAP.get(as_id, as_name) mapped_awakening = self.match_emoji(mapped_awakening) # Wrap superawakenings to the next line @@ -328,10 +329,10 @@ def make_embed(self, m: "MonsterModel"): if is_transform_base: - killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, + killers_row = '**Available latents:** [{} slots] {}'.format(m.latent_slots, self.get_killers_text(m)) else: - killers_row = '**Avail. killers (pre-transform):** [{} slots] {}'.format( + killers_row = '**Avail. latents (pre-xform):** [{} slots] {}'.format( transform_base.latent_slots, self.get_killers_text(transform_base)) @@ -399,9 +400,28 @@ def make_embed(self, m: "MonsterModel"): return embed def get_killers_text(self, m: "MonsterModel"): + return_text = '' if 'Any' in m.killers: - return 'Any' - return ' '.join([str(self.match_emoji('latent_killer_{}'.format(k.lower()))) for k in m.killers]) + return_text += 'Any killer' + if m.awakening_restricted_latents: + return_text += ', ' + else: + return_text += ' '.join([self.killer_latent_emoji(k) for k in m.killers]) + if m.awakening_restricted_latents: + return_text += ' ' + return_text += self.get_awakening_restricted_latents_text(m) + return return_text + + def get_awakening_restricted_latents_text(self, m: "MonsterModel"): + if not m.awakening_restricted_latents: + return '' + return ' ' + ' '.join([self.awakening_restricted_latent_emoji(x) for x in m.awakening_restricted_latents]) + + def killer_latent_emoji(self, latent: str): + return str(self.match_emoji('latent_killer_{}'.format(latent.lower()))) + + def awakening_restricted_latent_emoji(self, latent: "AwakeningRestrictedLatent"): + return str(self.match_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value]))) def make_otherinfo_embed(self, m: "MonsterModel"): embed = self.make_base_embed(m) @@ -486,7 +506,7 @@ def make_lssingle_embed(self, m: "MonsterModel"): return embed -AWAKENING_MAP = { +AWAKENING_ID_TO_EMOJI_NAME_MAP = { 1: 'boost_hp', 2: 'boost_atk', 3: 'boost_rcv', @@ -560,3 +580,10 @@ def make_lssingle_embed(self, m: "MonsterModel"): 71: 'misc_jammerboost', 72: 'misc_poisonboost', } + + +AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { + 606: 'absorb_pierce', + 607: 'spinner_clear', + 608: 'unmatchable_clear', +} From cc557bfa3b7e0d2a970101ba054316fb1c821d61 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 15 Dec 2020 00:14:11 -0800 Subject: [PATCH 0314/1220] [ChannelMirror] Stop relying on message cache (#277) * [DRAFT] [ChannelMod] Stop relying on message cache * Rename seniority.py to channelmod.py * Update channelmod.py * Update channelmod.py * Update channelmod.py * Same as last time but now it actually works * Update .gitignore --- .gitignore | 2 ++ channelmirror/channelmirror.py | 53 +++++++++++++++++----------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 0b9a1654..5a7bc711 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,5 @@ bot_data/ # 3way Merge .orig + +.DS_Store diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index b8822df6..aadcb7f8 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -1,9 +1,7 @@ -import aiohttp import discord import logging import re import time -import traceback from typing import Optional from datetime import datetime from io import BytesIO @@ -119,7 +117,6 @@ async def config(self, ctx, server_id: int = None): for page in pagify(msg): await ctx.send(box(page)) - @channelmirror.command(aliases=['guildmirrorconfig']) @checks.is_owner() async def guildconfig(self, ctx, server_id: int): @@ -166,7 +163,6 @@ async def guildconfig(self, ctx, server_id: int): await ctx.send(inline('* indicates multi-edit')) - @channelmirror.command() async def countreactions(self, ctx, message: discord.Message): """Count reactions on a message and all of its mirrors. @@ -239,7 +235,6 @@ async def mirror_msg(self, message): if (await self.bot.get_context(message)).prefix is not None: return - channel = message.channel mirrored_channels = self.settings.get_mirrored_channels(channel.id) @@ -269,10 +264,10 @@ async def mirror_msg(self, message): if await self.config.channel(message.channel).multiedit(): await message.delete() idmess = await message.channel.send("Pending...") - message = await message.channel.send(message.content, files=[await a.to_file() for a in message.attachments]) + message = await message.channel.send(message.content, + files=[await a.to_file() for a in message.attachments]) await idmess.edit(content=str(message.id)) - for dest_channel_id in mirrored_channels: dest_channel = self.bot.get_channel(dest_channel_id) if not dest_channel: @@ -316,28 +311,31 @@ async def mirror_msg(self, message): if attachment_bytes: attachment_bytes.close() - @commands.Cog.listener('on_message_edit') - async def mirror_msg_edit(self, before, after): - await self.mirror_msg_mod(before, new_message_content=after.content) - - @commands.Cog.listener('on_message_delete') - async def mirror_msg_delete(self, message): - if not await self.config.channel(message.channel).nodeletion(): - await self.mirror_msg_mod(message, delete_message_content=True) - - @commands.Cog.listener('on_reaction_add') - async def mirror_reaction_add(self, reaction, user): - message = reaction.message - if message.author.id != user.id and not await self.config.channel(message.channel).multiedit(): + @commands.Cog.listener('on_raw_message_edit') + async def mirror_msg_edit(self, payload): + message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) + await self.mirror_msg_mod(message, new_message_content=payload.data['content']) + + @commands.Cog.listener('on_raw_message_delete') + async def mirror_msg_delete(self, payload): + if not await self.config.channel(self.bot.get_channel(payload.channel_id)).nodeletion(): + fmessage = discord.Object(id=payload.message_id) + fmessage.channel = self.bot.get_channel(payload.channel_id) + await self.mirror_msg_mod(fmessage, delete_message_content=True) + + @commands.Cog.listener('on_raw_reaction_add') + async def mirror_reaction_add(self, payload): + message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) + if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): return - await self.mirror_msg_mod(message, new_message_reaction=reaction.emoji) + await self.mirror_msg_mod(message, new_message_reaction=payload.emoji) - @commands.Cog.listener('on_reaction_remove') - async def mirror_reaction_remove(self, reaction, user): - message = reaction.message - if message.author.id != user.id and not await self.config.channel(message.channel).multiedit(): + @commands.Cog.listener('on_raw_reaction_remove') + async def mirror_reaction_remove(self, payload): + message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) + if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): return - await self.mirror_msg_mod(message, delete_message_reaction=reaction.emoji) + await self.mirror_msg_mod(message, delete_message_reaction=payload.emoji) async def mirror_msg_mod(self, message, new_message_content: str = None, @@ -468,7 +466,8 @@ async def mirroredit(self, ctx, message, *, content): channel_id = await self.config.channel(ctx.channel).mirroredit_target() channel = self.bot.get_channel(channel_id) if channel is None: - await ctx.send("Invalid mirroredit channel. Please add one with `{0.prefix}setmirroreditchannel`".format(ctx)) + await ctx.send( + "Invalid mirroredit channel. Please add one with `{0.prefix}setmirroreditchannel`".format(ctx)) return try: message = await channel.fetch_message(int(message)) From 9e091eb5ad8f480825ac74713b3d3eca974cfa72 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 15 Dec 2020 00:24:41 -0800 Subject: [PATCH 0315/1220] [pdchu] add arls (#292) --- padbuildimg/assets/lat/abs.png | Bin 0 -> 3755 bytes padbuildimg/assets/lat/nabs.png | Bin 0 -> 3873 bytes padbuildimg/assets/lat/nemp.png | Bin 0 -> 2478 bytes padbuildimg/assets/lat/nspn.png | Bin 0 -> 4148 bytes padbuildimg/assets/lat/nunm.png | Bin 0 -> 3655 bytes padbuildimg/assets/lat/spn.png | Bin 0 -> 3870 bytes padbuildimg/assets/lat/unm.png | Bin 0 -> 3780 bytes padbuildimg/padbuildimg.py | 43 +++++++++++++++++++++++++------- 8 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 padbuildimg/assets/lat/abs.png create mode 100644 padbuildimg/assets/lat/nabs.png create mode 100644 padbuildimg/assets/lat/nemp.png create mode 100644 padbuildimg/assets/lat/nspn.png create mode 100644 padbuildimg/assets/lat/nunm.png create mode 100644 padbuildimg/assets/lat/spn.png create mode 100644 padbuildimg/assets/lat/unm.png diff --git a/padbuildimg/assets/lat/abs.png b/padbuildimg/assets/lat/abs.png new file mode 100644 index 0000000000000000000000000000000000000000..128f59d5ef1cadb4778b702d7f4cb9ce5137e480 GIT binary patch literal 3755 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/nabs.png b/padbuildimg/assets/lat/nabs.png new file mode 100644 index 0000000000000000000000000000000000000000..419fc46a06f4f2ea7df9377af80029a832db7d26 GIT binary patch literal 3873 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 F{{o{VQLO*~ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/nemp.png b/padbuildimg/assets/lat/nemp.png new file mode 100644 index 0000000000000000000000000000000000000000..737e22d4bb227fcd5b9400cda66eef579cd648fa GIT binary patch literal 2478 zcmb_e3vd%f7!Cz$t)LO{K^ceRpa{a{9?2!SD*@W3HmOalwbBA3<#M-4(JJ7{6UD;Xoe0C1v$P5D2NMqya5Z^*8C}ocm)fZOFIZhFclPd zZRHYprrepsmlyGjfDRvq3=Of6zz-A-3Hf~inGIRcFfR+A)n*(;!Vsm%f+nj0kvvBh zk}667G3p46CkQ=aVsxa@KvSk62t`l?PQovZktWt)X7v;j`JgbGB)HjZ>l2Y!@Xdl2 zC`ypU@zT;#U8!CtN*8-wq>YDZL<(|Cw475G1b8-(#S)1JY2%^Herh&BHdO+g zB1$=;=(|2qS=S{a6s04Pfew!M2Gk@5-7o}LIR#iyb>1+N!bmcQG_xeZ(j-QhS%T<* zIz++iF1rb;&!O}zNwSpTZ%}9&fm67D1q(du79~FiQ}+5f55R)~4~j$!$)<`vQG$V? zI{l69_SDoYNpyRCutUyHPeJTysYa498ZnqFcNd}{hj9YLsz%76%+f$MkE=kUJBde-PfQ%4E3zXT2(HtXS2GR&snRuwr zYyd6;DbR$TQjH(>5^M9yaF)v=OX(R<9C`A2k+6MOj#rn31?5#g00E6Odv8&XJBl0` zUs?bl=|8xnBf2cQl~PUu$sSm{JMbZXJNYs<{-4DY4CB(PrsZgw!wd$!5n~9G0W;9B zVkW>4j5?Ves%{bweM_(e#lrn1V)6ebKEloyZ~+g1`z((B_jK;eo3~5nre*klrh}^+ zY=_VAn=KgbyJ0hilNoM;JG}ZBJzC&P^l$sOGw>$BCTt*OP1{?#*j>^4`n>Z)4m zHl@903+A;6X7I|wmfVqxE3``|*2JBvdoBCjP2J8($E!0=8!NQV z&x!Sm8CN&$?*0=79IzGdc+NGuEk8wkwCtV9+L*gM)eWmN3m4`LNZUDOP@}efS55O< zPt3Z2#%Mjm@huQ?~gX~44%GLd#rAZzyF4BGQPZL zYB@e@BfkGVN2f1}k*fOU@fjOO$Ie<)Y^|#6SBu*ZJ)Qf;^jF`Xb+wo0fh*t6HEZ&C z&2OVZ>50{ID->6Tv#D>zx!;FXR32`&U#9ZY|Jd-5ZQkhngba6n;>ElHNvrb~L+DffA?zkox+2kG$b$dY;-1(TgH8KaKs}o@7qRA%G_~r?z@NHeijX^ zI5DYGo3>l*arRQe<2CVF8BJ4*kJWWvqKxUf=h&IvL$J$<$%9^!)^>YkbIg}M<2|uc zHmuuoX42Vp(_Z}HjYInuui9SK|I?JX#*s^R1)APGeE!<}ou~2_A#ppNKRR)D+m$~1 z3h9-jKd6?SnbR7vIjc&38Q8btz%uZH{m9kUWoN5KF5P@KJ$-*4T=yN@=5&rVB}{+w zRN2g_8;^Ik9%*kbe6M`r>(A!*`(;G!@J^be^R}IA!e@WgklWCJ->+Hm-17VSv|p2V zUU;3Z-Tv5|mI>=R(`(H=D&vqYji!rZn-UWmD!TCJPJQ3ndg0{Wy$4UWx7SYk@Yutq XE#EdTQZ98;|54e~oYrM2<0k(JV+DAu literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/nspn.png b/padbuildimg/assets/lat/nspn.png new file mode 100644 index 0000000000000000000000000000000000000000..2279945a0da9e36b0a61511a2893e13759e7699f GIT binary patch literal 4148 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/nunm.png b/padbuildimg/assets/lat/nunm.png new file mode 100644 index 0000000000000000000000000000000000000000..d628485615fbf6afb52726068e7402cf94e7d353 GIT binary patch literal 3655 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/spn.png b/padbuildimg/assets/lat/spn.png new file mode 100644 index 0000000000000000000000000000000000000000..9e6b8637d4ffba79aa02e0bdfadacac6061fa731 GIT binary patch literal 3870 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/unm.png b/padbuildimg/assets/lat/unm.png new file mode 100644 index 0000000000000000000000000000000000000000..49cd9266f2ae0e481dc618a1ff8cb6b7673582a0 GIT binary patch literal 3780 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 0 else None @@ -485,11 +501,15 @@ async def process_card(self, card_str, is_assist=False): return [] self.lexer.input(card_str) assist_str = None + ass_card = None card = None repeat = 1 for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value + ass_card, err, debug_info = await self.padinfo_cog.findMonster(tok.value) + if ass_card is None: + raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) elif tok.type == 'REPEAT': repeat = min(tok.value, MAX_LATENTS) elif tok.type == 'ID': @@ -524,8 +544,9 @@ async def process_card(self, card_str, is_assist=False): return [] elif card != DELAY_BUFFER: result_card['LATENT'] = validate_latents( - result_card['LATENT'], - [t.name for t in card.types] + result_card, + card, + ass_card ) result_card['LV'] = min( result_card['LV'], @@ -561,7 +582,8 @@ async def process_card(self, card_str, is_assist=False): parsed_cards = parsed_cards * repeat return parsed_cards - def combine_latents(self, latents): + def combine_latents(self, card): + latents = card['LATENT'] if not latents: return False if len(latents) > MAX_LATENTS: @@ -581,6 +603,9 @@ def combine_latents(self, latents): elif 600 <= l < 700: six_slot.append(l) six_slot.append(1) + elif -700 < l <= -600: + six_slot.append(l) + six_slot.append(2) sorted_latents = [] if len(one_slot) > len(two_slot): sorted_latents.extend(six_slot) @@ -700,7 +725,7 @@ def generate_build_image(self, include_instructions=False): (x_offset + x * self.params.PORTRAIT_WIDTH, y_offset + y * self.params.PORTRAIT_WIDTH)) if has_latents and idx % 2 == 0 and card['LATENT'] is not None: - latents = self.combine_latents(card['LATENT']) + latents = self.combine_latents(card) self.build_img.paste( latents, (x_offset + x * self.params.PORTRAIT_WIDTH, From bd6068f08494329626a45970f172d7aeb79c5077 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 18 Dec 2020 05:43:47 -0800 Subject: [PATCH 0316/1220] Allow for more files in channelmirror (#293) --- channelmirror/channelmirror.py | 79 +++++++++++++++++++++++----------- channelmirror/info.json | 2 +- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index aadcb7f8..feef1ace 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -7,7 +7,7 @@ from io import BytesIO from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline, pagify, box -from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server +from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server, doubleup logger = logging.getLogger('red.misc-cogs.channelmirror') @@ -255,17 +255,28 @@ async def mirror_msg(self, message): if message.attachments: # If we know we're copying a message and that message has an attachment, # pre download it and reuse it for every upload. - attachment = message.attachments[0] - if hasattr(attachment, 'url') and hasattr(attachment, 'filename'): - url = attachment.url - filename = attachment.filename - attachment_bytes = BytesIO(await attachment.read()) + attachment_bytes = [(BytesIO(await attachment.read()), attachment.filename) + for attachment in message.attachments + if hasattr(attachment, 'url') and hasattr(attachment, 'filename')] if await self.config.channel(message.channel).multiedit(): await message.delete() - idmess = await message.channel.send("Pending...") - message = await message.channel.send(message.content, - files=[await a.to_file() for a in message.attachments]) + idmess = await channel.send("Pending...") + attachments = message.attachments + try: + message = await channel.send(message.content, + files=[await a.to_file() for a in attachments]) + except discord.HTTPException: + try: + message = await channel.send(content=message.content) + for a in attachments: + await channel.send(file=await a.to_file()) + except discord.HTTPException: + if message.content: + message = await channel.send(message.content) + await channel.send( + f"<{author.mention} File too large for this channel. Other attachments not shown>") + await idmess.edit(content=str(message.id)) for dest_channel_id in mirrored_channels: @@ -279,10 +290,22 @@ async def mirror_msg(self, message): fmessage = await self.mformat(message.content, message.channel, dest_channel) - if attachment_bytes and filename: - attachment_bytes.seek(0) - dest_message = await dest_channel.send(file=discord.File(attachment_bytes, filename), - content=fmessage) + if attachment_bytes: + try: + [b.seek(0) for b, fn in attachment_bytes] + dest_message = await dest_channel.send( + files=[discord.File(b, fn) for b, fn in attachment_bytes], + content=fmessage) + except discord.HTTPException: + try: + [b.seek(0) for b, fn in attachment_bytes] + dest_message = await dest_channel.send(file=discord.File(*attachment_bytes[0]), + content=fmessage) + for b, fn in attachment_bytes[1:]: + await dest_channel.send(file=discord.File(b, fn)) + except discord.HTTPException: + dest_message = await dest_channel.send(fmessage) + await dest_channel.send("") elif message.content: dest_message = await dest_channel.send(fmessage) else: @@ -291,30 +314,36 @@ async def mirror_msg(self, message): self.settings.add_mirrored_message( channel.id, message.id, dest_channel.id, dest_message.id) - except Exception as ex: + except discord.Forbidden: if dest_channel.guild.owner: try: - message = ("Hi, {1.guild.owner}! This is an automated message from the Tsubaki team to let" - " you know that your server, {1.guild.name}, has been configured to mirror" - " messages from {0.name} (from {0.guild.name}) to {1.name}, but your channel" - " doesn't give me manage message permissions! Please do make sure to allow" - " me permissions to send messages, embed links, and attach files! It's also" - " okay to turn off message mirroring from your channel. If you need help, contact" - " us via `{2}feedback`!" - "").format(channel, dest_channel, (await self.bot.get_valid_prefixes())[0]) - await dest_channel.guild.owner.send(message) + notify = ("Hi, {1.guild.owner}! This is an automated message from the Tsubaki team to let" + " you know that your server, {1.guild.name}, has been configured to mirror" + " messages from {0.name} (from {0.guild.name}) to {1.name}, but your channel" + " doesn't give me manage message permissions! Please do make sure to allow" + " me permissions to send messages, embed links, and attach files! It's also" + " okay to turn off message mirroring from your channel. If you need help, contact" + " us via `{2}feedback`!" + "").format(channel, dest_channel, (await self.bot.get_valid_prefixes())[0]) + + fctx = await self.bot.get_context(message) + fctx.send = dest_channel.guild.owner.send + fctx.history = dest_channel.guild.owner.history + await doubleup(fctx, notify) except Exception: logger.exception("Owner message failed.") + except Exception as ex: logger.exception( 'Failed to mirror message from {} to {}: {}'.format(channel.id, dest_channel_id, str(ex))) if attachment_bytes: - attachment_bytes.close() + [b.close() for b, fn in attachment_bytes] @commands.Cog.listener('on_raw_message_edit') async def mirror_msg_edit(self, payload): message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) - await self.mirror_msg_mod(message, new_message_content=payload.data['content']) + if 'content' in payload.data: + await self.mirror_msg_mod(message, new_message_content=payload.data['content']) @commands.Cog.listener('on_raw_message_delete') async def mirror_msg_delete(self, payload): diff --git a/channelmirror/info.json b/channelmirror/info.json index f91e1ec1..6a18354c 100644 --- a/channelmirror/info.json +++ b/channelmirror/info.json @@ -4,7 +4,7 @@ ], "description": "Allows you to mirror channels across servers.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]channelmod to get started", + "install_msg": "Use [p]channelmirror to get started", "short": "Mirror channels across servers.", "min_bot_version": "3.4.0", "max_bot_version": "0.0.0", From 698914521c1c929d699b9e8e314ddd4c088d8685 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 18 Dec 2020 16:53:13 -0600 Subject: [PATCH 0317/1220] Fix arlatent order (#291) --- dadguide/models/enum_types.py | 4 ++-- dadguide/models/monster_model.py | 2 +- padinfo/id_menu.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 8a4b9606..66976de8 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -54,9 +54,9 @@ class Server(Enum): class AwakeningRestrictedLatent(Enum): """Latent awakenings with availability gated by having an awakening""" - AbsorbPierce = 606 + UnmatchableClear = 606 SpinnerClear = 607 - UnmatchableClear = 608 + AbsorbPierce = 608 def enum_or_none(enum, value, default=None): diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 30fea7cb..16a378f5 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -117,8 +117,8 @@ def killers(self): def awakening_restricted_latents(self): monster_awakening_to_allowed_latent_map = [ {27: AwakeningRestrictedLatent.UnmatchableClear}, # TPA - {62: AwakeningRestrictedLatent.AbsorbPierce}, # Combo orb {20: AwakeningRestrictedLatent.SpinnerClear}, # Bind clear + {62: AwakeningRestrictedLatent.AbsorbPierce}, # Combo orb ] latents = [] for row in monster_awakening_to_allowed_latent_map: diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index e27e8c20..a22328d2 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -583,7 +583,7 @@ def make_lssingle_embed(self, m: "MonsterModel"): AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { - 606: 'absorb_pierce', + 606: 'unmatchable_clear', 607: 'spinner_clear', - 608: 'unmatchable_clear', + 608: 'absorb_pierce', } From c237e08a6e0dd59414c50fb1f5a3fa1c77c9162f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 19 Dec 2020 15:50:24 -0600 Subject: [PATCH 0318/1220] Remove arlatent display from ^id (#294) --- padinfo/id_menu.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index a22328d2..1bc422c8 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -329,10 +329,10 @@ def make_embed(self, m: "MonsterModel"): if is_transform_base: - killers_row = '**Available latents:** [{} slots] {}'.format(m.latent_slots, + killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, self.get_killers_text(m)) else: - killers_row = '**Avail. latents (pre-xform):** [{} slots] {}'.format( + killers_row = '**Avail. killers (pre-xform):** [{} slots] {}'.format( transform_base.latent_slots, self.get_killers_text(transform_base)) @@ -376,7 +376,7 @@ def make_embed(self, m: "MonsterModel"): active_skill = m.active_skill if active_skill: active_header = '**Active Skill ({} -> {})**'.format(active_skill.turn_max, - active_skill.turn_min) + active_skill.turn_min) active_body = active_skill.desc embed.add_field(name=active_header, value=active_body, inline=False) @@ -402,17 +402,22 @@ def make_embed(self, m: "MonsterModel"): def get_killers_text(self, m: "MonsterModel"): return_text = '' if 'Any' in m.killers: - return_text += 'Any killer' + return_text += 'Any' if m.awakening_restricted_latents: - return_text += ', ' + pass + # return_text += ', ' else: return_text += ' '.join([self.killer_latent_emoji(k) for k in m.killers]) if m.awakening_restricted_latents: - return_text += ' ' - return_text += self.get_awakening_restricted_latents_text(m) + pass + # return_text += ' ' + # return_text += self.get_awakening_restricted_latents_text(m) return return_text def get_awakening_restricted_latents_text(self, m: "MonsterModel"): + """Not currently in use, but potentially could be in the future + if more ARLatents are added later + """ if not m.awakening_restricted_latents: return '' return ' ' + ' '.join([self.awakening_restricted_latent_emoji(x) for x in m.awakening_restricted_latents]) From 17728335551462a9761302a628e81ba87f8c6d98 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 19 Dec 2020 17:51:45 -0600 Subject: [PATCH 0319/1220] Change idmode to use idmode scroll subcommand (#298) --- padinfo/padinfo.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4de53fe4..d6bf06b7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -716,18 +716,22 @@ async def padsay(self, ctx, server, *, query: str = None): else: await ctx.send(self.makeFailureMsg(err)) - @commands.group(invoke_without_command=True) - async def idmode(self, ctx, id_type): - """Switch between number mode and evo mode + @commands.group() + async def idmode(self, ctx): + """id settings configuration""" + + @idmode.command() + async def scroll(self, ctx, value): + """Switch between number scroll and evo scroll - [p]idmode number - [p]idmode evo""" - if id_type in ['evo', 'default']: + [p]idmode scroll number + [p]idmode scroll evo""" + if value in ['evo', 'default']: if self.settings.setEvoID(ctx.author.id): await ctx.tick() else: await ctx.send(inline("You're already using evo mode")) - elif id_type in ['number']: + elif value in ['number']: if self.settings.rmEvoID(ctx.author.id): await ctx.tick() else: From 525aa8a4415beff88c92b5bb49ca9cbf951e2abb Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 19 Dec 2020 19:55:36 -0600 Subject: [PATCH 0320/1220] Add combo count awakening emojis (#299) --- padinfo/id_menu.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 1bc422c8..cd1f72c7 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -584,6 +584,11 @@ def make_lssingle_embed(self, m: "MonsterModel"): 70: 'res_poison_super', 71: 'misc_jammerboost', 72: 'misc_poisonboost', + 73: 'cc_fire', + 74: 'cc_water', + 75: 'cc_wood', + 76: 'cc_light', + 77: 'cc_dark', } From ba3ce40dcd42b6289b9802fe78aa44ac4442e619 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 19 Dec 2020 20:17:37 -0600 Subject: [PATCH 0321/1220] [padevents] Send a message if AEP list is empty (#300) --- padevents/padevents.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/padevents/padevents.py b/padevents/padevents.py index ade17c34..717d3182 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -377,6 +377,9 @@ async def aep_show(self, ctx, *, key): async def aep_list(self, ctx): """List all autoeventpings""" pingroles = await self.config.guild(ctx.guild).pingroles() + if len(pingroles) == 0: + await ctx.send('You have no auto event pings configured currently!') + return await ctx.send(box('\n'.join(pingroles))) @autoeventping.group(name="set") From fbc0f39ef15e61b244b4d854fa64239dd22b5741 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 19 Dec 2020 21:30:21 -0800 Subject: [PATCH 0322/1220] Add invite link to ^feedback (#301) * Add invite link to ^feedback * Make setserverinvite a subcommand of feedback --- feedback/feedback.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/feedback/feedback.py b/feedback/feedback.py index 6a7ddd35..04ce8c03 100644 --- a/feedback/feedback.py +++ b/feedback/feedback.py @@ -1,16 +1,20 @@ -import discord from io import BytesIO -from redbot.core import checks, commands, modlog -from redbot.core.utils.chat_formatting import box, inline, pagify + +import discord +from redbot.core import checks, commands, Config +from redbot.core.utils.chat_formatting import inline from tsutils import CogSettings class Feedback(commands.Cog): """Feedback Cog""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.settings = FeedbackSettings("feedback") + self.config = Config.get_conf(self, identifier=73308437) + self.config.register_global(feedback_server="") self.sessions = set() @@ -93,7 +97,7 @@ async def credits(self, ctx): await ctx.send(embed=embed) - @commands.command() + @commands.group(invoke_without_command=True) @commands.cooldown(1, 60, commands.BucketType.user) async def feedback(self, ctx, *, message: str): """Provide feedback on the bot. @@ -102,8 +106,8 @@ async def feedback(self, ctx, *, message: str): """ feedback_channel = self.bot.get_channel(int(self.settings.get_feedback_channel())) await self._send_feedback(ctx, message, feedback_channel, - " Join the Tsubaki Server to see any responses ({0.prefix}tsubakiserver).".format( - ctx)) + (" Join the Tsubaki Server to see any responses.\n" + "{}").format(await self.config.feedback_server())) @commands.command() @commands.guild_only() @@ -132,6 +136,13 @@ async def setblogfeedbackchannel(self, ctx, channel: discord.TextChannel): self.settings.set_blog_feedback_channel(channel.id) await ctx.tick() + @feedback.command() + @checks.is_owner() + async def setserverinvite(self, ctx, invite): + """Set the blog feedback destination channel.""" + await self.config.feedback_server.set(invite) + await ctx.tick() + class FeedbackSettings(CogSettings): def make_default_settings(self): From 10763a4f57e17c141c74f815bea403a01bdef573 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 19 Dec 2020 21:47:19 -0800 Subject: [PATCH 0323/1220] Add confirmation for edited pdg commands (#302) --- padglobal/padglobal.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index a748912d..e112ff66 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1,18 +1,19 @@ -import aiohttp import asyncio import csv import datetime import difflib -import discord import json import logging import os -import prettytable import re import time -import tsutils -from io import StringIO, BytesIO from collections import defaultdict +from io import StringIO, BytesIO + +import aiohttp +import discord +import prettytable +import tsutils from redbot.core import checks, data_manager from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify @@ -343,6 +344,10 @@ async def _add(self, ctx, command, text, confirm=True): return elif command in self.c_commands: op = 'EDITED' + if op == 'EDITED' and not await confirm_message(ctx, + "Are you sure you want to edit the command {}?".format( + command)): + return ted = self.c_commands[command] alias = False while ted in self.c_commands: @@ -587,6 +592,10 @@ async def addglossary(self, ctx, term, *, definition): definition = replace_emoji_names_with_code(self._get_emojis(), definition) op = 'EDITED' if term in self.settings.glossary() else 'ADDED' + if op == 'EDITED' and not await confirm_message(ctx, + "Are you sure you want to edit the glossary info for {}?".format( + term)): + return self.settings.addGlossary(term, definition) await ctx.send("PAD glossary term successfully {}.".format(op)) @@ -667,6 +676,10 @@ async def addboss(self, ctx, term, *, definition): If you want to use a multiple word boss name, enclose it in quotes.""" term = term.lower() op = 'EDITED' if term in self.settings.boss() else 'ADDED' + if op == 'EDITED' and not await confirm_message(ctx, + "Are you sure you want to edit the boss info for {}?".format( + term)): + return definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') definition = replace_emoji_names_with_code(self._get_emojis(), definition) @@ -807,6 +820,10 @@ async def addwhich(self, ctx, monster_id: int, *, definition): name = m.monster_id op = 'EDITED' if name in self.settings.which() else 'ADDED' + if op == 'EDITED' and not await confirm_message(ctx, + "Are you sure you want to edit the which info for {}?".format( + m.name_en)): + return self.settings.addWhich(name, definition) await ctx.send("PAD which info successfully {}.".format(op)) @@ -1167,6 +1184,10 @@ async def adddungeonguide(self, ctx, term: str, *, definition: str): """Adds a dungeon guide to the [p]guide command""" term = term.lower() op = 'EDITED' if term in self.settings.dungeonGuide() else 'ADDED' + if op == 'EDITED' and not await confirm_message(ctx, + "Are you sure you want to edit the dungeon guide info for {}?".format( + term)): + return self.settings.addDungeonGuide(term, definition) await ctx.send("PAD dungeon guide successfully {}.".format(op)) @@ -1179,7 +1200,7 @@ async def rmdungeonguide(self, ctx, term: str): return if not await confirm_message(ctx, "Are you sure you want to globally remove the dungeonguide data for {}?".format( - term)): + term)): return self.settings.rmDungeonGuide(term) await ctx.tick() @@ -1195,6 +1216,10 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): name = m.monster_id op = 'EDITED' if name in self.settings.leaderGuide() else 'ADDED' + if op == 'EDITED' and not await confirm_message(ctx, + "Are you sure you want to edit the boss info for {}?".format( + m.name_en)): + return self.settings.addLeaderGuide(name, definition) await ctx.send("PAD leader guide info successfully {}.".format(op)) @@ -1208,7 +1233,7 @@ async def rmleaderguide(self, ctx, monster_id: int): await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await confirm_message(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format( - m.name_en)): + m.name_en)): return name = m.monster_id From f11b1aa3572bfd90fba0d9af3bd390611811a201 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 19 Dec 2020 22:22:43 -0800 Subject: [PATCH 0324/1220] Feedback cooldown fix & remove inline() in response (#303) * Add invite link to ^feedback * Make setserverinvite a subcommand of feedback * Feedback cooldown fix * remove inline formatting --- feedback/feedback.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/feedback/feedback.py b/feedback/feedback.py index 04ce8c03..3a989cb6 100644 --- a/feedback/feedback.py +++ b/feedback/feedback.py @@ -58,9 +58,9 @@ async def _send_feedback(self, ctx, message: str, feedback_channel, success_mess except: await ctx.send(inline("I'm unable to deliver your message. Sorry.")) else: - await ctx.send(inline("Your message has been sent." - " Abusing this feature will result in a blacklist." - + success_message)) + await ctx.send(("Your message has been sent." + " Abusing this feature will result in a blacklist.") + + success_message) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -106,7 +106,7 @@ async def feedback(self, ctx, *, message: str): """ feedback_channel = self.bot.get_channel(int(self.settings.get_feedback_channel())) await self._send_feedback(ctx, message, feedback_channel, - (" Join the Tsubaki Server to see any responses.\n" + ("\nJoin the Tsubaki Server to see any responses.\n" "{}").format(await self.config.feedback_server())) @commands.command() @@ -138,7 +138,8 @@ async def setblogfeedbackchannel(self, ctx, channel: discord.TextChannel): @feedback.command() @checks.is_owner() - async def setserverinvite(self, ctx, invite): + @commands.cooldown(1, 0, commands.BucketType.user) + async def setserverinvite(self, ctx, *, invite): """Set the blog feedback destination channel.""" await self.config.feedback_server.set(invite) await ctx.tick() From a1aeac842ea5b722b2f81fdb03f3f856fa751da3 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 20 Dec 2020 02:08:14 -0600 Subject: [PATCH 0325/1220] Fix tsutils imports after library files change (#304) --- padboard/padboard.py | 6 +++--- padsearch/padsearch.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/padboard/padboard.py b/padboard/padboard.py index e63e4ee5..7341dca7 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -8,7 +8,7 @@ from collections import deque from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import inline -from tsutils import tsutils +from tsutils import extract_image_url DATA_DIR = os.path.join('data', 'padboard') @@ -41,7 +41,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): @commands.Cog.listener("on_message") async def log_message(self, message): - url = tsutils.extract_image_url(message) + url = extract_image_url(message) if url: self.logs[message.author.id].append(url) @@ -102,7 +102,7 @@ async def dawnglare(self, ctx, user: discord.Member = None): async def get_recent_image(self, ctx, user: discord.Member = None, message: discord.Message = None): user_id = user.id if user else ctx.author.id - image_url = tsutils.extract_image_url(message) + image_url = extract_image_url(message) if image_url is None: image_url = self.find_image(user_id) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index d69b23a5..5d61f059 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -7,7 +7,7 @@ from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify -from tsutils import tsutils +from tsutils import timeout_after logger = logging.getLogger('red.padbot-cogs.padsearch') @@ -55,7 +55,7 @@ """ -@tsutils.timeout_after(1) +@timeout_after(1) def filt_timeout(filts, ms): for f in filts: ms = [m for m in ms if f(m)] From d16ab49c96c9831e65c90f4694592733f1146243 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 20 Dec 2020 00:23:15 -0800 Subject: [PATCH 0326/1220] [padguidedb] Fix tsutils import (#305) --- padguidedb/padguidedb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 56a272f1..9b7590c3 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -9,7 +9,8 @@ from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import CogSettings, tsutils +from tsutils import CogSettings +import tsutils logger = logging.getLogger('red.padbot-cogs.padguidedb') From 63a7bee34f901ebf31942c9ea0af3c071dd05db7 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 21 Dec 2020 15:51:59 -0800 Subject: [PATCH 0327/1220] Remove second confirmation for pdg add (#307) --- padglobal/padglobal.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index e112ff66..5baf8d7c 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -344,10 +344,6 @@ async def _add(self, ctx, command, text, confirm=True): return elif command in self.c_commands: op = 'EDITED' - if op == 'EDITED' and not await confirm_message(ctx, - "Are you sure you want to edit the command {}?".format( - command)): - return ted = self.c_commands[command] alias = False while ted in self.c_commands: @@ -726,7 +722,7 @@ async def which(self, ctx, *, term: str = None): async def _resolve_which(self, ctx, term): db_context = self.bot.get_cog('Dadguide').database - db_context: DbContext + db_context: "DbContext" term = term.lower().replace('?', '') nm, _, _ = await lookup_named_monster(term) From 57631f28a4eb71259a1973d3e9fc4b61ae40e8b2 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 21 Dec 2020 17:59:30 -0800 Subject: [PATCH 0328/1220] Update cogs for TSUtils 3.0.0 (#308) --- channelmirror/channelmirror.py | 13 ++++++------ channelmirror/info.json | 2 +- chronomagia/chronomagia.py | 2 +- chronomagia/info.json | 2 +- dadguide/dadguide.py | 6 +++--- dadguide/info.json | 2 +- dadguide/models/monster_model.py | 2 +- dadguide/old_monster_index.py | 4 ++-- padevents/info.json | 2 +- padevents/padevents.py | 35 ++++++++++++++------------------ padguidedb/info.json | 2 +- padguidedb/padguidedb.py | 4 ++-- padinfo/info.json | 2 +- padinfo/padinfo.py | 4 ++-- padmonitor/info.json | 2 +- padmonitor/padmonitor.py | 2 +- profile/info.json | 2 +- profile/profile.py | 13 ++++-------- 18 files changed, 46 insertions(+), 55 deletions(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index feef1ace..d4ef1844 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -1,13 +1,15 @@ -import discord import logging import re import time -from typing import Optional from datetime import datetime from io import BytesIO +from typing import Optional + +import discord +import tsutils from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline, pagify, box -from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server, doubleup +from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server logger = logging.getLogger('red.misc-cogs.channelmirror') @@ -329,7 +331,7 @@ async def mirror_msg(self, message): fctx = await self.bot.get_context(message) fctx.send = dest_channel.guild.owner.send fctx.history = dest_channel.guild.owner.history - await doubleup(fctx, notify) + await tsutils.send_repeated_consecutive_messages(fctx, notify) except Exception: logger.exception("Owner message failed.") except Exception as ex: @@ -348,8 +350,7 @@ async def mirror_msg_edit(self, payload): @commands.Cog.listener('on_raw_message_delete') async def mirror_msg_delete(self, payload): if not await self.config.channel(self.bot.get_channel(payload.channel_id)).nodeletion(): - fmessage = discord.Object(id=payload.message_id) - fmessage.channel = self.bot.get_channel(payload.channel_id) + fmessage = tsutils.DummyObject(id=payload.message_id, channel=self.bot.get_channel(payload.channel_id)) await self.mirror_msg_mod(fmessage, delete_message_content=True) @commands.Cog.listener('on_raw_reaction_add') diff --git a/channelmirror/info.json b/channelmirror/info.json index 6a18354c..698974f8 100644 --- a/channelmirror/info.json +++ b/channelmirror/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils", + "tsutils>=3.0.0", "aiohttp" ], "tags": [ diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py index 93a68fd1..5b08985f 100644 --- a/chronomagia/chronomagia.py +++ b/chronomagia/chronomagia.py @@ -78,7 +78,7 @@ async def refresh_data(self): await self.bot.wait_until_ready() standard_expiry_secs = 2 * 60 * 60 - summary_text = await tsutils.makeAsyncCachedPlainRequest( + summary_text = await tsutils.async_cached_plain_request( _data_file('summary.csv'), SUMMARY_SHEET, standard_expiry_secs) file_reader = csv.reader(summary_text.splitlines(), delimiter=',') next(file_reader, None) # skip header diff --git a/chronomagia/info.json b/chronomagia/info.json index 6327b539..96d0182e 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils", + "tsutils>=3.0.0", "aiohttp" ], "tags": [ diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index f1f5f732..8a5af903 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -234,11 +234,11 @@ async def _download_files(self): async def _download_override_files(self): one_hour_secs = 1 * 60 * 60 - await tsutils.makeAsyncCachedPlainRequest( + await tsutils.async_cached_plain_request( NICKNAME_FILE_PATTERN, NICKNAME_OVERRIDES_SHEET, one_hour_secs) - await tsutils.makeAsyncCachedPlainRequest( + await tsutils.async_cached_plain_request( BASENAME_FILE_PATTERN, GROUP_BASENAMES_OVERRIDES_SHEET, one_hour_secs) - await tsutils.makeAsyncCachedPlainRequest( + await tsutils.async_cached_plain_request( PANTHNAME_FILE_PATTERN, PANTHNAME_OVERRIDES_SHEET, one_hour_secs) @commands.group() diff --git a/dadguide/info.json b/dadguide/info.json index 78e190ba..41e2545d 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils", + "tsutils>=3.0.0", "pytz", "romkan", "networkx" diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 16a378f5..1e345ff3 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -172,7 +172,7 @@ def make_roma_subname(name_ja): adjusted_subname = '' for part in subname.split('・'): roma_part = romkan.to_roma(part) - if part != roma_part and not tsutils.containsJa(roma_part): + if part != roma_part and not tsutils.contains_ja(roma_part): adjusted_subname += ' ' + roma_part.strip('-') return adjusted_subname.strip() diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 4845af55..58b2e51e 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -246,7 +246,7 @@ def find_monster(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_ja = tsutils.containsJa(query) + contains_ja = tsutils.contains_ja(query) if len(query) < 2 and contains_ja: return None, 'Japanese queries must be at least 2 characters', None elif len(query) < 4 and not contains_ja: @@ -347,7 +347,7 @@ def find_monster2(self, query): if query in self.all_entries: return self.all_entries[query], None, "Exact nickname" - contains_ja = tsutils.containsJa(query) + contains_ja = tsutils.contains_ja(query) if len(query) < 2 and contains_ja: return None, 'Japanese queries must be at least 2 characters', None elif len(query) < 4 and not contains_ja: diff --git a/padevents/info.json b/padevents/info.json index be7f15e6..07057df8 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -19,7 +19,7 @@ "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils", + "tsutils>=3.0.0", "pytz", "prettytable" ], diff --git a/padevents/padevents.py b/padevents/padevents.py index 717d3182..c5117a72 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -14,7 +14,7 @@ from redbot.core import checks from redbot.core import commands, Config from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import CogSettings, confirm_message +from tsutils import CogSettings, confirm_message, normalize_server_name from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -170,10 +170,10 @@ async def check_started(self): else: if not e.dungeon_type in [DungeonType.Normal]: - msg = self.makeActiveText(server) + msg = self.makeActiveText(e.server) for daily_registration in list(self.settings.listDailyReg()): try: - if server == daily_registration['server']: + if e.server == daily_registration['server']: await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), msg, channel_id=daily_registration['channel_id']) logger.info("daily_reg server") @@ -196,7 +196,7 @@ async def padevents(self, ctx): @padevents.command() @checks.is_owner() async def testevent(self, ctx, server, seconds: int = 0): - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -231,7 +231,7 @@ class Dungeon: pass @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) async def addchannel(self, ctx, server): - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -248,7 +248,7 @@ async def addchannel(self, ctx, server): @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) async def rmchannel(self, ctx, server): - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -265,7 +265,7 @@ async def rmchannel(self, ctx, server): @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) async def addchanneldaily(self, ctx, server): - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -282,7 +282,7 @@ async def addchanneldaily(self, ctx, server): @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) async def rmchanneldaily(self, ctx, server): - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -319,7 +319,7 @@ async def aep_add(self, ctx, key, server = None, searchstr = None, red: discord. } if green is not None: - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -454,7 +454,7 @@ async def aep_s_greenrole(self, ctx, key, role: discord.Role): @aep_set.command(name="server") async def aep_s_server(self, ctx, key, server): """Sets which server to listen to events in""" - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -513,7 +513,7 @@ async def autoeventdm(self, ctx): @autoeventdm.command(name="add") async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): """Add a new autoeventdm""" - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -654,7 +654,7 @@ def makeChannelList(self, reg_list, filt=None): @padevents.command() @checks.mod_or_permissions(manage_guild=True) async def active(self, ctx, server): - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -837,7 +837,7 @@ async def eventskr(self, ctx): await self.doPartial(ctx, 'KR') async def doPartial(self, ctx, server): - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return @@ -880,7 +880,7 @@ async def doPartial(self, ctx, server): def makeChannelReg(channel_id, server): - server = normalizeServer(server) + server = normalize_server_name(server) return { "channel_id": channel_id, "server": server @@ -1031,7 +1031,7 @@ def withFunc(self, func, exclude=False): return EventList(list(filter(func, self.event_list))) def withServer(self, server): - return self.withFunc(lambda e: e.server == normalizeServer(server)) + return self.withFunc(lambda e: e.server == normalize_server_name(server)) def withType(self, event_type): return self.withFunc(lambda e: e.event_type == event_type) @@ -1126,11 +1126,6 @@ def fmtDaysHrsMinsShort(sec): return '{:2}m'.format(int(minutes)) -def normalizeServer(server): - server = server.upper() - return 'NA' if server == 'US' else server - - def isEventWanted(event): name = event.name_and_modifier.lower() if 'castle of satan' in name: diff --git a/padguidedb/info.json b/padguidedb/info.json index f259baaa..c1099db5 100644 --- a/padguidedb/info.json +++ b/padguidedb/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils", + "tsutils>=3.0.0", "pymysql" ], "tags": [ diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 9b7590c3..9b3d6443 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -155,10 +155,10 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): if stderr: logger.error("Dungeon Load Error:\n{}\n\n{}".format(stdout.decode(), stderr.decode())) - await tsutils.doubleup(ctx, inline( + await tsutils.send_repeated_consecutive_messages(ctx, inline( 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) else: - await tsutils.doubleup(ctx, inline( + await tsutils.send_repeated_consecutive_messages(ctx, inline( 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) self.queue_size -= 1 diff --git a/padinfo/info.json b/padinfo/info.json index 0cdcab43..cba19d00 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=1.1.0", + "tsutils>=3.0.0", "prettytable" ], "tags": [ diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d6bf06b7..98bacedb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -74,7 +74,7 @@ def __init__(self, emoji_to_embed, m: "MonsterModel" = None, self.pad_info.settings.log_emoji("start_" + selected_emoji) - def on_update(self, ctx, selected_emoji): + async def on_update(self, ctx, selected_emoji): evoID = self.pad_info.settings.checkEvoID(ctx.author.id) self.pad_info.settings.log_emoji(selected_emoji) if evoID: @@ -124,7 +124,7 @@ def __init__(self, emoji_to_embed, m: "MonsterModel" = None, self.selected_emoji = selected_emoji self.bot = bot - def on_update(self, ctx, selected_emoji): + async def on_update(self, ctx, selected_emoji): index = self.ms.index(self.m) if selected_emoji == self.pad_info.first_monster_emoji: diff --git a/padmonitor/info.json b/padmonitor/info.json index bbec0c4a..4b8336d2 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -19,7 +19,7 @@ "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils" + "tsutils>=3.0.0" ], "tags": [ "PAD" diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 40a109ba..3a7d360e 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -69,7 +69,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: msg += '\n\t[{}] {}'.format(m.monster_no, m.name_en) - if tsutils.containsJa( + if tsutils.contains_ja( m.name_en) and m.name_en_override != m.name_en and m.name_en_override is not None: msg += ' ({})'.format(m.name_en_override) return msg diff --git a/profile/info.json b/profile/info.json index 1083e2c3..5d2d577c 100644 --- a/profile/info.json +++ b/profile/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils" + "tsutils>=3.0.0" ], "tags": [ "PAD" diff --git a/profile/profile.py b/profile/profile.py index c8151513..424b46e0 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -4,16 +4,11 @@ from io import BytesIO from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import CogSettings +from tsutils import CogSettings, normalize_server_name logger = logging.getLogger('red.padbot-cogs.profile') -def normalizeServer(server): - server = server.upper().strip() - return 'NA' if server == 'US' else server - - SUPPORTED_SERVERS = ["NA", "KR", "JP", "EU"] @@ -112,7 +107,7 @@ async def idFor(self, ctx, user: discord.Member, server=None): async def getServer(self, ctx, user_id, server=None): if server is None: server = self.settings.getDefaultServer(user_id) - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send(inline('Unsupported server: ' + server)) return @@ -144,7 +139,7 @@ async def setServer(self, ctx, server): This server is used to default the idme command if you don't provide a server. """ - server = normalizeServer(server) + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send(inline('Unsupported server: ' + server)) return @@ -212,7 +207,7 @@ async def clear(self, ctx, server=None): self.settings.clearProfile(user_id) await ctx.send(inline('Cleared your profile for all servers')) else: - server = normalizeServer(server) + server = normalize_server_name(server) self.settings.clearProfile(user_id, server) await ctx.send(inline('Cleared your profile for ' + server)) From 49bdb2d50eb83e47ca4d014eb58e9e75573b944f Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 22 Dec 2020 18:29:05 -0800 Subject: [PATCH 0329/1220] Add PadInfo Colored Embeds! (#309) --- padinfo/id_menu.py | 78 +++++++++++++++++++++++-------------------- padinfo/padinfo.py | 82 +++++++++++++++++++++++++++------------------- 2 files changed, 92 insertions(+), 68 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index cd1f72c7..b6795d2b 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -1,16 +1,14 @@ +import urllib.parse +from typing import TYPE_CHECKING + import discord import prettytable - import tsutils -import urllib.parse - -from redbot.core.utils.chat_formatting import box, inline +from redbot.core.utils.chat_formatting import box from .leader_skills import createMultiplierText from .leader_skills import createSingleMultiplierText -from typing import TYPE_CHECKING - if TYPE_CHECKING: from dadguide.database_context import DbContext from dadguide.models.monster_model import MonsterModel @@ -43,8 +41,9 @@ def get_pic_url(m: "MonsterModel"): return RPAD_PIC_TEMPLATE.format(m.monster_id) -class IdMenu(object): - def __init__(self, db_context: "DbContext" = None, allowed_emojis: list = None): +class IdMenu: + def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = None): + self.ctx = ctx self.db_context = db_context self.allowed_emojis = allowed_emojis @@ -85,9 +84,9 @@ def monster_evo_header(self, m: "MonsterModel", link=True): def make_thumbnail_url(m: "MonsterModel"): return get_portrait_url(m) - def make_base_embed(self, m: "MonsterModel"): + async def make_base_embed(self, m: "MonsterModel"): header = self.monster_long_header(m) - embed = discord.Embed() + embed = await self.make_custom_embed() embed.set_thumbnail(url=self.make_thumbnail_url(m)) embed.title = header embed.url = get_pdx_url(m) @@ -115,8 +114,8 @@ def _get_monster_attr_emoji(self, monster: "MonsterModel"): emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) return self.match_emoji(emoji) - def make_evo_embed(self, m: "MonsterModel"): - embed = self.make_base_embed(m) + async def make_evo_embed(self, m: "MonsterModel"): + embed = await self.make_base_embed(m) alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) @@ -155,8 +154,8 @@ def _add_mats_of_list(self, embed, monster_id_list, field_name): field_data += "{}\n".format(self.monster_long_header(ae, link=True)) embed.add_field(name=field_name, value=field_data) - def make_evo_mats_embed(self, m: "MonsterModel"): - embed = self.make_base_embed(m) + async def make_evo_mats_embed(self, m: "MonsterModel"): + embed = await self.make_base_embed(m) mats_for_evo = self.db_context.graph.evo_mats_by_monster(m) @@ -176,13 +175,13 @@ def make_evo_mats_embed(self, m: "MonsterModel"): self._add_mats_of_list(embed, self.db_context.graph.material_of_ids(evo_gem), "Evo gem is mat for") return embed - def make_pantheon_embed(self, m: "MonsterModel"): + async def make_pantheon_embed(self, m: "MonsterModel"): full_pantheon = self.db_context.get_monsters_by_series(m.series_id) pantheon_list = list(filter(lambda x: self.db_context.graph.monster_is_base(x), full_pantheon)) if len(pantheon_list) == 0 or len(pantheon_list) > 6: return None - embed = self.make_base_embed(m) + embed = await self.make_base_embed(m) field_name = 'Pantheon: ' + self.db_context.graph.get_monster(m.monster_no).series.name field_data = '' @@ -192,7 +191,7 @@ def make_pantheon_embed(self, m: "MonsterModel"): return embed - def make_skillups_embed(self, m: "MonsterModel"): + async def make_skillups_embed(self, m: "MonsterModel"): if m.active_skill is None: return None possible_skillups_list = self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) @@ -202,7 +201,7 @@ def make_skillups_embed(self, m: "MonsterModel"): if len(skillups_list) == 0: return None - embed = self.make_base_embed(m) + embed = await self.make_base_embed(m) field_name = 'Skillups' field_data = '' @@ -220,8 +219,8 @@ def make_skillups_embed(self, m: "MonsterModel"): return embed - def make_picture_embed(self, m: "MonsterModel", animated=False): - embed = self.make_base_embed(m) + async def make_picture_embed(self, m: "MonsterModel", animated=False): + embed = await self.make_base_embed(m) url = get_pic_url(m) embed.set_image(url=url) # Clear the thumbnail, don't need it on pic @@ -230,7 +229,8 @@ def make_picture_embed(self, m: "MonsterModel", animated=False): if animated: extra_links.append('Animation: {} -- {}'.format(self.monster_video_url(m), self.monster_gif_url(m))) if m.orb_skin_id is not None: - extra_links.append('Orb Skin: {} -- {}'.format(self.monster_orb_skin_url(m), self.monster_orb_skin_cb_url(m))) + extra_links.append( + 'Orb Skin: {} -- {}'.format(self.monster_orb_skin_url(m), self.monster_orb_skin_cb_url(m))) if len(extra_links) > 0: embed.add_field(name='Extra Links', value='\n'.join(extra_links)) @@ -252,13 +252,13 @@ def monster_orb_skin_url(m: "MonsterModel", link_text='Regular'): def monster_orb_skin_cb_url(m: "MonsterModel", link_text='Color Blind'): return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) - def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): + async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): lls = left_m.leader_skill rls = right_m.leader_skill multiplier_text = createMultiplierText(lls, rls) - embed = discord.Embed() + embed = await self.make_custom_embed() embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( @@ -271,9 +271,9 @@ def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): return embed - def make_header_embed(self, m: "MonsterModel"): + async def make_header_embed(self, m: "MonsterModel"): header = self.monster_long_header(m, link=True) - embed = discord.Embed() + embed = await self.make_custom_embed() embed.description = header return embed @@ -298,8 +298,8 @@ def _monster_acquisition_string(self, m: "MonsterModel"): acquire_text = 'MP Shop Evo' return acquire_text - def make_embed(self, m: "MonsterModel"): - embed = self.make_base_embed(m) + async def make_embed(self, m: "MonsterModel"): + embed = await self.make_base_embed(m) # in case we want to readd the type emojis later # types_row = ' '.join(['{} {}'.format(str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))), t.name) for t in m.types]) @@ -426,10 +426,11 @@ def killer_latent_emoji(self, latent: str): return str(self.match_emoji('latent_killer_{}'.format(latent.lower()))) def awakening_restricted_latent_emoji(self, latent: "AwakeningRestrictedLatent"): - return str(self.match_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value]))) + return str( + self.match_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value]))) - def make_otherinfo_embed(self, m: "MonsterModel"): - embed = self.make_base_embed(m) + async def make_otherinfo_embed(self, m: "MonsterModel"): + embed = await self.make_base_embed(m) # Clear the thumbnail, takes up too much space embed.set_thumbnail(url='') @@ -487,8 +488,8 @@ def make_otherinfo_embed(self, m: "MonsterModel"): return embed - def make_links_embed(self, m: "MonsterModel"): - embed = self.make_base_embed(m) + async def make_links_embed(self, m: "MonsterModel"): + embed = await self.make_base_embed(m) embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), SKYOZORA_TEMPLATE.format(m.monster_no_jp), @@ -497,10 +498,10 @@ def make_links_embed(self, m: "MonsterModel"): embed.set_footer(text='') return embed - def make_lssingle_embed(self, m: "MonsterModel"): + async def make_lssingle_embed(self, m: "MonsterModel"): multiplier_text = createSingleMultiplierText(m.leader_skill) - embed = discord.Embed() + embed = await self.make_custom_embed() embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( @@ -510,6 +511,14 @@ def make_lssingle_embed(self, m: "MonsterModel"): return embed + async def make_custom_embed(self): + pdicog = self.ctx.bot.get_cog("PadInfo") + color = await pdicog.config.user(self.ctx.author).color() + if color is None: + return discord.Embed() + else: + return discord.Embed(color=discord.Color(color)) + AWAKENING_ID_TO_EMOJI_NAME_MAP = { 1: 'boost_hp', @@ -591,7 +600,6 @@ def make_lssingle_embed(self, m: "MonsterModel"): 77: 'cc_dark', } - AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { 606: 'unmatchable_clear', 607: 'spinner_clear', diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 98bacedb..75f0d7f6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -14,7 +14,7 @@ from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline -from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, confirm_message +from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor from .find_monster import prefix_to_filter from .id_menu import IdMenu @@ -62,9 +62,10 @@ def _data_file(file_name: str) -> str: class IdEmojiUpdater(EmojiUpdater): - def __init__(self, emoji_to_embed, m: "MonsterModel" = None, + def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, selected_emoji=None, bot=None, db_context: "DbContext" = None): + self.ctx = ctx self.emoji_dict = emoji_to_embed self.m = m self.pad_info = pad_info @@ -108,15 +109,16 @@ async def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options( + self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, m=self.m, scroll=sorted( {*self.db_context.graph.get_alt_cards(self.m.monster_id)}) if evoID else [], menu_type=1) return True class ScrollEmojiUpdater(EmojiUpdater): - def __init__(self, emoji_to_embed, m: "MonsterModel" = None, + def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, ms: "list[int]" = None, selected_emoji=None, pad_info=None, bot=None): + self.ctx = ctx self.emoji_dict = emoji_to_embed self.m = m self.ms = ms @@ -142,7 +144,7 @@ async def on_update(self, ctx, selected_emoji): self.selected_emoji = selected_emoji return True - self.emoji_dict = self.pad_info.get_id_emoji_options(m=self.m, scroll=self.ms) + self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, m=self.m, scroll=self.ms) return True @@ -186,7 +188,7 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) self.config = Config.get_conf(self, identifier=9401770) - self.config.register_user(survey_mode=0) + self.config.register_user(survey_mode=0, color=None) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False) def cog_unload(self): @@ -402,50 +404,52 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): db_context = DGCOG.database alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_id) - emoji_to_embed = self.get_id_emoji_options( + emoji_to_embed = await self.get_id_emoji_options(ctx, m=m, scroll=sorted({*alt_versions}, key=lambda x: x.monster_id) if self.settings.checkEvoID( ctx.author.id) else [], menu_type=1) return await self._do_menu( ctx, starting_menu_emoji, - IdEmojiUpdater(emoji_to_embed, pad_info=self, + IdEmojiUpdater(ctx, emoji_to_embed, pad_info=self, m=m, selected_emoji=starting_menu_emoji, bot=self.bot, db_context=db_context) ) async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): - emoji_to_embed = self.get_id_emoji_options(m=m, scroll=ms) + emoji_to_embed = await self.get_id_emoji_options(ctx, m=m, scroll=ms) return await self._do_menu( ctx, starting_menu_emoji, - ScrollEmojiUpdater(emoji_to_embed, pad_info=self, bot=self.bot, + ScrollEmojiUpdater(ctx, emoji_to_embed, pad_info=self, bot=self.bot, m=m, ms=ms, selected_emoji=starting_menu_emoji) ) - def get_id_emoji_options(self, m=None, scroll=[], menu_type=0): + async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): + if scroll is None: + scroll = [] DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database - menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) - id_embed = menu.make_embed(m) - evo_embed = menu.make_evo_embed(m) - mats_embed = menu.make_evo_mats_embed(m) + id_embed = await menu.make_embed(m) + evo_embed = await menu.make_evo_embed(m) + mats_embed = await menu.make_evo_mats_embed(m) animated = m.has_animation - pic_embed = menu.make_picture_embed(m, animated=animated) - other_info_embed = menu.make_otherinfo_embed(m) + pic_embed = await menu.make_picture_embed(m, animated=animated) + other_info_embed = await menu.make_otherinfo_embed(m) emoji_to_embed = OrderedDict() emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed emoji_to_embed[self.mats_emoji] = mats_embed emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = menu.make_pantheon_embed(m) + pantheon_embed = await menu.make_pantheon_embed(m) if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed - skillups_embed = menu.make_skillups_embed(m) + skillups_embed = await menu.make_skillups_embed(m) if skillups_embed: emoji_to_embed[self.skillups_emoji] = skillups_embed @@ -475,7 +479,7 @@ async def _do_evolistmenu(self, ctx, sm): monsters.sort(key=lambda x: x.monster_id) emoji_to_embed = OrderedDict() - menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) starting_menu_emoji = None for idx, m in enumerate(monsters): chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" @@ -485,7 +489,7 @@ async def _do_evolistmenu(self, ctx, sm): return else: emoji = char_to_emoji(chars[idx]) - emoji_to_embed[emoji] = menu.make_embed(m) + emoji_to_embed[emoji] = await menu.make_embed(m) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji @@ -506,7 +510,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): result_embed.set_footer(text=discord.Embed.Empty) await result_msg.edit(embed=result_embed) except Exception as ex: - logger.error('Menu failure', exc_info=1) + logger.error('Menu failure', exc_info=True) @commands.command(aliases=['img']) @checks.bot_has_permissions(embed_links=True) @@ -524,8 +528,8 @@ async def links(self, ctx, *, query: str): """Monster links""" m, err, debug_info = await self.findMonster(query) if m is not None: - menu = IdMenu() - embed = menu.make_links_embed(m) + menu = IdMenu(ctx) + embed = await menu.make_links_embed(m) await ctx.send(embed=embed) else: @@ -547,8 +551,8 @@ async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" m, err, debug_info = await self.findMonster(query) if m is not None: - menu = IdMenu(allowed_emojis=self.get_emojis()) - embed = menu.make_header_embed(m) + menu = IdMenu(ctx, allowed_emojis=self.get_emojis()) + embed = await menu.make_header_embed(m) await ctx.send(embed=embed) else: await ctx.send(self.makeFailureMsg(err)) @@ -649,11 +653,11 @@ async def leaderskill(self, ctx, *, whole_query): await ctx.send(inline(err_msg.format('Right', right_query))) return - menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = menu.make_ls_embed(left_m, right_m) - emoji_to_embed[self.left_emoji] = menu.make_embed(left_m) - emoji_to_embed[self.right_emoji] = menu.make_embed(right_m) + emoji_to_embed[self.ls_emoji] = await menu.make_ls_embed(left_m, right_m) + emoji_to_embed[self.left_emoji] = await menu.make_embed(left_m) + emoji_to_embed[self.right_emoji] = await menu.make_embed(right_m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -667,10 +671,10 @@ async def leaderskillsingle(self, ctx, *, query): if err: await ctx.send(err) return - menu = IdMenu(db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = menu.make_lssingle_embed(m) - emoji_to_embed[self.left_emoji] = menu.make_embed(m) + emoji_to_embed[self.ls_emoji] = await menu.make_lssingle_embed(m) + emoji_to_embed[self.left_emoji] = await menu.make_embed(m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -753,6 +757,18 @@ async def survey(self, ctx, value): else: await ctx.send("value must be `always`, `sometimes`, or `never`") + @is_donor() + @idmode.command() + async def setembedcolor(self, ctx, *, color: discord.Color): + """(DONOR ONLY) Change the color of all your ID embeds! + + Examples: + [p]idmode setembedcolor green + [p]idmode setembedcolor #a10000 + """ + await self.config.user(ctx.author).color.set(color.value) + await ctx.tick() + @commands.group() @checks.is_owner() async def padinfo(self, ctx): From 3e4ad01885085ece5ebeb5f33fcf94e52917931f Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 22 Dec 2020 20:15:46 -0800 Subject: [PATCH 0330/1220] Rename idmode to idset & add random color (#312) * Add PadInfo Colored Embeds! * IDMode Color 2 * Add a comment * Remove set from setembedcolor --- padinfo/id_menu.py | 3 ++ padinfo/padinfo.py | 69 +++++++++++++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index b6795d2b..eb4cca70 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -1,3 +1,4 @@ +import random import urllib.parse from typing import TYPE_CHECKING @@ -516,6 +517,8 @@ async def make_custom_embed(self): color = await pdicog.config.user(self.ctx.author).color() if color is None: return discord.Embed() + elif color == "random": + return discord.Embed(color=random.randint(0x000000, 0xffffff)) else: return discord.Embed(color=discord.Color(color)) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 75f0d7f6..50c53f29 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,26 +1,24 @@ import asyncio -import discord import json import logging import os -import prettytable import random -import traceback -import tsutils +import re import urllib.parse -from io import BytesIO from collections import OrderedDict from enum import Enum +from io import BytesIO +from typing import TYPE_CHECKING + +import discord +import tsutils from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor -from .find_monster import prefix_to_filter from .id_menu import IdMenu -from typing import TYPE_CHECKING - if TYPE_CHECKING: from dadguide.database_context import DbContext from dadguide.models.monster_model import MonsterModel @@ -61,6 +59,19 @@ def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) +COLORS = { + **{c: getattr(discord.Colour, c)().value for c in discord.Colour.__dict__ if + isinstance(discord.Colour.__dict__[c], classmethod) and + discord.Colour.__dict__[c].__func__.__code__.co_argcount == 1 and + isinstance(getattr(discord.Colour, c)(), discord.Colour)}, + 'pink': 0xffa1dd, + + # Special + 'random': 'random', + 'clear': 0, +} + + class IdEmojiUpdater(EmojiUpdater): def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, selected_emoji=None, bot=None, @@ -110,7 +121,7 @@ async def on_update(self, ctx, selected_emoji): return True self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, - m=self.m, scroll=sorted( + m=self.m, scroll=sorted( {*self.db_context.graph.get_alt_cards(self.m.monster_id)}) if evoID else [], menu_type=1) return True @@ -302,7 +313,7 @@ async def send_survey_after(self, ctx, query, result): elif userres is False: await self.config.bad.set(await self.config.bad() + 1) m = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" - f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idmode" + f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idset" f" survey` to adjust how often this shows.\n\n<{url}>") await asyncio.sleep(15) await m.delete() @@ -405,7 +416,8 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_id) emoji_to_embed = await self.get_id_emoji_options(ctx, - m=m, scroll=sorted({*alt_versions}, key=lambda x: x.monster_id) if self.settings.checkEvoID( + m=m, scroll=sorted({*alt_versions}, key=lambda + x: x.monster_id) if self.settings.checkEvoID( ctx.author.id) else [], menu_type=1) return await self._do_menu( @@ -720,16 +732,16 @@ async def padsay(self, ctx, server, *, query: str = None): else: await ctx.send(self.makeFailureMsg(err)) - @commands.group() - async def idmode(self, ctx): + @commands.group(aliases=['idmode']) + async def idset(self, ctx): """id settings configuration""" - @idmode.command() + @idset.command() async def scroll(self, ctx, value): """Switch between number scroll and evo scroll - [p]idmode scroll number - [p]idmode scroll evo""" + [p]idset scroll number + [p]idset scroll evo""" if value in ['evo', 'default']: if self.settings.setEvoID(ctx.author.id): await ctx.tick() @@ -743,13 +755,13 @@ async def scroll(self, ctx, value): else: await ctx.send("id_type must be `number` or `evo`") - @idmode.command() + @idset.command() async def survey(self, ctx, value): """Change how often you see the id survey - [p]idmode survey always (Always see survey after using id) - [p]idmode survey sometimes (See survey some of the time after using id) - [p]idmode survey never (Never see survey after using id D:)""" + [p]idset survey always (Always see survey after using id) + [p]idset survey sometimes (See survey some of the time after using id) + [p]idset survey never (Never see survey after using id D:)""" vals = ['always', 'sometimes', 'never'] if value in vals: await self.config.user(ctx.author).survey_mode.set(vals.index(value)) @@ -758,15 +770,22 @@ async def survey(self, ctx, value): await ctx.send("value must be `always`, `sometimes`, or `never`") @is_donor() - @idmode.command() - async def setembedcolor(self, ctx, *, color: discord.Color): + @idset.command() + async def embedcolor(self, ctx, *, color): """(DONOR ONLY) Change the color of all your ID embeds! Examples: - [p]idmode setembedcolor green - [p]idmode setembedcolor #a10000 + [p]idset embedcolor green + [p]idset embedcolor #a10000 + [p]idset embedcolor random """ - await self.config.user(ctx.author).color.set(color.value) + if color in COLORS: + await self.config.user(ctx.author).color.set(COLORS[color]) + elif re.match(r"^#?[0-9a-fA-F]{6}$", color): + await self.config.user(ctx.author).color.set(int(color.lstrip("#"), 16)) + else: + await ctx.send("Invalid color! Valid colors are any hexcode and:\n"+", ".join(COLORS)) + return await ctx.tick() @commands.group() From 555b0930c02527c10c8cdc010ca530d49aad25fd Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 22 Dec 2020 20:39:57 -0800 Subject: [PATCH 0331/1220] embedcolor documentation (#314) * embedcolor documentation * Use corrrect docstring --- padinfo/padinfo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 50c53f29..2acf4cf0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -778,6 +778,8 @@ async def embedcolor(self, ctx, *, color): [p]idset embedcolor green [p]idset embedcolor #a10000 [p]idset embedcolor random + + Picking random will choose a random hex code every time you use [p]id! """ if color in COLORS: await self.config.user(ctx.author).color.set(COLORS[color]) From 600e554641e5585edddb88f26133634c29a291a2 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Dec 2020 01:25:25 -0800 Subject: [PATCH 0332/1220] Disable Chrono Ma:Gia cog (#316) F --- chronomagia/info.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chronomagia/info.json b/chronomagia/info.json index 96d0182e..2160d7b0 100644 --- a/chronomagia/info.json +++ b/chronomagia/info.json @@ -2,7 +2,7 @@ "author": [ "The Tsubaki Bot Team" ], - "description": "Displays data about Chrono Ma:Gia.", + "description": "Displays data about the now defunct GungHo game, Chrono Ma:Gia.", "end_user_data_statement": "No personal data is stored.", "install_msg": "Use [p]cmid to get started.", "short": "Shows Chrono Ma:Gia data", @@ -13,7 +13,7 @@ 8, 1 ], - "hidden": false, + "hidden": true, "disabled": false, "required_cogs": {}, "requirements": [ From 320bb63013309c19267800b3a30e7403c8b29a38 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Dec 2020 01:57:11 -0800 Subject: [PATCH 0333/1220] Remove Padvision --- padboard/info.json | 9 +++++---- padboard/padboard.py | 11 +++-------- {padvision => padboard}/padvision.py | 29 ++++------------------------ padvision/__init__.py | 7 ------- padvision/info.json | 26 ------------------------- 5 files changed, 12 insertions(+), 70 deletions(-) rename {padvision => padboard}/padvision.py (84%) delete mode 100644 padvision/__init__.py delete mode 100644 padvision/info.json diff --git a/padboard/info.json b/padboard/info.json index 9ee3f780..3c40153a 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -15,11 +15,12 @@ ], "hidden": false, "disabled": false, - "required_cogs": { - "padvision": "https://github.com/TsubakiBotPad/padbot-cogs" - }, + "required_cogs": {}, "requirements": [ - "tsutils" + "tsutils", + "Pillow", + "tensorflow", + "numpy" ], "tags": [ "PAD" diff --git a/padboard/padboard.py b/padboard/padboard.py index 7341dca7..dc79094a 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -2,7 +2,7 @@ import cv2 import discord import numpy as np -import os +from .padvision import NeuralClassifierBoardExtractor from io import BytesIO from collections import defaultdict from collections import deque @@ -10,14 +10,13 @@ from redbot.core.utils.chat_formatting import inline from tsutils import extract_image_url -DATA_DIR = os.path.join('data', 'padboard') - DAWNGLARE_BOARD_TEMPLATE = "https://pad.dawnglare.com/?patt={}" CNINJA_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" class PadBoard(commands.Cog): """Dawnglare Utilities""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -128,9 +127,5 @@ async def nc_classify(self, image_data): model_path = await self.config.tflite_path() if not model_path: return None - PDV_COG = self.bot.get_cog("PadVision") - if not PDV_COG: - raise IOError("PadVision is not loaded") - PDV_MODULE = __import__(PDV_COG.__module__) - img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, img_np, image_data) + img_extractor = NeuralClassifierBoardExtractor(model_path, img_np, image_data) return img_extractor.get_board() diff --git a/padvision/padvision.py b/padboard/padvision.py similarity index 84% rename from padvision/padvision.py rename to padboard/padvision.py index d8d3b35b..e591a221 100644 --- a/padvision/padvision.py +++ b/padboard/padvision.py @@ -1,32 +1,11 @@ -import PIL.Image import logging -import traceback from io import BytesIO -from redbot.core import commands - -ORB_IMG_SIZE = 40 - -logger = logging.getLogger('red.padbot-cogs.padvision') - -class PadVision(commands.Cog): - """Empty cog for PadVision help""" - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. +import PIL.Image - No personal data is stored in this cog. - """ - return +ORB_IMG_SIZE = 40 +logger = logging.getLogger('red.padbot-cogs.padboard.padvision') EXTRACTABLE = 'rbgldhjpmo' @@ -131,7 +110,7 @@ def process(self): try: self._process() except Exception as ex: - logger.error("orb extractor failed ", exc_info=1) + logger.error("orb extractor failed ", exc_info=True) def _process(self): import numpy as np diff --git a/padvision/__init__.py b/padvision/__init__.py deleted file mode 100644 index 0a59bc47..00000000 --- a/padvision/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .padvision import PadVision, NeuralClassifierBoardExtractor - -__red_end_user_data_statement__ = "No personal data is stored." - - -def setup(bot): - bot.add_cog(PadVision(bot)) diff --git a/padvision/info.json b/padvision/info.json deleted file mode 100644 index d161fb62..00000000 --- a/padvision/info.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "author": [ - "The Tsubaki Bot Team" - ], - "description": "A beefy helper cog for neural networks.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Make sure to use padboard with this cog!", - "short": "Neural net helper", - "min_bot_version": "3.4.0", - "max_bot_version": "0.0.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "required_cogs": {}, - "requirements": [ - "Pillow" - ], - "tags": [ - "PAD", - "sharedlibrary" - ] -} From ce10ad11deb435d368fa08553b8687076f418c84 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Dec 2020 02:02:59 -0800 Subject: [PATCH 0334/1220] Merge padvision cog files into padboard (#317) --- padboard/info.json | 9 +++++---- padboard/padboard.py | 11 +++-------- {padvision => padboard}/padvision.py | 29 ++++------------------------ padvision/__init__.py | 7 ------- padvision/info.json | 26 ------------------------- 5 files changed, 12 insertions(+), 70 deletions(-) rename {padvision => padboard}/padvision.py (84%) delete mode 100644 padvision/__init__.py delete mode 100644 padvision/info.json diff --git a/padboard/info.json b/padboard/info.json index 9ee3f780..3c40153a 100644 --- a/padboard/info.json +++ b/padboard/info.json @@ -15,11 +15,12 @@ ], "hidden": false, "disabled": false, - "required_cogs": { - "padvision": "https://github.com/TsubakiBotPad/padbot-cogs" - }, + "required_cogs": {}, "requirements": [ - "tsutils" + "tsutils", + "Pillow", + "tensorflow", + "numpy" ], "tags": [ "PAD" diff --git a/padboard/padboard.py b/padboard/padboard.py index 7341dca7..dc79094a 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -2,7 +2,7 @@ import cv2 import discord import numpy as np -import os +from .padvision import NeuralClassifierBoardExtractor from io import BytesIO from collections import defaultdict from collections import deque @@ -10,14 +10,13 @@ from redbot.core.utils.chat_formatting import inline from tsutils import extract_image_url -DATA_DIR = os.path.join('data', 'padboard') - DAWNGLARE_BOARD_TEMPLATE = "https://pad.dawnglare.com/?patt={}" CNINJA_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" class PadBoard(commands.Cog): """Dawnglare Utilities""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -128,9 +127,5 @@ async def nc_classify(self, image_data): model_path = await self.config.tflite_path() if not model_path: return None - PDV_COG = self.bot.get_cog("PadVision") - if not PDV_COG: - raise IOError("PadVision is not loaded") - PDV_MODULE = __import__(PDV_COG.__module__) - img_extractor = PDV_MODULE.NeuralClassifierBoardExtractor(model_path, img_np, image_data) + img_extractor = NeuralClassifierBoardExtractor(model_path, img_np, image_data) return img_extractor.get_board() diff --git a/padvision/padvision.py b/padboard/padvision.py similarity index 84% rename from padvision/padvision.py rename to padboard/padvision.py index d8d3b35b..e591a221 100644 --- a/padvision/padvision.py +++ b/padboard/padvision.py @@ -1,32 +1,11 @@ -import PIL.Image import logging -import traceback from io import BytesIO -from redbot.core import commands - -ORB_IMG_SIZE = 40 - -logger = logging.getLogger('red.padbot-cogs.padvision') - -class PadVision(commands.Cog): - """Empty cog for PadVision help""" - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. +import PIL.Image - No personal data is stored in this cog. - """ - return +ORB_IMG_SIZE = 40 +logger = logging.getLogger('red.padbot-cogs.padboard.padvision') EXTRACTABLE = 'rbgldhjpmo' @@ -131,7 +110,7 @@ def process(self): try: self._process() except Exception as ex: - logger.error("orb extractor failed ", exc_info=1) + logger.error("orb extractor failed ", exc_info=True) def _process(self): import numpy as np diff --git a/padvision/__init__.py b/padvision/__init__.py deleted file mode 100644 index 0a59bc47..00000000 --- a/padvision/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .padvision import PadVision, NeuralClassifierBoardExtractor - -__red_end_user_data_statement__ = "No personal data is stored." - - -def setup(bot): - bot.add_cog(PadVision(bot)) diff --git a/padvision/info.json b/padvision/info.json deleted file mode 100644 index d161fb62..00000000 --- a/padvision/info.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "author": [ - "The Tsubaki Bot Team" - ], - "description": "A beefy helper cog for neural networks.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Make sure to use padboard with this cog!", - "short": "Neural net helper", - "min_bot_version": "3.4.0", - "max_bot_version": "0.0.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "required_cogs": {}, - "requirements": [ - "Pillow" - ], - "tags": [ - "PAD", - "sharedlibrary" - ] -} From 77e1c0b7097cb59ff3109ded9248d3c1979b1573 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 24 Dec 2020 04:04:16 -0800 Subject: [PATCH 0335/1220] [channelmirror] Return early from listener in ignored messages (#321) * Delay API call in channelmirror * Change Logic --- channelmirror/channelmirror.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index d4ef1844..26aaca92 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -343,18 +343,24 @@ async def mirror_msg(self, message): @commands.Cog.listener('on_raw_message_edit') async def mirror_msg_edit(self, payload): + if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + return message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) if 'content' in payload.data: await self.mirror_msg_mod(message, new_message_content=payload.data['content']) @commands.Cog.listener('on_raw_message_delete') async def mirror_msg_delete(self, payload): + if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + return if not await self.config.channel(self.bot.get_channel(payload.channel_id)).nodeletion(): fmessage = tsutils.DummyObject(id=payload.message_id, channel=self.bot.get_channel(payload.channel_id)) await self.mirror_msg_mod(fmessage, delete_message_content=True) @commands.Cog.listener('on_raw_reaction_add') async def mirror_reaction_add(self, payload): + if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + return message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): return @@ -362,6 +368,8 @@ async def mirror_reaction_add(self, payload): @commands.Cog.listener('on_raw_reaction_remove') async def mirror_reaction_remove(self, payload): + if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + return message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): return From f97fba29c6ff1df36e010538eccac8dc146b4397 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 24 Dec 2020 17:44:25 -0800 Subject: [PATCH 0336/1220] Refactor PadEvents (#322) This also fixes testevent (Resolves #249) --- padevents/padevents.py | 509 ++++++++++++++++++++--------------------- 1 file changed, 249 insertions(+), 260 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index c5117a72..4da8635b 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,24 +1,23 @@ import asyncio import datetime -import discord -import logging import itertools -import prettytable -import pytz +import logging import re -import traceback -from io import BytesIO from collections import defaultdict from datetime import timedelta from enum import Enum +from io import BytesIO +from typing import TYPE_CHECKING + +import discord +import prettytable +import pytz from redbot.core import checks from redbot.core import commands, Config -from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import CogSettings, confirm_message, normalize_server_name +from redbot.core.utils.chat_formatting import box, pagify +from tsutils import CogSettings, confirm_message, normalize_server_name, DummyObject, is_donor -from typing import TYPE_CHECKING if TYPE_CHECKING: - from dadguide.database_context import DbContext from dadguide.models.scheduled_event_model import ScheduledEventModel logger = logging.getLogger('red.padbot-cogs.padevents') @@ -29,6 +28,7 @@ class PadEvents(commands.Cog): """Pad Event Tracker""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -94,25 +94,26 @@ async def check_started(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadEvents'): try: - events = filter(lambda e: not e.key in self.started_events, self.events) - for e in events: + events = filter(lambda e: e.key not in self.started_events, self.events) + for event in events: for gid, data in (await self.config.all_guilds()).items(): guild = self.bot.get_guild(gid) - if guild is None: continue + if guild is None: + continue for key, aep in data.get('pingroles', {}).items(): - if e.start_from_now_sec() > aep['offset'] * 60 \ - or not aep['enabled'] \ - or e.server != aep['server'] \ - or (key, e.key) in self.rolepinged_events: + if event.start_from_now_sec() > aep['offset'] * 60 \ + or not aep['enabled'] \ + or event.server != aep['server'] \ + or (key, event.key) in self.rolepinged_events: continue elif aep['regex']: - matches = re.search(aep['searchstr'], e.clean_dungeon_name) + matches = re.search(aep['searchstr'], event.clean_dungeon_name) else: - matches = aep['searchstr'] in e.clean_dungeon_name + matches = aep['searchstr'] in event.clean_dungeon_name - self.rolepinged_events.add((key, e.key)) + self.rolepinged_events.add((key, event.key)) if matches: - index = GROUPS.index(e.group) + index = GROUPS.index(event.group) channel = guild.get_channel(aep['channels'][index]) if channel is not None: role = guild.get_role(aep['roles'][index]) @@ -121,64 +122,70 @@ async def check_started(self): if aep['offset']: offsetstr = " in {} minute(s)".format(aep['offset']) try: - await channel.send("{}{} {}".format(e.name_and_modifier, offsetstr, ment), allowed_mentions=discord.AllowedMentions(roles=True)) + await channel.send("{}{} {}".format(event.name_and_modifier, offsetstr, ment), + allowed_mentions=discord.AllowedMentions(roles=True)) except Exception: logger.exception("Failed to send AEP in channel {}".format(channel.id)) for uid, data in (await self.config.all_users()).items(): user = self.bot.get_user(uid) - if user is None: continue + if user is None: + continue for aed in data.get('dmevents', []): - if e.start_from_now_sec() > aed['offset'] * 60 \ - or e.group != aed['group'] \ - or e.server != aed['server'] \ - or (aed['key'], e.key) in self.rolepinged_events: + if event.start_from_now_sec() > aed['offset'] * 60 \ + or event.group != aed['group'] \ + or event.server != aed['server'] \ + or (aed['key'], event.key) in self.rolepinged_events: continue - self.rolepinged_events.add((aed['key'], e.key)) - if aed['searchstr'] in e.clean_dungeon_name: + self.rolepinged_events.add((aed['key'], event.key)) + if aed['searchstr'] in event.clean_dungeon_name: offsetstr = " starts now!" if aed['offset']: offsetstr = " starts in {} minute(s)!".format(aed['offset']) try: - await user.send(e.clean_dungeon_name + offsetstr) + await user.send(event.clean_dungeon_name + offsetstr) except Exception: logger.exception("Failed to send AED to user {}".format(user.id)) - events = filter(lambda e: e.is_started() and not e.key in self.started_events, self.events) + events = filter(lambda e: e.is_started() and e.key not in self.started_events, self.events) daily_refresh_servers = set() - for e in events: - self.started_events.add(e.key) - if e.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, EventType.Week]: - for gr in list(self.settings.listGuerrillaReg()): - if e.server == gr['server']: + for event in events: + self.started_events.add(event.key) + if event.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, + EventType.Week]: + for gr in list(self.settings.list_guerrilla_reg()): + if event.server == gr['server']: try: channel = self.bot.get_channel(int(gr['channel_id'])) if channel is None: continue - - role_name = '{}_group_{}'.format(e.server, e.groupLongName()) + + role_name = '{}_group_{}'.format(event.server, event.group_long_name()) role = channel.guild.get_role(role_name) if role and role.mentionable: - message = "{} `: {} is starting`".format(role.mention, e.name_and_modifier) + message = "{} `: {} is starting`".format(role.mention, event.name_and_modifier) else: - message = box("Server " + e.server + ", group " + e.groupLongName() + " : " + e.name_and_modifier) + message = box( + "Server " + event.server + ", group " + event.group_long_name() + + " : " + event.name_and_modifier + ) await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) except Exception as ex: - # self.settings.removeGuerrillaReg(gr['channel_id'], gr['server']) + # self.settings.remove_guerrilla_reg(gr['channel_id'], gr['server']) logger.exception("caught exception while sending guerrilla msg:") else: - if not e.dungeon_type in [DungeonType.Normal]: - msg = self.makeActiveText(e.server) - for daily_registration in list(self.settings.listDailyReg()): + if event.dungeon_type not in [DungeonType.Normal]: + msg = self.make_active_text(event.server) + for daily_registration in list(self.settings.list_daily_reg()): try: - if e.server == daily_registration['server']: - await self.pageOutput(self.bot.get_channel(daily_registration['channel_id']), - msg, channel_id=daily_registration['channel_id']) + if event.server == daily_registration['server']: + await self.page_output(self.bot.get_channel(daily_registration['channel_id']), + msg, channel_id=daily_registration['channel_id']) logger.info("daily_reg server") except Exception as ex: - # self.settings.removeDailyReg( + # self.settings.remove_daily_reg( # daily_registration['channel_id'], daily_registration['server']) logger.exception("caught exception while sending daily msg:") @@ -203,29 +210,27 @@ async def testevent(self, ctx, server, seconds: int = 0): dg_cog = self.bot.get_cog('Dadguide') await dg_cog.wait_until_ready() - dg_module = __import__(dg_cog.__module__) - - te = dg_module.ScheduledEventModule({'dungeon_id': 1}) - - te.server = server - te.server_id = SUPPORTED_SERVERS.index(server) - te.event_type_id = EventType.Guerrilla.value - te.event_seq = 0 - fuid = self.fake_uid = self.fake_uid - 1 - te.key = lambda: fuid - te.group_name = 'red' - - te.open_datetime = datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds) - te.close_datetime = te.open_datetime + timedelta(minutes=1) - - class Dungeon: pass - d = Dungeon() - d.name_en = 'fake_dungeon_name' - d.dungeon_type = DungeonType.Unknown7 - te.event_modifier = 'fake_event_modifier' + # TODO: Don't use this awful importing hack + dg_module = __import__('.'.join(dg_cog.__module__.split('.')[:-1]) + ".models.scheduled_event_model") + timestamp = int((datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds)).timestamp()) + self.fake_uid -= 1 + + te = dg_module.models.scheduled_event_model.ScheduledEventModel( + event_id=self.fake_uid, + server_id=SUPPORTED_SERVERS.index(server), + event_type_id=EventType.Guerrilla.value, + start_timestamp=timestamp, + end_timestamp=timestamp + 60, + group_name='red', + dungeon_model=DummyObject( + name_en='fake_dungeon_name', + clean_name_en='fake_dungeon_name', + dungeon_type=DungeonType.Unknown7, + dungeon_id=1, + ) + ) self.events.append(Event(te, self.bot.get_cog('Dadguide').database)) - - await ctx.send("Fake event injected.") + await ctx.tick() @padevents.command() @commands.guild_only() @@ -237,11 +242,11 @@ async def addchannel(self, ctx, server): return channel_id = ctx.channel.id - if self.settings.checkGuerrillaReg(channel_id, server): + if self.settings.check_guerrilla_reg(channel_id, server): await ctx.send("Channel already active.") return - self.settings.addGuerrillaReg(channel_id, server) + self.settings.add_guerrilla_reg(channel_id, server) await ctx.send("Channel now active.") @padevents.command() @@ -254,11 +259,11 @@ async def rmchannel(self, ctx, server): return channel_id = ctx.channel.id - if not self.settings.checkGuerrillaReg(channel_id, server): + if not self.settings.check_guerrilla_reg(channel_id, server): await ctx.send("Channel is not active.") return - self.settings.removeGuerrillaReg(channel_id, server) + self.settings.remove_guerrilla_reg(channel_id, server) await ctx.send("Channel deactivated.") @padevents.command() @@ -271,11 +276,11 @@ async def addchanneldaily(self, ctx, server): return channel_id = ctx.channel.id - if self.settings.checkDailyReg(channel_id, server): + if self.settings.check_daily_reg(channel_id, server): await ctx.send("Channel already active.") return - self.settings.addDailyReg(channel_id, server) + self.settings.add_daily_reg(channel_id, server) await ctx.send("Channel now active.") @padevents.command() @@ -288,11 +293,11 @@ async def rmchanneldaily(self, ctx, server): return channel_id = ctx.channel.id - if not self.settings.checkDailyReg(channel_id, server): + if not self.settings.check_daily_reg(channel_id, server): await ctx.send("Channel is not active.") return - self.settings.removeDailyReg(channel_id, server) + self.settings.remove_daily_reg(channel_id, server) await ctx.send("Channel deactivated.") @commands.group(aliases=['aep']) @@ -302,7 +307,8 @@ async def autoeventping(self, ctx): """Auto Event Pings""" @autoeventping.command(name="add") - async def aep_add(self, ctx, key, server = None, searchstr = None, red: discord.Role = None, blue: discord.Role = None, green: discord.Role = None): + async def aep_add(self, ctx, key, server=None, searchstr=None, red: discord.Role = None, blue: discord.Role = None, + green: discord.Role = None): """Add a new autoeventping""" if green is None and server is not None: await ctx.send("Multi-word keys must be in quotes.") @@ -353,24 +359,24 @@ async def aep_show(self, ctx, *, key): await ctx.send("That key does not exist.") return roles = [ctx.guild.get_role(role).mention - if ctx.guild.get_role(role) is not None - else "No Role" - for role - in pingroles[key]['roles'] + if ctx.guild.get_role(role) is not None + else "No Role" + for role + in pingroles[key]['roles'] ] chans = [ctx.bot.get_channel(c).mention - if ctx.bot.get_channel(c) is not None - else "No Channel" - for c - in pingroles[key]['channels'] + if ctx.bot.get_channel(c) is not None + else "No Channel" + for c + in pingroles[key]['channels'] ] pr = (f"{key} ({'enabled' if pingroles[key]['enabled'] else 'disabled'})\n" - f" Search String: '{pingroles[key]['searchstr']}' {'(regex search)' if pingroles[key]['regex'] else ''}\n" - f" Server: {pingroles[key]['server']}\n" - f" Red: {roles[0]} (In {chans[0]})\n" - f" Blue: {roles[1]} (In {chans[1]})\n" - f" Green: {roles[2]} (In {chans[2]})\n" - f" Offset: {pingroles[key]['offset']} minutes") + f"\tSearch String: '{pingroles[key]['searchstr']}' {'(regex search)' * pingroles[key]['regex']}\n" + f"\tServer: {pingroles[key]['server']}\n" + f"\tRed: {roles[0]} (In {chans[0]})\n" + f"\tBlue: {roles[1]} (In {chans[1]})\n" + f"\tGreen: {roles[2]} (In {chans[2]})\n" + f"\tOffset: {pingroles[key]['offset']} minutes") await ctx.send(pr) @autoeventping.command(name="list") @@ -406,7 +412,7 @@ async def aepg(self, ctx, key): @aep_set.command(name="channel") async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping for all groups""" - await self.aeps(ctx, key, 'channels', [channel.id]*3) + await self.aeps(ctx, key, 'channels', [channel.id] * 3) await ctx.tick() @aep_set.command(name="redchannel") @@ -510,6 +516,7 @@ async def aep_s_disabled(self, ctx, key, offset: int): async def autoeventdm(self, ctx): """Auto Event DMs""" + @is_donor() @autoeventdm.command(name="add") async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): """Add a new autoeventdm""" @@ -524,16 +531,6 @@ async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): if offset < 0: await ctx.send("Offset cannot be negative") return - elif offset > 0: - DONOR_COG = self.bot.get_cog("Donations") - if DONOR_COG is None: - await ctx.send(inline("Donor Cog not loaded. Please contact a bot admin.")) - elif not DONOR_COG.is_donor(ctx): - await ctx.send(("AED offset is a donor only feature due to server loads." - " Your auto event DM will be created, but the offset will not" - " be in affect until you're a donor. You can donate any time" - " at https://www.patreon.com/tsubaki_bot. Use `{}donate` to" - " view this link at any time").format(ctx.prefix)) default = { 'key': datetime.datetime.now().timestamp(), @@ -555,9 +552,9 @@ async def aed_remove(self, ctx, index: int): await ctx.send("That isn't a valid index.") return if not await confirm_message(ctx, ("Are you sure you want to delete autoeventdm with searchstring '{}'" - "").format(dmevents[index-1]['searchstr'])): + "").format(dmevents[index - 1]['searchstr'])): return - dmevents.pop(index-1) + dmevents.pop(index - 1) await ctx.tick() @autoeventdm.command(name="list") @@ -585,27 +582,18 @@ async def aed_purge(self, ctx): async def aed_e(self, ctx): """Edit a property of the autoeventdm""" + @is_donor() @aed_e.command(name="offset") async def aed_e_offset(self, ctx, index, offset): """Set offset of an autoeventdm (Donor only)""" if offset < 0: await ctx.send("Offset cannot be negative") return - elif offset > 0: - DONOR_COG = self.bot.get_cog("Donations") - if DONOR_COG is None: - await ctx.send(inline("Donor Cog not loaded. Please contact a bot admin.")) - elif not DONOR_COG.is_donor(ctx): - await ctx.send(("AED offset is a donor only feature due to server loads." - " Your auto event DM will be created, but the offset will not" - " be in affect until you're a donor. You can donate any time" - " at https://www.patreon.com/tsubaki_bot. Use `{}donate` to" - " view this link at any time").format(ctx.prefix)) async with self.config.user(ctx.author).dmevents() as dmevents: if not 0 < index <= len(dmevents): await ctx.send("That isn't a valid index.") return - dmevents[index-1]['offset'] = offset + dmevents[index - 1]['offset'] = offset await ctx.tick() @aed_e.command(name="searchstr") @@ -615,32 +603,33 @@ async def aed_e_searchstr(self, ctx, index, *, searchstr): if not 0 < index <= len(dmevents): await ctx.send("That isn't a valid index.") return - dmevents[index-1]['searchstr'] = searchstr + dmevents[index - 1]['searchstr'] = searchstr await ctx.tick() - @padevents.command(name="listallchannels") + @padevents.command() @checks.is_owner() - async def _listallchannel(self, ctx): + async def listallchannels(self, ctx): msg = 'Following daily channels are registered:\n' - msg += self.makeChannelList(self.settings.listDailyReg()) + msg += self.make_channel_list(self.settings.list_daily_reg()) msg += "\n" msg += 'Following guerilla channels are registered:\n' - msg += self.makeChannelList(self.settings.listGuerrillaReg()) - await self.pageOutput(ctx, msg) + msg += self.make_channel_list(self.settings.list_guerrilla_reg()) + await self.page_output(ctx, msg) - @padevents.command(name="listchannels") + @padevents.command() @checks.mod_or_permissions(manage_guild=True) - async def _listchannel(self, ctx): + async def listchannels(self, ctx): msg = 'Following daily channels are registered:\n' - msg += self.makeChannelList(self.settings.listDailyReg(), lambda c: c in ctx.guild.channels) + msg += self.make_channel_list(self.settings.list_daily_reg(), lambda c: c in ctx.guild.channels) msg += "\n" msg += 'Following guerilla channels are registered:\n' - msg += self.makeChannelList(self.settings.listGuerrillaReg(), lambda c: c in ctx.guild.channels) - await self.pageOutput(ctx, msg) + msg += self.make_channel_list(self.settings.list_guerrilla_reg(), lambda c: c in ctx.guild.channels) + await self.page_output(ctx, msg) - def makeChannelList(self, reg_list, filt=None): + def make_channel_list(self, reg_list, filt=None): if filt is None: - filt = lambda x: x + def filt(x): + return x msg = "" for cr in reg_list: reg_channel_id = cr['channel_id'] @@ -659,86 +648,86 @@ async def active(self, ctx, server): await ctx.send("Unsupported server, pick one of NA, KR, JP") return - msg = self.makeActiveText(server) - await self.pageOutput(ctx, msg) + msg = self.make_active_text(server) + await self.page_output(ctx, msg) - def makeActiveText(self, server): - server_events = EventList(self.events).withServer(server) - active_events = server_events.activeOnly() - pending_events = server_events.pendingOnly() - available_events = server_events.availableOnly() + def make_active_text(self, server): + server_events = EventList(self.events).with_server(server) + active_events = server_events.active_only() + pending_events = server_events.pending_only() + available_events = server_events.available_only() msg = "Listing all events for " + server """ - special_events = active_events.withType( - EventType.Special).itemsByCloseTime() + special_events = active_events.with_type( + EventType.Special).items_by_close_time() if len(special_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Special Events', special_events) + msg += "\n\n" + self.make_active_output('Special Events', special_events) - all_etc_events = active_events.withType(EventType.Etc) + all_etc_events = active_events.with_type(EventType.Etc) - etc_events = all_etc_events.withDungeonType( - DungeonType.Etc).excludeUnwantedEvents().itemsByCloseTime() + etc_events = all_etc_events.with_dungeon_type( + DungeonType.Etc).exclude_unwanted_events().items_by_close_time() if len(etc_events) > 0: - msg += "\n\n" + self.makeActiveOutput('Etc Events', etc_events) + msg += "\n\n" + self.make_active_output('Etc Events', etc_events) # Old-style guerrillas - active_guerrilla_events = active_events.withType(EventType.Guerrilla).items() + active_guerrilla_events = active_events.with_type(EventType.Guerrilla).items() if len(active_guerrilla_events) > 0: msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + self.make_active_guerrilla_output('Active Guerrillas', active_guerrilla_events) - guerrilla_events = pending_events.withType(EventType.Guerrilla).items() + guerrilla_events = pending_events.with_type(EventType.Guerrilla).items() if len(guerrilla_events) > 0: - msg += "\n\n" + self.makeFullGuerrillaOutput('Guerrilla Events', guerrilla_events) + msg += "\n\n" + self.make_full_guerrilla_output('Guerrilla Events', guerrilla_events) # New-style guerrillas - active_guerrilla_events = active_events.withType(EventType.SpecialWeek).items() + active_guerrilla_events = active_events.with_type(EventType.SpecialWeek).items() if len(active_guerrilla_events) > 0: msg += "\n\n" + \ - self.makeActiveGuerrillaOutput('Active Guerrillas', active_guerrilla_events) + self.make_active_guerrilla_output('Active Guerrillas', active_guerrilla_events) - guerrilla_events = pending_events.withType(EventType.SpecialWeek).items() + guerrilla_events = pending_events.with_type(EventType.SpecialWeek).items() if len(guerrilla_events) > 0: msg += "\n\n" + \ - self.makeFullGuerrillaOutput( + self.make_full_guerrilla_output( 'Guerrilla Events', guerrilla_events, starter_guerilla=True) """ - active_cdo_events = active_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped(True).items() + active_cdo_events = active_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped(True).items() if len(active_cdo_events) > 0: msg += "\n\n" + \ - self.makeActiveOutput( + self.make_active_output( 'Active Events', active_cdo_events) """ - cdo_events = pending_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped(False).items() + cdo_events = pending_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped(False).items() if len(cdo_events) > 0: msg += "\n\n" + \ - self.makeActiveOutput( + self.make_active_output( 'Reward Events', cdo_events) """ - active_grouped_cdo_events = active_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped().items() + active_grouped_cdo_events = active_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped().items() if len(active_grouped_cdo_events) > 0: msg += "\n\n" + \ - self.makeActiveGuerrillaOutput( + self.make_active_guerrilla_output( 'Active Guerrillas', active_grouped_cdo_events) - grouped_cdo_events = pending_events.withDungeonType(DungeonType.CoinDailyOther).isGrouped().items() + grouped_cdo_events = pending_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped().items() if len(grouped_cdo_events) > 0: msg += "\n\n" + \ - self.makeFullGuerrillaOutput( + self.make_full_guerrilla_output( 'Guerrillas', grouped_cdo_events, starter_guerilla=True) - active_etc_events = active_events.withDungeonType(DungeonType.Etc).items() + active_etc_events = active_events.with_dungeon_type(DungeonType.Etc).items() if len(active_etc_events) > 0: msg += "\n\n" + \ - self.makeActiveOutput( + self.make_active_output( 'Active Other Events', active_etc_events) """ - etc_events = pending_events.withDungeonType(DungeonType.Etc).items() + etc_events = pending_events.with_dungeon_type(DungeonType.Etc).items() if len(etc_events) > 0: msg += "\n\n" + \ - self.makeActiveOutput( + self.make_active_output( 'Other Events', etc_events) """ # clean up long headers @@ -746,7 +735,7 @@ def makeActiveText(self, server): return msg - async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): + async def page_output(self, ctx, msg, channel_id=None, format_type=box): msg = msg.strip() msg = pagify(msg, ["\n"], shorten_by=20) for page in msg: @@ -758,38 +747,38 @@ async def pageOutput(self, ctx, msg, channel_id=None, format_type=box): except Exception as e: logger.exception("page output failed " + str(e), "tried to output: " + page) - def makeActiveOutput(self, table_name, event_list): + def make_active_output(self, table_name, event_list): tbl = prettytable.PrettyTable(["Time", table_name]) tbl.hrules = prettytable.HEADER tbl.vrules = prettytable.NONE tbl.align[table_name] = "l" tbl.align["Time"] = "r" for e in event_list: - tbl.add_row([e.endFromNowFullMin().strip(), e.name_and_modifier]) + tbl.add_row([e.end_from_now_full_min().strip(), e.name_and_modifier]) return tbl.get_string() - def makeActiveGuerrillaOutput(self, table_name, event_list): + def make_active_guerrilla_output(self, table_name, event_list): tbl = prettytable.PrettyTable([table_name, "Group", "Time"]) tbl.hrules = prettytable.HEADER tbl.vrules = prettytable.NONE tbl.align[table_name] = "l" tbl.align["Time"] = "r" for e in event_list: - tbl.add_row([e.name_and_modifier, e.group, e.endFromNowFullMin().strip()]) + tbl.add_row([e.name_and_modifier, e.group, e.end_from_now_full_min().strip()]) return tbl.get_string() - def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False): + def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=False): events_by_name = defaultdict(list) - for e in event_list: - events_by_name[e.name_and_modifier].append(e) + for event in event_list: + events_by_name[event.name_and_modifier].append(event) rows = list() grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] for name, events in events_by_name.items(): events = sorted(events, key=lambda e: e.open_datetime) events_by_group = defaultdict(list) - for e in events: - events_by_group[e.group.upper()].append(e) + for event in events: + events_by_group[event.group.upper()].append(event) done = False while not done: @@ -802,8 +791,8 @@ def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False row.append("") else: did_work = True - e = grp_list.pop(0) - row.append(e.toGuerrillaStr()) + event = grp_list.pop(0) + row.append(event.to_guerrilla_str()) if did_work: rows.append(row) else: @@ -824,37 +813,37 @@ def makeFullGuerrillaOutput(self, table_name, event_list, starter_guerilla=False @commands.command(aliases=['events']) async def eventsna(self, ctx): """Display upcoming daily events for NA.""" - await self.doPartial(ctx, 'NA') + await self.do_partial(ctx, 'NA') @commands.command() async def eventsjp(self, ctx): """Display upcoming daily events for JP.""" - await self.doPartial(ctx, 'JP') + await self.do_partial(ctx, 'JP') @commands.command() async def eventskr(self, ctx): """Display upcoming daily events for KR.""" - await self.doPartial(ctx, 'KR') + await self.do_partial(ctx, 'KR') - async def doPartial(self, ctx, server): + async def do_partial(self, ctx, server): server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return events = EventList(self.events) - events = events.withServer(server) - events = events.inDungeonType([DungeonType.Etc, DungeonType.CoinDailyOther]) - events = events.isGrouped() + events = events.with_server(server) + events = events.in_dungeon_type([DungeonType.Etc, DungeonType.CoinDailyOther]) + events = events.is_grouped() - active_events = events.activeOnly().itemsByOpenTime(reverse=True) - pending_events = events.pendingOnly().itemsByOpenTime(reverse=True) + active_events = events.active_only().items_by_open_time(reverse=True) + pending_events = events.pending_only().items_by_open_time(reverse=True) group_to_active_event = {e.group: e for e in active_events} group_to_pending_event = {e.group: e for e in pending_events} - #active_events = list(group_to_active_event.values()) - #pending_events = list(group_to_pending_event.values()) + # active_events = list(group_to_active_event.values()) + # pending_events = list(group_to_pending_event.values()) active_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) pending_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) @@ -868,18 +857,18 @@ async def doPartial(self, ctx, server): if len(active_events) > 0: output += "\n\n" + " Remaining Dungeon" for e in active_events: - output += "\n" + e.toPartialEvent(self) + output += "\n" + e.to_partial_event(self) if len(pending_events) > 0: output += "\n\n" + " PT ET ETA Dungeon" for e in pending_events: - output += "\n" + e.toPartialEvent(self) + output += "\n" + e.to_partial_event(self) for page in pagify(output): await ctx.send(box(page)) -def makeChannelReg(channel_id, server): +def make_channel_reg(channel_id, server): server = normalize_server_name(server) return { "channel_id": channel_id, @@ -895,34 +884,34 @@ def make_default_settings(self): } return config - def listGuerrillaReg(self): + def list_guerrilla_reg(self): return self.bot_settings['guerrilla_regs'] - def addGuerrillaReg(self, channel_id, server): - self.listGuerrillaReg().append(makeChannelReg(channel_id, server)) + def add_guerrilla_reg(self, channel_id, server): + self.list_guerrilla_reg().append(make_channel_reg(channel_id, server)) self.save_settings() - def checkGuerrillaReg(self, channel_id, server): - return makeChannelReg(channel_id, server) in self.listGuerrillaReg() + def check_guerrilla_reg(self, channel_id, server): + return make_channel_reg(channel_id, server) in self.list_guerrilla_reg() - def removeGuerrillaReg(self, channel_id, server): - if self.checkGuerrillaReg(channel_id, server): - self.listGuerrillaReg().remove(makeChannelReg(channel_id, server)) + def remove_guerrilla_reg(self, channel_id, server): + if self.check_guerrilla_reg(channel_id, server): + self.list_guerrilla_reg().remove(make_channel_reg(channel_id, server)) self.save_settings() - def listDailyReg(self): + def list_daily_reg(self): return self.bot_settings['daily_regs'] - def addDailyReg(self, channel_id, server): - self.listDailyReg().append(makeChannelReg(channel_id, server)) + def add_daily_reg(self, channel_id, server): + self.list_daily_reg().append(make_channel_reg(channel_id, server)) self.save_settings() - def checkDailyReg(self, channel_id, server): - return makeChannelReg(channel_id, server) in self.listDailyReg() + def check_daily_reg(self, channel_id, server): + return make_channel_reg(channel_id, server) in self.list_daily_reg() - def removeDailyReg(self, channel_id, server): - if self.checkDailyReg(channel_id, server): - self.listDailyReg().remove(makeChannelReg(channel_id, server)) + def remove_daily_reg(self, channel_id, server): + if self.check_daily_reg(channel_id, server): + self.list_daily_reg().remove(make_channel_reg(channel_id, server)) self.save_settings() @@ -978,101 +967,101 @@ def is_available(self): return not self.is_finished() def tostr(self): - return fmtTime(self.open_datetime) + "," + fmtTime( - self.close_datetime) + "," + self.group + "," + self.dungeon_code + "," + self.event_type + "," + self.event_seq + return fmt_time(self.open_datetime) + "," + fmt_time( + self.close_datetime) + "," + self.group + "," + self.dungeon_name + "," + self.event_type - def startPst(self): + def start_pst(self): tz = pytz.timezone('US/Pacific') return self.open_datetime.astimezone(tz) - def startEst(self): + def start_est(self): tz = pytz.timezone('US/Eastern') return self.open_datetime.astimezone(tz) - def startFromNow(self): - return fmtHrsMins(self.start_from_now_sec()) + def start_from_now(self): + return fmt_hrs_mins(self.start_from_now_sec()) - def endFromNow(self): - return fmtHrsMins(self.end_from_now_sec()) + def end_from_now(self): + return fmt_hrs_mins(self.end_from_now_sec()) - def endFromNowFullMin(self): - return fmtDaysHrsMinsShort(self.end_from_now_sec()) + def end_from_now_full_min(self): + return fmt_days_hrs_mins_short(self.end_from_now_sec()) - def toGuerrillaStr(self): - return fmtTimeShort(self.startPst()) + def to_guerrilla_str(self): + return fmt_time_short(self.start_pst()) - def toDateStr(self): - return self.server + "," + self.group + "," + fmtTime(self.startPst()) + "," + fmtTime( - self.startEst()) + "," + self.startFromNow() + def to_date_str(self): + return self.server + "," + self.group + "," + fmt_time(self.start_pst()) + "," + fmt_time( + self.start_est()) + "," + self.start_from_now() - def groupShortName(self): + def group_short_name(self): return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') - def groupLongName(self): + def group_long_name(self): return self.group.upper() if self.group is not None else "UNGROUPED" - def toPartialEvent(self, pe): - group = self.groupShortName() + def to_partial_event(self, pe): + group = self.group_short_name() if self.is_started(): - return group + " " + self.endFromNow() + " " + self.name_and_modifier + return group + " " + self.end_from_now() + " " + self.name_and_modifier else: - return group + " " + fmtTimeShort(self.startPst()) + " " + fmtTimeShort( - self.startEst()) + " " + self.startFromNow() + " " + self.name_and_modifier + return group + " " + fmt_time_short(self.start_pst()) + " " + fmt_time_short( + self.start_est()) + " " + self.start_from_now() + " " + self.name_and_modifier class EventList: def __init__(self, event_list): self.event_list = event_list - def withFunc(self, func, exclude=False): + def with_func(self, func, exclude=False): if exclude: return EventList(list(itertools.filterfalse(func, self.event_list))) else: return EventList(list(filter(func, self.event_list))) - def withServer(self, server): - return self.withFunc(lambda e: e.server == normalize_server_name(server)) + def with_server(self, server): + return self.with_func(lambda e: e.server == normalize_server_name(server)) - def withType(self, event_type): - return self.withFunc(lambda e: e.event_type == event_type) + def with_type(self, event_type): + return self.with_func(lambda e: e.event_type == event_type) - def inType(self, event_types): - return self.withFunc(lambda e: e.event_type in event_types) + def in_type(self, event_types): + return self.with_func(lambda e: e.event_type in event_types) - def withDungeonType(self, dungeon_type, exclude=False): - return self.withFunc(lambda e: e.dungeon_type == dungeon_type, exclude) + def with_dungeon_type(self, dungeon_type, exclude=False): + return self.with_func(lambda e: e.dungeon_type == dungeon_type, exclude) - def inDungeonType(self, dungeon_types, exclude=False): - return self.withFunc(lambda e: e.dungeon_type in dungeon_types, exclude) + def in_dungeon_type(self, dungeon_types, exclude=False): + return self.with_func(lambda e: e.dungeon_type in dungeon_types, exclude) - def isGrouped(self, exclude=False): - return self.withFunc(lambda e: e.group is not None, exclude) + def is_grouped(self, exclude=False): + return self.with_func(lambda e: e.group is not None, exclude) - def withNameContains(self, name, exclude=False): - return self.withFunc(lambda e: name.lower() in e.dungeon_name.lower(), exclude) + def with_name_contains(self, name, exclude=False): + return self.with_func(lambda e: name.lower() in e.dungeon_name.lower(), exclude) - def excludeUnwantedEvents(self): - return self.withFunc(isEventWanted) + def exclude_unwanted_events(self): + return self.with_func(is_event_wanted) def items(self): return self.event_list - def startedOnly(self): - return self.withFunc(lambda e: e.is_started()) + def started_only(self): + return self.with_func(lambda e: e.is_started()) - def pendingOnly(self): - return self.withFunc(lambda e: e.is_pending()) + def pending_only(self): + return self.with_func(lambda e: e.is_pending()) - def activeOnly(self): - return self.withFunc(lambda e: e.is_active()) + def active_only(self): + return self.with_func(lambda e: e.is_active()) - def availableOnly(self): - return self.withFunc(lambda e: e.is_available()) + def available_only(self): + return self.with_func(lambda e: e.is_available()) - def itemsByOpenTime(self, reverse=False): + def items_by_open_time(self, reverse=False): return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) - def itemsByCloseTime(self, reverse=False): + def items_by_close_time(self, reverse=False): return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) @@ -1099,21 +1088,21 @@ class DungeonType(Enum): Unknown9 = 9 -def fmtTime(dt): +def fmt_time(dt): return dt.strftime("%Y-%m-%d %H:%M") -def fmtTimeShort(dt): +def fmt_time_short(dt): return dt.strftime("%H:%M") -def fmtHrsMins(seconds): +def fmt_hrs_mins(seconds): hours = seconds // 3600 minutes = (seconds % 3600) // 60 return '{:2}h {:2}m'.format(int(hours), int(minutes)) -def fmtDaysHrsMinsShort(sec): +def fmt_days_hrs_mins_short(sec): days, sec = divmod(sec, 86400) hours, sec = divmod(sec, 3600) minutes, sec = divmod(sec, 60) @@ -1126,7 +1115,7 @@ def fmtDaysHrsMinsShort(sec): return '{:2}m'.format(int(minutes)) -def isEventWanted(event): +def is_event_wanted(event): name = event.name_and_modifier.lower() if 'castle of satan' in name: # eliminate things like : TAMADRA Invades in [Castle of Satan][Castle of Satan in the Abyss] From 27370bf9bef328b02c6d76cc93e6bf8568c011fc Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 24 Dec 2020 17:53:36 -0800 Subject: [PATCH 0337/1220] Strip quotes from searchstr (#323) --- padevents/padevents.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/padevents/padevents.py b/padevents/padevents.py index 4da8635b..dad6d365 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -470,6 +470,7 @@ async def aep_s_server(self, ctx, key, server): @aep_set.command(name="searchstr") async def aep_s_searchstr(self, ctx, key, *, searchstr): """Sets what string is tested against event name""" + searchstr = searchstr.strip('"') if (await self.aepg(ctx, key))['regex']: try: re.compile(searchstr) @@ -599,6 +600,7 @@ async def aed_e_offset(self, ctx, index, offset): @aed_e.command(name="searchstr") async def aed_e_searchstr(self, ctx, index, *, searchstr): """Set search string of an autoeventdm""" + searchstr = searchstr.strip('"') async with self.config.user(ctx.author).dmevents() as dmevents: if not 0 < index <= len(dmevents): await ctx.send("That isn't a valid index.") From f0c3885f10762fe3a436413f5eae292b5f62e618 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 30 Dec 2020 12:00:01 -0800 Subject: [PATCH 0338/1220] [PadInfo] Remove unicode quotes from ls queries (#328) --- padinfo/padinfo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2acf4cf0..b1ae1418 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -630,6 +630,9 @@ async def leaderskill(self, ctx, *, whole_query): DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database + # Remove unicode quotation marks + whole_query = re.sub("[\u201c\u201d]", '"', whole_query) + # deliberate order in case of multiple different separators. for sep in ('"', '/', ',', ' '): if sep in whole_query: From 0f9e96e026d18630281e9e9580fbfb30d11d5d9e Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Jan 2021 18:56:57 -0800 Subject: [PATCH 0339/1220] Add id3-3 (#330) --- dadguide/dadguide.py | 8 ++ dadguide/models/monster_model.py | 3 + dadguide/monster_graph.py | 3 + dadguide/monster_index.py | 211 +++++++++++++++++++++++++----- dadguide/old_monster_index.py | 4 +- dadguide/token_mappings.py | 69 ++++++++++ padinfo/padinfo.py | 217 +++++++++++++++++++++++++++---- 7 files changed, 454 insertions(+), 61 deletions(-) create mode 100644 dadguide/token_mappings.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 8a5af903..1669f8cf 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -21,6 +21,7 @@ from .database_manager import * from .old_monster_index import MonsterIndex +from .monster_index import MonsterIndex2 from .database_loader import load_database from .models.monster_model import MonsterModel @@ -78,6 +79,7 @@ def __init__(self, bot, *args, **kwargs): self.database = None self.index = None # type: MonsterIndex + self.index2 = None # type: MonsterIndex2 async def wait_until_ready(self): """Wait until the Dadguide cog is ready. @@ -108,6 +110,11 @@ async def create_index(self, accept_filter=None): self.panthname_overrides, accept_filter=accept_filter) + async def create_index2(self): + """Exported function that allows a client cog to create a monster index""" + await self.wait_until_ready() + return await MonsterIndex2(self.database.get_all_monsters(False), []) + def get_monster(self, monster_id: int) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" return self.database.graph.get_monster(monster_id) @@ -185,6 +192,7 @@ async def download_and_refresh_nicknames(self): logger.debug('Building dg monster index') self.index = await MonsterIndex(self.database, self.nickname_overrides, self.basename_overrides, self.panthname_overrides) + self.index2 = await MonsterIndex2(self.database.get_all_monsters(False), self.database) logger.debug('Writing dg monster computed names') self.write_monster_computed_names() diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 1e345ff3..77c7619d 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -183,6 +183,9 @@ def to_dict(self): 'name_en': self.name_en, } + def __repr__(self): + return "Monster<{} ({})>".format(self.name_en, self.monster_id) + class MonsterSearchHelper(object): def __init__(self, m: MonsterModel): diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 5b0b2505..d0e919d9 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -313,6 +313,9 @@ def get_base_id_by_id(self, monster_id): return None return sorted(alt_cards)[0] + def get_base_id(self, monster): + return self.get_base_id_by_id(monster.monster_id) + def get_base_monster_by_id(self, monster_id): return self.get_monster(self.get_base_id_by_id(monster_id)) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index f448ca97..c747ff27 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,34 +1,179 @@ -from token_mappings import * -from collections import defaultdict - - -class MonsterIndex2: - def __init__(self, monsters: 'List[MonsterModel]', series: 'List[DgSeries]'): - self.monsters = monsters - self.series = series - self.index = defaultdict(list) - self._build_index() - - def _build_index(self): - self.index = defaultdict(list) - self.monster_name_index = self._build_monster_name_index(self.monsters) - self.series_name_index = self._build_series_name_index(self.series) - - def _build_monster_name_index(self, monsters): - tokens = defaultdict(list) - for monster in monsters: - for token in self._name_to_tokens(monster.name_en): - tokens[token.lower()].append(monster) - return tokens - - def _build_series_name_index(self, series): - tokens = defaultdict(list) - for s in series: - for token in self._name_to_tokens(s.name_en): - tokens[token.lower()].append(s) - return tokens - - def _name_to_tokens(self, name): - name = re.sub(r'[\-+]', ' ', name.lower()) +import csv +import io +import re + +import aiohttp +from redbot.core.utils import AsyncIter +from tsutils import aobject + +from .token_mappings import * + +SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY' \ + '/pub?gid={}&single=true&output=csv' +NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') +GROUP_BASENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') +PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') + + +class MonsterIndex2(aobject): + async def __ainit__(self, monsters, db): + self.manual, self.tokens, self.prefix = await self._build_monster_index(monsters, db.graph) + + __init__ = __ainit__ + + async def _build_monster_index(self, monsters, graph): + manual = defaultdict(set) + tokens = defaultdict(set) + prefix = defaultdict(set) + + nicks = await self._sheet_to_reader(NICKNAME_OVERRIDES_SHEET) + idtonick = defaultdict(set) + for nick, mid, *_ in nicks: + if mid.isdigit(): + idtonick[int(mid)].add(nick) + gnicks = await self._sheet_to_reader(GROUP_BASENAMES_OVERRIDES_SHEET) + idtognick = defaultdict(set) + for mid, nick, *_ in gnicks: + if mid.isdigit(): + idtognick[int(mid)].add(nick) + + async for m in AsyncIter(monsters): + prefix[m] = self.get_prefixes(m, graph) + + # ID + tokens[str(m.monster_id)].add(m) + + # Name Tokens + for token in self._name_to_tokens(m.name_en): + tokens[token.lower()].add(m) + for repl in TOKEN_REPLACEMENTS[token.lower()]: + tokens[repl].add(m) + for pm in PREFIX_MAPS: + for pas in pm.values(): + if token in pas: + prefix[m].update(pas) + + # Monster Nickname + for nick in idtonick[m.monster_id]: + tokens[nick].add(m) + manual[nick].add(m) + + # Tree Nickname + base_id = graph.get_base_id(m) + for nick in idtognick[base_id]: + tokens[nick].add(m) + manual[nick].add(m) + + return manual, tokens, prefix + + @staticmethod + def _name_to_tokens(oname): + oname = oname.lower() + name = re.sub(r'[\-+]', ' ', oname) name = re.sub(r'[^a-z0-9 ]', '', name) - return [t.strip() for t in name.split() if t] + return [t.strip() for t in set(name.split() + oname.split()) if t] + + @staticmethod + async def _sheet_to_reader(url): + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + file = io.StringIO(await response.text()) + return csv.reader(file, delimiter=',') + + @staticmethod + def get_prefixes(m, graph): + prefix = set() + + # Main Color + for t in COLOR_MAP[m.attr1.value]: + prefix.add(t) + prefix.add('main_attr_' + t) + + # Sub Color + for t in COLOR_MAP[m.attr2.value]: + prefix.add('sub_attr_' + t) + + # Both Colors + for t1 in COLOR_MAP[m.attr1.value]: + for t2 in COLOR_MAP[m.attr2.value]: + prefix.add(t1 + t2) + prefix.add(t1 + "/" + t2) + + # Series + if m.series_id in SERIES_MAP: + for t in SERIES_MAP[m.series_id]: + prefix.add(t) + + # Base + if graph.monster_is_base(m): + for t in EVO_PREFIX_MAP[EvoTypes.BASE]: + prefix.add(t) + + special_evo = ('覚醒' in m.name_ja or 'awoken' in m.name_en or + '転生' in m.name_ja or graph.true_evo_type_by_monster(m).value == "Reincarnated" or + 'reincarnated' in m.name_en or graph.true_evo_type_by_monster(m).value == "Super Reincarnated" or + m.is_equip or '極醒' in m.name_ja) + + # Evo + if graph.cur_evo_type_by_monster(m).value == 1 and not special_evo: + for t in EVO_PREFIX_MAP[EvoTypes.EVO]: + prefix.add(t) + + # Uvo + if graph.cur_evo_type_by_monster(m).value == 2 and not special_evo: + for t in EVO_PREFIX_MAP[EvoTypes.UVO]: + prefix.add(t) + + # UUvo + if graph.cur_evo_type_by_monster(m).value == 3 and not special_evo: + for t in EVO_PREFIX_MAP[EvoTypes.UUVO]: + prefix.add(t) + + # Transform + if not graph.monster_is_transform_base(m): + for t in EVO_PREFIX_MAP[EvoTypes.TRANS]: + prefix.add(t) + + # Awoken + if '覚醒' in m.name_ja or 'awoken' in m.name_en.lower(): + for t in EVO_PREFIX_MAP[EvoTypes.AWOKEN]: + prefix.add(t) + + # Mega Awoken + if '極醒' in m.name_ja or 'mega awoken' in m.name_en.lower(): + for t in EVO_PREFIX_MAP[EvoTypes.MEGA]: + prefix.add(t) + + # Reincarnated + if '転生' in m.name_ja or graph.true_evo_type_by_monster(m).value == "Reincarnated": + for t in EVO_PREFIX_MAP[EvoTypes.REVO]: + prefix.add(t) + + # Super Reincarnated + if '超転生' in m.name_ja or graph.true_evo_type_by_monster(m).value == "Super Reincarnated": + for t in EVO_PREFIX_MAP[EvoTypes.SREVO]: + prefix.add(t) + + # Pixel + if (m.name_ja.startswith('ドット') or + m.name_en.startswith('pixel') or + graph.true_evo_type_by_monster(m).value == "Pixel"): + for t in EVO_PREFIX_MAP[EvoTypes.PIXEL]: + prefix.add(t) + + # Equip + if m.is_equip: + for t in EVO_PREFIX_MAP[EvoTypes.EQUIP]: + prefix.add(t) + + # Chibi + if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or 'ミニ' in m.name_ja: + for t in MISC_PREFIX_MAP[MiscPrefixes.CHIBI]: + prefix.add(t) + + # Farmable + if graph.monster_is_farmable_evo(m) or graph.monster_is_mp_evo(m): + for t in MISC_PREFIX_MAP[MiscPrefixes.FARMABLE]: + prefix.add(t) + + return prefix diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 58b2e51e..383ca445 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -17,7 +17,7 @@ class MonsterIndex(tsutils.aobject): - async def __init__(self, monster_database: DbContext, nickname_overrides, basename_overrides, + async def __ainit__(self, monster_database: DbContext, nickname_overrides, basename_overrides, panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory self.db_context = monster_database @@ -135,6 +135,8 @@ def named_monsters_sort(named_mon: NamedMonster): for nickname in nicknames: self.all_entries[nickname] = nm + __init__ = __ainit__ + def init_index(self): pass diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py new file mode 100644 index 00000000..a3b31ea9 --- /dev/null +++ b/dadguide/token_mappings.py @@ -0,0 +1,69 @@ +from collections import defaultdict +from enum import Enum + +COLOR_MAP = {0: ('r', 'red', 'fire'), + 1: ('b', 'blue', 'water'), + 2: ('g', 'green', 'wood'), + 3: ('l', 'light', 'yellow', 'white'), + 4: ('d', 'dark', 'purple', 'black'), + 6: ('x', 'none', 'null', 'nil')} + +SERIES_MAP = {130: ('halloween', 'hw', 'h'), + 136: ('xmas', 'christmas'), + 125: ('summer', 'beach'), + 114: ('school', 'academy', 'gakuen'), + 139: ('new years', 'ny'), + 149: ('wedding', 'bride'), + 154: ('padr',), + 175: ('valentines', 'vday', 'v'), + 183: ('gh', 'gungho'), + 117: ('gh', 'gungho'), + 187: ('sam3', 'samurai3', 'samiii')} + + +class EvoTypes(Enum): + BASE = 'base' + EVO = 'evolved' + UVO = 'ulimate' + UUVO = 'super ultimate' + TRANS = 'transform' + AWOKEN = 'awoken' + MEGA = 'mega awoken' + REVO = 'reincarnated' + SREVO = 'super reincarnated' + PIXEL = 'pixel' + EQUIP = 'equip' + + +EVO_PREFIX_MAP = {EvoTypes.BASE: ('base',), + EvoTypes.EVO: ('evo', 'evolved'), + EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), + EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo'), + EvoTypes.TRANS: ('trans', 'transform', 'transformed'), + EvoTypes.AWOKEN: ('awo', 'a', 'awoken'), + EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'mega'), + EvoTypes.REVO: ('revo', 'reincarnated'), + EvoTypes.SREVO: ('srevo', 'super', 'sr'), + EvoTypes.PIXEL: ('p', 'pixel', 'dot'), + EvoTypes.EQUIP: ('equip', 'assist')} + + +class MiscPrefixes(Enum): + CHIBI = 'chibi' + FARMABLE = 'farmable' + + +MISC_PREFIX_MAP = {MiscPrefixes.CHIBI: ('chibi', 'mini'), + MiscPrefixes.FARMABLE: ('farmable', 'nrem')} + +PREFIX_MAPS = [ + COLOR_MAP, + SERIES_MAP, + EVO_PREFIX_MAP, + MISC_PREFIX_MAP, +] + +TOKEN_REPLACEMENTS = defaultdict(tuple, { + 'tamadra': ('tama',), + 'evolution': ('evo',), +}) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b1ae1418..0bc03bfa 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,4 +1,6 @@ import asyncio +import difflib +import importlib import json import logging import os @@ -14,7 +16,7 @@ import tsutils from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter -from redbot.core.utils.chat_formatting import box, inline +from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor from .id_menu import IdMenu @@ -43,8 +45,11 @@ {0.prefix}id rd ares (select a specific evo for ares, the red/dark one) {0.prefix}id r/d ares (slashes, spaces work too) -computed nickname list and overrides: https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/edit -submit an override suggestion: https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" +computed nickname list and overrides: + https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/edit + +submit an override suggestion: + https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" EMBED_NOT_GENERATED = -1 @@ -87,9 +92,9 @@ def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, self.pad_info.settings.log_emoji("start_" + selected_emoji) async def on_update(self, ctx, selected_emoji): - evoID = self.pad_info.settings.checkEvoID(ctx.author.id) + evo_id = self.pad_info.settings.checkEvoID(ctx.author.id) self.pad_info.settings.log_emoji(selected_emoji) - if evoID: + if evo_id: evos = sorted({*self.db_context.graph.get_alt_cards(self.m.monster_id)}) index = evos.index(self.m.monster_id) if selected_emoji == self.pad_info.previous_monster_emoji: @@ -122,7 +127,7 @@ async def on_update(self, ctx, selected_emoji): self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, m=self.m, scroll=sorted( - {*self.db_context.graph.get_alt_cards(self.m.monster_id)}) if evoID else [], menu_type=1) + {*self.db_context.graph.get_alt_cards(self.m.monster_id)}) if evo_id else [], menu_type=1) return True @@ -200,7 +205,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -267,9 +272,9 @@ async def jpname(self, ctx, *, query: str): await ctx.send(IdMenu.monster_header(m)) await ctx.send(box(m.name_ja)) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) - @commands.command(name="id", aliases="iD Id ID".split()) + @commands.command(name="id", aliases=["iD", "Id", "ID", "id1", "idold", "oldid"]) @checks.bot_has_permissions(embed_links=True) async def _id(self, ctx, *, query: str): """Monster info (main tab)""" @@ -297,7 +302,7 @@ async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) async def send_survey_after(self, ctx, query, result): sm = await self.config.user(ctx.author).survey_mode() @@ -364,7 +369,17 @@ async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) + + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def id3(self, ctx, *, query: str): + """Monster info (main tab)""" + m = await self.findMonster3(query) + if m is not None: + await self._do_idmenu(ctx, m, self.id_emoji) + else: + await self.makeFailureMsg(ctx, "No monster matched") @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) @@ -374,7 +389,7 @@ async def evos(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.evo_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command(name="mats", aliases=['evomats', 'evomat']) @checks.bot_has_permissions(embed_links=True) @@ -384,7 +399,7 @@ async def evomats(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.mats_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -396,7 +411,7 @@ async def pantheon(self, ctx, *, query: str): if menu == EMBED_NOT_GENERATED: await ctx.send(inline('Not a pantheon monster')) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -408,7 +423,7 @@ async def skillups(self, ctx, *, query: str): if menu == EMBED_NOT_GENERATED: await ctx.send(inline('No skillups available')) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) async def _do_idmenu(self, ctx, m, starting_menu_emoji): DGCOG = self.bot.get_cog("Dadguide") @@ -532,7 +547,7 @@ async def pic(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.pic_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -545,7 +560,7 @@ async def links(self, ctx, *, query: str): await ctx.send(embed=embed) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command(aliases=['stats']) @checks.bot_has_permissions(embed_links=True) @@ -555,7 +570,7 @@ async def otherinfo(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.other_info_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -567,7 +582,7 @@ async def lookup(self, ctx, *, query: str): embed = await menu.make_header_embed(m) await ctx.send(embed=embed) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -577,7 +592,7 @@ async def evolist(self, ctx, *, query): if m is not None: await self._do_evolistmenu(ctx, m) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command(aliases=['collabscroll']) @checks.bot_has_permissions(embed_links=True) @@ -600,7 +615,7 @@ async def seriesscroll(self, ctx, *, query: str): if m is not None: await self._do_scrollmenu(ctx, m, ms, self.id_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -614,7 +629,7 @@ async def evoscroll(self, ctx, *, query: str): await self._do_scrollmenu(ctx, m, sorted(db_context.graph.get_alt_monsters(m), key=lambda x: x.monster_id), self.id_emoji) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) @@ -699,6 +714,68 @@ async def helpid(self, ctx): """Whispers you info on how to craft monster queries for [p]id""" await ctx.author.send(box(HELP_MSG.format(ctx))) + @commands.group() + # @checks.is_owner() + async def idtest(self, ctx): + """ID Test suite subcommands""" + + @idtest.command(name="add") + async def idt_add(self, ctx, id: int, *, query): + async with self.config.test_suite() as suite: + suite[query] = {'result': id} + await ctx.tick() + + @idtest.command(name="remove", aliases=["delete", "rm"]) + async def idt_remove(self, ctx, number: int): + async with self.config.test_suite() as suite: + if number >= len(suite): + await ctx.react_quietly("\N{CROSS MARK}") + return + del suite[sorted(suite)[number]] + await ctx.tick() + + @idtest.command(name="setreason", aliases=["addreason"]) + async def idt_setreason(self, ctx, number: int, *, reason): + async with self.config.test_suite() as suite: + if number >= len(suite): + await ctx.react_quietly("\N{CROSS MARK}") + return + suite[sorted(suite)[number]]['reason'] = reason + await ctx.tick() + + @idtest.command(name="list") + async def idt_list(self, ctx): + suite = await self.config.test_suite() + o = "" + ml = len(max(suite, key=len)) + for c, kv in enumerate(sorted(suite.items())): + o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + + @idtest.command(name="run", aliases=["test"]) + async def idt_run(self, ctx): + suite = await self.config.test_suite() + c = 0 + o = "" + ml = len(max(suite, key=len)) + 2 + for q, r in suite.items(): + m = await self.findMonster3(q) + if m and m.monster_id != r['result'] or m is None and r['result'] >= 0: + reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' + q = '"' + q + '"' + o += f"{q.ljust(ml)} - Ex: {m and m.monster_id}, Ac: {r['result']}{reason}\n" + else: + c += 1 + if c: + o += f"\n\nTests complete. {c}/{len(suite)} succeeded." + else: + o += "\n\nAll tests succeeded." + for page in pagify(o): + await ctx.send(box(page)) + @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" @@ -733,7 +810,7 @@ async def padsay(self, ctx, server, *, query: str = None): await ctx.send('Speaking for ' + header) await speech_cog.play_path(channel, voice_file) else: - await ctx.send(self.makeFailureMsg(err)) + await self.makeFailureMsg(ctx, err) @commands.group(aliases=['idmode']) async def idset(self, ctx): @@ -789,7 +866,7 @@ async def embedcolor(self, ctx, *, color): elif re.match(r"^#?[0-9a-fA-F]{6}$", color): await self.config.user(ctx.author).color.set(int(color.lstrip("#"), 16)) else: - await ctx.send("Invalid color! Valid colors are any hexcode and:\n"+", ".join(COLORS)) + await ctx.send("Invalid color! Valid colors are any hexcode and:\n" + ", ".join(COLORS)) return await ctx.tick() @@ -871,12 +948,11 @@ def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] - @staticmethod - def makeFailureMsg(err): + async def makeFailureMsg(self, ctx: discord.abc.Messageable, err): msg = ('Lookup failed: {}.\n' 'Try one of , , [argbld]/[rgbld] . ' 'Unexpected results? Use ^helpid for more info.').format(err) - return box(msg) + await ctx.send(box(msg)) async def findMonster(self, query, server_filter=ServerFilter.any): query = rmdiacritics(query) @@ -930,6 +1006,93 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster2(query) + async def findMonster3(self, query): + DGCOG = self.bot.get_cog("Dadguide") + tm = importlib.import_module(DGCOG.__module__.rstrip("dadguide") + 'token_mappings') + mi = importlib.import_module(DGCOG.__module__.rstrip("dadguide") + 'monster_index') + if DGCOG is None: + raise ValueError("Dadguide cog is not loaded") + + query = rmdiacritics(query).lower() + query = re.sub(r'\b([rbgldx]) ([rbgldx])\b', r'main_attr_\1 sub_attr_\2', query) + query = query.split() + + base = False + if 'base' in query: + query.remove('base') + base = True + + prefixes = set() + name = set() + valid_prefixes = {p for ps in DGCOG.index2.prefix.values() for p in ps} + for c, t in enumerate(query): + if difflib.get_close_matches(t, + [p for p in valid_prefixes if len(p) > 8], + n=1, cutoff=.8) or t in valid_prefixes: + prefixes.add(t) + else: + name.add(t) + name.update(query[c + 1:]) + break + + def calc_ratio(s1, s2): + return difflib.SequenceMatcher(None, s1, s2).ratio() + + # print(prefixes, name) + + monsterscore = {m: 0 for m in DGCOG.database.get_all_monsters()} + monstergen = set(monsterscore) + + for t in name: + valid = set() + + ms = difflib.get_close_matches(t, list(DGCOG.index2.tokens) + list(DGCOG.index2.manual), n=10000, cutoff=.8) + if not ms: + return + for match in ms: + for m in DGCOG.index2.manual[match]: + if m not in valid: + monsterscore[m] += calc_ratio(t, match) * 2 + valid.add(m) + for m in DGCOG.index2.tokens[match]: + if m not in valid: + monsterscore[m] += calc_ratio(t, match) + valid.add(m) + + ftr = set() + for m in valid: + if m in monstergen: + ftr.add(m) + monstergen = ftr + + # print({m: monsterscore[m] for m in monstergen}) + + def matches(m, t): + if len(t) < 6: + return t in DGCOG.index2.prefix[m] + else: + return bool(difflib.get_close_matches(t, DGCOG.index2.prefix[m], n=10000, cutoff=.8)) + + monstergen_base = monstergen + monstergen = sum((list(DGCOG.database.graph.get_alt_monsters(m)) for m in monstergen), []) + + for t in prefixes: + monstergen = {m for m in monstergen if matches(m, t)} + if not monstergen: + return + + monstergen = set(monstergen).intersection(monstergen_base) or monstergen + + mon = max(monstergen, key=lambda m: (not m.is_equip, + monsterscore[m], + -DGCOG.database.graph.get_base_id(m), + m.rarity, + m.monster_no_na)) + if base: + return DGCOG.database.graph.get_base_monster(mon) + else: + return mon + class PadInfoSettings(CogSettings): def make_default_settings(self): From ea794f2d0d46133bda0add65a6fd5c5717cd511a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 1 Jan 2021 21:03:24 -0600 Subject: [PATCH 0340/1220] Add chibi prefix when "chibi" appears in the name (#320) --- dadguide/old_monster_index.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 383ca445..eee6ec86 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -166,6 +166,9 @@ def compute_prefixes(self, m: MonsterModel, evotree: list): # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) prefixes.add('chibi') + if 'chibi' in lower_name: + prefixes.add('chibi') + true_evo_type = self.db_context.graph.true_evo_type_by_monster(m) awoken = lower_name.startswith('awoken') or '覚醒' in lower_name revo = true_evo_type == InternalEvoType.Reincarnated From b947b269f2d80363f18d353d0b4d9b6e33ee23db Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Jan 2021 19:38:12 -0800 Subject: [PATCH 0341/1220] Add nonpixel, nonchibi, and fix an error (#331) --- dadguide/monster_index.py | 6 ++++++ dadguide/token_mappings.py | 2 ++ padinfo/padinfo.py | 3 +++ 3 files changed, 11 insertions(+) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index c747ff27..dfa1645d 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -160,6 +160,9 @@ def get_prefixes(m, graph): graph.true_evo_type_by_monster(m).value == "Pixel"): for t in EVO_PREFIX_MAP[EvoTypes.PIXEL]: prefix.add(t) + else: + for t in EVO_PREFIX_MAP[EvoTypes.NONPIXEL]: + prefix.add(t) # Equip if m.is_equip: @@ -170,6 +173,9 @@ def get_prefixes(m, graph): if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or 'ミニ' in m.name_ja: for t in MISC_PREFIX_MAP[MiscPrefixes.CHIBI]: prefix.add(t) + else: + for t in MISC_PREFIX_MAP[MiscPrefixes.NONCHIBI]: + prefix.add(t) # Farmable if graph.monster_is_farmable_evo(m) or graph.monster_is_mp_evo(m): diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index a3b31ea9..947b9102 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -45,6 +45,7 @@ class EvoTypes(Enum): EvoTypes.REVO: ('revo', 'reincarnated'), EvoTypes.SREVO: ('srevo', 'super', 'sr'), EvoTypes.PIXEL: ('p', 'pixel', 'dot'), + EvoTypes.NONPIXEL: ('np', 'nonpixel'), EvoTypes.EQUIP: ('equip', 'assist')} @@ -54,6 +55,7 @@ class MiscPrefixes(Enum): MISC_PREFIX_MAP = {MiscPrefixes.CHIBI: ('chibi', 'mini'), + MiscPrefixes.NONCHIBI: ('nonchibi', 'nc'), MiscPrefixes.FARMABLE: ('farmable', 'nrem')} PREFIX_MAPS = [ diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0bc03bfa..af5fb6be 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1083,6 +1083,9 @@ def matches(m, t): monstergen = set(monstergen).intersection(monstergen_base) or monstergen + if not monstergen: + return + mon = max(monstergen, key=lambda m: (not m.is_equip, monsterscore[m], -DGCOG.database.graph.get_base_id(m), From 6b47440860077d6eb9851e0ee55b9e8f85d7a313 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Jan 2021 19:41:26 -0800 Subject: [PATCH 0342/1220] Add necessary attributes to enums (#333) * Add nonpixel, nonchibi, and fix an error * Add attributes to enums --- dadguide/token_mappings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 947b9102..e207ec3f 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -32,6 +32,7 @@ class EvoTypes(Enum): REVO = 'reincarnated' SREVO = 'super reincarnated' PIXEL = 'pixel' + NONPIXEL = 'nonpixel' EQUIP = 'equip' @@ -51,6 +52,7 @@ class EvoTypes(Enum): class MiscPrefixes(Enum): CHIBI = 'chibi' + NONCHIBI = 'chibi' FARMABLE = 'farmable' From bfbc18b0264629722388b236d0767a1110d5d5d4 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Jan 2021 19:51:01 -0800 Subject: [PATCH 0343/1220] Add to definition of chibi so it encompasses gh chibis (#334) * Add nonpixel, nonchibi, and fix an error * Add attributes to enums * Fix chibis --- dadguide/monster_index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index dfa1645d..b436fecd 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -170,7 +170,8 @@ def get_prefixes(m, graph): prefix.add(t) # Chibi - if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or 'ミニ' in m.name_ja: + if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or \ + 'ミニ' in m.name_ja or '(chibi)' in m.name_en: for t in MISC_PREFIX_MAP[MiscPrefixes.CHIBI]: prefix.add(t) else: From 4ae995d9ef85ff03f11b8faacfdba9e96e5e6068 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Jan 2021 20:05:57 -0800 Subject: [PATCH 0344/1220] Fix nonchibi enum value (#335) * Add nonpixel, nonchibi, and fix an error * Add attributes to enums * Fix chibis * Fix enum value --- dadguide/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index e207ec3f..44e449fc 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -52,7 +52,7 @@ class EvoTypes(Enum): class MiscPrefixes(Enum): CHIBI = 'chibi' - NONCHIBI = 'chibi' + NONCHIBI = 'nonchibi' FARMABLE = 'farmable' From af4f21b831ea98bd15a03714ffa212cd130191b7 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Jan 2021 23:11:12 -0800 Subject: [PATCH 0345/1220] Allow Beta Id3 Opt-In (#337) * Add nonpixel, nonchibi, and fix an error * Add attributes to enums * Fix chibis * Fix enum value * Allow users to opt into beta id3 * change aliases * change dobetaid3 to beta --- padinfo/padinfo.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index af5fb6be..c59bf12f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -204,7 +204,7 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) self.config = Config.get_conf(self, identifier=9401770) - self.config.register_user(survey_mode=0, color=None) + self.config.register_user(survey_mode=0, color=None, beta_id3=False) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) def cog_unload(self): @@ -274,12 +274,19 @@ async def jpname(self, ctx, *, query: str): else: await self.makeFailureMsg(ctx, err) - @commands.command(name="id", aliases=["iD", "Id", "ID", "id1", "idold", "oldid"]) + @commands.command(name="id", aliases=["iD", "Id", "ID"]) @checks.bot_has_permissions(embed_links=True) async def _id(self, ctx, *, query: str): """Monster info (main tab)""" - prefix = ctx.prefix + "id" - query = prefix.join(filter(None, query.split(prefix))) + if await self.config.user(ctx.author).beta_id3(): + await self._do_id3(ctx, query) + else: + await self._do_id(ctx, query) + + @commands.command(aliases=["idold", "oldid"]) + @checks.bot_has_permissions(embed_links=True) + async def id1(self, ctx, *, query): + """Do a search via id1""" await self._do_id(ctx, query) @commands.command() @@ -375,6 +382,9 @@ async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): @checks.bot_has_permissions(embed_links=True) async def id3(self, ctx, *, query: str): """Monster info (main tab)""" + await self._do_id3(ctx, query) + + async def _do_id3(self, ctx, query): m = await self.findMonster3(query) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) @@ -849,6 +859,12 @@ async def survey(self, ctx, value): else: await ctx.send("value must be `always`, `sometimes`, or `never`") + @idset.command() + async def beta(self, ctx, value: bool = True): + """Opt in (or out D:) to the id3 beta test!""" + await self.config.user(ctx.author).beta_id3.set(value) + await ctx.tick() + @is_donor() @idset.command() async def embedcolor(self, ctx, *, color): From 28c1dfeb4b90d8515bf74fda24ffdb1a530b6b32 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Jan 2021 12:44:10 -0800 Subject: [PATCH 0346/1220] Fix id3 search for equips by number (#341) --- padinfo/padinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c59bf12f..f7cd320b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1102,8 +1102,8 @@ def matches(m, t): if not monstergen: return - mon = max(monstergen, key=lambda m: (not m.is_equip, - monsterscore[m], + mon = max(monstergen, key=lambda m: (monsterscore[m], + not m.is_equip, -DGCOG.database.graph.get_base_id(m), m.rarity, m.monster_no_na)) From 86f462b4b3005a0c263e6a026c2c171596ecbe2e Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Jan 2021 13:21:43 -0800 Subject: [PATCH 0347/1220] Add pantheon search to id3 (#342) * Add support for id3 ignore on sheet * add pantheons * refactor * comment out print statements --- dadguide/dadguide.py | 14 ++--- dadguide/monster_index.py | 101 +++++++++++++++++++++---------------- dadguide/token_mappings.py | 5 ++ padinfo/padinfo.py | 13 ++++- 4 files changed, 81 insertions(+), 52 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 1669f8cf..e4cf16fa 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -170,7 +170,7 @@ async def download_and_refresh_nicknames(self): logger.info('Loading dg name overrides') nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN) - basename_overrides = self._csv_to_tuples(BASENAME_FILE_PATTERN) + basename_overrides = self._csv_to_tuples(BASENAME_FILE_PATTERN, 5) panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) self.nickname_overrides = defaultdict(set) @@ -180,24 +180,24 @@ async def download_and_refresh_nicknames(self): self.basename_overrides = defaultdict(set) for x in basename_overrides: - k, v = x - if k.isdigit(): + k, v, _, _, i = x + if k.isdigit() and not i: self.basename_overrides[int(k)].add(v.lower()) self.panthname_overrides = {x[0].lower(): x[1].lower() for x in panthname_overrides} self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) - logger.debug('Loading dg database') + logger.info('Loading dg database') self.database = load_database(self.database) - logger.debug('Building dg monster index') + logger.info('Building dg monster index') self.index = await MonsterIndex(self.database, self.nickname_overrides, self.basename_overrides, self.panthname_overrides) self.index2 = await MonsterIndex2(self.database.get_all_monsters(False), self.database) - logger.debug('Writing dg monster computed names') + logger.info('Writing dg monster computed names') self.write_monster_computed_names() - logger.debug('Done refreshing dg data') + logger.info('Done refreshing dg data') def write_monster_computed_names(self): results = {} diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index b436fecd..5316a968 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,8 +1,5 @@ -import csv -import io import re -import aiohttp from redbot.core.utils import AsyncIter from tsutils import aobject @@ -17,28 +14,45 @@ class MonsterIndex2(aobject): async def __ainit__(self, monsters, db): - self.manual, self.tokens, self.prefix = await self._build_monster_index(monsters, db.graph) + self.graph = db.graph + + self.idtonick = defaultdict(set) + self.idtognick = defaultdict(set) + self.sidtopnick = defaultdict(set, {k: set(v) for k, v in SERIES_MAP.items()}) + self.mwtokens = set() + + nicks = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) + for nick, mid, *data in nicks: + _, i, *_ = data + [None, None] + if mid.isdigit() and not i: + if " " in nick: + self.mwtokens.add(nick) + self.idtonick[int(mid)].add(nick.replace(" ", "")) + gnicks = await sheet_to_reader(GROUP_BASENAMES_OVERRIDES_SHEET) + for mid, nick, *data in gnicks: + _, i, *_ = data + [None, None] + if mid.isdigit() and not i: + if " " in nick: + self.mwtokens.add(nick) + self.idtognick[int(mid)].add(nick.replace(" ", "")) + pnicks = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) + for nick, _, sid, *_ in pnicks: + if sid.isdigit(): + if " " in nick: + self.mwtokens.add(nick) + self.sidtopnick[int(sid)].add(nick.replace(" ", "")) + + self.manual, self.tokens, self.prefix = await self._build_monster_index(monsters) __init__ = __ainit__ - async def _build_monster_index(self, monsters, graph): + async def _build_monster_index(self, monsters): manual = defaultdict(set) tokens = defaultdict(set) prefix = defaultdict(set) - nicks = await self._sheet_to_reader(NICKNAME_OVERRIDES_SHEET) - idtonick = defaultdict(set) - for nick, mid, *_ in nicks: - if mid.isdigit(): - idtonick[int(mid)].add(nick) - gnicks = await self._sheet_to_reader(GROUP_BASENAMES_OVERRIDES_SHEET) - idtognick = defaultdict(set) - for mid, nick, *_ in gnicks: - if mid.isdigit(): - idtognick[int(mid)].add(nick) - async for m in AsyncIter(monsters): - prefix[m] = self.get_prefixes(m, graph) + prefix[m] = await self.get_prefixes(m) # ID tokens[str(m.monster_id)].add(m) @@ -54,13 +68,13 @@ async def _build_monster_index(self, monsters, graph): prefix[m].update(pas) # Monster Nickname - for nick in idtonick[m.monster_id]: + for nick in self.idtonick[m.monster_id]: tokens[nick].add(m) manual[nick].add(m) # Tree Nickname - base_id = graph.get_base_id(m) - for nick in idtognick[base_id]: + base_id = self.graph.get_base_id(m) + for nick in self.idtognick[base_id]: tokens[nick].add(m) manual[nick].add(m) @@ -73,15 +87,7 @@ def _name_to_tokens(oname): name = re.sub(r'[^a-z0-9 ]', '', name) return [t.strip() for t in set(name.split() + oname.split()) if t] - @staticmethod - async def _sheet_to_reader(url): - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - file = io.StringIO(await response.text()) - return csv.reader(file, delimiter=',') - - @staticmethod - def get_prefixes(m, graph): + async def get_prefixes(self, m): prefix = set() # Main Color @@ -100,37 +106,38 @@ def get_prefixes(m, graph): prefix.add(t1 + "/" + t2) # Series - if m.series_id in SERIES_MAP: - for t in SERIES_MAP[m.series_id]: + if m.series_id in self.sidtopnick: + for t in self.sidtopnick[m.series_id]: prefix.add(t) # Base - if graph.monster_is_base(m): + if self.graph.monster_is_base(m): for t in EVO_PREFIX_MAP[EvoTypes.BASE]: prefix.add(t) - special_evo = ('覚醒' in m.name_ja or 'awoken' in m.name_en or - '転生' in m.name_ja or graph.true_evo_type_by_monster(m).value == "Reincarnated" or - 'reincarnated' in m.name_en or graph.true_evo_type_by_monster(m).value == "Super Reincarnated" or + special_evo = ('覚醒' in m.name_ja or 'awoken' in m.name_en or '転生' in m.name_ja or + self.graph.true_evo_type_by_monster(m).value == "Reincarnated" or + 'reincarnated' in m.name_en or + self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated" or m.is_equip or '極醒' in m.name_ja) # Evo - if graph.cur_evo_type_by_monster(m).value == 1 and not special_evo: + if self.graph.cur_evo_type_by_monster(m).value == 1 and not special_evo: for t in EVO_PREFIX_MAP[EvoTypes.EVO]: prefix.add(t) # Uvo - if graph.cur_evo_type_by_monster(m).value == 2 and not special_evo: + if self.graph.cur_evo_type_by_monster(m).value == 2 and not special_evo: for t in EVO_PREFIX_MAP[EvoTypes.UVO]: prefix.add(t) # UUvo - if graph.cur_evo_type_by_monster(m).value == 3 and not special_evo: + if self.graph.cur_evo_type_by_monster(m).value == 3 and not special_evo: for t in EVO_PREFIX_MAP[EvoTypes.UUVO]: prefix.add(t) # Transform - if not graph.monster_is_transform_base(m): + if not self.graph.monster_is_transform_base(m): for t in EVO_PREFIX_MAP[EvoTypes.TRANS]: prefix.add(t) @@ -145,19 +152,19 @@ def get_prefixes(m, graph): prefix.add(t) # Reincarnated - if '転生' in m.name_ja or graph.true_evo_type_by_monster(m).value == "Reincarnated": + if '転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Reincarnated": for t in EVO_PREFIX_MAP[EvoTypes.REVO]: prefix.add(t) # Super Reincarnated - if '超転生' in m.name_ja or graph.true_evo_type_by_monster(m).value == "Super Reincarnated": + if '超転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated": for t in EVO_PREFIX_MAP[EvoTypes.SREVO]: prefix.add(t) # Pixel if (m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') or - graph.true_evo_type_by_monster(m).value == "Pixel"): + self.graph.true_evo_type_by_monster(m).value == "Pixel"): for t in EVO_PREFIX_MAP[EvoTypes.PIXEL]: prefix.add(t) else: @@ -179,8 +186,16 @@ def get_prefixes(m, graph): prefix.add(t) # Farmable - if graph.monster_is_farmable_evo(m) or graph.monster_is_mp_evo(m): + if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): for t in MISC_PREFIX_MAP[MiscPrefixes.FARMABLE]: prefix.add(t) return prefix + + +# TODO: Move this to TSUtils +async def sheet_to_reader(url): + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + file = io.StringIO(await response.text()) + return csv.reader(file, delimiter=',') diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 44e449fc..e885318d 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -1,5 +1,9 @@ from collections import defaultdict from enum import Enum +import aiohttp +import csv +import io + COLOR_MAP = {0: ('r', 'red', 'fire'), 1: ('b', 'blue', 'water'), @@ -21,6 +25,7 @@ 187: ('sam3', 'samurai3', 'samiii')} + class EvoTypes(Enum): BASE = 'base' EVO = 'evolved' diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f7cd320b..480e25a9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1085,9 +1085,16 @@ def calc_ratio(s1, s2): def matches(m, t): if len(t) < 6: - return t in DGCOG.index2.prefix[m] + if t in DGCOG.index2.prefix[m]: + monsterscore[m] += 1 + return True else: - return bool(difflib.get_close_matches(t, DGCOG.index2.prefix[m], n=10000, cutoff=.8)) + dlm = difflib.get_close_matches(t, DGCOG.index2.prefix[m], n=10000, cutoff=.8) + if dlm: + monsterscore[m] += max(calc_ratio(t, p) for p in dlm) + return True + return False + monstergen_base = monstergen monstergen = sum((list(DGCOG.database.graph.get_alt_monsters(m)) for m in monstergen), []) @@ -1107,6 +1114,8 @@ def matches(m, t): -DGCOG.database.graph.get_base_id(m), m.rarity, m.monster_no_na)) + + if base: return DGCOG.database.graph.get_base_monster(mon) else: From f834789831f43257965b15b75fbdbbfee26fd724 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Jan 2021 22:21:07 -0800 Subject: [PATCH 0348/1220] Rename basename to treename in code (#348) --- dadguide/dadguide.py | 30 +++++----- dadguide/monster_index.py | 4 +- dadguide/old_monster_index.py | 108 +++++++++++++++++----------------- padbuildimg/padbuildimg.py | 2 +- padglobal/padglobal.py | 18 +++--- 5 files changed, 81 insertions(+), 81 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index e4cf16fa..ba8b1677 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -38,16 +38,16 @@ def _data_file(file_name: str) -> str: # have any cog data path loaded CSV_FILE_PATTERN = '{}.csv' NAMES_EXPORT_PATH = _data_file('computed_names.json') - BASENAMES_EXPORT_PATH = _data_file('base_names.json') + TREENAMES_EXPORT_PATH = _data_file('base_names.json') TRANSLATEDNAMES_EXPORT_PATH = _data_file('translated_names.json') SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/pub?gid={}&single=true&output=csv' NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') - GROUP_BASENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') + GROUP_TREENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') NICKNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('nicknames')) - BASENAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('basenames')) + TREENAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('treenames')) PANTHNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('panthnames')) DB_DUMP_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/db/dadguide.sqlite' @@ -69,8 +69,8 @@ def __init__(self, bot, *args, **kwargs): # A string -> int mapping, nicknames to monster_id_na self.nickname_overrides = {} - # An int -> set(string), monster_id_na to set of basename overrides - self.basename_overrides = defaultdict(set) + # An int -> set(string), monster_id_na to set of treename overrides + self.treename_overrides = defaultdict(set) self.panthname_overrides = defaultdict(set) @@ -106,7 +106,7 @@ async def create_index(self, accept_filter=None): await self.wait_until_ready() return await MonsterIndex(self.database, self.nickname_overrides, - self.basename_overrides, + self.treename_overrides, self.panthname_overrides, accept_filter=accept_filter) @@ -153,7 +153,7 @@ async def reload_data_task(self): async def reload_config_files(self): os.remove(NICKNAME_FILE_PATTERN) - os.remove(BASENAME_FILE_PATTERN) + os.remove(TREENAME_FILE_PATTERN) os.remove(PANTHNAME_FILE_PATTERN) await self.download_and_refresh_nicknames() @@ -170,7 +170,7 @@ async def download_and_refresh_nicknames(self): logger.info('Loading dg name overrides') nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN) - basename_overrides = self._csv_to_tuples(BASENAME_FILE_PATTERN, 5) + treename_overrides = self._csv_to_tuples(TREENAME_FILE_PATTERN, 5) panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) self.nickname_overrides = defaultdict(set) @@ -178,11 +178,11 @@ async def download_and_refresh_nicknames(self): if id.isdigit(): self.nickname_overrides[int(id)].add(nick.lower()) - self.basename_overrides = defaultdict(set) - for x in basename_overrides: + self.treename_overrides = defaultdict(set) + for x in treename_overrides: k, v, _, _, i = x if k.isdigit() and not i: - self.basename_overrides[int(k)].add(v.lower()) + self.treename_overrides[int(k)].add(v.lower()) self.panthname_overrides = {x[0].lower(): x[1].lower() for x in panthname_overrides} self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) @@ -191,7 +191,7 @@ async def download_and_refresh_nicknames(self): self.database = load_database(self.database) logger.info('Building dg monster index') self.index = await MonsterIndex(self.database, self.nickname_overrides, - self.basename_overrides, self.panthname_overrides) + self.treename_overrides, self.panthname_overrides) self.index2 = await MonsterIndex2(self.database.get_all_monsters(False), self.database) logger.info('Writing dg monster computed names') @@ -209,12 +209,12 @@ def write_monster_computed_names(self): results = {} for nm in self.index.all_monsters: - entry = {'bn': list(nm.group_basenames)} + entry = {'bn': list(nm.group_treenames)} if nm.extra_nicknames: entry['nn'] = list(nm.extra_nicknames) results[int(tsutils.get_pdx_id_dadguide(nm))] = entry - with open(BASENAMES_EXPORT_PATH, 'w', encoding='utf-8') as f: + with open(TREENAMES_EXPORT_PATH, 'w', encoding='utf-8') as f: json.dump(results, f) def _csv_to_tuples(self, file_path: str, cols: int = 2): @@ -245,7 +245,7 @@ async def _download_override_files(self): await tsutils.async_cached_plain_request( NICKNAME_FILE_PATTERN, NICKNAME_OVERRIDES_SHEET, one_hour_secs) await tsutils.async_cached_plain_request( - BASENAME_FILE_PATTERN, GROUP_BASENAMES_OVERRIDES_SHEET, one_hour_secs) + TREENAME_FILE_PATTERN, GROUP_TREENAMES_OVERRIDES_SHEET, one_hour_secs) await tsutils.async_cached_plain_request( PANTHNAME_FILE_PATTERN, PANTHNAME_OVERRIDES_SHEET, one_hour_secs) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 5316a968..a9a91e53 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -8,7 +8,7 @@ SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY' \ '/pub?gid={}&single=true&output=csv' NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') -GROUP_BASENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') +GROUP_TREENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') @@ -28,7 +28,7 @@ async def __ainit__(self, monsters, db): if " " in nick: self.mwtokens.add(nick) self.idtonick[int(mid)].add(nick.replace(" ", "")) - gnicks = await sheet_to_reader(GROUP_BASENAMES_OVERRIDES_SHEET) + gnicks = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) for mid, nick, *data in gnicks: _, i, *_ = data + [None, None] if mid.isdigit() and not i: diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index eee6ec86..553b7348 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -17,7 +17,7 @@ class MonsterIndex(tsutils.aobject): - async def __ainit__(self, monster_database: DbContext, nickname_overrides, basename_overrides, + async def __ainit__(self, monster_database: DbContext, nickname_overrides, treename_overrides, panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory self.db_context = monster_database @@ -64,10 +64,10 @@ async def __ainit__(self, monster_database: DbContext, nickname_overrides, basen base_id = base_mon.monster_id base_monster = monster_database.graph.get_monster(base_id) series = base_monster.series - group_basename_overrides = basename_overrides.get(base_id, []) + group_treename_overrides = treename_overrides.get(base_id, []) evolution_tree = [monster_database.graph.get_monster(m) for m in monster_database.get_evolution_tree_ids(base_id)] - named_mg = NamedMonsterGroup(evolution_tree, group_basename_overrides) + named_mg = NamedMonsterGroup(evolution_tree, group_treename_overrides) named_evolution_tree = [] for monster in evolution_tree: if accept_filter and not accept_filter(monster): @@ -464,7 +464,7 @@ def pick_best_monster(self): class NamedMonsterGroup(object): - def __init__(self, evolution_tree: list, basename_overrides: list): + def __init__(self, evolution_tree: list, treename_overrides: list): self.is_low_priority = ( self._is_low_priority_monster(evolution_tree[0]) or self._is_low_priority_group(evolution_tree)) @@ -475,59 +475,59 @@ def __init__(self, evolution_tree: list, basename_overrides: list): self.base_monster_no = base_monster.monster_id self.base_monster_no_na = base_monster.monster_no_na - self.monster_no_to_basename = { - m.monster_id: self._compute_monster_basename(m) for m in evolution_tree + self.monster_no_to_treename = { + m.monster_id: self._compute_monster_treename(m) for m in evolution_tree } - self.computed_basename = self._compute_group_basename(evolution_tree) - self.computed_basenames = {self.computed_basename} - if '-' in self.computed_basename: - self.computed_basenames.add(self.computed_basename.replace('-', ' ')) + self.computed_treename = self._compute_group_treename(evolution_tree) + self.computed_treenames = {self.computed_treename} + if '-' in self.computed_treename: + self.computed_treenames.add(self.computed_treename.replace('-', ' ')) - self.basenames = basename_overrides or self.computed_basenames + self.treenames = treename_overrides or self.computed_treenames @staticmethod - def _compute_monster_basename(m: MonsterModel): - basename = m.name_en.lower() - if ',' in basename: - name_parts = basename.split(',') + def _compute_monster_treename(m: MonsterModel): + treename = m.name_en.lower() + if ',' in treename: + name_parts = treename.split(',') if name_parts[1].strip().startswith('the '): # handle names like 'xxx, the yyy' where xxx is the name - basename = name_parts[0] + treename = name_parts[0] else: # otherwise, grab the chunk after the last comma - basename = name_parts[-1] + treename = name_parts[-1] for x in ['awoken', 'reincarnated']: - if basename.startswith(x): - basename = basename.replace(x, '') + if treename.startswith(x): + treename = treename.replace(x, '') # Fix for DC collab garbage - basename = basename.replace('(comics)', '') - basename = basename.replace('(film)', '') + treename = treename.replace('(comics)', '') + treename = treename.replace('(film)', '') - return basename.strip() + return treename.strip() - def _compute_group_basename(self, monsters): - """Computes the basename for a group of monsters. + def _compute_group_treename(self, monsters): + """Computes the treename for a group of monsters. - Prefer a basename with the largest count across the group. If all the - groups have equal size, prefer the lowest monster number basename. + Prefer a treename with the largest count across the group. If all the + groups have equal size, prefer the lowest monster number treename. This monster in general has better names, particularly when all the names are unique, e.g. for male/female hunters.""" def count_and_id(): return [0, 0] - basename_to_info = defaultdict(count_and_id) + treename_to_info = defaultdict(count_and_id) for m in monsters: - basename = self.monster_no_to_basename[m.monster_id] - entry = basename_to_info[basename] + treename = self.monster_no_to_treename[m.monster_id] + entry = treename_to_info[treename] entry[0] += 1 entry[1] = max(entry[1], m.monster_id) - entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in basename_to_info.items()] + entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in treename_to_info.items()] return max(entries)[2] @staticmethod @@ -568,7 +568,7 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref self.base_monster_no_na = base_monster.monster_no_na # This stuff is important for nickname generation - self.group_basenames = monster_group.basenames + self.group_treenames = monster_group.treenames self.prefixes = prefixes # Pantheon @@ -585,20 +585,20 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref self.name_ja = monster.name_ja # These are just extra metadata - self.monster_basename = monster_group.monster_no_to_basename[self.monster_id] - self.group_computed_basename = monster_group.computed_basename + self.monster_treename = monster_group.monster_no_to_treename[self.monster_id] + self.group_computed_treename = monster_group.computed_treename self.extra_nicknames = extra_nicknames # Compute any extra prefixes - if self.monster_basename in ('ana', 'ace'): - self.prefixes.add(self.monster_basename) + if self.monster_treename in ('ana', 'ace'): + self.prefixes.add(self.monster_treename) - # Compute extra basenames by checking for two-word basenames and using the second half - self.two_word_basenames = set() - for basename in self.group_basenames: - basename_words = basename.split(' ') - if len(basename_words) == 2: - self.two_word_basenames.add(basename_words[1]) + # Compute extra treenames by checking for two-word treenames and using the second half + self.two_word_treenames = set() + for treename in self.group_treenames: + treename_words = treename.split(' ') + if len(treename_words) == 2: + self.two_word_treenames.add(treename_words[1]) # The primary result nicknames self.final_nicknames = set() @@ -608,23 +608,23 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref if monster.roma_subname: self.final_nicknames.add(monster.roma_subname) - # For each basename, add nicknames - for basename in self.group_basenames: - # Add the basename directly - self.final_nicknames.add(basename) - # Add the prefix plus basename, and the prefix with a space between basename + # For each treename, add nicknames + for treename in self.group_treenames: + # Add the treename directly + self.final_nicknames.add(treename) + # Add the prefix plus treename, and the prefix with a space between treename for prefix in self.prefixes: - self.final_nicknames.add(prefix + basename) - self.final_nicknames.add(prefix + ' ' + basename) + self.final_nicknames.add(prefix + treename) + self.final_nicknames.add(prefix + ' ' + treename) self.final_two_word_nicknames = set() - # Slightly different process for two-word basenames. Does this make sense? Who knows. - for basename in self.two_word_basenames: - self.final_two_word_nicknames.add(basename) - # Add the prefix plus basename, and the prefix with a space between basename + # Slightly different process for two-word treenames. Does this make sense? Who knows. + for treename in self.two_word_treenames: + self.final_two_word_nicknames.add(treename) + # Add the prefix plus treename, and the prefix with a space between treename for prefix in self.prefixes: - self.final_two_word_nicknames.add(prefix + basename) - self.final_two_word_nicknames.add(prefix + ' ' + basename) + self.final_two_word_nicknames.add(prefix + treename) + self.final_two_word_nicknames.add(prefix + ' ' + treename) def set_evolution_tree(self, evolution_tree): """ diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index e8e12a45..00a33cbf 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -228,7 +228,7 @@ async def downloadAllAssets(self, awk_ids): await self.downloadAssets(REMOTE_ASSET_URL + AWK_CIRCLE + '.png', params.ASSETS_DIR + AWK_CIRCLE + '.png') await self.downloadAssets(REMOTE_ASSET_URL + AWK_STAR + '.png', params.ASSETS_DIR + AWK_STAR + '.png') await self.downloadAssets(REMOTE_ASSET_URL + DELAY_BUFFER + '.png', params.ASSETS_DIR + DELAY_BUFFER + '.png') - font_name = os.path.basename(params.FONT_NAME) + font_name = os.path.treename(params.FONT_NAME) await self.downloadAssets(REMOTE_ASSET_URL + font_name, params.ASSETS_DIR + font_name) def dmOnly(self, server_id): diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 5baf8d7c..a4bb2537 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -233,12 +233,12 @@ def list_or_none(l): return 'NONE' msg += "\n\nNickname original components:" - msg += "\n monster_basename: {}".format(m.monster_basename) - msg += "\n group_computed_basename: {}".format(m.group_computed_basename) + msg += "\n monster_treename: {}".format(m.monster_treename) + msg += "\n group_computed_treename: {}".format(m.group_computed_treename) msg += "\n extra_nicknames: {}".format(list_or_none(m.extra_nicknames)) msg += "\n\nNickname final components:" - msg += "\n basenames: {}".format(list_or_none(m.group_basenames)) + msg += "\n treenames: {}".format(list_or_none(m.group_treenames)) msg += "\n prefixes: {}".format(list_or_none(m.prefixes)) msg += "\n\nAccepted nickname entries:" @@ -730,7 +730,7 @@ async def _resolve_which(self, ctx, term): await ctx.send(inline('No monster matched that query')) return None, None, None, None - name = nm.group_computed_basename.title() + name = nm.group_computed_treename.title() monster_id = nm.base_monster_no definition = self.settings.which().get(monster_id, None) timestamp = "2000-01-01" @@ -777,7 +777,7 @@ def which_to_text(self): nm = monster_id_to_named_monster(monster_id) if m is None or nm is None: continue - name = nm.group_computed_basename.title() + name = nm.group_computed_treename.title() grp = m.series.name monsters[grp].append(name) @@ -852,7 +852,7 @@ async def getwhich(self, ctx): w %= 10000 nm = monster_id_to_named_monster(w) - name = nm.group_computed_basename.title() + name = nm.group_computed_treename.title() result = self.settings.which()[w] if isinstance(result, list): @@ -878,7 +878,7 @@ async def debuglookup(self, ctx, *, term: str): """Shows why a query matches to a monster""" term = term.lower().replace('?', '') nm, err, deb = await lookup_named_monster(term) - base = nm.group_computed_basename.title() if nm else nm + base = nm.group_computed_treename.title() if nm else nm name = nm.name_en if nm else nm monster_id = nm.monster_id if nm else nm definition = self.settings.which().get(monster_id, None) @@ -1127,7 +1127,7 @@ async def get_guide_text(self, term: str): if nm is None: return None, None, 'No dungeon or monster matched that query' - name = nm.group_computed_basename.title() + name = nm.group_computed_treename.title() definition = self.settings.leaderGuide().get(str(nm.base_monster_no), None) if definition is None: return None, None, 'A monster matched that query but has no guide' @@ -1151,7 +1151,7 @@ def guide_to_text(self): nm = monster_id_to_named_monster(monster_id) if nm is None: continue - name = nm.group_computed_basename.title() + name = nm.group_computed_treename.title() msg += '\n**{}** :\n{}\n'.format(name, definition) return msg From 069a2478af70062879ec86a0bf743e2062001316 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Jan 2021 22:23:17 -0800 Subject: [PATCH 0349/1220] Create fir3 (#347) --- dadguide/dadguide.py | 6 +++--- padglobal/padglobal.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index ba8b1677..8299e2fb 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -102,7 +102,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): return async def create_index(self, accept_filter=None): - """Exported function that allows a client cog to create a monster index""" + """Exported function that allows a client cog to create an id1/2 monster index""" await self.wait_until_ready() return await MonsterIndex(self.database, self.nickname_overrides, @@ -111,9 +111,9 @@ async def create_index(self, accept_filter=None): accept_filter=accept_filter) async def create_index2(self): - """Exported function that allows a client cog to create a monster index""" + """Exported function that allows a client cog to create an id3 monster index""" await self.wait_until_ready() - return await MonsterIndex2(self.database.get_all_monsters(False), []) + return await MonsterIndex2(self.database.get_all_monsters(False), self.database) def get_monster(self, monster_id: int) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index a4bb2537..fd2d45a7 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -287,7 +287,7 @@ def list_or_none(l): await tsutils.await_and_remove(self.bot, sent_messages[-1], ctx.author, delete_msgs=sent_messages, timeout=30) - @commands.command() + @commands.command(aliases=['fir']) @is_padglobal_admin() async def forceindexreload(self, ctx): async with ctx.typing(): @@ -299,6 +299,15 @@ async def forceindexreload(self, ctx): await padinfo_cog.refresh_index() await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) + @commands.command(aliases=['fir3']) + @is_padglobal_admin() + async def forceindexreload3(self, ctx): + async with ctx.typing(): + start = time.perf_counter() + dadguide_cog = self.bot.get_cog('Dadguide') + dadguide_cog.index2 = await dadguide_cog.create_index2() + await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) + @commands.group(aliases=['pdg']) @is_padglobal_admin() async def padglobal(self, ctx): From aa832dd65822752b34834b3947770ef7f13a2848 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Jan 2021 22:26:53 -0800 Subject: [PATCH 0350/1220] Allow for multi-word tokens & add rarity prefix (#346) --- dadguide/monster_index.py | 41 +++++++++++++++++++++++++++++++++++--- dadguide/token_mappings.py | 18 +++++++---------- padinfo/padinfo.py | 11 +++++----- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a9a91e53..2b02d7c9 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,5 +1,9 @@ +import csv +import difflib +import io import re +import aiohttp from redbot.core.utils import AsyncIter from tsutils import aobject @@ -26,20 +30,20 @@ async def __ainit__(self, monsters, db): _, i, *_ = data + [None, None] if mid.isdigit() and not i: if " " in nick: - self.mwtokens.add(nick) + self.mwtokens.add(tuple(nick.split(" "))) self.idtonick[int(mid)].add(nick.replace(" ", "")) gnicks = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) for mid, nick, *data in gnicks: _, i, *_ = data + [None, None] if mid.isdigit() and not i: if " " in nick: - self.mwtokens.add(nick) + self.mwtokens.add(tuple(nick.split(" "))) self.idtognick[int(mid)].add(nick.replace(" ", "")) pnicks = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) for nick, _, sid, *_ in pnicks: if sid.isdigit(): if " " in nick: - self.mwtokens.add(nick) + self.mwtokens.add(tuple(nick.split(" "))) self.sidtopnick[int(sid)].add(nick.replace(" ", "")) self.manual, self.tokens, self.prefix = await self._build_monster_index(monsters) @@ -110,6 +114,9 @@ async def get_prefixes(self, m): for t in self.sidtopnick[m.series_id]: prefix.add(t) + # Rarity + prefix.add(str(m.rarity)+"*") + # Base if self.graph.monster_is_base(m): for t in EVO_PREFIX_MAP[EvoTypes.BASE]: @@ -192,6 +199,34 @@ async def get_prefixes(self, m): return prefix + def mwreplace(self, query): + tokens = [] + iprg = {} + s = 0 + mwts = sorted(self.mwtokens, key=len, reverse=True) + query = query.split() + for c1, token in enumerate(query): + if s: + s -= 1 + continue + for mwt in mwts: + if len(mwt) > len(query) - c1: + continue + for c2, t in enumerate(mwt): + if calc_ratio(query[c1 + c2], t) < .8: + break + else: + s = len(mwt) + tokens.append("".join(mwt)) + break + else: + tokens.append(token) + return " ".join(tokens) + + +def calc_ratio(s1, s2): + return difflib.SequenceMatcher(None, s1, s2).ratio() + # TODO: Move this to TSUtils async def sheet_to_reader(url): diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index e885318d..1f0659db 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -1,8 +1,5 @@ from collections import defaultdict from enum import Enum -import aiohttp -import csv -import io COLOR_MAP = {0: ('r', 'red', 'fire'), @@ -13,7 +10,7 @@ 6: ('x', 'none', 'null', 'nil')} SERIES_MAP = {130: ('halloween', 'hw', 'h'), - 136: ('xmas', 'christmas'), + 136: ('christmas', 'xmas'), 125: ('summer', 'beach'), 114: ('school', 'academy', 'gakuen'), 139: ('new years', 'ny'), @@ -21,8 +18,7 @@ 154: ('padr',), 175: ('valentines', 'vday', 'v'), 183: ('gh', 'gungho'), - 117: ('gh', 'gungho'), - 187: ('sam3', 'samurai3', 'samiii')} + 117: ('gh', 'gungho'),} @@ -45,13 +41,13 @@ class EvoTypes(Enum): EvoTypes.EVO: ('evo', 'evolved'), EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo'), - EvoTypes.TRANS: ('trans', 'transform', 'transformed'), - EvoTypes.AWOKEN: ('awo', 'a', 'awoken'), - EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'mega'), + EvoTypes.TRANS: ('transform', 'trans', 'transformed'), + EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), + EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma'), EvoTypes.REVO: ('revo', 'reincarnated'), EvoTypes.SREVO: ('srevo', 'super', 'sr'), - EvoTypes.PIXEL: ('p', 'pixel', 'dot'), - EvoTypes.NONPIXEL: ('np', 'nonpixel'), + EvoTypes.PIXEL: ('pixel', 'p', 'dot'), + EvoTypes.NONPIXEL: ('nonpixel', 'np'), EvoTypes.EQUIP: ('equip', 'assist')} diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 480e25a9..154d8b44 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1031,6 +1031,7 @@ async def findMonster3(self, query): query = rmdiacritics(query).lower() query = re.sub(r'\b([rbgldx]) ([rbgldx])\b', r'main_attr_\1 sub_attr_\2', query) + query = DGCOG.index2.mwreplace(query) query = query.split() base = False @@ -1081,15 +1082,13 @@ def calc_ratio(s1, s2): ftr.add(m) monstergen = ftr - # print({m: monsterscore[m] for m in monstergen}) - def matches(m, t): if len(t) < 6: if t in DGCOG.index2.prefix[m]: monsterscore[m] += 1 return True else: - dlm = difflib.get_close_matches(t, DGCOG.index2.prefix[m], n=10000, cutoff=.8) + dlm = difflib.get_close_matches(t, DGCOG.index2.prefix[m], n=1, cutoff=.8) if dlm: monsterscore[m] += max(calc_ratio(t, p) for p in dlm) return True @@ -1097,14 +1096,16 @@ def matches(m, t): monstergen_base = monstergen - monstergen = sum((list(DGCOG.database.graph.get_alt_monsters(m)) for m in monstergen), []) + monstergen = set(sum((list(DGCOG.database.graph.get_alt_monsters(m)) for m in monstergen), [])) for t in prefixes: monstergen = {m for m in monstergen if matches(m, t)} if not monstergen: return - monstergen = set(monstergen).intersection(monstergen_base) or monstergen + monstergen = monstergen.intersection(monstergen_base) or monstergen + + # print({m: monsterscore[m] for m in monstergen}) if not monstergen: return From f982aff2796287f62f2bbe77e21ef4897fe30b3b Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 3 Jan 2021 01:02:20 -0800 Subject: [PATCH 0351/1220] Fix issue with manual vs name token (#349) --- padinfo/padinfo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 154d8b44..e850e122 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1024,6 +1024,7 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): async def findMonster3(self, query): DGCOG = self.bot.get_cog("Dadguide") + await DGCOG.wait_until_ready() tm = importlib.import_module(DGCOG.__module__.rstrip("dadguide") + 'token_mappings') mi = importlib.import_module(DGCOG.__module__.rstrip("dadguide") + 'monster_index') if DGCOG is None: @@ -1069,7 +1070,7 @@ def calc_ratio(s1, s2): for match in ms: for m in DGCOG.index2.manual[match]: if m not in valid: - monsterscore[m] += calc_ratio(t, match) * 2 + monsterscore[m] += calc_ratio(t, match) + .001 valid.add(m) for m in DGCOG.index2.tokens[match]: if m not in valid: From 93a226aaa2cbdb2bf36cdcb5056e8bf6ae76ea2b Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 3 Jan 2021 12:17:31 -0800 Subject: [PATCH 0352/1220] Fix transforms in id1 (#350) --- dadguide/old_monster_index.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 553b7348..66e42ed4 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -65,8 +65,7 @@ async def __ainit__(self, monster_database: DbContext, nickname_overrides, treen base_monster = monster_database.graph.get_monster(base_id) series = base_monster.series group_treename_overrides = treename_overrides.get(base_id, []) - evolution_tree = [monster_database.graph.get_monster(m) for m in - monster_database.get_evolution_tree_ids(base_id)] + evolution_tree = [monster_database.graph.get_alt_monsters_by_id(base_id)] named_mg = NamedMonsterGroup(evolution_tree, group_treename_overrides) named_evolution_tree = [] for monster in evolution_tree: From 3cd92fc3ea1e9b71782f3bba2b0559b667b9e5bb Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 3 Jan 2021 13:00:33 -0800 Subject: [PATCH 0353/1220] Fix type of evolution_tree (#351) --- dadguide/old_monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 66e42ed4..d246fffc 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -65,7 +65,7 @@ async def __ainit__(self, monster_database: DbContext, nickname_overrides, treen base_monster = monster_database.graph.get_monster(base_id) series = base_monster.series group_treename_overrides = treename_overrides.get(base_id, []) - evolution_tree = [monster_database.graph.get_alt_monsters_by_id(base_id)] + evolution_tree = monster_database.graph.get_alt_monsters_by_id(base_id) named_mg = NamedMonsterGroup(evolution_tree, group_treename_overrides) named_evolution_tree = [] for monster in evolution_tree: From 0a11ae5e5cd8cf4ca8b078247c9697244ec7a178 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 3 Jan 2021 16:43:36 -0800 Subject: [PATCH 0354/1220] Fix transform monsters that have equips in id1 (#353) The query used before was counting transforms as separate base monsters in their own trees. So use our correct list of base monsters instead --- dadguide/old_monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index d246fffc..ae1171b9 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -21,7 +21,7 @@ async def __ainit__(self, monster_database: DbContext, nickname_overrides, treen panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory self.db_context = monster_database - base_monster_ids = monster_database.get_base_monster_ids() + base_monster_ids = monster_database.get_monsters_where(monster_database.graph.monster_is_base) self.attr_short_prefix_map = { Attribute.Fire: ['r'], From bec2e01cdb7614005485eef43b5937463b8a3b71 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 3 Jan 2021 17:37:27 -0800 Subject: [PATCH 0355/1220] [MonsterIndex1] Don't assume order from evolution_tree (#354) This fixes a bug where monsters with both transforms & equips were returning the equip instead of the transformed version in the tree because the transformed version was being incorrectly marked as low priority. The issue was occluded until now because we were manually assigning a nickname to literally every single monster with a transformation in its tree, but we deleted all of these transforms during id3 development & this bug surfaced suddenly. --- dadguide/old_monster_index.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index ae1171b9..8c746ed6 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -464,12 +464,13 @@ def pick_best_monster(self): class NamedMonsterGroup(object): def __init__(self, evolution_tree: list, treename_overrides: list): + base_monster = min(evolution_tree, key=lambda m: m.monster_id) + + self.is_low_priority = ( - self._is_low_priority_monster(evolution_tree[0]) + self._is_low_priority_monster(base_monster) or self._is_low_priority_group(evolution_tree)) - base_monster = evolution_tree[0] - self.group_size = len(evolution_tree) self.base_monster_no = base_monster.monster_id self.base_monster_no_na = base_monster.monster_no_na From 9def688dd38976b2df0fdbb7b3995bec7dec699d Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jan 2021 17:51:26 -0800 Subject: [PATCH 0356/1220] Add base rarity prefix to id3 (#360) --- dadguide/monster_index.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 2b02d7c9..fc4682e0 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -94,6 +94,8 @@ def _name_to_tokens(oname): async def get_prefixes(self, m): prefix = set() + basemon = self.graph.get_base_monster(m) + # Main Color for t in COLOR_MAP[m.attr1.value]: prefix.add(t) @@ -116,6 +118,7 @@ async def get_prefixes(self, m): # Rarity prefix.add(str(m.rarity)+"*") + prefix.add(str(basemon.rarity)+"*b") # Base if self.graph.monster_is_base(m): From 62963b1337dbc979e9f2db33ed06217c60dbf224 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jan 2021 17:57:37 -0800 Subject: [PATCH 0357/1220] Add typing to idtest run (#362) --- padinfo/padinfo.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e850e122..c7ace55b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -771,14 +771,15 @@ async def idt_run(self, ctx): c = 0 o = "" ml = len(max(suite, key=len)) + 2 - for q, r in suite.items(): - m = await self.findMonster3(q) - if m and m.monster_id != r['result'] or m is None and r['result'] >= 0: - reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' - q = '"' + q + '"' - o += f"{q.ljust(ml)} - Ex: {m and m.monster_id}, Ac: {r['result']}{reason}\n" - else: - c += 1 + async with ctx.typing(): + for q, r in suite.items(): + m = await self.findMonster3(q) + if m and m.monster_id != r['result'] or m is None and r['result'] >= 0: + reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' + q = '"' + q + '"' + o += f"{q.ljust(ml)} - Ex: {m and m.monster_id}, Ac: {r['result']}{reason}\n" + else: + c += 1 if c: o += f"\n\nTests complete. {c}/{len(suite)} succeeded." else: From df2c36c9e20040258390f152dd2380153ea658bf Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jan 2021 21:44:56 -0800 Subject: [PATCH 0358/1220] Use id3 for everything if turned on (#368) --- padinfo/padinfo.py | 69 ++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c7ace55b..af04db8b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -78,9 +78,9 @@ def _data_file(file_name: str) -> str: class IdEmojiUpdater(EmojiUpdater): - def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, - pad_info=None, selected_emoji=None, bot=None, - db_context: "DbContext" = None): + def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, selected_emoji=None, bot=None, + db_context: "DbContext" = None, **kwargs): + super().__init__(emoji_to_embed, **kwargs) self.ctx = ctx self.emoji_dict = emoji_to_embed self.m = m @@ -132,8 +132,9 @@ async def on_update(self, ctx, selected_emoji): class ScrollEmojiUpdater(EmojiUpdater): - def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, - ms: "list[int]" = None, selected_emoji=None, pad_info=None, bot=None): + def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, ms: "list[int]" = None, selected_emoji=None, + pad_info=None, bot=None, **kwargs): + super().__init__(emoji_to_embed, **kwargs) self.ctx = ctx self.emoji_dict = emoji_to_embed self.m = m @@ -267,7 +268,7 @@ def get_monster(self, monster_id: int): @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: await ctx.send(IdMenu.monster_header(m)) await ctx.send(box(m.name_ja)) @@ -302,7 +303,7 @@ async def idjp(self, ctx, *, query: str): await self._do_id(ctx, query, server_filter=ServerFilter.jp) async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = await self.findMonster(query, server_filter=server_filter) + m, err, debug_info = await self.findMonster1(query, server_filter=server_filter) if await self.config.do_survey(): asyncio.create_task(self.send_survey_after(ctx, query, m.name_en if m else "None")) @@ -395,7 +396,7 @@ async def _do_id3(self, ctx, query): @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): """Monster info (evolutions tab)""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: await self._do_idmenu(ctx, m, self.evo_emoji) else: @@ -405,7 +406,7 @@ async def evos(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: await self._do_idmenu(ctx, m, self.mats_emoji) else: @@ -415,7 +416,7 @@ async def evomats(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) if menu == EMBED_NOT_GENERATED: @@ -427,7 +428,7 @@ async def pantheon(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def skillups(self, ctx, *, query: str): """Monster info (evolutions tab)""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: menu = await self._do_idmenu(ctx, m, self.skillups_emoji) if menu == EMBED_NOT_GENERATED: @@ -553,7 +554,7 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): @checks.bot_has_permissions(embed_links=True) async def pic(self, ctx, *, query: str): """Monster info (full image tab)""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: await self._do_idmenu(ctx, m, self.pic_emoji) else: @@ -563,7 +564,7 @@ async def pic(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def links(self, ctx, *, query: str): """Monster links""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: menu = IdMenu(ctx) embed = await menu.make_links_embed(m) @@ -576,7 +577,7 @@ async def links(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: await self._do_idmenu(ctx, m, self.other_info_emoji) else: @@ -586,7 +587,7 @@ async def otherinfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: menu = IdMenu(ctx, allowed_emojis=self.get_emojis()) embed = await menu.make_header_embed(m) @@ -598,7 +599,7 @@ async def lookup(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): """Monster info (for all monsters in the evo tree)""" - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: await self._do_evolistmenu(ctx, m) else: @@ -609,7 +610,7 @@ async def evolist(self, ctx, *, query): async def seriesscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" DGCOG = self.bot.get_cog("Dadguide") - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) ms = DGCOG.database.get_monsters_by_series(m.series.series_id) ms.sort(key=lambda x: x.monster_id) @@ -633,7 +634,7 @@ async def evoscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" DGCOG = self.bot.get_cog("Dadguide") db_context: "DbContext" = DGCOG.database - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: await self._do_scrollmenu(ctx, m, sorted(db_context.graph.get_alt_monsters(m), key=lambda x: x.monster_id), @@ -679,9 +680,9 @@ async def leaderskill(self, ctx, *, whole_query): else: # no separators left_query, right_query = whole_query, None - left_m, left_err, _ = await self.findMonster(left_query) + left_m, left_err, _ = await self.findMonsterCustom(ctx, left_query) if right_query: - right_m, right_err, _ = await self.findMonster(right_query) + right_m, right_err, _ = await self.findMonsterCustom(ctx, right_query) else: right_m, right_err, = left_m, left_err @@ -707,7 +708,7 @@ async def leaderskillsingle(self, ctx, *, query): DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database - m, err, _ = await self.findMonster(query) + m, err, _ = await self.findMonsterCustom(ctx, query) if err: await ctx.send(err) return @@ -774,7 +775,7 @@ async def idt_run(self, ctx): async with ctx.typing(): for q, r in suite.items(): m = await self.findMonster3(q) - if m and m.monster_id != r['result'] or m is None and r['result'] >= 0: + if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' o += f"{q.ljust(ml)} - Ex: {m and m.monster_id}, Ac: {r['result']}{reason}\n" @@ -806,7 +807,7 @@ async def padsay(self, ctx, server, *, query: str = None): server = 'na' query = query.strip().lower() - m, err, debug_info = await self.findMonster(query) + m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: @@ -942,7 +943,7 @@ async def iddiff(self, ctx): s = 0 f = [] async for c, query in AsyncIter(enumerate(hist_aggreg)): - m1, err1, debug_info1 = await self.findMonster(query) + m1, err1, debug_info1 = await self.findMonsterCustom(ctx, query) m2, err2, debug_info2 = await self.findMonster2(query) if c % 50 == 0: await ctx.send(inline("{}/{} complete.".format(c, len(hist_aggreg)))) @@ -971,7 +972,17 @@ async def makeFailureMsg(self, ctx: discord.abc.Messageable, err): 'Unexpected results? Use ^helpid for more info.').format(err) await ctx.send(box(msg)) - async def findMonster(self, query, server_filter=ServerFilter.any): + async def findMonsterCustom(self, ctx, query, server_filter=ServerFilter.any): + if self.config.user(ctx.author).beta_id3(): + m = await self.findMonster3(query) + if m: + return m, "", "" + else: + return None, "Monster not found", "" + else: + return await self.findMonster1(query, server_filter) + + async def findMonster1(self, query, server_filter=ServerFilter.any): query = rmdiacritics(query) nm, err, debug_info = await self._findMonster(query, server_filter) @@ -987,11 +998,11 @@ async def _findMonster(self, query, server_filter=ServerFilter.any) -> "NamedMon while self.index_lock.locked(): await asyncio.sleep(1) - if server_filter == ServerFilter.any: + if server_filter.value == ServerFilter.any.value: monster_index = self.index_all - elif server_filter == ServerFilter.na: + elif server_filter.value == ServerFilter.na.value: monster_index = self.index_na - elif server_filter == ServerFilter.jp: + elif server_filter.value == ServerFilter.jp.value: monster_index = self.index_jp else: raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) @@ -1096,7 +1107,6 @@ def matches(m, t): return True return False - monstergen_base = monstergen monstergen = set(sum((list(DGCOG.database.graph.get_alt_monsters(m)) for m in monstergen), [])) @@ -1118,7 +1128,6 @@ def matches(m, t): m.rarity, m.monster_no_na)) - if base: return DGCOG.database.graph.get_base_monster(mon) else: From 005fd415faa8a77198472c29e09dd2c2e6e9ea0c Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jan 2021 21:50:04 -0800 Subject: [PATCH 0359/1220] Update Tokens Names (#369) * Removes support for multi-color searches with spaces (closes #365) * Also fixes 5* b g spectre (closes #364) * Consolidates all series names into gdoc (closes #366) --- dadguide/monster_index.py | 11 ++++++----- dadguide/token_mappings.py | 23 +++++------------------ padinfo/padinfo.py | 19 ++----------------- 3 files changed, 13 insertions(+), 40 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index fc4682e0..afd83027 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -22,7 +22,7 @@ async def __ainit__(self, monsters, db): self.idtonick = defaultdict(set) self.idtognick = defaultdict(set) - self.sidtopnick = defaultdict(set, {k: set(v) for k, v in SERIES_MAP.items()}) + self.sidtopnick = defaultdict(set) self.mwtokens = set() nicks = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) @@ -99,16 +99,17 @@ async def get_prefixes(self, m): # Main Color for t in COLOR_MAP[m.attr1.value]: prefix.add(t) - prefix.add('main_attr_' + t) # Sub Color - for t in COLOR_MAP[m.attr2.value]: - prefix.add('sub_attr_' + t) + if m.attr1.value == 6: + for t in COLOR_MAP[m.attr2.value]: + prefix.add(t) # Both Colors for t1 in COLOR_MAP[m.attr1.value]: for t2 in COLOR_MAP[m.attr2.value]: - prefix.add(t1 + t2) + if len(t1) + len(t2) == 2: + prefix.add(t1 + t2) prefix.add(t1 + "/" + t2) # Series diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 1f0659db..7f22e545 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -5,21 +5,9 @@ COLOR_MAP = {0: ('r', 'red', 'fire'), 1: ('b', 'blue', 'water'), 2: ('g', 'green', 'wood'), - 3: ('l', 'light', 'yellow', 'white'), - 4: ('d', 'dark', 'purple', 'black'), - 6: ('x', 'none', 'null', 'nil')} - -SERIES_MAP = {130: ('halloween', 'hw', 'h'), - 136: ('christmas', 'xmas'), - 125: ('summer', 'beach'), - 114: ('school', 'academy', 'gakuen'), - 139: ('new years', 'ny'), - 149: ('wedding', 'bride'), - 154: ('padr',), - 175: ('valentines', 'vday', 'v'), - 183: ('gh', 'gungho'), - 117: ('gh', 'gungho'),} - + 3: ('l', 'light', 'yellow'), + 4: ('d', 'dark', 'purple'), + 6: ('x', 'none', 'null', 'nil', 'white')} class EvoTypes(Enum): @@ -43,9 +31,9 @@ class EvoTypes(Enum): EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo'), EvoTypes.TRANS: ('transform', 'trans', 'transformed'), EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), - EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma'), + EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'mega awoken'), EvoTypes.REVO: ('revo', 'reincarnated'), - EvoTypes.SREVO: ('srevo', 'super', 'sr'), + EvoTypes.SREVO: ('srevo', 'super', 'sr', 'super reincarnated'), EvoTypes.PIXEL: ('pixel', 'p', 'dot'), EvoTypes.NONPIXEL: ('nonpixel', 'np'), EvoTypes.EQUIP: ('equip', 'assist')} @@ -63,7 +51,6 @@ class MiscPrefixes(Enum): PREFIX_MAPS = [ COLOR_MAP, - SERIES_MAP, EVO_PREFIX_MAP, MISC_PREFIX_MAP, ] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index af04db8b..6103dd6d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1037,21 +1037,13 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): async def findMonster3(self, query): DGCOG = self.bot.get_cog("Dadguide") await DGCOG.wait_until_ready() - tm = importlib.import_module(DGCOG.__module__.rstrip("dadguide") + 'token_mappings') - mi = importlib.import_module(DGCOG.__module__.rstrip("dadguide") + 'monster_index') if DGCOG is None: raise ValueError("Dadguide cog is not loaded") query = rmdiacritics(query).lower() - query = re.sub(r'\b([rbgldx]) ([rbgldx])\b', r'main_attr_\1 sub_attr_\2', query) query = DGCOG.index2.mwreplace(query) query = query.split() - base = False - if 'base' in query: - query.remove('base') - base = True - prefixes = set() name = set() valid_prefixes = {p for ps in DGCOG.index2.prefix.values() for p in ps} @@ -1089,11 +1081,7 @@ def calc_ratio(s1, s2): monsterscore[m] += calc_ratio(t, match) valid.add(m) - ftr = set() - for m in valid: - if m in monstergen: - ftr.add(m) - monstergen = ftr + monstergen.intersection_update(valid) def matches(m, t): if len(t) < 6: @@ -1128,10 +1116,7 @@ def matches(m, t): m.rarity, m.monster_no_na)) - if base: - return DGCOG.database.graph.get_base_monster(mon) - else: - return mon + return mon class PadInfoSettings(CogSettings): From 8d81700417ba0a20f4ee324e930ec947f67c80d1 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jan 2021 21:56:53 -0800 Subject: [PATCH 0360/1220] Add [p]pdb pipeline dungeonprocessor (#370) --- padguidedb/padguidedb.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 9b3d6443..00cd3f3d 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -34,6 +34,7 @@ def __init__(self, bot, *args, **kwargs): self.queue_size = 0 self.full_etl_lock = asyncio.Lock() + self.dungeon_lock = asyncio.Lock() self.extract_images_lock = asyncio.Lock() self.dungeon_load_lock = asyncio.Lock() @@ -247,6 +248,28 @@ async def fulletl(self, ctx): return await ctx.send(inline('Full ETL finished')) + @pipeline.command() + @is_padguidedb_admin() + async def processdungeon(self, ctx): + """Runs the dungeon processor script.""" + if self.dungeon_lock.locked(): + await ctx.send(inline('Dungeon processor already running')) + return + + async with self.dungeon_lock: + await ctx.send(inline('Running dungeon processor: this could take a while')) + process = await asyncio.create_subprocess_exec( + 'bash', + self.settings.dungeonProcessorFile(), + + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + for page in pagify(str(stderr)): + await ctx.send(box(page)) + await ctx.send(inline('Dungeon processing finished')) + @pipeline.command() @is_padguidedb_admin() async def extractimages(self, ctx): @@ -286,6 +309,13 @@ async def setfulletlfile(self, ctx, *, full_etl_file): self.settings.setFullETLFile(full_etl_file) await ctx.tick() + @padguidedb.command() + @checks.is_owner() + async def setdungeonprocessorfile(self, ctx, *, full_etl_file): + """Set the full ETL file.""" + self.settings.setDungeonProcessorFile(full_etl_file) + await ctx.tick() + @padguidedb.command() @checks.is_owner() async def setimageupdatefile(self, ctx, *, image_update_file): @@ -315,6 +345,7 @@ def make_default_settings(self): 'config_file': '', 'dungeon_script_file': '', 'full_etl_file': '', + 'dungeon_processor_file': '', 'update_image_file': '', 'python_executable': '/usr/bin/python3', 'users': {}, @@ -361,6 +392,13 @@ def setFullETLFile(self, full_etl_file): self.bot_settings['full_etl_file'] = full_etl_file self.save_settings() + def dungeonProcessorFile(self): + return self.bot_settings.get('dungeon_processor_file', '') + + def setDungeonProcessorFile(self, dungeon_processor_file): + self.bot_settings['dungeon_processor_file'] = dungeon_processor_file + self.save_settings() + def imageUpdateFile(self): return self.bot_settings.get('update_image_file', '') From c64c7334144981c245aead4c598fecdce4107ba9 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jan 2021 22:08:40 -0800 Subject: [PATCH 0361/1220] Await config value get (#372) Fixes a bug where everyone was forced to id3 --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6103dd6d..e75f4a3a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -973,7 +973,7 @@ async def makeFailureMsg(self, ctx: discord.abc.Messageable, err): await ctx.send(box(msg)) async def findMonsterCustom(self, ctx, query, server_filter=ServerFilter.any): - if self.config.user(ctx.author).beta_id3(): + if await self.config.user(ctx.author).beta_id3(): m = await self.findMonster3(query) if m: return m, "", "" From 918ea4132e2cd0238dbbc3f62b0d8c81ae6b6b68 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jan 2021 22:27:33 -0800 Subject: [PATCH 0362/1220] Add padglobal whichinvade (#373) Temporary command during mystics & spectres event --- padglobal/padglobal.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index fd2d45a7..8c7e2322 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1249,6 +1249,13 @@ async def rmleaderguide(self, ctx, monster_id: int): self.settings.rmLeaderGuide(name) await ctx.tick() + @commands.command(aliases=['currentinvade']) + async def whichinvade(self, ctx): + if datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=10))).strftime("%p") == "AM": + await ctx.send(self.c_commands['redinvadecurrent']) + else: + await ctx.send(self.c_commands['blueinvadecurrent']) + def emojify(self, message): emojis = list() emoteservers = self.settings.emojiServers() From 542d598b0bc30a7773631d8671e69f3955913466 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 5 Jan 2021 10:37:20 -0800 Subject: [PATCH 0363/1220] [PadGlobal] Give time until next invade in whichinvade (#377) --- padglobal/info.json | 3 ++- padglobal/padglobal.py | 35 +++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/padglobal/info.json b/padglobal/info.json index 036cbb7e..9d9bea25 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -22,7 +22,8 @@ "requirements": [ "tsutils", "aiohttp", - "prettytable" + "prettytable", + "pytz" ], "tags": [ "PAD" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8c7e2322..8bc8c281 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -5,6 +5,7 @@ import json import logging import os +import pytz import re import time from collections import defaultdict @@ -16,7 +17,7 @@ import tsutils from redbot.core import checks, data_manager from redbot.core import commands -from redbot.core.utils.chat_formatting import box, inline, pagify +from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json logger = logging.getLogger('red.padbot-cogs.padglobal') @@ -73,7 +74,7 @@ async def lookup_named_monster(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') if padinfo_cog is None: raise Exception("Cog not Loaded") - nm, err, debug_info = await padinfo_cog._findMonster(str(query)) + nm, err, debug_info = await padinfo_cog.findMonster1(str(query)) return nm, err, debug_info @@ -224,11 +225,11 @@ async def debugid(self, ctx, *, query): msg = "{}. {}".format(m.monster_no_na, m.name_en) msg += "\nLookup type: {}".format(debug_info) - def list_or_none(l): - if len(l) == 1: - return '\n\t{}'.format(''.join(l)) - elif len(l): - return '\n\t' + '\n\t'.join(sorted(l)) + def list_or_none(li): + if len(li) == 1: + return '\n\t{}'.format(''.join(li)) + elif len(li): + return '\n\t' + '\n\t'.join(sorted(li)) else: return 'NONE' @@ -359,7 +360,7 @@ async def _add(self, ctx, command, text, confirm=True): ted = self.c_commands[ted] alias = True if confirm: - conf = await confirm_message(ctx, "Are you sure you want to edit the {}command {}?" \ + conf = await confirm_message(ctx, "Are you sure you want to edit the {}command {}?" .format("alias to " if alias else "", ted)) if not conf: return @@ -956,7 +957,7 @@ def _get_emojis(self): for server_id in self.settings.emojiServers(): try: emojis.extend(self.bot.get_guild(int(server_id)).emojis) - except: + except Exception: pass return emojis @@ -998,6 +999,9 @@ async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): emoji_server = self.bot.get_guild(int(server_id)) if len(emoji_server.emojis) < 50: break + else: + await ctx.send("There is no room. Add a new emoji server to add more emoji.") + return try: async with aiohttp.ClientSession() as sess: @@ -1082,10 +1086,10 @@ async def get_prefix(self, message): for p in await self.bot.get_prefix(message): if message.content.startswith(p): return p - return False + return None def format_cc(self, command, message): - results = re.findall(r"\{([^}]+)\}", command) + results = re.findall(r"{([^}]+)}", command) for result in results: param = self.transform_parameter(result, message) command = command.replace("{" + result + "}", param) @@ -1251,10 +1255,17 @@ async def rmleaderguide(self, ctx, monster_id: int): @commands.command(aliases=['currentinvade']) async def whichinvade(self, ctx): - if datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=10))).strftime("%p") == "AM": + pst = pytz.timezone("America/Los_Angeles") + curtime = datetime.datetime.now(pst) + if datetime.time(6) < curtime.time() < datetime.time(18): await ctx.send(self.c_commands['redinvadecurrent']) + totime = curtime.replace(hour=18, minute=0, second=0, microsecond=0) else: await ctx.send(self.c_commands['blueinvadecurrent']) + totime = curtime.replace(hour=6, minute=0, second=0, microsecond=0) + if totime < curtime: + totime += datetime.timedelta(1) + await ctx.send(inline("Invade switches in: " + humanize_timedelta(timedelta=totime-curtime))) def emojify(self, message): emojis = list() From 95d142749cd41b35bce876d16a8790689eb7434f Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 5 Jan 2021 11:27:37 -0800 Subject: [PATCH 0364/1220] findMonster -> findMonster1 in padbuildimg (#375) This stops pdchu from being broken for now; later we'll want to make it use id3 though --- padbuildimg/padbuildimg.py | 4 ++-- padsearch/padsearch.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 00a33cbf..5134dd7f 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -507,7 +507,7 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card, err, debug_info = await self.padinfo_cog.findMonster(tok.value) + ass_card, err, debug_info = await self.padinfo_cog.findMonster1(tok.value) if ass_card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) elif tok.type == 'REPEAT': @@ -517,7 +517,7 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = await self.padinfo_cog.findMonster(tok.value) + card, err, debug_info = await self.padinfo_cog.findMonster1(tok.value) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) if not card.is_inheritable: diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index 5d61f059..e8070c69 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -814,7 +814,7 @@ def _make_search_config(self, input): @checks.is_owner() async def debugsearch(self, ctx, *, query): padinfo_cog = self.bot.get_cog('PadInfo') - m, err, debug_info = await padinfo_cog.findMonster(query) + m, err, debug_info = await padinfo_cog.findMonster1(query) if m is None: await ctx.send(box('No match: ' + err)) From c665a0a0a55b986c404fb230c8d90ada81170fb1 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 5 Jan 2021 15:36:40 -0800 Subject: [PATCH 0365/1220] Make id3 slightly faster (#376) --- dadguide/monster_index.py | 4 +++- padinfo/padinfo.py | 37 ++++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index afd83027..cfca1f21 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -47,6 +47,8 @@ async def __ainit__(self, monsters, db): self.sidtopnick[int(sid)].add(nick.replace(" ", "")) self.manual, self.tokens, self.prefix = await self._build_monster_index(monsters) + self.name_tokens = list(self.manual) + list(self.tokens) + self.all_prefixes = {p for ps in self.prefix.values() for p in ps} __init__ = __ainit__ @@ -207,7 +209,7 @@ def mwreplace(self, query): tokens = [] iprg = {} s = 0 - mwts = sorted(self.mwtokens, key=len, reverse=True) + mwts = sorted(self.mwtokens, key=lambda x: (len(x), len(''.join(x))), reverse=True) query = query.split() for c1, token in enumerate(query): if s: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e75f4a3a..db216a27 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -7,7 +7,7 @@ import random import re import urllib.parse -from collections import OrderedDict +from collections import OrderedDict, defaultdict from enum import Enum from io import BytesIO from typing import TYPE_CHECKING @@ -1046,11 +1046,10 @@ async def findMonster3(self, query): prefixes = set() name = set() - valid_prefixes = {p for ps in DGCOG.index2.prefix.values() for p in ps} for c, t in enumerate(query): - if difflib.get_close_matches(t, - [p for p in valid_prefixes if len(p) > 8], - n=1, cutoff=.8) or t in valid_prefixes: + if t in DGCOG.index2.all_prefixes or difflib.get_close_matches(t, + [p for p in DGCOG.index2.all_prefixes if len(p) > 8], + n=1, cutoff=.8): prefixes.add(t) else: name.add(t) @@ -1062,13 +1061,13 @@ def calc_ratio(s1, s2): # print(prefixes, name) - monsterscore = {m: 0 for m in DGCOG.database.get_all_monsters()} - monstergen = set(monsterscore) + monsterscore = defaultdict(int) + monstergen = None for t in name: valid = set() - ms = difflib.get_close_matches(t, list(DGCOG.index2.tokens) + list(DGCOG.index2.manual), n=10000, cutoff=.8) + ms = difflib.get_close_matches(t, DGCOG.index2.name_tokens, n=10000, cutoff=.8) if not ms: return for match in ms: @@ -1080,8 +1079,19 @@ def calc_ratio(s1, s2): if m not in valid: monsterscore[m] += calc_ratio(t, match) valid.add(m) + + if monstergen is not None: + monstergen.intersection_update(valid) + else: + monstergen = valid - monstergen.intersection_update(valid) + if monstergen is None: + monstergen = {*DGCOG.database.get_all_monsters()} + monstergen_evos = monstergen + else: + monstergen_evos = set() + for m in monstergen: + monstergen_evos.update(DGCOG.database.graph.get_alt_monsters(m)) def matches(m, t): if len(t) < 6: @@ -1095,15 +1105,12 @@ def matches(m, t): return True return False - monstergen_base = monstergen - monstergen = set(sum((list(DGCOG.database.graph.get_alt_monsters(m)) for m in monstergen), [])) - for t in prefixes: - monstergen = {m for m in monstergen if matches(m, t)} - if not monstergen: + monstergen_evos = {m for m in monstergen_evos if matches(m, t)} + if not monstergen_evos: return - monstergen = monstergen.intersection(monstergen_base) or monstergen + monstergen = monstergen_evos.intersection(monstergen) or monstergen_evos # print({m: monsterscore[m] for m in monstergen}) From 382164a125b8d3135738a292e64d94a273ea4774 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 5 Jan 2021 19:05:54 -0800 Subject: [PATCH 0366/1220] Fix which and debugid (#382) * Remove Padvision * Fix lookup_named_monster to return a NamedMonster --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8bc8c281..651f66a7 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -74,7 +74,7 @@ async def lookup_named_monster(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') if padinfo_cog is None: raise Exception("Cog not Loaded") - nm, err, debug_info = await padinfo_cog.findMonster1(str(query)) + nm, err, debug_info = await padinfo_cog._findMonster(str(query)) return nm, err, debug_info From 9d98e662605db103b3ac0f0b780980845ef7fd52 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 6 Jan 2021 02:51:53 -0800 Subject: [PATCH 0367/1220] Create debugid3 (#385) --- dadguide/monster_index.py | 52 ++++++++++++++++++++------------------ dadguide/token_mappings.py | 46 ++++++++++++++++++++++++--------- padinfo/padinfo.py | 16 ++++++++++++ 3 files changed, 77 insertions(+), 37 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index cfca1f21..e2b300aa 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -46,45 +46,43 @@ async def __ainit__(self, monsters, db): self.mwtokens.add(tuple(nick.split(" "))) self.sidtopnick[int(sid)].add(nick.replace(" ", "")) - self.manual, self.tokens, self.prefix = await self._build_monster_index(monsters) + self.manual = self.tokens = self.prefix = None + await self._build_monster_index(monsters) + self.manual = combine_tokens(self.manual_nick, self.manual_tree) self.name_tokens = list(self.manual) + list(self.tokens) self.all_prefixes = {p for ps in self.prefix.values() for p in ps} __init__ = __ainit__ async def _build_monster_index(self, monsters): - manual = defaultdict(set) - tokens = defaultdict(set) - prefix = defaultdict(set) + self.manual_nick = defaultdict(set) + self.manual_tree = defaultdict(set) + self.tokens = defaultdict(set) + self.prefix = defaultdict(set) async for m in AsyncIter(monsters): - prefix[m] = await self.get_prefixes(m) + self.prefix[m] = await self.get_prefixes(m) # ID - tokens[str(m.monster_id)].add(m) + self.tokens[str(m.monster_id)].add(m) # Name Tokens for token in self._name_to_tokens(m.name_en): - tokens[token.lower()].add(m) + self.tokens[token.lower()].add(m) for repl in TOKEN_REPLACEMENTS[token.lower()]: - tokens[repl].add(m) - for pm in PREFIX_MAPS: - for pas in pm.values(): - if token in pas: - prefix[m].update(pas) + self.tokens[repl].add(m) + for pas in PREFIX_MAPS.values(): + if token in pas: + self.prefix[m].update(pas) # Monster Nickname for nick in self.idtonick[m.monster_id]: - tokens[nick].add(m) - manual[nick].add(m) + self.manual_nick[nick].add(m) # Tree Nickname base_id = self.graph.get_base_id(m) for nick in self.idtognick[base_id]: - tokens[nick].add(m) - manual[nick].add(m) - - return manual, tokens, prefix + self.manual_tree[nick].add(m) @staticmethod def _name_to_tokens(oname): @@ -99,20 +97,17 @@ async def get_prefixes(self, m): basemon = self.graph.get_base_monster(m) # Main Color - for t in COLOR_MAP[m.attr1.value]: + for t in COLOR_MAP[Colors(m.attr1.value)]: prefix.add(t) # Sub Color if m.attr1.value == 6: - for t in COLOR_MAP[m.attr2.value]: + for t in COLOR_MAP[Colors(m.attr2.value)]: prefix.add(t) # Both Colors - for t1 in COLOR_MAP[m.attr1.value]: - for t2 in COLOR_MAP[m.attr2.value]: - if len(t1) + len(t2) == 2: - prefix.add(t1 + t2) - prefix.add(t1 + "/" + t2) + for t in DUAL_COLOR_MAP[(Colors(m.attr1.value), Colors(m.attr2.value))]: + prefix.add(t) # Series if m.series_id in self.sidtopnick: @@ -240,3 +235,10 @@ async def sheet_to_reader(url): async with session.get(url) as response: file = io.StringIO(await response.text()) return csv.reader(file, delimiter=',') + + +def combine_tokens(d1, d2): + do = defaultdict(set, d1) + for k, v in d2.items(): + do[k].update(v) + return do diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 7f22e545..bb78f9de 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -2,12 +2,33 @@ from enum import Enum -COLOR_MAP = {0: ('r', 'red', 'fire'), - 1: ('b', 'blue', 'water'), - 2: ('g', 'green', 'wood'), - 3: ('l', 'light', 'yellow'), - 4: ('d', 'dark', 'purple'), - 6: ('x', 'none', 'null', 'nil', 'white')} +class Colors(Enum): + RED = 0 + BLUE = 1 + GREEN = 2 + LIGHT = 3 + DARK = 4 + NIL = 6 + + +COLOR_MAP = {Colors.RED: ('r', 'red', 'fire'), + Colors.BLUE: ('b', 'blue', 'water'), + Colors.GREEN: ('g', 'green', 'wood'), + Colors.LIGHT: ('l', 'light', 'yellow'), + Colors.DARK: ('d', 'dark', 'purple'), + Colors.NIL: ('x', 'none', 'null', 'nil', 'white')} + + +DUAL_COLOR_MAP = {} +for cid1, cns1 in COLOR_MAP.items(): + for cid2, cns2 in COLOR_MAP.items(): + ts = () + for t1 in cns1: + for t2 in cns2: + if len(t1) + len(t2) == 2: + ts += (t1 + t2,) + ts += (t1 + "/" + t2,) + DUAL_COLOR_MAP[(cid1, cid2)] = ts class EvoTypes(Enum): @@ -34,7 +55,7 @@ class EvoTypes(Enum): EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'mega awoken'), EvoTypes.REVO: ('revo', 'reincarnated'), EvoTypes.SREVO: ('srevo', 'super', 'sr', 'super reincarnated'), - EvoTypes.PIXEL: ('pixel', 'p', 'dot'), + EvoTypes.PIXEL: ('pixel', 'p', 'dot', 'px'), EvoTypes.NONPIXEL: ('nonpixel', 'np'), EvoTypes.EQUIP: ('equip', 'assist')} @@ -49,11 +70,12 @@ class MiscPrefixes(Enum): MiscPrefixes.NONCHIBI: ('nonchibi', 'nc'), MiscPrefixes.FARMABLE: ('farmable', 'nrem')} -PREFIX_MAPS = [ - COLOR_MAP, - EVO_PREFIX_MAP, - MISC_PREFIX_MAP, -] +PREFIX_MAPS = { + **COLOR_MAP, + **DUAL_COLOR_MAP, + **EVO_PREFIX_MAP, + **MISC_PREFIX_MAP, +} TOKEN_REPLACEMENTS = defaultdict(tuple, { 'tamadra': ('tama',), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index db216a27..d3752b90 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1124,6 +1124,22 @@ def matches(m, t): m.monster_no_na)) return mon + + @commands.command() + async def debugid3(self, ctx, *, query): + DGCOG = self.bot.get_cog("Dadguide") + m = await self.findMonster3(query) + bm = DGCOG.database.graph.get_base_monster(m) + o = (f"{m.name_en} ({m.monster_id})\n" + f"Base: {bm.name_en} ({bm.monster_id})\n" + f"Series: {m.series.name_en} ({m.series_id})\n\n" + f"Name Tokens: {' '.join(t for t, ms in DGCOG.index2.tokens.items() if m in ms)}\n\n" + f"Manual Tokens: \n" + f"\tNicknames: {' '.join(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms)}\n" + f"\tTreenames: {' '.join(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms)}\n\n" + f"Prefix Tokens: {' '.join(DGCOG.index2.prefix[m])}\n") + for page in pagify(o): + await ctx.send(box(page)) class PadInfoSettings(CogSettings): From 3b4b8462c90535deb33f7bef06020566b9baca06 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 6 Jan 2021 03:18:00 -0800 Subject: [PATCH 0368/1220] Make debugid3 better (#388) * Create debugid3 * Make debugid3 better --- padinfo/padinfo.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d3752b90..650c84d2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1130,14 +1130,14 @@ async def debugid3(self, ctx, *, query): DGCOG = self.bot.get_cog("Dadguide") m = await self.findMonster3(query) bm = DGCOG.database.graph.get_base_monster(m) - o = (f"{m.name_en} ({m.monster_id})\n" - f"Base: {bm.name_en} ({bm.monster_id})\n" + o = (f"[{m.monster_id}] {m.name_en}\n" + f"Base: [{bm.monster_id}] {bm.name_en}\n" f"Series: {m.series.name_en} ({m.series_id})\n\n" - f"Name Tokens: {' '.join(t for t, ms in DGCOG.index2.tokens.items() if m in ms)}\n\n" + f"Name Tokens: {' '.join(sorted(t for t, ms in DGCOG.index2.tokens.items() if m in ms))}\n\n" f"Manual Tokens: \n" - f"\tNicknames: {' '.join(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms)}\n" - f"\tTreenames: {' '.join(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms)}\n\n" - f"Prefix Tokens: {' '.join(DGCOG.index2.prefix[m])}\n") + f"\tTreenames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms))}\n" + f"\tNicknames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms))}\n\n" + f"Prefix Tokens: {' '.join(sorted(DGCOG.index2.prefix[m]))}\n") for page in pagify(o): await ctx.send(box(page)) From 10ac67ce139369f624f73995d34b7cc6fe3ab8c7 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Wed, 6 Jan 2021 18:04:18 -0800 Subject: [PATCH 0369/1220] Refactoring findmonster3 --- dadguide/monster_index.py | 69 +++++++++++---------- padinfo/find_monster.py | 127 ++++++++++++++++++++++++++++++++++++-- padinfo/padinfo.py | 105 ++++++++----------------------- 3 files changed, 183 insertions(+), 118 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index e2b300aa..fa5efed1 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -20,37 +20,39 @@ class MonsterIndex2(aobject): async def __ainit__(self, monsters, db): self.graph = db.graph - self.idtonick = defaultdict(set) - self.idtognick = defaultdict(set) - self.sidtopnick = defaultdict(set) - self.mwtokens = set() + self.monster_id_to_nickname = defaultdict(set) + self.monster_id_to_treename = defaultdict(set) + self.series_id_to_pantheon_nickname = defaultdict(set) + self.multi_word_tokens = set() - nicks = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) - for nick, mid, *data in nicks: + nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) + for name, m_id, *data in nickname_data: _, i, *_ = data + [None, None] - if mid.isdigit() and not i: - if " " in nick: - self.mwtokens.add(tuple(nick.split(" "))) - self.idtonick[int(mid)].add(nick.replace(" ", "")) - gnicks = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) - for mid, nick, *data in gnicks: + if m_id.isdigit() and not i: + if " " in name: + self.multi_word_tokens.add(tuple(name.split(" "))) + self.monster_id_to_nickname[int(m_id)].add(name.replace(" ", "")) + + treenames_data = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) + for m_id, name, *data in treenames_data: _, i, *_ = data + [None, None] - if mid.isdigit() and not i: - if " " in nick: - self.mwtokens.add(tuple(nick.split(" "))) - self.idtognick[int(mid)].add(nick.replace(" ", "")) - pnicks = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) - for nick, _, sid, *_ in pnicks: + if m_id.isdigit() and not i: + if " " in name: + self.multi_word_tokens.add(tuple(name.split(" "))) + self.monster_id_to_treename[int(m_id)].add(name.replace(" ", "")) + + pantheon_data = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) + for name, _, sid, *_ in pantheon_data: if sid.isdigit(): - if " " in nick: - self.mwtokens.add(tuple(nick.split(" "))) - self.sidtopnick[int(sid)].add(nick.replace(" ", "")) + if " " in name: + self.multi_word_tokens.add(tuple(name.split(" "))) + self.series_id_to_pantheon_nickname[int(sid)].add(name.replace(" ", "")) - self.manual = self.tokens = self.prefix = None + self.manual = self.tokens = self.monster_prefixes = None await self._build_monster_index(monsters) self.manual = combine_tokens(self.manual_nick, self.manual_tree) self.name_tokens = list(self.manual) + list(self.tokens) - self.all_prefixes = {p for ps in self.prefix.values() for p in ps} + self.all_prefixes = {p for ps in self.monster_prefixes.values() for p in ps} __init__ = __ainit__ @@ -58,10 +60,10 @@ async def _build_monster_index(self, monsters): self.manual_nick = defaultdict(set) self.manual_tree = defaultdict(set) self.tokens = defaultdict(set) - self.prefix = defaultdict(set) + self.monster_prefixes = defaultdict(set) async for m in AsyncIter(monsters): - self.prefix[m] = await self.get_prefixes(m) + self.monster_prefixes[m] = await self.get_prefixes(m) # ID self.tokens[str(m.monster_id)].add(m) @@ -73,15 +75,15 @@ async def _build_monster_index(self, monsters): self.tokens[repl].add(m) for pas in PREFIX_MAPS.values(): if token in pas: - self.prefix[m].update(pas) + self.monster_prefixes[m].update(pas) # Monster Nickname - for nick in self.idtonick[m.monster_id]: + for nick in self.monster_id_to_nickname[m.monster_id]: self.manual_nick[nick].add(m) # Tree Nickname base_id = self.graph.get_base_id(m) - for nick in self.idtognick[base_id]: + for nick in self.monster_id_to_treename[base_id]: self.manual_tree[nick].add(m) @staticmethod @@ -110,8 +112,8 @@ async def get_prefixes(self, m): prefix.add(t) # Series - if m.series_id in self.sidtopnick: - for t in self.sidtopnick[m.series_id]: + if m.series_id in self.series_id_to_pantheon_nickname: + for t in self.series_id_to_pantheon_nickname[m.series_id]: prefix.add(t) # Rarity @@ -200,11 +202,10 @@ async def get_prefixes(self, m): return prefix - def mwreplace(self, query): + def tokenize_query(self, query): tokens = [] - iprg = {} s = 0 - mwts = sorted(self.mwtokens, key=lambda x: (len(x), len(''.join(x))), reverse=True) + mwts = sorted(self.multi_word_tokens, key=lambda x: (len(x), len(''.join(x))), reverse=True) query = query.split() for c1, token in enumerate(query): if s: @@ -222,8 +223,8 @@ def mwreplace(self, query): break else: tokens.append(token) - return " ".join(tokens) + return " ".join(tokens) def calc_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 2816d275..9d0cfb9a 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -1,22 +1,29 @@ import re +from collections import defaultdict + +from tsutils import rmdiacritics +from typing import List, Set +import difflib COLOR_MAP = {} -for r in ('r','red','fire'): COLOR_MAP[r] = 'r' -for b in ('b','blue','water'): COLOR_MAP[b] = 'b' -for g in ('g','green','wood'): COLOR_MAP[g] = 'g' -for l in ('l','light','yellow','white'): COLOR_MAP[l] = 'l' -for d in ('d','dark','purple','black'): COLOR_MAP[d] = 'd' -for x in ('x','none','null','nil'): COLOR_MAP[x] = 'x' +for r in ('r', 'red', 'fire'): COLOR_MAP[r] = 'r' +for b in ('b', 'blue', 'water'): COLOR_MAP[b] = 'b' +for g in ('g', 'green', 'wood'): COLOR_MAP[g] = 'g' +for l in ('l', 'light', 'yellow', 'white'): COLOR_MAP[l] = 'l' +for d in ('d', 'dark', 'purple', 'black'): COLOR_MAP[d] = 'd' +for x in ('x', 'none', 'null', 'nil'): COLOR_MAP[x] = 'x' COLORS = 'rbgld?x' + def prefix_to_filter(prefix): prefix = prefix.lower() # light, nil, wood if prefix in COLOR_MAP: m = COLORS.index(COLOR_MAP[prefix]) + def filter_via_attribute(monster): return monster.attr1.value == m @@ -24,6 +31,114 @@ def filter_via_attribute(monster): match = re.match(r'^({0})/?({0})$'.format('|'.join(COLOR_MAP)), prefix) if match: m, s = [COLORS.index(COLOR_MAP[a]) for a in match.groups()] + def filter_via_attributes(monster): return monster.attr1.value == m and monster.attr2.value == s + return filter_via_attributes + + +def calc_ratio(s1, s2): + return difflib.SequenceMatcher(None, s1, s2).ratio() + + +class FindMonster: + def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): + result = [] + s = 0 + multi_word_tokens_sorted = sorted(valid_multi_word_tokens, + key=lambda x: (len(x), len(''.join(x))), + reverse=True) + for c1, token in enumerate(tokens): + if s: + s -= 1 + continue + for mwt in multi_word_tokens_sorted: + if len(mwt) > len(tokens) - c1: + continue + for c2, t in enumerate(mwt): + if calc_ratio(tokens[c1 + c2], t) < .8: + break + else: + s = len(mwt) + result.append("".join(mwt)) + break + else: + result.append(token) + return result + + def _monster_has_token(self, monster, token, monsterscore, prefixes_for_monster): + if len(token) < 6: + if token in prefixes_for_monster: + monsterscore[monster] += 1 + return True + else: + dlm = difflib.get_close_matches(token, prefixes_for_monster, n=1, cutoff=.8) + if dlm: + monsterscore[monster] += max(calc_ratio(token, p) for p in dlm) + return True + return False + + def interpret_query(self, raw_query: str, valid_multi_word_tokens, all_prefixes) -> (Set[str], Set[str]): + tokenized_query = raw_query.split() + tokenized_query = self._merge_multi_word_tokens(tokenized_query, valid_multi_word_tokens) + + prefixes = set() + name = set() + longer_prefixes = [p for p in all_prefixes if len(p) > 8] + for i, token in enumerate(tokenized_query): + if token in all_prefixes or difflib.get_close_matches(token, longer_prefixes, n=1, cutoff=.8): + prefixes.add(token) + else: + name.add(token) + name.update(tokenized_query[i + 1:]) + break + + return prefixes, name + + def process_name_tokens(self, name_query_tokens, all_monster_name_tokens, all_name_override_tokens, + all_name_tokens): + monstergen = None + monsterscore = defaultdict(int) + + for t in name_query_tokens: + valid = set() + + ms = difflib.get_close_matches(t, all_monster_name_tokens, n=10000, cutoff=.8) + if not ms: + return None, None, True + for match in ms: + for m in all_name_override_tokens[match]: + if m not in valid: + monsterscore[m] += calc_ratio(t, match) + .001 + valid.add(m) + for m in all_name_tokens[match]: + if m not in valid: + monsterscore[m] += calc_ratio(t, match) + valid.add(m) + + if monstergen is not None: + monstergen.intersection_update(valid) + else: + monstergen = valid + + return monstergen, monsterscore, False + + def process_prefix_tokens(self, prefix_query_tokens, monsterscore, potential_evos, monster_prefixes): + for t in prefix_query_tokens: + potential_evos = {m for m in potential_evos if self._monster_has_token(m, t, monsterscore, monster_prefixes[m])} + if not potential_evos: + return None, True + + return potential_evos, False + + def get_monster_evos(self, database, monster_gen): + if monster_gen is None: + # This happens when no name tokens were specified, and we're searching over the entire world + return {*database.get_all_monsters()}, None + + monster_evos = set() + for m in monster_gen: + monster_evos.update(database.graph.get_alt_monsters(m)) + + return monster_evos diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 650c84d2..bc706c74 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -19,6 +19,7 @@ from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor +from .find_monster import FindMonster from .id_menu import IdMenu if TYPE_CHECKING: @@ -326,7 +327,7 @@ async def send_survey_after(self, ctx, query, result): elif userres is False: await self.config.bad.set(await self.config.bad() + 1) m = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" - f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idset" + f" by filling out this survey!\nPRO TIP: Use `{ctx.monster_prefixes}idset" f" survey` to adjust how often this shows.\n\n<{url}>") await asyncio.sleep(15) await m.delete() @@ -523,7 +524,7 @@ async def _do_evolistmenu(self, ctx, sm): chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" if idx > 19: await ctx.send( - "There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.prefix)) + "There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.monster_prefixes)) return else: emoji = char_to_emoji(chars[idx]) @@ -1034,97 +1035,43 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster2(query) - async def findMonster3(self, query): + async def findMonster3(self, raw_query): DGCOG = self.bot.get_cog("Dadguide") await DGCOG.wait_until_ready() if DGCOG is None: raise ValueError("Dadguide cog is not loaded") - query = rmdiacritics(query).lower() - query = DGCOG.index2.mwreplace(query) - query = query.split() - - prefixes = set() - name = set() - for c, t in enumerate(query): - if t in DGCOG.index2.all_prefixes or difflib.get_close_matches(t, - [p for p in DGCOG.index2.all_prefixes if len(p) > 8], - n=1, cutoff=.8): - prefixes.add(t) - else: - name.add(t) - name.update(query[c + 1:]) - break - - def calc_ratio(s1, s2): - return difflib.SequenceMatcher(None, s1, s2).ratio() - - # print(prefixes, name) - - monsterscore = defaultdict(int) - monstergen = None - - for t in name: - valid = set() - - ms = difflib.get_close_matches(t, DGCOG.index2.name_tokens, n=10000, cutoff=.8) - if not ms: - return - for match in ms: - for m in DGCOG.index2.manual[match]: - if m not in valid: - monsterscore[m] += calc_ratio(t, match) + .001 - valid.add(m) - for m in DGCOG.index2.tokens[match]: - if m not in valid: - monsterscore[m] += calc_ratio(t, match) - valid.add(m) - - if monstergen is not None: - monstergen.intersection_update(valid) - else: - monstergen = valid - - if monstergen is None: - monstergen = {*DGCOG.database.get_all_monsters()} - monstergen_evos = monstergen - else: - monstergen_evos = set() - for m in monstergen: - monstergen_evos.update(DGCOG.database.graph.get_alt_monsters(m)) - - def matches(m, t): - if len(t) < 6: - if t in DGCOG.index2.prefix[m]: - monsterscore[m] += 1 - return True - else: - dlm = difflib.get_close_matches(t, DGCOG.index2.prefix[m], n=1, cutoff=.8) - if dlm: - monsterscore[m] += max(calc_ratio(t, p) for p in dlm) - return True - return False - - for t in prefixes: - monstergen_evos = {m for m in monstergen_evos if matches(m, t)} - if not monstergen_evos: - return + raw_query = rmdiacritics(raw_query).lower() + find_monster = FindMonster() + prefix_tokens, name_query_tokens = find_monster.interpret_query(raw_query, DGCOG.index2.multi_word_tokens, DGCOG.index2.all_prefixes) - monstergen = monstergen_evos.intersection(monstergen) or monstergen_evos + monster_gen, monster_score, err = find_monster.process_name_tokens( + name_query_tokens, DGCOG.index2.name_tokens, DGCOG.index2.manual, DGCOG.index2.tokens) + if err: + # failed to recognize any name token + return - # print({m: monsterscore[m] for m in monstergen}) + # Expand search to the evo tree + potential_evos = find_monster.get_monster_evos(DGCOG.database, monster_gen) + potential_evos, err = find_monster.process_prefix_tokens(prefix_tokens, monster_score, potential_evos, DGCOG.index2.monster_prefixes) + if err: + # no prefixes match any monster in the evo tree + return - if not monstergen: + monster_gen = potential_evos.intersection(monster_gen) or potential_evos + if not monster_gen: + # No results including the monster itself. return - mon = max(monstergen, key=lambda m: (monsterscore[m], + # Return most likely candidate based on query. + mon = max(monster_gen, key=lambda m: (monster_score[m], not m.is_equip, -DGCOG.database.graph.get_base_id(m), m.rarity, m.monster_no_na)) return mon - + @commands.command() async def debugid3(self, ctx, *, query): DGCOG = self.bot.get_cog("Dadguide") @@ -1137,10 +1084,12 @@ async def debugid3(self, ctx, *, query): f"Manual Tokens: \n" f"\tTreenames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms))}\n" f"\tNicknames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms))}\n\n" - f"Prefix Tokens: {' '.join(sorted(DGCOG.index2.prefix[m]))}\n") + f"Prefix Tokens: {' '.join(sorted(DGCOG.index2.monster_prefixes[m]))}\n") for page in pagify(o): await ctx.send(box(page)) +def calc_ratio(s1, s2): + return difflib.SequenceMatcher(None, s1, s2).ratio() class PadInfoSettings(CogSettings): def make_default_settings(self): From 98b6114e3ad1b3e631126b7ab08170b535ef7738 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 6 Jan 2021 20:10:00 -0800 Subject: [PATCH 0370/1220] Fix issues in id3 refactor (#395) * Fix issues in id3 refactor * Remove unneccessary code * Add comments --- dadguide/monster_index.py | 25 +---------------- padinfo/find_monster.py | 58 ++++++++------------------------------- padinfo/padinfo.py | 49 ++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 92 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index fa5efed1..bd84c8cb 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -48,7 +48,7 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.split(" "))) self.series_id_to_pantheon_nickname[int(sid)].add(name.replace(" ", "")) - self.manual = self.tokens = self.monster_prefixes = None + self.manual = self.tokens = self.monster_prefixes = defaultdict(set) await self._build_monster_index(monsters) self.manual = combine_tokens(self.manual_nick, self.manual_tree) self.name_tokens = list(self.manual) + list(self.tokens) @@ -202,29 +202,6 @@ async def get_prefixes(self, m): return prefix - def tokenize_query(self, query): - tokens = [] - s = 0 - mwts = sorted(self.multi_word_tokens, key=lambda x: (len(x), len(''.join(x))), reverse=True) - query = query.split() - for c1, token in enumerate(query): - if s: - s -= 1 - continue - for mwt in mwts: - if len(mwt) > len(query) - c1: - continue - for c2, t in enumerate(mwt): - if calc_ratio(query[c1 + c2], t) < .8: - break - else: - s = len(mwt) - tokens.append("".join(mwt)) - break - else: - tokens.append(token) - - return " ".join(tokens) def calc_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 9d0cfb9a..c25cab86 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -5,38 +5,6 @@ from typing import List, Set import difflib -COLOR_MAP = {} - -for r in ('r', 'red', 'fire'): COLOR_MAP[r] = 'r' -for b in ('b', 'blue', 'water'): COLOR_MAP[b] = 'b' -for g in ('g', 'green', 'wood'): COLOR_MAP[g] = 'g' -for l in ('l', 'light', 'yellow', 'white'): COLOR_MAP[l] = 'l' -for d in ('d', 'dark', 'purple', 'black'): COLOR_MAP[d] = 'd' -for x in ('x', 'none', 'null', 'nil'): COLOR_MAP[x] = 'x' - -COLORS = 'rbgld?x' - - -def prefix_to_filter(prefix): - prefix = prefix.lower() - - # light, nil, wood - if prefix in COLOR_MAP: - m = COLORS.index(COLOR_MAP[prefix]) - - def filter_via_attribute(monster): - return monster.attr1.value == m - - # light/red, l/r, yellow/fire - match = re.match(r'^({0})/?({0})$'.format('|'.join(COLOR_MAP)), prefix) - if match: - m, s = [COLORS.index(COLOR_MAP[a]) for a in match.groups()] - - def filter_via_attributes(monster): - return monster.attr1.value == m and monster.attr2.value == s - - return filter_via_attributes - def calc_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() @@ -57,7 +25,7 @@ def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): if len(mwt) > len(tokens) - c1: continue for c2, t in enumerate(mwt): - if calc_ratio(tokens[c1 + c2], t) < .8: + if (tokens[c1 + c2] != t and len(t) < 5) or calc_ratio(tokens[c1 + c2], t) < .8: break else: s = len(mwt) @@ -96,23 +64,22 @@ def interpret_query(self, raw_query: str, valid_multi_word_tokens, all_prefixes) return prefixes, name - def process_name_tokens(self, name_query_tokens, all_monster_name_tokens, all_name_override_tokens, - all_name_tokens): + def process_name_tokens(self, name_query_tokens, index2): monstergen = None monsterscore = defaultdict(int) for t in name_query_tokens: valid = set() - ms = difflib.get_close_matches(t, all_monster_name_tokens, n=10000, cutoff=.8) + ms = difflib.get_close_matches(t, index2.name_tokens, n=10000, cutoff=.8) if not ms: - return None, None, True + return None, None for match in ms: - for m in all_name_override_tokens[match]: + for m in index2.manual[match]: if m not in valid: monsterscore[m] += calc_ratio(t, match) + .001 valid.add(m) - for m in all_name_tokens[match]: + for m in index2.tokens[match]: if m not in valid: monsterscore[m] += calc_ratio(t, match) valid.add(m) @@ -122,23 +89,22 @@ def process_name_tokens(self, name_query_tokens, all_monster_name_tokens, all_na else: monstergen = valid - return monstergen, monsterscore, False + return monstergen, monsterscore def process_prefix_tokens(self, prefix_query_tokens, monsterscore, potential_evos, monster_prefixes): for t in prefix_query_tokens: potential_evos = {m for m in potential_evos if self._monster_has_token(m, t, monsterscore, monster_prefixes[m])} if not potential_evos: - return None, True + return None - return potential_evos, False + return potential_evos def get_monster_evos(self, database, monster_gen): - if monster_gen is None: - # This happens when no name tokens were specified, and we're searching over the entire world - return {*database.get_all_monsters()}, None - monster_evos = set() for m in monster_gen: monster_evos.update(database.graph.get_alt_monsters(m)) return monster_evos + + +find_monster = FindMonster() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bc706c74..8443f454 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,6 +1,5 @@ import asyncio import difflib -import importlib import json import logging import os @@ -19,7 +18,7 @@ from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor -from .find_monster import FindMonster +from .find_monster import find_monster from .id_menu import IdMenu if TYPE_CHECKING: @@ -327,7 +326,7 @@ async def send_survey_after(self, ctx, query, result): elif userres is False: await self.config.bad.set(await self.config.bad() + 1) m = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" - f" by filling out this survey!\nPRO TIP: Use `{ctx.monster_prefixes}idset" + f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idset" f" survey` to adjust how often this shows.\n\n<{url}>") await asyncio.sleep(15) await m.delete() @@ -524,7 +523,7 @@ async def _do_evolistmenu(self, ctx, sm): chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" if idx > 19: await ctx.send( - "There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.monster_prefixes)) + "There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.prefix)) return else: emoji = char_to_emoji(chars[idx]) @@ -1042,33 +1041,37 @@ async def findMonster3(self, raw_query): raise ValueError("Dadguide cog is not loaded") raw_query = rmdiacritics(raw_query).lower() - find_monster = FindMonster() - prefix_tokens, name_query_tokens = find_monster.interpret_query(raw_query, DGCOG.index2.multi_word_tokens, DGCOG.index2.all_prefixes) + prefix_tokens, name_query_tokens = find_monster.interpret_query(raw_query, DGCOG.index2.multi_word_tokens, + DGCOG.index2.all_prefixes) - monster_gen, monster_score, err = find_monster.process_name_tokens( - name_query_tokens, DGCOG.index2.name_tokens, DGCOG.index2.manual, DGCOG.index2.tokens) - if err: - # failed to recognize any name token - return + # print(prefix_tokens, name_query_tokens) + + if name_query_tokens: + monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) + if monster_gen is None: + # No monsters match the given name tokens + return + else: + # There are no name tokens in the query + monster_gen = {*DGCOG.database.get_all_monsters()} + monster_score = defaultdict(int) # Expand search to the evo tree - potential_evos = find_monster.get_monster_evos(DGCOG.database, monster_gen) - potential_evos, err = find_monster.process_prefix_tokens(prefix_tokens, monster_score, potential_evos, DGCOG.index2.monster_prefixes) - if err: + monster_gen = find_monster.get_monster_evos(DGCOG.database, monster_gen) + monster_gen = find_monster.process_prefix_tokens(prefix_tokens, monster_score, monster_gen, + DGCOG.index2.monster_prefixes) + if monster_gen is None: # no prefixes match any monster in the evo tree return - monster_gen = potential_evos.intersection(monster_gen) or potential_evos - if not monster_gen: - # No results including the monster itself. - return + # print(monster_gen) # Return most likely candidate based on query. mon = max(monster_gen, key=lambda m: (monster_score[m], - not m.is_equip, - -DGCOG.database.graph.get_base_id(m), - m.rarity, - m.monster_no_na)) + not m.is_equip, + -DGCOG.database.graph.get_base_id(m), + m.rarity, + m.monster_no_na)) return mon @@ -1088,9 +1091,11 @@ async def debugid3(self, ctx, *, query): for page in pagify(o): await ctx.send(box(page)) + def calc_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() + class PadInfoSettings(CogSettings): def make_default_settings(self): config = { From fa2ed6c2f48cd8bb1792900078734d53a461dc4d Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Thu, 7 Jan 2021 14:52:46 +0800 Subject: [PATCH 0371/1220] Moving MonsterStats to separate file and implementing statLatents (#397) --- dadguide/models/monster_model.py | 32 ++-------------- dadguide/models/monster_stats.py | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 dadguide/models/monster_stats.py diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 77c7619d..4110082d 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -10,6 +10,8 @@ from collections import defaultdict import romkan +from .monster_stats import monster_stats + class MonsterModel(BaseModel): def __init__(self, **m): @@ -134,36 +136,10 @@ def history_us(self): return '[{}] New Added'.format(self.reg_date) def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): - s_min = float(self.stat_values[key]['min']) - s_max = float(self.stat_values[key]['max']) - if self.level > 1: - scale = self.stat_values[key]['scale'] - s_val = s_min + (s_max - s_min) * ((min(lv, self.level) - 1) / (self.level - 1)) ** scale - else: - s_val = s_min - if lv > 99: - s_val *= 1 + (self.limit_mult / 11 * (lv - 99)) / 100 - plus_dict = {'hp': 10, 'atk': 5, 'rcv': 3} - s_val += plus_dict[key] * max(min(plus, 99), 0) - if inherit: - inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} - if not is_plus_297: - s_val -= plus_dict[key] * max(min(plus, 99), 0) - s_val *= inherit_dict[key] - return int(round(s_val)) + return monster_stats.stat(self, key, lv, plus=plus, inherit=inherit, is_plus_297=is_plus_297) def stats(self, lv=99, plus=0, inherit=False): - is_plus_297 = False - if plus == 297: - plus = (99, 99, 99) - is_plus_297 = True - elif plus == 0: - plus = (0, 0, 0) - hp = self.stat('hp', lv, plus[0], inherit, is_plus_297) - atk = self.stat('atk', lv, plus[1], inherit, is_plus_297) - rcv = self.stat('rcv', lv, plus[2], inherit, is_plus_297) - weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) - return hp, atk, rcv, weighted + return monster_stats.stats(self, lv, plus=plus, inherit=inherit) @staticmethod def make_roma_subname(name_ja): diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py new file mode 100644 index 00000000..3a77e5ea --- /dev/null +++ b/dadguide/models/monster_stats.py @@ -0,0 +1,64 @@ +from typing import Literal + +StatType = Literal['hp', 'atk', 'rcv'] + + +class MonsterStatLatentInput: + NUM_ATK = 0 + NUM_HP = 0 + NUM_RCV = 0 + NUM_ATKpp = 0 + NUM_HPpp = 0 + NUM_RCVpp = 0 + NUM_ALL_STAT = 0 + + def get_multiplier(self, key: StatType): + if key == 'hp': + return 1 + self.NUM_HP * 0.015 + self.NUM_HPpp * 0.045 + self.NUM_ALL_STAT * 0.03 + elif key == 'atk': + return 1 + self.NUM_ATK * 0.01 + self.NUM_ATKpp * 0.03 + self.NUM_ALL_STAT * 0.02 + elif key == 'rcv': + return 1 + self.NUM_RCV * 0.1 + self.NUM_RCVpp * 0.3 + self.NUM_ALL_STAT * 0.2 + return 1 + + +class MonsterStats: + def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus_297=True, + stat_latents: MonsterStatLatentInput = None): + s_min = float(monster_model.stat_values[key]['min']) + s_max = float(monster_model.stat_values[key]['max']) + if monster_model.level > 1: + scale = monster_model.stat_values[key]['scale'] + s_val = s_min + (s_max - s_min) * ((min(lv, monster_model.level) - 1) / (monster_model.level - 1)) ** scale + else: + s_val = s_min + if lv > 99: + s_val *= 1 + (monster_model.limit_mult / 11 * (lv - 99)) / 100 + plus_dict = {'hp': 10, 'atk': 5, 'rcv': 3} + + if stat_latents: + s_val *= stat_latents.get_multiplier(key) + + s_val += plus_dict[key] * max(min(plus, 99), 0) + if inherit: + inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} + if not is_plus_297: + s_val -= plus_dict[key] * max(min(plus, 99), 0) + s_val *= inherit_dict[key] + return int(round(s_val)) + + def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatLatentInput = None): + is_plus_297 = False + if plus == 297: + plus = (99, 99, 99) + is_plus_297 = True + elif plus == 0: + plus = (0, 0, 0) + hp = self.stat(monster_model, 'hp', lv, plus[0], inherit, is_plus_297, stat_latents) + atk = self.stat(monster_model, 'atk', lv, plus[1], inherit, is_plus_297, stat_latents) + rcv = self.stat(monster_model, 'rcv', lv, plus[2], inherit, is_plus_297, stat_latents) + weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) + return hp, atk, rcv, weighted + + +monster_stats = MonsterStats() From 970b57fe944d1fdf4744508793a66173d00cb1aa Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Thu, 7 Jan 2021 16:38:10 +0800 Subject: [PATCH 0372/1220] Calculating stats based on voice awakening (#400) --- dadguide/dadguide.py | 4 ++ dadguide/models/monster_model.py | 4 ++ dadguide/models/monster_stats.py | 79 ++++++++++++++++++++++++-------- padinfo/id_menu.py | 3 +- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 8299e2fb..b2825bec 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -20,6 +20,7 @@ from redbot.core import commands from .database_manager import * +from .models.monster_stats import monster_stats, MonsterStatModifierInput from .old_monster_index import MonsterIndex from .monster_index import MonsterIndex2 from .database_loader import load_database @@ -81,6 +82,9 @@ def __init__(self, bot, *args, **kwargs): self.index = None # type: MonsterIndex self.index2 = None # type: MonsterIndex2 + self.monster_stats = monster_stats + self.MonsterStatLatentInput = MonsterStatModifierInput + async def wait_until_ready(self): """Wait until the Dadguide cog is ready. diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 4110082d..f38a83be 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -135,6 +135,10 @@ def awakening_restricted_latents(self): def history_us(self): return '[{}] New Added'.format(self.reg_date) + @property + def has_voice(self): + return any([x for x in self.awakenings if x.awoken_skill_id == 63]) + def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): return monster_stats.stat(self, key, lv, plus=plus, inherit=inherit, is_plus_297=is_plus_297) diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 3a77e5ea..8691fd1f 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -3,7 +3,7 @@ StatType = Literal['hp', 'atk', 'rcv'] -class MonsterStatLatentInput: +class MonsterStatModifierInput: NUM_ATK = 0 NUM_HP = 0 NUM_RCV = 0 @@ -11,20 +11,66 @@ class MonsterStatLatentInput: NUM_HPpp = 0 NUM_RCVpp = 0 NUM_ALL_STAT = 0 + NUM_ATK_AWAKENING = 0 + NUM_HP_AWAKENING = 0 + NUM_RCV_AWAKENING = 0 - def get_multiplier(self, key: StatType): + def __init__(self, num_atk=0, num_hp=0, num_rcv=0, num_atkpp=0, num_hppp=0, num_rcvpp=0, num_all_stat=0, + num_atk_awakening=0, num_hp_awakening=0, num_rcv_awakening=0, num_voice_awakening=0): + self.NUM_ATK = num_atk + self.NUM_HP = num_hp + self.NUM_RCV = num_rcv + self.NUM_ATKpp = num_atkpp + self.NUM_HPpp = num_hppp + self.NUM_RCVpp = num_rcvpp + self.NUM_ALL_STAT = num_all_stat + self.NUM_ATK_AWAKENING = num_atk_awakening + self.NUM_HP_AWAKENING = num_hp_awakening + self.NUM_RCV_AWAKENING = num_rcv_awakening + self.NUM_VOICE_AWAKENING = num_voice_awakening + + def get_latent_multiplier(self, key: StatType): + if key == 'hp': + return self.NUM_HP * 0.015 + self.NUM_HPpp * 0.045 + self.NUM_ALL_STAT * 0.03 + elif key == 'atk': + return self.NUM_ATK * 0.01 + self.NUM_ATKpp * 0.03 + self.NUM_ALL_STAT * 0.02 + elif key == 'rcv': + return self.NUM_RCV * 0.1 + self.NUM_RCVpp * 0.3 + self.NUM_ALL_STAT * 0.2 + return 0 + + def get_awakening_addition(self, key: StatType): if key == 'hp': - return 1 + self.NUM_HP * 0.015 + self.NUM_HPpp * 0.045 + self.NUM_ALL_STAT * 0.03 + return 500 * self.NUM_HP_AWAKENING elif key == 'atk': - return 1 + self.NUM_ATK * 0.01 + self.NUM_ATKpp * 0.03 + self.NUM_ALL_STAT * 0.02 + return 100 * self.NUM_ATK_AWAKENING elif key == 'rcv': - return 1 + self.NUM_RCV * 0.1 + self.NUM_RCVpp * 0.3 + self.NUM_ALL_STAT * 0.2 - return 1 + return 200 * self.NUM_RCV_AWAKENING + return 0 class MonsterStats: + PLUS_DICT = {'hp': 10, 'atk': 5, 'rcv': 3} + def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus_297=True, - stat_latents: MonsterStatLatentInput = None): + stat_latents: MonsterStatModifierInput = None): + s_val = self.base_stat(key, lv, monster_model) + + if stat_latents: + latents = s_val * stat_latents.get_latent_multiplier(key) + stat_awakenings = stat_latents.get_awakening_addition(key) + voice = s_val * stat_latents.NUM_VOICE_AWAKENING * 0.1 + s_val += latents + stat_awakenings + voice + + # include plus calculations. todo: is there a way to do this without subtraction? + s_val += self.PLUS_DICT[key] * max(min(plus, 99), 0) + if inherit: + inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} + if not is_plus_297: + s_val -= self.PLUS_DICT[key] * max(min(plus, 99), 0) + s_val *= inherit_dict[key] + return int(round(s_val)) + + def base_stat(self, key, lv, monster_model): s_min = float(monster_model.stat_values[key]['min']) s_max = float(monster_model.stat_values[key]['max']) if monster_model.level > 1: @@ -34,26 +80,21 @@ def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus s_val = s_min if lv > 99: s_val *= 1 + (monster_model.limit_mult / 11 * (lv - 99)) / 100 - plus_dict = {'hp': 10, 'atk': 5, 'rcv': 3} + return s_val - if stat_latents: - s_val *= stat_latents.get_multiplier(key) - - s_val += plus_dict[key] * max(min(plus, 99), 0) - if inherit: - inherit_dict = {'hp': 0.10, 'atk': 0.05, 'rcv': 0.15} - if not is_plus_297: - s_val -= plus_dict[key] * max(min(plus, 99), 0) - s_val *= inherit_dict[key] - return int(round(s_val)) + def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatModifierInput = None): + if not stat_latents: + stat_latents = MonsterStatModifierInput() - def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatLatentInput = None): is_plus_297 = False if plus == 297: plus = (99, 99, 99) is_plus_297 = True elif plus == 0: plus = (0, 0, 0) + + stat_latents.NUM_VOICE_AWAKENING = len([x for x in monster_model.awakenings if x.awoken_skill_id == 63]) + hp = self.stat(monster_model, 'hp', lv, plus[0], inherit, is_plus_297, stat_latents) atk = self.stat(monster_model, 'atk', lv, plus[1], inherit, is_plus_297, stat_latents) rcv = self.stat(monster_model, 'rcv', lv, plus[2], inherit, is_plus_297, stat_latents) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index eb4cca70..3d74ad13 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -362,7 +362,8 @@ async def make_embed(self, m: "MonsterModel"): hp, atk, rcv, weighted = m.stats() if m.limit_mult > 0: lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_row_1 = 'Stats (LB, +{}%)'.format(m.limit_mult) + stats_icon = self.match_emoji(AWAKENING_ID_TO_EMOJI_NAME_MAP.get(63)) if m.has_voice else '' + stats_row_1 = '{} Stats (LB, +{}%)'.format(stats_icon, m.limit_mult) stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( hp, lb_hp, atk, lb_atk, rcv, lb_rcv) else: From 05b6d84c0afb0dafb79111e75995b8468336ab01 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Thu, 7 Jan 2021 17:35:08 +0800 Subject: [PATCH 0373/1220] Adding buttoninfo command (#402) --- dadguide/dadguide.py | 2 +- dadguide/models/monster_stats.py | 47 +++++++---------- padinfo/button_info.py | 87 ++++++++++++++++++++++++++++++++ padinfo/padinfo.py | 12 +++++ 4 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 padinfo/button_info.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b2825bec..995a91c9 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -83,7 +83,7 @@ def __init__(self, bot, *args, **kwargs): self.index2 = None # type: MonsterIndex2 self.monster_stats = monster_stats - self.MonsterStatLatentInput = MonsterStatModifierInput + self.MonsterStatModifierInput = MonsterStatModifierInput async def wait_until_ready(self): """Wait until the Dadguide cog is ready. diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 8691fd1f..f458ff29 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -4,47 +4,36 @@ class MonsterStatModifierInput: - NUM_ATK = 0 - NUM_HP = 0 - NUM_RCV = 0 - NUM_ATKpp = 0 - NUM_HPpp = 0 - NUM_RCVpp = 0 - NUM_ALL_STAT = 0 - NUM_ATK_AWAKENING = 0 - NUM_HP_AWAKENING = 0 - NUM_RCV_AWAKENING = 0 - def __init__(self, num_atk=0, num_hp=0, num_rcv=0, num_atkpp=0, num_hppp=0, num_rcvpp=0, num_all_stat=0, num_atk_awakening=0, num_hp_awakening=0, num_rcv_awakening=0, num_voice_awakening=0): - self.NUM_ATK = num_atk - self.NUM_HP = num_hp - self.NUM_RCV = num_rcv - self.NUM_ATKpp = num_atkpp - self.NUM_HPpp = num_hppp - self.NUM_RCVpp = num_rcvpp - self.NUM_ALL_STAT = num_all_stat - self.NUM_ATK_AWAKENING = num_atk_awakening - self.NUM_HP_AWAKENING = num_hp_awakening - self.NUM_RCV_AWAKENING = num_rcv_awakening - self.NUM_VOICE_AWAKENING = num_voice_awakening + self.num_atk = num_atk + self.num_hp = num_hp + self.num_rcv = num_rcv + self.num_atkpp = num_atkpp + self.num_hppp = num_hppp + self.num_rcvpp = num_rcvpp + self.num_all_stat = num_all_stat + self.num_atk_awakening = num_atk_awakening + self.num_hp_awakening = num_hp_awakening + self.num_rcv_awakening = num_rcv_awakening + self.num_voice_awakening = num_voice_awakening def get_latent_multiplier(self, key: StatType): if key == 'hp': - return self.NUM_HP * 0.015 + self.NUM_HPpp * 0.045 + self.NUM_ALL_STAT * 0.03 + return self.num_hp * 0.015 + self.num_hppp * 0.045 + self.num_all_stat * 0.03 elif key == 'atk': - return self.NUM_ATK * 0.01 + self.NUM_ATKpp * 0.03 + self.NUM_ALL_STAT * 0.02 + return self.num_atk * 0.01 + self.num_atkpp * 0.03 + self.num_all_stat * 0.02 elif key == 'rcv': - return self.NUM_RCV * 0.1 + self.NUM_RCVpp * 0.3 + self.NUM_ALL_STAT * 0.2 + return self.num_rcv * 0.1 + self.num_rcvpp * 0.3 + self.num_all_stat * 0.2 return 0 def get_awakening_addition(self, key: StatType): if key == 'hp': - return 500 * self.NUM_HP_AWAKENING + return 500 * self.num_hp_awakening elif key == 'atk': - return 100 * self.NUM_ATK_AWAKENING + return 100 * self.num_atk_awakening elif key == 'rcv': - return 200 * self.NUM_RCV_AWAKENING + return 200 * self.num_rcv_awakening return 0 @@ -58,7 +47,7 @@ def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus if stat_latents: latents = s_val * stat_latents.get_latent_multiplier(key) stat_awakenings = stat_latents.get_awakening_addition(key) - voice = s_val * stat_latents.NUM_VOICE_AWAKENING * 0.1 + voice = s_val * stat_latents.num_voice_awakening * 0.1 s_val += latents + stat_awakenings + voice # include plus calculations. todo: is there a way to do this without subtraction? diff --git a/padinfo/button_info.py b/padinfo/button_info.py new file mode 100644 index 00000000..190b8726 --- /dev/null +++ b/padinfo/button_info.py @@ -0,0 +1,87 @@ +LIMIT_BREAK_LEVEL = 110 + +INFO_STRING = """[{}] {} +(Co-op mode) + +Without Latents: +Base: {} +Subattr: {} +Total: {} + +With Latents: +Base: {} +Subattr: {} +Total: {} +""" + + +class ButtonInfo: + def get_info(self, dgcog, monster_model): + + """ + Usage: ^buttoninfo Vajrayasaka + + Shows (main, sub, main+sub)damage x (with | without) atk++ latents + + Optional arguments include: + + [coop|solo], default coop + e.g: + + button dmg: 7668 + with subattr: 9968.4 + just subattr: whatever + + button dmg with atklatent: 8454.51 + with subattr: 10990.863 + just subattr: whatever + """ + max_level = LIMIT_BREAK_LEVEL if monster_model.limit_mult != 0 else monster_model.level + max_atk_latents = monster_model.latent_slots / 2 + + sub_attr_multiplier = self._get_sub_attr_multiplier(monster_model) + + result = ButtonInfoResult() + result.main_damage = self._calculate_damage(dgcog, monster_model, max_level, 0) + result.sub_damage = result.main_damage * sub_attr_multiplier + result.total_damage = result.main_damage + result.sub_damage + + result.main_damage_with_atk_latent = self._calculate_damage(dgcog, monster_model, max_level, max_atk_latents) + result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier + result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent + return result + + def _calculate_damage(self, dgcog, monster_model, level, num_atkpp_latent=0): + stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=num_atkpp_latent) + stat_latents.num_atk_awakening = len([x for x in monster_model.awakenings if x.awoken_skill_id == 1]) + + dmg = dgcog.monster_stats.stat(monster_model, 'atk', level, stat_latents=stat_latents) + num_mult_boost = len([x for x in monster_model.awakenings if x.awoken_skill_id == 30]) + + dmg *= 1.5 ** num_mult_boost + return dmg + + def _get_sub_attr_multiplier(self, monster_model): + if monster_model.attr2.value == 6 or monster_model.attr1.value == 6: + return 0 + if monster_model.attr2.value == monster_model.attr1.value: + return 1 / 10 + return 1 / 3 + + def to_string(self, monster, info): + return INFO_STRING.format(monster.monster_id, monster.name_en, info.main_damage, info.sub_damage, + info.total_damage, + info.main_damage_with_atk_latent, info.sub_damage_with_atk_latent, + info.total_damage_with_atk_latent) + + +class ButtonInfoResult: + main_damage: float + total_damage: float + sub_damage: float + main_damage_with_atk_latent: float + total_damage_with_atk_latent: float + sub_damage_with_atk_latent: float + + +button_info = ButtonInfo() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8443f454..bfc1d057 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -18,6 +18,7 @@ from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor +from .button_info import button_info from .find_monster import find_monster from .id_menu import IdMenu @@ -583,6 +584,17 @@ async def otherinfo(self, ctx, *, query: str): else: await self.makeFailureMsg(ctx, err) + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def buttoninfo(self, ctx, *, query: str): + """Button farming theorycrafting info""" + monster = await self.findMonster3(query) + DGCOG = self.bot.get_cog("Dadguide") + info = button_info.get_info(DGCOG, monster) + info_str = button_info.to_string(monster, info) + for page in pagify(info_str): + await ctx.send(box(page)) + @commands.command() @checks.bot_has_permissions(embed_links=True) async def lookup(self, ctx, *, query: str): From 0b0675fd03fa346fbff07b4e9dd7b48881d871ee Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 7 Jan 2021 22:45:30 -0800 Subject: [PATCH 0374/1220] Add other emoji to ID (but not for equips) (#404) * Oh I totally forgot about this * Remove reduce stat emoji * Remove emojis from equips * or inherit --- dadguide/models/monster_model.py | 5 ++--- dadguide/models/monster_stats.py | 23 +++++++++++++---------- padinfo/id_menu.py | 26 ++++++++++++++++++++------ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index f38a83be..ff6fbb90 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -135,9 +135,8 @@ def awakening_restricted_latents(self): def history_us(self): return '[{}] New Added'.format(self.reg_date) - @property - def has_voice(self): - return any([x for x in self.awakenings if x.awoken_skill_id == 63]) + def awakening_count(self, awid): + return len([x for x in self.awakenings if x.awoken_skill_id == awid]) def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): return monster_stats.stat(self, key, lv, plus=plus, inherit=inherit, is_plus_297=is_plus_297) diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index f458ff29..3ae3af2c 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -4,17 +4,17 @@ class MonsterStatModifierInput: - def __init__(self, num_atk=0, num_hp=0, num_rcv=0, num_atkpp=0, num_hppp=0, num_rcvpp=0, num_all_stat=0, - num_atk_awakening=0, num_hp_awakening=0, num_rcv_awakening=0, num_voice_awakening=0): - self.num_atk = num_atk + def __init__(self, num_hp=0, num_atk=0, num_rcv=0, num_hppp=0, num_atkpp=0, num_rcvpp=0, num_all_stat=0, + num_hp_awakening=0, num_atk_awakening=0, num_rcv_awakening=0, num_voice_awakening=0): self.num_hp = num_hp + self.num_atk = num_atk self.num_rcv = num_rcv - self.num_atkpp = num_atkpp self.num_hppp = num_hppp + self.num_atkpp = num_atkpp self.num_rcvpp = num_rcvpp self.num_all_stat = num_all_stat - self.num_atk_awakening = num_atk_awakening self.num_hp_awakening = num_hp_awakening + self.num_atk_awakening = num_atk_awakening self.num_rcv_awakening = num_rcv_awakening self.num_voice_awakening = num_voice_awakening @@ -44,7 +44,7 @@ def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus stat_latents: MonsterStatModifierInput = None): s_val = self.base_stat(key, lv, monster_model) - if stat_latents: + if stat_latents and not (monster_model.is_equip or inherit): latents = s_val * stat_latents.get_latent_multiplier(key) stat_awakenings = stat_latents.get_awakening_addition(key) voice = s_val * stat_latents.num_voice_awakening * 0.1 @@ -72,9 +72,6 @@ def base_stat(self, key, lv, monster_model): return s_val def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatModifierInput = None): - if not stat_latents: - stat_latents = MonsterStatModifierInput() - is_plus_297 = False if plus == 297: plus = (99, 99, 99) @@ -82,7 +79,13 @@ def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: Monst elif plus == 0: plus = (0, 0, 0) - stat_latents.NUM_VOICE_AWAKENING = len([x for x in monster_model.awakenings if x.awoken_skill_id == 63]) + if not stat_latents: + stat_latents = MonsterStatModifierInput( + num_hp_awakening=monster_model.awakening_count(1), + num_atk_awakening=monster_model.awakening_count(2), + num_rcv_awakening=monster_model.awakening_count(3), + num_voice_awakening=monster_model.awakening_count(63) + ) hp = self.stat(monster_model, 'hp', lv, plus[0], inherit, is_plus_297, stat_latents) atk = self.stat(monster_model, 'atk', lv, plus[1], inherit, is_plus_297, stat_latents) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 3d74ad13..9c2b0332 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -49,6 +49,8 @@ def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = N self.allowed_emojis = allowed_emojis def match_emoji(self, name): + if isinstance(name, int): + name = AWAKENING_ID_TO_EMOJI_NAME_MAP[name] for e in self.allowed_emojis: if e.name == name: return e @@ -362,13 +364,25 @@ async def make_embed(self, m: "MonsterModel"): hp, atk, rcv, weighted = m.stats() if m.limit_mult > 0: lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_icon = self.match_emoji(AWAKENING_ID_TO_EMOJI_NAME_MAP.get(63)) if m.has_voice else '' - stats_row_1 = '{} Stats (LB, +{}%)'.format(stats_icon, m.limit_mult) - stats_row_2 = '**HP** {} ({})\n**ATK** {} ({})\n**RCV** {} ({})'.format( - hp, lb_hp, atk, lb_atk, rcv, lb_rcv) + stats_icons = [ + self.match_emoji(63) if m.awakening_count(63) and not m.is_equip else '', + self.match_emoji(1) if m.awakening_count(1) and not m.is_equip else '', + self.match_emoji(2) if m.awakening_count(2) and not m.is_equip else '', + self.match_emoji(3) if m.awakening_count(3) and not m.is_equip else '', + ] + stats_row_1 = '{} Stats (LB, +{}%)'.format(stats_icons[0], m.limit_mult) + stats_row_2 = '**HP** {} ({}) {stats[1]}\n**ATK** {} ({}) {stats[2]}\n**RCV** {} ({}) {stats[3]}'.format( + hp, lb_hp, atk, lb_atk, rcv, lb_rcv, stats=stats_icons) else: - stats_row_1 = 'Stats' - stats_row_2 = '**HP** {}\n**ATK** {}\n**RCV** {}'.format(hp, atk, rcv) + stats_icons = [ + '', + self.match_emoji(1) if m.awakening_count(1) and not m.is_equip else '', + self.match_emoji(2) if m.awakening_count(2) and not m.is_equip else '', + self.match_emoji(3) if m.awakening_count(3) and not m.is_equip else '', + ] + stats_row_1 = '{} Stats'.format(stats_icons[0]) + stats_row_2 = '**HP** {} {stats[1]}\n**ATK** {} {stats[2]}\n**RCV** {} {stats[3]}'.format( + hp, atk, rcv, stats=stats_icons) if any(x if x.name == 'Enhance' else None for x in m.types): stats_row_2 += '\n**Fodder EXP** {:,}'.format(m.fodder_exp) embed.add_field(name=stats_row_1, value=stats_row_2) From fbfd6575508bff5fb4782bdec8a555badaf799c4 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 7 Jan 2021 23:48:42 -0800 Subject: [PATCH 0375/1220] Add id3 survey (#405) --- padinfo/padinfo.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bfc1d057..f06254e4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -306,19 +306,20 @@ async def idjp(self, ctx, *, query: str): async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): m, err, debug_info = await self.findMonster1(query, server_filter=server_filter) - if await self.config.do_survey(): - asyncio.create_task(self.send_survey_after(ctx, query, m.name_en if m else "None")) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: await self.makeFailureMsg(ctx, err) - async def send_survey_after(self, ctx, query, result): + async def send_survey_after(self, ctx, query, result_monster): sm = await self.config.user(ctx.author).survey_mode() sms = [1, await self.config.sometimes_perc() / 100, 0][sm] if random.random() < sms: - params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': result}) - url = "https://docs.google.com/forms/d/e/1FAIpQLSf66fE76epgslagdYteQR68HZAhxM43bmgsvurEzmHKsbaBDA/viewform?" + params + m1, _, _ = await self.findMonster1(query) + id1res = f"{m1.name_en} ({m1.monster_id})" if m1 else "None" + id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" + params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': id3res, 'entry.1016180044': id1res}) + url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", yemoji=char_to_emoji('y'), nemoji=char_to_emoji('n')) @@ -388,6 +389,10 @@ async def id3(self, ctx, *, query: str): async def _do_id3(self, ctx, query): m = await self.findMonster3(query) + + if await self.config.do_survey(): + asyncio.create_task(self.send_survey_after(ctx, query, m)) + if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: From 6906075c627b3a3ac23dd66918d261fd9044e77c Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 7 Jan 2021 23:59:12 -0800 Subject: [PATCH 0376/1220] Fix whatever was wrong with id3 (#407) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f06254e4..eafd5cc4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1077,7 +1077,7 @@ async def findMonster3(self, raw_query): monster_gen = find_monster.get_monster_evos(DGCOG.database, monster_gen) monster_gen = find_monster.process_prefix_tokens(prefix_tokens, monster_score, monster_gen, DGCOG.index2.monster_prefixes) - if monster_gen is None: + if not monster_gen: # no prefixes match any monster in the evo tree return From 8c75a07685cb0c32cacb9fabdfb617f3073be74f Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 03:38:24 -0800 Subject: [PATCH 0377/1220] Add awoken/type prefixes, improve subattrs (#392) * Add awakenings to token map This also adds suvo * Add types and integrate awakenings * Add aliases * Add subattr tokens * Make the subattr tokens actually work Add ?light and ?red etc prefixes when main attr isn't known. Also stop adding white for subattr that are nil. --- dadguide/monster_index.py | 18 ++- dadguide/token_mappings.py | 239 ++++++++++++++++++++++++++++++++----- 2 files changed, 224 insertions(+), 33 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index bd84c8cb..513be536 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -99,18 +99,25 @@ async def get_prefixes(self, m): basemon = self.graph.get_base_monster(m) # Main Color - for t in COLOR_MAP[Colors(m.attr1.value)]: + for t in COLOR_MAP[m.attr1]: prefix.add(t) # Sub Color + for t in SUB_COLOR_MAP[m.attr2]: + prefix.add(t) if m.attr1.value == 6: - for t in COLOR_MAP[Colors(m.attr2.value)]: + for t in COLOR_MAP[m.attr2]: prefix.add(t) # Both Colors - for t in DUAL_COLOR_MAP[(Colors(m.attr1.value), Colors(m.attr2.value))]: + for t in DUAL_COLOR_MAP[(m.attr1, m.attr2)]: prefix.add(t) + # Type + for mt in m.types: + for t in TYPE_MAP[mt]: + prefix.add(t) + # Series if m.series_id in self.series_id_to_pantheon_nickname: for t in self.series_id_to_pantheon_nickname[m.series_id]: @@ -186,6 +193,11 @@ async def get_prefixes(self, m): for t in EVO_PREFIX_MAP[EvoTypes.EQUIP]: prefix.add(t) + # Awakenings + for aw in m.awakenings: + for t in AWOKEN_PREFIX_MAP[Awakenings(aw.awoken_skill_id)]: + prefix.add(t) + # Chibi if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or \ 'ミニ' in m.name_ja or '(chibi)' in m.name_en: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index bb78f9de..7f3071f2 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -1,23 +1,18 @@ from collections import defaultdict from enum import Enum +from .models.enum_types import Attribute, MonsterType -class Colors(Enum): - RED = 0 - BLUE = 1 - GREEN = 2 - LIGHT = 3 - DARK = 4 - NIL = 6 - - -COLOR_MAP = {Colors.RED: ('r', 'red', 'fire'), - Colors.BLUE: ('b', 'blue', 'water'), - Colors.GREEN: ('g', 'green', 'wood'), - Colors.LIGHT: ('l', 'light', 'yellow'), - Colors.DARK: ('d', 'dark', 'purple'), - Colors.NIL: ('x', 'none', 'null', 'nil', 'white')} +COLOR_MAP = { + Attribute.Fire: ('r', 'red', 'fire'), + Attribute.Water: ('b', 'blue', 'water'), + Attribute.Wood: ('g', 'green', 'wood'), + Attribute.Light: ('l', 'light', 'yellow'), + Attribute.Dark: ('d', 'dark', 'purple'), + Attribute.Nil: ('nil', 'x', 'none', 'null', 'white') +} +SUB_COLOR_MAP = {k: tuple('?'+t for t in v) for k, v in COLOR_MAP.items()} DUAL_COLOR_MAP = {} for cid1, cns1 in COLOR_MAP.items(): @@ -25,12 +20,30 @@ class Colors(Enum): ts = () for t1 in cns1: for t2 in cns2: + if t2 in ("white",): + continue if len(t1) + len(t2) == 2: ts += (t1 + t2,) ts += (t1 + "/" + t2,) DUAL_COLOR_MAP[(cid1, cid2)] = ts +TYPE_MAP = { + MonsterType.Evolve: ('evolve',), + MonsterType.Balance: ('balanced', 'bal'), + MonsterType.Physical: ('physical', 'phys'), + MonsterType.Healer: ('healer',), + MonsterType.Dragon: ('dragon', 'dra'), + MonsterType.God: ('god',), + MonsterType.Attacker: ('attacker', 'atk'), + MonsterType.Devil: ('devil', 'dv'), + MonsterType.Machine: ('machine', 'mech'), + MonsterType.Awoken: ('awoken',), + MonsterType.Enhance: ('enhance', 'fodder', 'enh'), + MonsterType.Vendor: ('vendor', 'redeemable'), +} + + class EvoTypes(Enum): BASE = 'base' EVO = 'evolved' @@ -46,18 +59,181 @@ class EvoTypes(Enum): EQUIP = 'equip' -EVO_PREFIX_MAP = {EvoTypes.BASE: ('base',), - EvoTypes.EVO: ('evo', 'evolved'), - EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), - EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo'), - EvoTypes.TRANS: ('transform', 'trans', 'transformed'), - EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), - EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'mega awoken'), - EvoTypes.REVO: ('revo', 'reincarnated'), - EvoTypes.SREVO: ('srevo', 'super', 'sr', 'super reincarnated'), - EvoTypes.PIXEL: ('pixel', 'p', 'dot', 'px'), - EvoTypes.NONPIXEL: ('nonpixel', 'np'), - EvoTypes.EQUIP: ('equip', 'assist')} +EVO_PREFIX_MAP = { + EvoTypes.BASE: ('base',), + EvoTypes.EVO: ('evo', 'evolved'), + EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), + EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo', 'suvo'), + EvoTypes.TRANS: ('transform', 'trans', 'transformed'), + EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), + EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'mega awoken'), + EvoTypes.REVO: ('revo', 'reincarnated'), + EvoTypes.SREVO: ('srevo', 'super', 'sr', 'super reincarnated'), + EvoTypes.PIXEL: ('pixel', 'p', 'dot', 'px'), + EvoTypes.NONPIXEL: ('nonpixel', 'np'), + EvoTypes.EQUIP: ('equip', 'assist') +} + + +class Awakenings(Enum): + ENHANCEDHP = 1 + ENHANCEDATK = 2 + ENHANCEDRCV = 3 + REDUCERED = 4 + REDUCEBLUE = 5 + REDUCEGREEN = 6 + REDUCELIGHT = 7 + REDUCEDARK = 8 + AUTOHEAL = 9 + BINDRES = 10 + BLINDRES = 11 + JAMMERRES = 12 + POISONRES = 13 + ENHANCEDRED = 14 + ENHANCEDBLUE = 15 + ENHANCEDGREEN = 16 + ENHANCEDLIGHT = 17 + ENHANCEDDARK = 18 + EXTMOVE = 19 + BINDRECOVERY = 20 + SKILLBOOST = 21 + REDROW = 22 + BLUEROW = 23 + GREENROW = 24 + LIGHTROW = 25 + DARKROW = 26 + TPA = 27 + SKILLBINDRES = 28 + ENHANCEDHEAL = 29 + MULTIBOOST = 30 + DRAGONKILLER = 31 + GODKILLER = 32 + DEVILKILLER = 33 + MACHINEKILLER = 34 + BALANCEDKILLER = 35 + ATTACKERKILLER = 36 + PHYSICALKILLER = 37 + HEALERKILLER = 38 + EVOMATKILLER = 39 + AWOKENKILLER = 40 + FODDERKILLER = 41 + REDEEMKILLER = 42 + ENHCOMBO7C = 43 + GUARDBREAK = 44 + FUA = 45 + ENHTEAMHP = 46 + ENHTEAMRCV = 47 + VDP = 48 + EQUIP = 49 + SUPERFUA = 50 + SKILLCHARGE = 51 + UNBINDABLE = 52 + EXTMOVEPLUS = 53 + CLOUDRESIST = 54 + TAPERESIST = 55 + SKILLBOOSTPLUS = 56 + HP80ORMORE = 57 + HP50ORLESS = 58 + ELSHIELD = 59 + ELATTACK = 60 + ENHCOMBO10C = 61 + COMBOORB = 62 + VOICE = 63 + DUNGEONBONUS = 64 + REDUCEDHP = 65 + REDUCEDATK = 66 + REDUCEDRCV = 67 + UNBLINDABLE = 68 + UNJAMMABLE = 69 + UNPOISONABLE = 70 + JAMMERBLESSING = 71 + POISONBLESSING = 72 + REDCOMBOCOUNT = 73 + BLUECOMBOCOUNT = 74 + GREENCOMBOCOUNT = 75 + LIGHTCOMBOCOUNT = 76 + DARKCOMBOCOUNT = 77 + + +AWOKEN_PREFIX_MAP = { + Awakenings.ENHANCEDHP: ('hp+', '+hp'), + Awakenings.ENHANCEDATK: ('atk+', '+atk'), + Awakenings.ENHANCEDRCV: ('rcv+', '+rcv'), + Awakenings.REDUCERED: (), + Awakenings.REDUCEBLUE: (), + Awakenings.REDUCEGREEN: (), + Awakenings.REDUCELIGHT: (), + Awakenings.REDUCEDARK: (), + Awakenings.AUTOHEAL: ('autoheal',), + Awakenings.BINDRES: (), + Awakenings.BLINDRES: (), + Awakenings.JAMMERRES: (), + Awakenings.POISONRES: (), + Awakenings.ENHANCEDRED: ('roe', 'oe'), + Awakenings.ENHANCEDBLUE: ('boe', 'oe'), + Awakenings.ENHANCEDGREEN: ('goe', 'oe'), + Awakenings.ENHANCEDLIGHT: ('loe', 'oe'), + Awakenings.ENHANCEDDARK: ('doe', 'oe'), + Awakenings.EXTMOVE: ('te', 'finger'), + Awakenings.BINDRECOVERY: (), + Awakenings.SKILLBOOST: ('sb',), + Awakenings.REDROW: (), + Awakenings.BLUEROW: (), + Awakenings.GREENROW: (), + Awakenings.LIGHTROW: (), + Awakenings.DARKROW: (), + Awakenings.TPA: ('tpa', 'pronged'), + Awakenings.SKILLBINDRES: ('sbr',), + Awakenings.ENHANCEDHEAL: (), + Awakenings.MULTIBOOST: ('multi',), + Awakenings.DRAGONKILLER: (), + Awakenings.GODKILLER: ('gk',), + Awakenings.DEVILKILLER: ('dk',), + Awakenings.MACHINEKILLER: ('mk',), + Awakenings.BALANCEDKILLER: ('bk',), + Awakenings.ATTACKERKILLER: ('ak',), + Awakenings.PHYSICALKILLER: ('pk',), + Awakenings.HEALERKILLER: ('hk',), + Awakenings.EVOMATKILLER: (), + Awakenings.AWOKENKILLER: (), + Awakenings.FODDERKILLER: (), + Awakenings.REDEEMKILLER: ('rk',), + Awakenings.ENHCOMBO7C: (), + Awakenings.GUARDBREAK: (), + Awakenings.FUA: ('fua',), + Awakenings.ENHTEAMHP: (), + Awakenings.ENHTEAMRCV: (), + Awakenings.VDP: ('vdp',), + Awakenings.EQUIP: (), + Awakenings.SUPERFUA: ('sfua',), + Awakenings.SKILLCHARGE: (), + Awakenings.UNBINDABLE: ('unbindable',), + Awakenings.EXTMOVEPLUS: ('te+', 'finger+'), + Awakenings.CLOUDRESIST: ('cloudres',), + Awakenings.TAPERESIST: ('taperes',), + Awakenings.SKILLBOOSTPLUS: ('sb+',), + Awakenings.HP80ORMORE: ('80+', '+80'), + Awakenings.HP50ORLESS: ('50-', '-50'), + Awakenings.ELSHIELD: ('el',), + Awakenings.ELATTACK: ('el',), + Awakenings.ENHCOMBO10C: ('10c',), + Awakenings.COMBOORB: (), + Awakenings.VOICE: (), + Awakenings.DUNGEONBONUS: (), + Awakenings.REDUCEDHP: (), + Awakenings.REDUCEDATK: (), + Awakenings.REDUCEDRCV: (), + Awakenings.UNBLINDABLE: (), + Awakenings.UNJAMMABLE: (), + Awakenings.UNPOISONABLE: (), + Awakenings.JAMMERBLESSING: (), + Awakenings.POISONBLESSING: (), + Awakenings.REDCOMBOCOUNT: (), + Awakenings.BLUECOMBOCOUNT: (), + Awakenings.GREENCOMBOCOUNT: (), + Awakenings.LIGHTCOMBOCOUNT: (), + Awakenings.DARKCOMBOCOUNT: (), +} class MiscPrefixes(Enum): @@ -66,12 +242,15 @@ class MiscPrefixes(Enum): FARMABLE = 'farmable' -MISC_PREFIX_MAP = {MiscPrefixes.CHIBI: ('chibi', 'mini'), - MiscPrefixes.NONCHIBI: ('nonchibi', 'nc'), - MiscPrefixes.FARMABLE: ('farmable', 'nrem')} +MISC_PREFIX_MAP = { + MiscPrefixes.CHIBI: ('chibi', 'mini'), + MiscPrefixes.NONCHIBI: ('nonchibi', 'nc'), + MiscPrefixes.FARMABLE: ('farmable', 'nrem') +} PREFIX_MAPS = { **COLOR_MAP, + **TYPE_MAP, **DUAL_COLOR_MAP, **EVO_PREFIX_MAP, **MISC_PREFIX_MAP, From 34f4256ffdd2d18661e253f8bd66a3a55edd343a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 04:01:33 -0800 Subject: [PATCH 0378/1220] Add ^helpid (#413) --- padinfo/padinfo.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index eafd5cc4..5c400ec1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1094,8 +1094,12 @@ async def findMonster3(self, raw_query): @commands.command() async def debugid3(self, ctx, *, query): + """Get helpful id information about a monster""" DGCOG = self.bot.get_cog("Dadguide") m = await self.findMonster3(query) + if m is None: + await ctx.send(box("Your query didn't match any monsters.")) + return bm = DGCOG.database.graph.get_base_monster(m) o = (f"[{m.monster_id}] {m.name_en}\n" f"Base: [{bm.monster_id}] {bm.name_en}\n" @@ -1108,9 +1112,17 @@ async def debugid3(self, ctx, *, query): for page in pagify(o): await ctx.send(box(page)) - -def calc_ratio(s1, s2): - return difflib.SequenceMatcher(None, s1, s2).ratio() + @commands.command() + async def idhelp(self, ctx, *, query=""): + """Get help with an id query""" + if query: + await ctx.send("See for " + "documentation on ^id!") + await self.debugid3(ctx, query=query) + else: + await ctx.send("See for " + "documentation on `^id`! You can also run `[p]idhelp ` to get " + "help with querying a specific monster.") class PadInfoSettings(CogSettings): From b1954918d8f9670d72a6278be3aab03021603fab Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 8 Jan 2021 06:48:14 -0600 Subject: [PATCH 0379/1220] Add some more awakening prefixes (#415) * Add some more awakening prefixes * Add pdchu killers & fix skillcharge * Fix oe order * Change hp conditionals to avoid using - * Move equip/assist prefixes to the awakening map * hoe -> oeh --- dadguide/monster_index.py | 5 -- dadguide/token_mappings.py | 116 ++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 64 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 513be536..4c108450 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -188,11 +188,6 @@ async def get_prefixes(self, m): for t in EVO_PREFIX_MAP[EvoTypes.NONPIXEL]: prefix.add(t) - # Equip - if m.is_equip: - for t in EVO_PREFIX_MAP[EvoTypes.EQUIP]: - prefix.add(t) - # Awakenings for aw in m.awakenings: for t in AWOKEN_PREFIX_MAP[Awakenings(aw.awoken_skill_id)]: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 7f3071f2..e129a2fc 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -56,7 +56,6 @@ class EvoTypes(Enum): SREVO = 'super reincarnated' PIXEL = 'pixel' NONPIXEL = 'nonpixel' - EQUIP = 'equip' EVO_PREFIX_MAP = { @@ -71,7 +70,6 @@ class EvoTypes(Enum): EvoTypes.SREVO: ('srevo', 'super', 'sr', 'super reincarnated'), EvoTypes.PIXEL: ('pixel', 'p', 'dot', 'px'), EvoTypes.NONPIXEL: ('nonpixel', 'np'), - EvoTypes.EQUIP: ('equip', 'assist') } @@ -159,80 +157,80 @@ class Awakenings(Enum): Awakenings.ENHANCEDHP: ('hp+', '+hp'), Awakenings.ENHANCEDATK: ('atk+', '+atk'), Awakenings.ENHANCEDRCV: ('rcv+', '+rcv'), - Awakenings.REDUCERED: (), - Awakenings.REDUCEBLUE: (), - Awakenings.REDUCEGREEN: (), - Awakenings.REDUCELIGHT: (), - Awakenings.REDUCEDARK: (), + Awakenings.REDUCERED: ('elresr', 'elres'), + Awakenings.REDUCEBLUE: ('elresb', 'elres'), + Awakenings.REDUCEGREEN: ('elresg', 'elres'), + Awakenings.REDUCELIGHT: ('elresl', 'elres'), + Awakenings.REDUCEDARK: ('elresd', 'elres'), Awakenings.AUTOHEAL: ('autoheal',), - Awakenings.BINDRES: (), - Awakenings.BLINDRES: (), - Awakenings.JAMMERRES: (), - Awakenings.POISONRES: (), - Awakenings.ENHANCEDRED: ('roe', 'oe'), - Awakenings.ENHANCEDBLUE: ('boe', 'oe'), - Awakenings.ENHANCEDGREEN: ('goe', 'oe'), - Awakenings.ENHANCEDLIGHT: ('loe', 'oe'), - Awakenings.ENHANCEDDARK: ('doe', 'oe'), + Awakenings.BINDRES: ('unbindable',), + Awakenings.BLINDRES: ('resb',), + Awakenings.JAMMERRES: ('resj',), + Awakenings.POISONRES: ('resp',), + Awakenings.ENHANCEDRED: ('oer', 'oe'), + Awakenings.ENHANCEDBLUE: ('oeb', 'oe'), + Awakenings.ENHANCEDGREEN: ('oeg', 'oe'), + Awakenings.ENHANCEDLIGHT: ('oel', 'oe'), + Awakenings.ENHANCEDDARK: ('oed', 'oe'), Awakenings.EXTMOVE: ('te', 'finger'), - Awakenings.BINDRECOVERY: (), + Awakenings.BINDRECOVERY: ('bindrcv',), Awakenings.SKILLBOOST: ('sb',), - Awakenings.REDROW: (), - Awakenings.BLUEROW: (), - Awakenings.GREENROW: (), - Awakenings.LIGHTROW: (), - Awakenings.DARKROW: (), + Awakenings.REDROW: ('rowr', 'row'), + Awakenings.BLUEROW: ('rowb', 'row'), + Awakenings.GREENROW: ('rowg', 'row'), + Awakenings.LIGHTROW: ('rowl', 'row'), + Awakenings.DARKROW: ('rowd', 'row'), Awakenings.TPA: ('tpa', 'pronged'), Awakenings.SKILLBINDRES: ('sbr',), - Awakenings.ENHANCEDHEAL: (), + Awakenings.ENHANCEDHEAL: ('htpa', 'oeh'), Awakenings.MULTIBOOST: ('multi',), - Awakenings.DRAGONKILLER: (), - Awakenings.GODKILLER: ('gk',), - Awakenings.DEVILKILLER: ('dk',), - Awakenings.MACHINEKILLER: ('mk',), - Awakenings.BALANCEDKILLER: ('bk',), - Awakenings.ATTACKERKILLER: ('ak',), - Awakenings.PHYSICALKILLER: ('pk',), - Awakenings.HEALERKILLER: ('hk',), - Awakenings.EVOMATKILLER: (), - Awakenings.AWOKENKILLER: (), - Awakenings.FODDERKILLER: (), - Awakenings.REDEEMKILLER: ('rk',), - Awakenings.ENHCOMBO7C: (), - Awakenings.GUARDBREAK: (), + Awakenings.DRAGONKILLER: ('dk', 'drk'), + Awakenings.GODKILLER: ('gk', 'gok'), + Awakenings.DEVILKILLER: ('vk', 'dek'), + Awakenings.MACHINEKILLER: ('mk', 'mak'), + Awakenings.BALANCEDKILLER: ('bk', 'bak'), + Awakenings.ATTACKERKILLER: ('ak', 'aak'), + Awakenings.PHYSICALKILLER: ('pk', 'phk'), + Awakenings.HEALERKILLER: ('hk', 'hek'), + Awakenings.EVOMATKILLER: ('evok',), + Awakenings.AWOKENKILLER: ('awok',), + Awakenings.FODDERKILLER: ('enhk',), + Awakenings.REDEEMKILLER: ('vendork',), + Awakenings.ENHCOMBO7C: ('7c',), + Awakenings.GUARDBREAK: ('gb',), Awakenings.FUA: ('fua',), - Awakenings.ENHTEAMHP: (), - Awakenings.ENHTEAMRCV: (), + Awakenings.ENHTEAMHP: ('teamhp',), + Awakenings.ENHTEAMRCV: ('teamrcv',), Awakenings.VDP: ('vdp',), - Awakenings.EQUIP: (), + Awakenings.EQUIP: ('equip', 'assist'), Awakenings.SUPERFUA: ('sfua',), - Awakenings.SKILLCHARGE: (), + Awakenings.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), Awakenings.UNBINDABLE: ('unbindable',), Awakenings.EXTMOVEPLUS: ('te+', 'finger+'), Awakenings.CLOUDRESIST: ('cloudres',), Awakenings.TAPERESIST: ('taperes',), Awakenings.SKILLBOOSTPLUS: ('sb+',), - Awakenings.HP80ORMORE: ('80+', '+80'), - Awakenings.HP50ORLESS: ('50-', '-50'), - Awakenings.ELSHIELD: ('el',), + Awakenings.HP80ORMORE: ('>80', 'highhp'), + Awakenings.HP50ORLESS: ('<50', 'lowhp'), + Awakenings.ELSHIELD: ('elshield',), Awakenings.ELATTACK: ('el',), Awakenings.ENHCOMBO10C: ('10c',), - Awakenings.COMBOORB: (), - Awakenings.VOICE: (), + Awakenings.COMBOORB: ('co', 'corb'), + Awakenings.VOICE: ('voice',), Awakenings.DUNGEONBONUS: (), - Awakenings.REDUCEDHP: (), - Awakenings.REDUCEDATK: (), - Awakenings.REDUCEDRCV: (), - Awakenings.UNBLINDABLE: (), - Awakenings.UNJAMMABLE: (), - Awakenings.UNPOISONABLE: (), - Awakenings.JAMMERBLESSING: (), - Awakenings.POISONBLESSING: (), - Awakenings.REDCOMBOCOUNT: (), - Awakenings.BLUECOMBOCOUNT: (), - Awakenings.GREENCOMBOCOUNT: (), - Awakenings.LIGHTCOMBOCOUNT: (), - Awakenings.DARKCOMBOCOUNT: (), + Awakenings.REDUCEDHP: ('hp-',), + Awakenings.REDUCEDATK: ('atk-',), + Awakenings.REDUCEDRCV: ('rcv-',), + Awakenings.UNBLINDABLE: ('resb+', 'b+',), + Awakenings.UNJAMMABLE: ('resj+', 'j+',), + Awakenings.UNPOISONABLE: ('resp+', 'p+',), + Awakenings.JAMMERBLESSING: ('jblessing',), + Awakenings.POISONBLESSING: ('pblessing',), + Awakenings.REDCOMBOCOUNT: ('ccr',), + Awakenings.BLUECOMBOCOUNT: ('ccb',), + Awakenings.GREENCOMBOCOUNT: ('ccg',), + Awakenings.LIGHTCOMBOCOUNT: ('ccl',), + Awakenings.DARKCOMBOCOUNT: ('ccd',), } From 36104b594f2866e51c7b4066f0a825c1b3b05e57 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 04:49:16 -0800 Subject: [PATCH 0380/1220] Cache alt monster ids in the index (#414) --- dadguide/monster_graph.py | 15 +++++++++++---- padinfo/padinfo.py | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index d0e919d9..50ad1a5b 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -236,6 +236,10 @@ def build_graph(self): mat, evo_model.to_id, type="material_of", model=evo_model) already_used_in_this_evo.append(mat) + # Caching + for mid in self.graph.nodes: + self.graph.nodes[mid]['alt_versions'] = self.process_alt_versions(mid) + self.edges = self.graph.edges self.nodes = self.graph.nodes @@ -285,7 +289,7 @@ def get_transform_tree(self, monster_id): ids.add(mid) return ids - def get_alt_cards(self, monster_id): + def process_alt_versions(self, monster_id): ids = set() to_check = {monster_id} while to_check: @@ -300,15 +304,18 @@ def get_alt_cards(self, monster_id): ids.add(mid) return ids + def get_alt_ids_by_id(self, monster_id): + return self.nodes[monster_id]['alt_versions'] + def get_alt_monsters_by_id(self, monster_id): - ids = self.get_alt_cards(monster_id) + ids = self.get_alt_ids_by_id(monster_id) return [self.get_monster(m_id) for m_id in ids] def get_alt_monsters(self, monster: MonsterModel): return self.get_alt_monsters_by_id(monster.monster_id) def get_base_id_by_id(self, monster_id): - alt_cards = self.get_alt_cards(monster_id) + alt_cards = self.get_alt_ids_by_id(monster_id) if alt_cards is None: return None return sorted(alt_cards)[0] @@ -356,7 +363,7 @@ def monster_is_transform_base(self, monster: MonsterModel) -> bool: return self.monster_is_transform_base_by_id(monster.monster_no) def get_numerical_sort_top_id_by_id(self, monster_id): - alt_cards = self.get_alt_cards(monster_id) + alt_cards = self.get_alt_ids_by_id(monster_id) if alt_cards is None: return None return sorted(alt_cards)[-1] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5c400ec1..16cc9bda 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -96,7 +96,7 @@ async def on_update(self, ctx, selected_emoji): evo_id = self.pad_info.settings.checkEvoID(ctx.author.id) self.pad_info.settings.log_emoji(selected_emoji) if evo_id: - evos = sorted({*self.db_context.graph.get_alt_cards(self.m.monster_id)}) + evos = sorted({*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) index = evos.index(self.m.monster_id) if selected_emoji == self.pad_info.previous_monster_emoji: new_id = evos[index - 1] @@ -128,7 +128,7 @@ async def on_update(self, ctx, selected_emoji): self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, m=self.m, scroll=sorted( - {*self.db_context.graph.get_alt_cards(self.m.monster_id)}) if evo_id else [], menu_type=1) + {*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) if evo_id else [], menu_type=1) return True From eff51243b456ea70d5386ff7b2b0286ac091bb04 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 05:28:06 -0800 Subject: [PATCH 0381/1220] Prio following tree over fuzzy matching (#416) * Add infix matching for tokens * Add treematching priority * Remove Substring Matching --- padinfo/find_monster.py | 10 +++++++--- padinfo/padinfo.py | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index c25cab86..c021ef06 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -99,10 +99,14 @@ def process_prefix_tokens(self, prefix_query_tokens, monsterscore, potential_evo return potential_evos - def get_monster_evos(self, database, monster_gen): + def get_monster_evos(self, database, monster_gen, monster_score): monster_evos = set() - for m in monster_gen: - monster_evos.update(database.graph.get_alt_monsters(m)) + for m in sorted(monster_gen, key=lambda m: monster_score[m], reverse=True): + for evo in database.graph.get_alt_monsters(m): + monster_evos.add(evo) + if monster_score[evo] < monster_score[m]: + monster_score[evo] = monster_score[m] - .003 + return monster_evos diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 16cc9bda..f2a1978d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,5 +1,4 @@ import asyncio -import difflib import json import logging import os @@ -318,7 +317,8 @@ async def send_survey_after(self, ctx, query, result_monster): m1, _, _ = await self.findMonster1(query) id1res = f"{m1.name_en} ({m1.monster_id})" if m1 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" - params = urllib.parse.urlencode({'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': id3res, 'entry.1016180044': id1res}) + params = urllib.parse.urlencode( + {'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': id3res, 'entry.1016180044': id1res}) url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", @@ -1074,7 +1074,7 @@ async def findMonster3(self, raw_query): monster_score = defaultdict(int) # Expand search to the evo tree - monster_gen = find_monster.get_monster_evos(DGCOG.database, monster_gen) + monster_gen = find_monster.get_monster_evos(DGCOG.database, monster_gen, monster_score) monster_gen = find_monster.process_prefix_tokens(prefix_tokens, monster_score, monster_gen, DGCOG.index2.monster_prefixes) if not monster_gen: From 1aa73ad3154900259d6c53b73139b32787841438 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 05:46:58 -0800 Subject: [PATCH 0382/1220] Remove helpid and alias it to idhelp (#422) --- padinfo/padinfo.py | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f2a1978d..0c38f2c7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -28,28 +28,6 @@ logger = logging.getLogger('red.padbot-cogs.padinfo') -HELP_MSG = """ -{0.prefix}helpid : shows this message -{0.prefix}id : look up a monster and show a link to puzzledragonx -{0.prefix}pic : Look up a monster and display its image inline - -Options for - : Find a monster by ID - {0.prefix}id 1234 (picks sun quan) - : Take the best guess for a monster, picks the most recent monster - {0.prefix}id kali (picks mega awoken d kali) - : Limit by element or awoken, e.g. - {0.prefix}id ares (selects the most recent, revo ares) - {0.prefix}id aares (explicitly selects awoken ares) - {0.prefix}id a ares (spaces work too) - {0.prefix}id rd ares (select a specific evo for ares, the red/dark one) - {0.prefix}id r/d ares (slashes, spaces work too) - -computed nickname list and overrides: - https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/edit - -submit an override suggestion: - https://docs.google.com/forms/d/1kJH9Q0S8iqqULwrRqB9dSxMOMebZj6uZjECqi4t9_z0/edit""" EMBED_NOT_GENERATED = -1 @@ -736,12 +714,6 @@ async def leaderskillsingle(self, ctx, *, query): await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) - @commands.command(aliases=['helppic', 'helpimg']) - @checks.bot_has_permissions(embed_links=True) - async def helpid(self, ctx): - """Whispers you info on how to craft monster queries for [p]id""" - await ctx.author.send(box(HELP_MSG.format(ctx))) - @commands.group() # @checks.is_owner() async def idtest(self, ctx): @@ -1112,7 +1084,7 @@ async def debugid3(self, ctx, *, query): for page in pagify(o): await ctx.send(box(page)) - @commands.command() + @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query=""): """Get help with an id query""" if query: From 513924c1e45ad3df280d61a8a990e261c86acacf Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 06:00:26 -0800 Subject: [PATCH 0383/1220] Add gdoc nicknames ignore in id1 (#423) --- dadguide/dadguide.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 995a91c9..6b7df318 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -173,20 +173,19 @@ async def download_and_refresh_nicknames(self): await self._download_override_files() logger.info('Loading dg name overrides') - nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN) + nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN, 5) treename_overrides = self._csv_to_tuples(TREENAME_FILE_PATTERN, 5) panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) self.nickname_overrides = defaultdict(set) - for nick, id in nickname_overrides: - if id.isdigit(): + for nick, id, _, _, i in nickname_overrides: + if id.isdigit() and not i: self.nickname_overrides[int(id)].add(nick.lower()) self.treename_overrides = defaultdict(set) - for x in treename_overrides: - k, v, _, _, i = x - if k.isdigit() and not i: - self.treename_overrides[int(k)].add(v.lower()) + for id, treename, _, _, i in treename_overrides: + if id.isdigit() and not i: + self.treename_overrides[int(id)].add(treename.lower()) self.panthname_overrides = {x[0].lower(): x[1].lower() for x in panthname_overrides} self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) From 74b5696275a6bb13891b40f9894bf07690e8ee77 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 8 Jan 2021 08:19:44 -0600 Subject: [PATCH 0384/1220] Remove unused dbcontext methods (#425) --- dadguide/database_context.py | 26 ------------------------- tests/dadguide/database_context_test.py | 1 - 2 files changed, 27 deletions(-) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 5659ed5a..7b5382b8 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -28,22 +28,6 @@ def get_awoken_skill_ids(self): self.database.query_many( SELECT_AWOKEN_SKILL_IDS, (), as_generator=True)] - def get_next_evolutions_by_monster(self, monster_id): - return self.graph.get_next_evolutions_by_monster_id(monster_id) - - def get_evolution_tree_ids(self, base_monster_id): - # is not a tree i lied - base_id = base_monster_id - evolution_tree = [base_id] - n_evos = deque() - n_evos.append(base_id) - while len(n_evos) > 0: - n_evo_id = n_evos.popleft() - for e in self.get_next_evolutions_by_monster(n_evo_id): - n_evos.append(e) - evolution_tree.append(e) - return evolution_tree - def get_monsters_where(self, f): return [m for m in self.get_all_monsters() if f(m)] @@ -81,16 +65,6 @@ def get_all_events(self) -> ScheduledEventModel: **se) yield ScheduledEventModel(**se) - def get_base_monster_ids(self): - SELECT_BASE_MONSTER_ID = ''' - SELECT evolutions.from_id as monster_id FROM evolutions WHERE evolutions.from_id NOT IN (SELECT DISTINCT evolutions.to_id FROM evolutions) - UNION - SELECT monsters.monster_id FROM monsters WHERE monsters.monster_id NOT IN (SELECT evolutions.from_id FROM evolutions UNION SELECT evolutions.to_id FROM evolutions)''' - return self.database.query_many( - SELECT_BASE_MONSTER_ID, - (), - as_generator=True) - def has_database(self): return self.database.has_database() diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index acdcb4b8..a444e513 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -11,7 +11,6 @@ assert db_context.graph.monster_is_farmable_by_id(4) assert not db_context.graph.monster_is_farmable_by_id(5156) assert db_context.graph.get_base_monster_by_id(1074).monster_id == 1073 # evo pandora -assert 6352 in db_context.get_evolution_tree_ids(1074) assert db_context.get_monsters_by_series(1)[0].name_en == 'Tyrra' assert db_context.get_monsters_by_active(1)[0].name_en == 'Tyrra' From f54bc2194a290d26e4a7f4c7d16a9ecec33ac4a9 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 06:24:20 -0800 Subject: [PATCH 0385/1220] Add en series names as prefixers (#426) --- dadguide/monster_index.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 4c108450..5b0b23b1 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -22,7 +22,9 @@ async def __ainit__(self, monsters, db): self.monster_id_to_nickname = defaultdict(set) self.monster_id_to_treename = defaultdict(set) - self.series_id_to_pantheon_nickname = defaultdict(set) + self.series_id_to_pantheon_nickname = defaultdict(set, {m.series_id: {m.series.name_en} + for m + in db.get_all_monsters()}) self.multi_word_tokens = set() nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) @@ -124,8 +126,8 @@ async def get_prefixes(self, m): prefix.add(t) # Rarity - prefix.add(str(m.rarity)+"*") - prefix.add(str(basemon.rarity)+"*b") + prefix.add(str(m.rarity) + "*") + prefix.add(str(basemon.rarity) + "*b") # Base if self.graph.monster_is_base(m): From 1005d899bb56bc40bc9f8a52c3c9943f386c6a0a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 8 Jan 2021 14:08:38 -0800 Subject: [PATCH 0386/1220] Support negated prefixes in queries (#421) --- padinfo/find_monster.py | 29 +++++++++++++++++++---------- padinfo/padinfo.py | 7 ++++--- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index c021ef06..9f9cfd96 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -1,9 +1,6 @@ -import re -from collections import defaultdict - -from tsutils import rmdiacritics -from typing import List, Set import difflib +from collections import defaultdict +from typing import Set def calc_ratio(s1, s2): @@ -52,17 +49,23 @@ def interpret_query(self, raw_query: str, valid_multi_word_tokens, all_prefixes) tokenized_query = self._merge_multi_word_tokens(tokenized_query, valid_multi_word_tokens) prefixes = set() + nprefixes = set() name = set() longer_prefixes = [p for p in all_prefixes if len(p) > 8] for i, token in enumerate(tokenized_query): + negated = token.startswith("-") + token = token.lstrip('-') if token in all_prefixes or difflib.get_close_matches(token, longer_prefixes, n=1, cutoff=.8): - prefixes.add(token) + if negated: + nprefixes.add(token) + else: + prefixes.add(token) else: name.add(token) name.update(tokenized_query[i + 1:]) break - return prefixes, name + return prefixes, nprefixes, name def process_name_tokens(self, name_query_tokens, index2): monstergen = None @@ -91,9 +94,16 @@ def process_name_tokens(self, name_query_tokens, index2): return monstergen, monsterscore - def process_prefix_tokens(self, prefix_query_tokens, monsterscore, potential_evos, monster_prefixes): + def process_prefix_tokens(self, prefix_query_tokens, nprefix_query_tokens, monsterscore, potential_evos, + monster_prefixes): for t in prefix_query_tokens: - potential_evos = {m for m in potential_evos if self._monster_has_token(m, t, monsterscore, monster_prefixes[m])} + potential_evos = {m for m in potential_evos if + self._monster_has_token(m, t, monsterscore, monster_prefixes[m])} + if not potential_evos: + return None + for t in nprefix_query_tokens: + potential_evos = {m for m in potential_evos if + not self._monster_has_token(m, t, monsterscore, monster_prefixes[m])} if not potential_evos: return None @@ -107,7 +117,6 @@ def get_monster_evos(self, database, monster_gen, monster_score): if monster_score[evo] < monster_score[m]: monster_score[evo] = monster_score[m] - .003 - return monster_evos diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0c38f2c7..c3c8b20e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1030,8 +1030,9 @@ async def findMonster3(self, raw_query): raise ValueError("Dadguide cog is not loaded") raw_query = rmdiacritics(raw_query).lower() - prefix_tokens, name_query_tokens = find_monster.interpret_query(raw_query, DGCOG.index2.multi_word_tokens, - DGCOG.index2.all_prefixes) + prefix_tokens, nprefix_tokens, name_query_tokens = find_monster.interpret_query(raw_query, + DGCOG.index2.multi_word_tokens, + DGCOG.index2.all_prefixes) # print(prefix_tokens, name_query_tokens) @@ -1047,7 +1048,7 @@ async def findMonster3(self, raw_query): # Expand search to the evo tree monster_gen = find_monster.get_monster_evos(DGCOG.database, monster_gen, monster_score) - monster_gen = find_monster.process_prefix_tokens(prefix_tokens, monster_score, monster_gen, + monster_gen = find_monster.process_prefix_tokens(prefix_tokens, nprefix_tokens, monster_score, monster_gen, DGCOG.index2.monster_prefixes) if not monster_gen: # no prefixes match any monster in the evo tree From d46d1d296ebb6580795db1452e54ca22994469f7 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 8 Jan 2021 16:22:08 -0600 Subject: [PATCH 0387/1220] Remove + before stat awakening prefixes (#427) --- dadguide/token_mappings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index e129a2fc..6de67927 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -154,9 +154,9 @@ class Awakenings(Enum): AWOKEN_PREFIX_MAP = { - Awakenings.ENHANCEDHP: ('hp+', '+hp'), - Awakenings.ENHANCEDATK: ('atk+', '+atk'), - Awakenings.ENHANCEDRCV: ('rcv+', '+rcv'), + Awakenings.ENHANCEDHP: ('hp+', 'hp'), + Awakenings.ENHANCEDATK: ('atk+', 'atk'), + Awakenings.ENHANCEDRCV: ('rcv+', 'rcv'), Awakenings.REDUCERED: ('elresr', 'elres'), Awakenings.REDUCEBLUE: ('elresb', 'elres'), Awakenings.REDUCEGREEN: ('elresg', 'elres'), From a80da7b79ba63e42be8bee3eeb6b826b1b1fbd01 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 8 Jan 2021 17:51:00 -0600 Subject: [PATCH 0388/1220] Update failed query text for id & id3 (#430) --- padinfo/padinfo.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c3c8b20e..1475ae1f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -956,10 +956,14 @@ def get_emojis(self): return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] async def makeFailureMsg(self, ctx: discord.abc.Messageable, err): + if await self.config.user(ctx.author).beta_id3(): + await self.idhelp(ctx, is_failed_query=True) + return msg = ('Lookup failed: {}.\n' 'Try one of , , [argbld]/[rgbld] . ' 'Unexpected results? Use ^helpid for more info.').format(err) await ctx.send(box(msg)) + await ctx.send('Looking for the beta test? Type `^idset beta y`') async def findMonsterCustom(self, ctx, query, server_filter=ServerFilter.any): if await self.config.user(ctx.author).beta_id3(): @@ -1086,16 +1090,17 @@ async def debugid3(self, ctx, *, query): await ctx.send(box(page)) @commands.command(aliases=['helpid']) - async def idhelp(self, ctx, *, query=""): + async def idhelp(self, ctx, *, query="", is_failed_query=False): """Get help with an id query""" + failed_query_msg = "Query matched no results! " if is_failed_query else "" if query: await ctx.send("See for " "documentation on ^id!") await self.debugid3(ctx, query=query) else: - await ctx.send("See for " + await ctx.send("{}See for " "documentation on `^id`! You can also run `[p]idhelp ` to get " - "help with querying a specific monster.") + "help with querying a specific monster.".format(failed_query_msg)) class PadInfoSettings(CogSettings): From 12ad736fd331d9dceefe87de67c5482ed6d9ed96 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 8 Jan 2021 21:52:51 -0600 Subject: [PATCH 0389/1220] Add more awakening prefix options (#428) * Add more awakening prefix options * Add thp for team hp * Add "killer" as prefix for killers --- dadguide/token_mappings.py | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 6de67927..46253349 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -157,7 +157,7 @@ class Awakenings(Enum): Awakenings.ENHANCEDHP: ('hp+', 'hp'), Awakenings.ENHANCEDATK: ('atk+', 'atk'), Awakenings.ENHANCEDRCV: ('rcv+', 'rcv'), - Awakenings.REDUCERED: ('elresr', 'elres'), + Awakenings.REDUCERED: ('elresr', 'elres'), # element resist Awakenings.REDUCEBLUE: ('elresb', 'elres'), Awakenings.REDUCEGREEN: ('elresg', 'elres'), Awakenings.REDUCELIGHT: ('elresl', 'elres'), @@ -183,49 +183,49 @@ class Awakenings(Enum): Awakenings.TPA: ('tpa', 'pronged'), Awakenings.SKILLBINDRES: ('sbr',), Awakenings.ENHANCEDHEAL: ('htpa', 'oeh'), - Awakenings.MULTIBOOST: ('multi',), - Awakenings.DRAGONKILLER: ('dk', 'drk'), - Awakenings.GODKILLER: ('gk', 'gok'), - Awakenings.DEVILKILLER: ('vk', 'dek'), - Awakenings.MACHINEKILLER: ('mk', 'mak'), - Awakenings.BALANCEDKILLER: ('bk', 'bak'), - Awakenings.ATTACKERKILLER: ('ak', 'aak'), - Awakenings.PHYSICALKILLER: ('pk', 'phk'), - Awakenings.HEALERKILLER: ('hk', 'hek'), - Awakenings.EVOMATKILLER: ('evok',), - Awakenings.AWOKENKILLER: ('awok',), - Awakenings.FODDERKILLER: ('enhk',), - Awakenings.REDEEMKILLER: ('vendork',), + Awakenings.MULTIBOOST: ('multi', 'mb'), + Awakenings.DRAGONKILLER: ('dk', 'drk', 'killer'), + Awakenings.GODKILLER: ('gk', 'gok', 'killer'), + Awakenings.DEVILKILLER: ('vk', 'dek', 'killer'), + Awakenings.MACHINEKILLER: ('mk', 'mak', 'killer'), + Awakenings.BALANCEDKILLER: ('bk', 'bak', 'killer'), + Awakenings.ATTACKERKILLER: ('ak', 'aak', 'killer'), + Awakenings.PHYSICALKILLER: ('pk', 'phk', 'killer'), + Awakenings.HEALERKILLER: ('hk', 'hek', 'killer'), + Awakenings.EVOMATKILLER: ('evok', 'a2killer'), + Awakenings.AWOKENKILLER: ('awok', 'a2killer'), + Awakenings.FODDERKILLER: ('enhk', 'a2killer'), + Awakenings.REDEEMKILLER: ('vendork', 'a2killer'), Awakenings.ENHCOMBO7C: ('7c',), Awakenings.GUARDBREAK: ('gb',), Awakenings.FUA: ('fua',), - Awakenings.ENHTEAMHP: ('teamhp',), - Awakenings.ENHTEAMRCV: ('teamrcv',), + Awakenings.ENHTEAMHP: ('teamhp', 'thp'), + Awakenings.ENHTEAMRCV: ('teamrcv', 'trcv'), Awakenings.VDP: ('vdp',), - Awakenings.EQUIP: ('equip', 'assist'), + Awakenings.EQUIP: ('equip', 'assist', 'eq'), Awakenings.SUPERFUA: ('sfua',), Awakenings.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), Awakenings.UNBINDABLE: ('unbindable',), Awakenings.EXTMOVEPLUS: ('te+', 'finger+'), - Awakenings.CLOUDRESIST: ('cloudres',), - Awakenings.TAPERESIST: ('taperes',), + Awakenings.CLOUDRESIST: ('cloudres', 'cloud'), + Awakenings.TAPERESIST: ('taperes', 'tape'), Awakenings.SKILLBOOSTPLUS: ('sb+',), Awakenings.HP80ORMORE: ('>80', 'highhp'), Awakenings.HP50ORLESS: ('<50', 'lowhp'), - Awakenings.ELSHIELD: ('elshield',), + Awakenings.ELSHIELD: ('elshield', 'elh', 'hel'), Awakenings.ELATTACK: ('el',), Awakenings.ENHCOMBO10C: ('10c',), Awakenings.COMBOORB: ('co', 'corb'), Awakenings.VOICE: ('voice',), - Awakenings.DUNGEONBONUS: (), + Awakenings.DUNGEONBONUS: ('dgbonus', 'dgboost'), Awakenings.REDUCEDHP: ('hp-',), Awakenings.REDUCEDATK: ('atk-',), Awakenings.REDUCEDRCV: ('rcv-',), Awakenings.UNBLINDABLE: ('resb+', 'b+',), Awakenings.UNJAMMABLE: ('resj+', 'j+',), Awakenings.UNPOISONABLE: ('resp+', 'p+',), - Awakenings.JAMMERBLESSING: ('jblessing',), - Awakenings.POISONBLESSING: ('pblessing',), + Awakenings.JAMMERBLESSING: ('jblessing', 'sfj', 'jsurge'), + Awakenings.POISONBLESSING: ('pblessing', 'sfp', 'psurge'), Awakenings.REDCOMBOCOUNT: ('ccr',), Awakenings.BLUECOMBOCOUNT: ('ccb',), Awakenings.GREENCOMBOCOUNT: ('ccg',), From 089b82b9018802cc9e715be3ef572c9b4b24c2b9 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Jan 2021 02:01:13 -0800 Subject: [PATCH 0390/1220] Add beginning-of-token name token matching (#419) * Add prefix matching * Add precedence --- padinfo/find_monster.py | 12 ++++++++++-- padinfo/padinfo.py | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 9f9cfd96..a9035a1c 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -6,6 +6,13 @@ def calc_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() +def calc_ratio_prefix(token, full_word): + if full_word == token: + return 1 + elif full_word.startswith(token): + return 1 - len(full_word)/100 + return difflib.SequenceMatcher(None, token, full_word).ratio() + class FindMonster: def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): @@ -75,16 +82,17 @@ def process_name_tokens(self, name_query_tokens, index2): valid = set() ms = difflib.get_close_matches(t, index2.name_tokens, n=10000, cutoff=.8) + ms += [token for token in index2.name_tokens if token.startswith(t)] if not ms: return None, None for match in ms: for m in index2.manual[match]: if m not in valid: - monsterscore[m] += calc_ratio(t, match) + .001 + monsterscore[m] += calc_ratio_prefix(t, match) + .001 valid.add(m) for m in index2.tokens[match]: if m not in valid: - monsterscore[m] += calc_ratio(t, match) + monsterscore[m] += calc_ratio_prefix(t, match) valid.add(m) if monstergen is not None: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1475ae1f..3dba4701 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1038,7 +1038,7 @@ async def findMonster3(self, raw_query): DGCOG.index2.multi_word_tokens, DGCOG.index2.all_prefixes) - # print(prefix_tokens, name_query_tokens) + print(prefix_tokens, name_query_tokens) if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) @@ -1058,7 +1058,7 @@ async def findMonster3(self, raw_query): # no prefixes match any monster in the evo tree return - # print(monster_gen) + print(monster_gen) # Return most likely candidate based on query. mon = max(monster_gen, key=lambda m: (monster_score[m], From b851150e1e0944da3c15555f59b51d69eede56c7 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Jan 2021 03:06:28 -0800 Subject: [PATCH 0391/1220] Add jp, idjp, na, idna (solves voltron/pr) (#442) * Add prefix matching * Add precidence * Add jp, na, idna, and idjp * Comment --- dadguide/monster_index.py | 12 ++++++++++++ dadguide/token_mappings.py | 6 +++++- padinfo/padinfo.py | 7 ++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 5b0b23b1..462cdbae 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -209,6 +209,18 @@ async def get_prefixes(self, m): for t in MISC_PREFIX_MAP[MiscPrefixes.FARMABLE]: prefix.add(t) + # Server + if m.on_jp: + for t in MISC_PREFIX_MAP[MiscPrefixes.INJP]: + prefix.add(t) + if m.on_na: + for t in MISC_PREFIX_MAP[MiscPrefixes.INNA]: + prefix.add(t) + if m.monster_id+10000 in self.graph.nodes: + prefix.add("idjp") + if m.monster_id > 10000: + prefix.add("idna") + return prefix diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 46253349..7505f32b 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -238,12 +238,16 @@ class MiscPrefixes(Enum): CHIBI = 'chibi' NONCHIBI = 'nonchibi' FARMABLE = 'farmable' + INJP = 'injp' + INNA = 'inna' MISC_PREFIX_MAP = { MiscPrefixes.CHIBI: ('chibi', 'mini'), MiscPrefixes.NONCHIBI: ('nonchibi', 'nc'), - MiscPrefixes.FARMABLE: ('farmable', 'nrem') + MiscPrefixes.FARMABLE: ('farmable', 'nrem'), + MiscPrefixes.INJP: ('jp',), + MiscPrefixes.INNA: ('na',) } PREFIX_MAPS = { diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3dba4701..79c066ba 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1027,14 +1027,14 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) return monster_index.find_monster2(query) - async def findMonster3(self, raw_query): + async def findMonster3(self, query): DGCOG = self.bot.get_cog("Dadguide") await DGCOG.wait_until_ready() if DGCOG is None: raise ValueError("Dadguide cog is not loaded") - raw_query = rmdiacritics(raw_query).lower() - prefix_tokens, nprefix_tokens, name_query_tokens = find_monster.interpret_query(raw_query, + query = rmdiacritics(query).lower() + prefix_tokens, nprefix_tokens, name_query_tokens = find_monster.interpret_query(query, DGCOG.index2.multi_word_tokens, DGCOG.index2.all_prefixes) @@ -1063,6 +1063,7 @@ async def findMonster3(self, raw_query): # Return most likely candidate based on query. mon = max(monster_gen, key=lambda m: (monster_score[m], not m.is_equip, + m.monster_id > 10000 and re.search("\d{4}", query), # Match na on id overlap -DGCOG.database.graph.get_base_id(m), m.rarity, m.monster_no_na)) From 60567baef4683c70eae61854955cbc7cbd2cba0d Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Jan 2021 03:12:01 -0800 Subject: [PATCH 0392/1220] Add MP and REM as prefixes (#443) --- dadguide/monster_index.py | 12 ++++++++++-- dadguide/token_mappings.py | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 462cdbae..3deb8c3c 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -204,11 +204,19 @@ async def get_prefixes(self, m): for t in MISC_PREFIX_MAP[MiscPrefixes.NONCHIBI]: prefix.add(t) - # Farmable + # Method of Obtaining if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): for t in MISC_PREFIX_MAP[MiscPrefixes.FARMABLE]: prefix.add(t) + if self.graph.monster_is_mp_evo(m): + for t in MISC_PREFIX_MAP[MiscPrefixes.MP]: + prefix.add(t) + + if self.graph.monster_is_rem_evo(m): + for t in MISC_PREFIX_MAP[MiscPrefixes.REM]: + prefix.add(t) + # Server if m.on_jp: for t in MISC_PREFIX_MAP[MiscPrefixes.INJP]: @@ -216,7 +224,7 @@ async def get_prefixes(self, m): if m.on_na: for t in MISC_PREFIX_MAP[MiscPrefixes.INNA]: prefix.add(t) - if m.monster_id+10000 in self.graph.nodes: + if m.monster_id + 10000 in self.graph.nodes: prefix.add("idjp") if m.monster_id > 10000: prefix.add("idna") diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 7505f32b..ac1cceee 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -238,6 +238,8 @@ class MiscPrefixes(Enum): CHIBI = 'chibi' NONCHIBI = 'nonchibi' FARMABLE = 'farmable' + REM = 'rem' + REM = 'mp' INJP = 'injp' INNA = 'inna' @@ -246,6 +248,8 @@ class MiscPrefixes(Enum): MiscPrefixes.CHIBI: ('chibi', 'mini'), MiscPrefixes.NONCHIBI: ('nonchibi', 'nc'), MiscPrefixes.FARMABLE: ('farmable', 'nrem'), + MiscPrefixes.REM: ('rem',), + MiscPrefixes.MP: ('mp',), MiscPrefixes.INJP: ('jp',), MiscPrefixes.INNA: ('na',) } From d1decc64ef9e30d0052a62d2e58dd26d9465cdf0 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Jan 2021 03:14:44 -0800 Subject: [PATCH 0393/1220] Fix reused key in MiscPrefixes enum (#444) --- dadguide/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index ac1cceee..605c8eb1 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -239,7 +239,7 @@ class MiscPrefixes(Enum): NONCHIBI = 'nonchibi' FARMABLE = 'farmable' REM = 'rem' - REM = 'mp' + MP = 'mp' INJP = 'injp' INNA = 'inna' From aac54e8fec9482cfb169546cb65f4fa7ccb40ac7 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Jan 2021 03:19:26 -0800 Subject: [PATCH 0394/1220] Fix match for monsters with id over 10000 (#445) Voltron/Power Rangers fix --- dadguide/monster_index.py | 1 + padinfo/padinfo.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 3deb8c3c..36f8287a 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -69,6 +69,7 @@ async def _build_monster_index(self, monsters): # ID self.tokens[str(m.monster_id)].add(m) + self.tokens[str(m.monster_id % 10000)].add(m) # Name Tokens for token in self._name_to_tokens(m.name_en): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 79c066ba..5204babd 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -767,7 +767,7 @@ async def idt_run(self, ctx): if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{q.ljust(ml)} - Ex: {m and m.monster_id}, Ac: {r['result']}{reason}\n" + o += f"{q.ljust(ml)} - Ex: {r['result']}, Ac: {m and m.monster_id}{reason}\n" else: c += 1 if c: @@ -1063,7 +1063,7 @@ async def findMonster3(self, query): # Return most likely candidate based on query. mon = max(monster_gen, key=lambda m: (monster_score[m], not m.is_equip, - m.monster_id > 10000 and re.search("\d{4}", query), # Match na on id overlap + bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap -DGCOG.database.graph.get_base_id(m), m.rarity, m.monster_no_na)) From 43a6d626bcbbe4506bfa9e1bdb4500d4559269c1 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Jan 2021 03:43:02 -0800 Subject: [PATCH 0395/1220] Use lowercase for all tokens (#447) * Use lowercase for all tokens * Add to MWTokens --- dadguide/monster_index.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 36f8287a..e4854135 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -22,33 +22,37 @@ async def __ainit__(self, monsters, db): self.monster_id_to_nickname = defaultdict(set) self.monster_id_to_treename = defaultdict(set) - self.series_id_to_pantheon_nickname = defaultdict(set, {m.series_id: {m.series.name_en} + self.series_id_to_pantheon_nickname = defaultdict(set, {m.series_id: {m.series.name_en.lower().replace(" ", "")} for m in db.get_all_monsters()}) - self.multi_word_tokens = set() + + self.multi_word_tokens = {tuple(m.series.name_en.lower().split()) + for m + in db.get_all_monsters() + if " " in m.series.name_en} nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) for name, m_id, *data in nickname_data: _, i, *_ = data + [None, None] if m_id.isdigit() and not i: if " " in name: - self.multi_word_tokens.add(tuple(name.split(" "))) - self.monster_id_to_nickname[int(m_id)].add(name.replace(" ", "")) + self.multi_word_tokens.add(tuple(name.lower().split(" "))) + self.monster_id_to_nickname[int(m_id)].add(name.lower().replace(" ", "")) treenames_data = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) for m_id, name, *data in treenames_data: _, i, *_ = data + [None, None] if m_id.isdigit() and not i: if " " in name: - self.multi_word_tokens.add(tuple(name.split(" "))) - self.monster_id_to_treename[int(m_id)].add(name.replace(" ", "")) + self.multi_word_tokens.add(tuple(name.lower().split(" "))) + self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) pantheon_data = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) for name, _, sid, *_ in pantheon_data: if sid.isdigit(): if " " in name: - self.multi_word_tokens.add(tuple(name.split(" "))) - self.series_id_to_pantheon_nickname[int(sid)].add(name.replace(" ", "")) + self.multi_word_tokens.add(tuple(name.lower().split(" "))) + self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) self.manual = self.tokens = self.monster_prefixes = defaultdict(set) await self._build_monster_index(monsters) From cc74b3924efdfee2b25867fe7779c2806e9f1455 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 9 Jan 2021 18:02:29 -0600 Subject: [PATCH 0396/1220] Rename debugid to 1 & debugid3 to debugid (#449) --- padglobal/padglobal.py | 4 ++-- padinfo/padinfo.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 651f66a7..32753878 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -196,7 +196,7 @@ async def togglepadglobal(self, ctx): @commands.command() @is_padglobal_admin() - async def debugiddump(self, ctx): + async def debugid1dump(self, ctx): padinfo_cog = self.bot.get_cog('PadInfo') mi = padinfo_cog.index_all @@ -213,7 +213,7 @@ async def write_send(nn_map, file_name): @commands.command(aliases=['iddebug']) @is_padglobal_admin() - async def debugid(self, ctx, *, query): + async def debugid1(self, ctx, *, query): padinfo_cog = self.bot.get_cog('PadInfo') # m is a named monster m, err, debug_info = await lookup_named_monster(query) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5204babd..4040425f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1071,7 +1071,7 @@ async def findMonster3(self, query): return mon @commands.command() - async def debugid3(self, ctx, *, query): + async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" DGCOG = self.bot.get_cog("Dadguide") m = await self.findMonster3(query) @@ -1097,7 +1097,7 @@ async def idhelp(self, ctx, *, query="", is_failed_query=False): if query: await ctx.send("See for " "documentation on ^id!") - await self.debugid3(ctx, query=query) + await self.debugid(ctx, query=query) else: await ctx.send("{}See for " "documentation on `^id`! You can also run `[p]idhelp ` to get " From 8d2a43f009c9229a444b6dfeb2ed75287ef2693c Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 01:57:04 -0800 Subject: [PATCH 0397/1220] Bump TSUtils version on PadInfo (#453) Requires version that fixes menus not working in DM bug --- padinfo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/info.json b/padinfo/info.json index cba19d00..563f77df 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.0.0", + "tsutils>=3.0.1", "prettytable" ], "tags": [ From be85ea59c1fd7a0d00a12e1d53c6050f5588487d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 10 Jan 2021 04:54:57 -0600 Subject: [PATCH 0398/1220] Fix cloud awo name token/prefix conflict (#454) --- dadguide/token_mappings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 605c8eb1..5256d99b 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -207,8 +207,8 @@ class Awakenings(Enum): Awakenings.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), Awakenings.UNBINDABLE: ('unbindable',), Awakenings.EXTMOVEPLUS: ('te+', 'finger+'), - Awakenings.CLOUDRESIST: ('cloudres', 'cloud'), - Awakenings.TAPERESIST: ('taperes', 'tape'), + Awakenings.CLOUDRESIST: ('cloudres',), # don't have cloud because that's a name token + Awakenings.TAPERESIST: ('taperes',), Awakenings.SKILLBOOSTPLUS: ('sb+',), Awakenings.HP80ORMORE: ('>80', 'highhp'), Awakenings.HP50ORLESS: ('<50', 'lowhp'), From c82809fcd7c436b065f5fa21f6be1669ccf584df Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 03:52:46 -0800 Subject: [PATCH 0399/1220] Sort DebugId prefix tokens (#456) Also removes nonhibi & nc prefixes --- dadguide/dadguide.py | 3 +++ dadguide/monster_index.py | 3 --- dadguide/token_mappings.py | 27 +++++++++++++++++++++++---- padglobal/padglobal.py | 2 +- padinfo/padinfo.py | 36 +++++++++++++++++++++++++++--------- 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 6b7df318..c7c379ea 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -24,6 +24,7 @@ from .old_monster_index import MonsterIndex from .monster_index import MonsterIndex2 from .database_loader import load_database +from . import token_mappings from .models.monster_model import MonsterModel @@ -85,6 +86,8 @@ def __init__(self, bot, *args, **kwargs): self.monster_stats = monster_stats self.MonsterStatModifierInput = MonsterStatModifierInput + self.token_maps = token_mappings + async def wait_until_ready(self): """Wait until the Dadguide cog is ready. diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index e4854135..6f0212d5 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -205,9 +205,6 @@ async def get_prefixes(self, m): 'ミニ' in m.name_ja or '(chibi)' in m.name_en: for t in MISC_PREFIX_MAP[MiscPrefixes.CHIBI]: prefix.add(t) - else: - for t in MISC_PREFIX_MAP[MiscPrefixes.NONCHIBI]: - prefix.add(t) # Method of Obtaining if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 5256d99b..c33c47b9 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -12,7 +12,7 @@ Attribute.Nil: ('nil', 'x', 'none', 'null', 'white') } -SUB_COLOR_MAP = {k: tuple('?'+t for t in v) for k, v in COLOR_MAP.items()} +SUB_COLOR_MAP = {k: tuple('?'+t for t in v if t != "white") for k, v in COLOR_MAP.items()} DUAL_COLOR_MAP = {} for cid1, cns1 in COLOR_MAP.items(): @@ -24,7 +24,8 @@ continue if len(t1) + len(t2) == 2: ts += (t1 + t2,) - ts += (t1 + "/" + t2,) + if (len(t1) == 1) == (len(t2) == 1): + ts += (t1 + "/" + t2,) DUAL_COLOR_MAP[(cid1, cid2)] = ts @@ -246,7 +247,6 @@ class MiscPrefixes(Enum): MISC_PREFIX_MAP = { MiscPrefixes.CHIBI: ('chibi', 'mini'), - MiscPrefixes.NONCHIBI: ('nonchibi', 'nc'), MiscPrefixes.FARMABLE: ('farmable', 'nrem'), MiscPrefixes.REM: ('rem',), MiscPrefixes.MP: ('mp',), @@ -256,12 +256,31 @@ class MiscPrefixes(Enum): PREFIX_MAPS = { **COLOR_MAP, - **TYPE_MAP, + **SUB_COLOR_MAP, **DUAL_COLOR_MAP, + **TYPE_MAP, + **AWOKEN_PREFIX_MAP, **EVO_PREFIX_MAP, **MISC_PREFIX_MAP, } +COLOR_TOKENS = { + *sum(COLOR_MAP.values(), ()), + *sum(SUB_COLOR_MAP.values(), ()), + *sum(DUAL_COLOR_MAP.values(), ()), +} + +AWAKENING_TOKENS = {*sum(AWOKEN_PREFIX_MAP.values(), ())} +EVO_TOKENS = {*sum(EVO_PREFIX_MAP.values(), ())} +TYPE_TOKENS = {*sum(TYPE_MAP.values(), ())} + + +OTHER_HIDDEN_TOKENS = set()\ + .union(COLOR_TOKENS)\ + .union(AWAKENING_TOKENS)\ + .union(EVO_TOKENS)\ + .union(TYPE_TOKENS) + TOKEN_REPLACEMENTS = defaultdict(tuple, { 'tamadra': ('tama',), 'evolution': ('evo',), diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 32753878..b2e419f4 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -211,7 +211,7 @@ async def write_send(nn_map, file_name): await write_send(mi.all_entries, 'all_entries.csv') await write_send(mi.two_word_entries, 'two_word_entries.csv') - @commands.command(aliases=['iddebug']) + @commands.command(aliases=['iddebug1']) @is_padglobal_admin() async def debugid1(self, ctx, *, query): padinfo_cog = self.bot.get_cog('PadInfo') diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4040425f..8f23c8bd 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -14,7 +14,8 @@ import tsutils from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter -from redbot.core.utils.chat_formatting import box, inline, pagify +from redbot.core.utils.chat_formatting import box, inline, pagify, text_to_file +from tabulate import tabulate from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor from .button_info import button_info @@ -28,7 +29,6 @@ logger = logging.getLogger('red.padbot-cogs.padinfo') - EMBED_NOT_GENERATED = -1 @@ -1063,14 +1063,15 @@ async def findMonster3(self, query): # Return most likely candidate based on query. mon = max(monster_gen, key=lambda m: (monster_score[m], not m.is_equip, - bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap + bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), + # Match na on id overlap -DGCOG.database.graph.get_base_id(m), m.rarity, m.monster_no_na)) return mon - @commands.command() + @commands.command(aliases=["iddebug"]) async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" DGCOG = self.bot.get_cog("Dadguide") @@ -1079,14 +1080,20 @@ async def debugid(self, ctx, *, query): await ctx.send(box("Your query didn't match any monsters.")) return bm = DGCOG.database.graph.get_base_monster(m) + pfxs = DGCOG.index2.monster_prefixes[m] + EVOANDTYPE = DGCOG.token_maps.EVO_TOKENS.union(DGCOG.token_maps.TYPE_TOKENS) o = (f"[{m.monster_id}] {m.name_en}\n" f"Base: [{bm.monster_id}] {bm.name_en}\n" f"Series: {m.series.name_en} ({m.series_id})\n\n" - f"Name Tokens: {' '.join(sorted(t for t, ms in DGCOG.index2.tokens.items() if m in ms))}\n\n" - f"Manual Tokens: \n" - f"\tTreenames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms))}\n" - f"\tNicknames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms))}\n\n" - f"Prefix Tokens: {' '.join(sorted(DGCOG.index2.monster_prefixes[m]))}\n") + f"[Name Tokens] {' '.join(sorted(t for t, ms in DGCOG.index2.tokens.items() if m in ms))}\n\n" + f"[Manual Tokens]\n" + f" Treenames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms))}\n\n" + f"[Prefix Tokens]\n" + f" Attribute: {' '.join(sorted(t for t in pfxs if t in DGCOG.token_maps.COLOR_TOKENS))}\n" + f" Awakening: {' '.join(sorted(t for t in pfxs if t in DGCOG.token_maps.AWAKENING_TOKENS))}\n" + f" Evo & Type: {' '.join(sorted(t for t in pfxs if t in EVOANDTYPE))}\n" + f" Other: {' '.join(sorted(t for t in pfxs if t not in DGCOG.token_maps.OTHER_HIDDEN_TOKENS))}\n") for page in pagify(o): await ctx.send(box(page)) @@ -1103,6 +1110,17 @@ async def idhelp(self, ctx, *, query="", is_failed_query=False): "documentation on `^id`! You can also run `[p]idhelp ` to get " "help with querying a specific monster.".format(failed_query_msg)) + @commands.command() + async def exportprefixes(self, ctx): + tms = self.bot.get_cog("Dadguide").token_maps + + ctable = [] + ctable += [(k.name, ",".join(v)) for k, v in tms.COLOR_MAP.items()] + ctable += [("sub " + k.name, ",".join(v)) for k, v in tms.SUB_COLOR_MAP.items()] + ctable += [(k[0].name + "/" + k[1].name, ",".join(v)) for k, v in tms.DUAL_COLOR_MAP.items()] + + await ctx.send(file=text_to_file(tabulate(ctable, headers=["Category", "Tokens"], tablefmt="github"))) + class PadInfoSettings(CogSettings): def make_default_settings(self): From a41509865eb88fd92bb612f634305b574549cfff Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 05:58:43 -0800 Subject: [PATCH 0400/1220] Add exportprefixes (#458) --- dadguide/database_context.py | 15 ++++++++------ dadguide/models/base_model.py | 2 +- dadguide/token_mappings.py | 38 +++++++++++++++++------------------ padinfo/padinfo.py | 36 +++++++++++++++++++++++++-------- 4 files changed, 57 insertions(+), 34 deletions(-) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 7b5382b8..00558231 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -1,11 +1,10 @@ -from datetime import datetime -from collections import OrderedDict, defaultdict, deque -from typing import Optional +from typing import Generator, List from .database_manager import DadguideDatabase -from .monster_graph import MonsterGraph -from .models.scheduled_event_model import ScheduledEventModel +from .models.awoken_skill_model import AwokenSkillModel from .models.dungeon_model import DungeonModel +from .models.scheduled_event_model import ScheduledEventModel +from .monster_graph import MonsterGraph SCHEDULED_EVENT_QUERY = """SELECT schedule.*, @@ -56,7 +55,7 @@ def get_all_monsters(self, as_generator=True): return [*monsters] return monsters - def get_all_events(self) -> ScheduledEventModel: + def get_all_events(self) -> Generator[ScheduledEventModel, None, None]: result = self.database.query_many(SCHEDULED_EVENT_QUERY, ()) for se in result: se['dungeon_model'] = DungeonModel(name_ja=se['d_name_ja'], @@ -65,6 +64,10 @@ def get_all_events(self) -> ScheduledEventModel: **se) yield ScheduledEventModel(**se) + def get_all_awoken_skills(self) -> List[AwokenSkillModel]: + result = self.database.query_many("SELECT * FROM awoken_skills", ()) + return [AwokenSkillModel(**r) for r in result] + def has_database(self): return self.database.has_database() diff --git a/dadguide/models/base_model.py b/dadguide/models/base_model.py index 4df9c912..47e99e47 100644 --- a/dadguide/models/base_model.py +++ b/dadguide/models/base_model.py @@ -7,7 +7,7 @@ def to_dict(self): raise NotImplementedError def to_string(self): - return json.dumps(self.to_dict()) + return self.__class__.__name__ + "<" + json.dumps(self.to_dict())[1:-1] + ">" def __repr__(self): try: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index c33c47b9..a369c8a2 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -46,17 +46,17 @@ class EvoTypes(Enum): - BASE = 'base' - EVO = 'evolved' - UVO = 'ulimate' - UUVO = 'super ultimate' - TRANS = 'transform' - AWOKEN = 'awoken' - MEGA = 'mega awoken' - REVO = 'reincarnated' - SREVO = 'super reincarnated' - PIXEL = 'pixel' - NONPIXEL = 'nonpixel' + BASE = 'Base' + EVO = 'Evolved' + UVO = 'Ulimate' + UUVO = 'Super Ultimate' + TRANS = 'Transform' + AWOKEN = 'Awoken' + MEGA = 'Mega Awoken' + REVO = 'Reincarnated' + SREVO = 'Super Reincarnated' + PIXEL = 'Pixel' + NONPIXEL = 'Nonpixel' EVO_PREFIX_MAP = { @@ -236,18 +236,18 @@ class Awakenings(Enum): class MiscPrefixes(Enum): - CHIBI = 'chibi' - NONCHIBI = 'nonchibi' - FARMABLE = 'farmable' - REM = 'rem' - MP = 'mp' - INJP = 'injp' - INNA = 'inna' + CHIBI = 'Chibi' + NONCHIBI = 'Non-Chibi' + FARMABLE = 'Farmable' + REM = 'REM' + MP = 'MP' + INJP = 'In JP Server' + INNA = 'In NA Server' MISC_PREFIX_MAP = { MiscPrefixes.CHIBI: ('chibi', 'mini'), - MiscPrefixes.FARMABLE: ('farmable', 'nrem'), + MiscPrefixes.FARMABLE: ('farmable',), MiscPrefixes.REM: ('rem',), MiscPrefixes.MP: ('mp',), MiscPrefixes.INJP: ('jp',), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8f23c8bd..8dfa46ac 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1112,14 +1112,34 @@ async def idhelp(self, ctx, *, query="", is_failed_query=False): @commands.command() async def exportprefixes(self, ctx): - tms = self.bot.get_cog("Dadguide").token_maps - - ctable = [] - ctable += [(k.name, ",".join(v)) for k, v in tms.COLOR_MAP.items()] - ctable += [("sub " + k.name, ",".join(v)) for k, v in tms.SUB_COLOR_MAP.items()] - ctable += [(k[0].name + "/" + k[1].name, ",".join(v)) for k, v in tms.DUAL_COLOR_MAP.items()] - - await ctx.send(file=text_to_file(tabulate(ctable, headers=["Category", "Tokens"], tablefmt="github"))) + DGCOG = self.bot.get_cog("Dadguide") + tms = DGCOG.token_maps + o = ("Jump to:\n\n" + "* [Types](#types)\n" + "* [Evolutions](#evolutions)\n" + "* [Misc](#misc)\n" + "* [Awakenings](#awakenings)\n" + "* [Attributes](#attributes)\n\n\n\n") + + anames = DGCOG.database.get_all_awoken_skills() + + etable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.EVO_PREFIX_MAP.items()] + o += "\n\n### Evolutions\n\n"+tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") + ttable = [(k.name, ", ".join(map(inline, v))) for k, v in tms.TYPE_MAP.items()] + o += "\n\n### Types\n\n"+tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") + mtable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.MISC_PREFIX_MAP.items()] + o += "\n\n### Misc\n\n"+tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") + atable = [(anames[k.value - 1].name_en, ", ".join(map(inline, v))) for k, v in tms.AWOKEN_PREFIX_MAP.items()] + o += "\n\n### Awakenings\n\n"+tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") + ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in tms.COLOR_MAP.items()] + ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in tms.SUB_COLOR_MAP.items()] + for k, v in tms.DUAL_COLOR_MAP.items(): + k0name = k[0].name.replace("Nil", "None") + k1name = k[1].name.replace("Nil", "None") + ctable.append((k0name + "/" + k1name, ", ".join(map(inline, v)))) + o += "### Attributes\n\n"+tabulate(ctable, headers=["Meaning", "Tokens"], tablefmt="github") + + await ctx.send(file=text_to_file(o, filename="table.md")) class PadInfoSettings(CogSettings): From f09d24e1c2dafacde4d2229c5776bdba72335d13 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 06:11:18 -0800 Subject: [PATCH 0401/1220] Fix prefix in id3 query failure message (#459) --- padinfo/padinfo.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8dfa46ac..1f9d2c3a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -959,11 +959,11 @@ async def makeFailureMsg(self, ctx: discord.abc.Messageable, err): if await self.config.user(ctx.author).beta_id3(): await self.idhelp(ctx, is_failed_query=True) return - msg = ('Lookup failed: {}.\n' + msg = ('Lookup failed: {0}.\n' 'Try one of , , [argbld]/[rgbld] . ' - 'Unexpected results? Use ^helpid for more info.').format(err) + 'Unexpected results? Use {1.prefix}helpid for more info.').format(err, ctx) await ctx.send(box(msg)) - await ctx.send('Looking for the beta test? Type `^idset beta y`') + await ctx.send('Looking for the beta test? Type `{0.prefix}idset beta y`'.format(ctx)) async def findMonsterCustom(self, ctx, query, server_filter=ServerFilter.any): if await self.config.user(ctx.author).beta_id3(): @@ -1103,12 +1103,12 @@ async def idhelp(self, ctx, *, query="", is_failed_query=False): failed_query_msg = "Query matched no results! " if is_failed_query else "" if query: await ctx.send("See for " - "documentation on ^id!") + "documentation on {0.prefix}id!".format(ctx)) await self.debugid(ctx, query=query) else: - await ctx.send("{}See for " - "documentation on `^id`! You can also run `[p]idhelp ` to get " - "help with querying a specific monster.".format(failed_query_msg)) + await ctx.send("{0}See for " + "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " + "help with querying a specific monster.".format(failed_query_msg, ctx)) @commands.command() async def exportprefixes(self, ctx): From 560f550b2baaa0a30b8e66c297f95bbdfc502c69 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 06:16:24 -0800 Subject: [PATCH 0402/1220] Add betacount (#460) --- padinfo/padinfo.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1f9d2c3a..8bd4f75d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -856,6 +856,16 @@ async def beta(self, ctx, value: bool = True): await self.config.user(ctx.author).beta_id3.set(value) await ctx.tick() + @idset.command() + @checks.is_owner() + async def betacount(self, ctx): + """Check the number of beta testers""" + c = 0 + for v in (await self.config.all_users()).values(): + if v['beta_id3']: + c += 1 + await ctx.send(inline(str(c) + " user(s) have opted in.")) + @is_donor() @idset.command() async def embedcolor(self, ctx, *, color): From b9ad2c7078cd42a0c48be8ba44d70d9c05f9e707 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 06:24:07 -0800 Subject: [PATCH 0403/1220] Store historic lookups for id3 (#462) --- padinfo/padinfo.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8bd4f75d..c3c21f46 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -182,6 +182,9 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) + self.historic_lookups_file_path_id3 = _data_file('historic_lookups_id3.json') + self.historic_lookups_id3 = safe_read_json(self.historic_lookups_file_path_id3) + self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) @@ -193,6 +196,7 @@ def cog_unload(self): self.index_jp = None self.historic_lookups = {} self.historic_lookups_id2 = {} + self.historic_lookups_id3 = {} async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -1038,6 +1042,15 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): return monster_index.find_monster2(query) async def findMonster3(self, query): + m = self._findMonster3(query) + + monster_no = nm.monster_id if nm else -1 + self.historic_lookups_id3[query] = monster_no + json.dump(self.historic_lookups_id3, open(self.historic_lookups_file_path_id3, "w+")) + + return m + + async def _findMonster3(self, query): DGCOG = self.bot.get_cog("Dadguide") await DGCOG.wait_until_ready() if DGCOG is None: From 3f26e71744578bb2d8b37ca92260aa5f90b2dd39 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 06:28:59 -0800 Subject: [PATCH 0404/1220] Fix recording historic id3 queries (#464) * Store historic lookups for id3 * await _findMonster3 --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c3c21f46..5aa854e3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1042,7 +1042,7 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): return monster_index.find_monster2(query) async def findMonster3(self, query): - m = self._findMonster3(query) + m = await self._findMonster3(query) monster_no = nm.monster_id if nm else -1 self.historic_lookups_id3[query] = monster_no From 098eeb24289323afd952b71e5c07d9956ec9cc7f Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 10 Jan 2021 06:31:15 -0800 Subject: [PATCH 0405/1220] Actually fix id3 historic lookups PR (#465) * Store historic lookups for id3 * await _findMonster3 * Use MonsterModel not NamedMonster --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5aa854e3..908404d7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1044,7 +1044,7 @@ async def _findMonster2(self, query, server_filter=ServerFilter.any): async def findMonster3(self, query): m = await self._findMonster3(query) - monster_no = nm.monster_id if nm else -1 + monster_no = m.monster_id if m else -1 self.historic_lookups_id3[query] = monster_no json.dump(self.historic_lookups_id3, open(self.historic_lookups_file_path_id3, "w+")) From ca21d987fea983e801ad280f98c4b87772bdbbce Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 12 Jan 2021 15:32:28 -0600 Subject: [PATCH 0406/1220] Add tabulate to requirements (#468) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0b6d3995..ba6bd09b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ aiohttp aiofiles aioodbc networkx +tabulate tsutils From 5113f38cbb8b054316b2b0ad3faf30af830eb8a4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 12 Jan 2021 15:32:38 -0600 Subject: [PATCH 0407/1220] Improve failed query message for id3 (#467) --- padinfo/padinfo.py | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 908404d7..4eca32e5 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -255,7 +255,7 @@ async def jpname(self, ctx, *, query: str): await ctx.send(IdMenu.monster_header(m)) await ctx.send(box(m.name_ja)) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command(name="id", aliases=["iD", "Id", "ID"]) @checks.bot_has_permissions(embed_links=True) @@ -290,7 +290,7 @@ async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) async def send_survey_after(self, ctx, query, result_monster): sm = await self.config.user(ctx.author).survey_mode() @@ -361,7 +361,7 @@ async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -378,7 +378,7 @@ async def _do_id3(self, ctx, query): if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: - await self.makeFailureMsg(ctx, "No monster matched") + await self.makeFailureMsg(ctx, query, "No monster matched") @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) @@ -388,7 +388,7 @@ async def evos(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.evo_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command(name="mats", aliases=['evomats', 'evomat']) @checks.bot_has_permissions(embed_links=True) @@ -398,7 +398,7 @@ async def evomats(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.mats_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -410,7 +410,7 @@ async def pantheon(self, ctx, *, query: str): if menu == EMBED_NOT_GENERATED: await ctx.send(inline('Not a pantheon monster')) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -422,7 +422,7 @@ async def skillups(self, ctx, *, query: str): if menu == EMBED_NOT_GENERATED: await ctx.send(inline('No skillups available')) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) async def _do_idmenu(self, ctx, m, starting_menu_emoji): DGCOG = self.bot.get_cog("Dadguide") @@ -546,7 +546,7 @@ async def pic(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.pic_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -559,7 +559,7 @@ async def links(self, ctx, *, query: str): await ctx.send(embed=embed) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command(aliases=['stats']) @checks.bot_has_permissions(embed_links=True) @@ -569,7 +569,7 @@ async def otherinfo(self, ctx, *, query: str): if m is not None: await self._do_idmenu(ctx, m, self.other_info_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -592,7 +592,7 @@ async def lookup(self, ctx, *, query: str): embed = await menu.make_header_embed(m) await ctx.send(embed=embed) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -602,7 +602,7 @@ async def evolist(self, ctx, *, query): if m is not None: await self._do_evolistmenu(ctx, m) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command(aliases=['collabscroll']) @checks.bot_has_permissions(embed_links=True) @@ -625,7 +625,7 @@ async def seriesscroll(self, ctx, *, query: str): if m is not None: await self._do_scrollmenu(ctx, m, ms, self.id_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -639,7 +639,7 @@ async def evoscroll(self, ctx, *, query: str): await self._do_scrollmenu(ctx, m, sorted(db_context.graph.get_alt_monsters(m), key=lambda x: x.monster_id), self.id_emoji) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) @@ -815,7 +815,7 @@ async def padsay(self, ctx, server, *, query: str = None): await ctx.send('Speaking for ' + header) await speech_cog.play_path(channel, voice_file) else: - await self.makeFailureMsg(ctx, err) + await self.makeFailureMsg(ctx, query, err) @commands.group(aliases=['idmode']) async def idset(self, ctx): @@ -969,9 +969,9 @@ def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] - async def makeFailureMsg(self, ctx: discord.abc.Messageable, err): + async def makeFailureMsg(self, ctx: discord.abc.Messageable, query: str, err): if await self.config.user(ctx.author).beta_id3(): - await self.idhelp(ctx, is_failed_query=True) + await self.idhelp(ctx, query=query, is_failed_query=True) return msg = ('Lookup failed: {0}.\n' 'Try one of , , [argbld]/[rgbld] . ' @@ -1123,13 +1123,14 @@ async def debugid(self, ctx, *, query): @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query="", is_failed_query=False): """Get help with an id query""" - failed_query_msg = "Query matched no results! " if is_failed_query else "" - if query: - await ctx.send("See for " + query_display = '`{}` '.format(query) if query != '' else '' + failed_query_msg = "Sorry, your query {}didn't match any results :( ".format(query_display) if is_failed_query else "" + if not is_failed_query: + await ctx.send("See for " "documentation on {0.prefix}id!".format(ctx)) await self.debugid(ctx, query=query) else: - await ctx.send("{0}See for " + await ctx.send("{0}See for " "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " "help with querying a specific monster.".format(failed_query_msg, ctx)) From 8d40c1b7dd47a454fd6e7b198b8fca12903b2deb Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Tue, 12 Jan 2021 16:38:17 -0800 Subject: [PATCH 0408/1220] Refactoring id_menu and starting to use discord-menu --- padinfo/common/__init__.py | 0 padinfo/common/emoji_map.py | 105 ++++++++++ padinfo/common/padx.py | 12 ++ padinfo/id_menu.py | 203 +++++--------------- padinfo/padinfo.py | 9 +- padinfo/view/__init__.py | 0 padinfo/view/components/__init__.py | 0 padinfo/view/components/base.py | 18 ++ padinfo/view/components/monster/__init__.py | 0 padinfo/view/components/monster/header.py | 40 ++++ padinfo/view/components/monster/image.py | 19 ++ padinfo/view/evos.py | 74 +++++++ requirements.txt | 22 +-- 13 files changed, 335 insertions(+), 167 deletions(-) create mode 100644 padinfo/common/__init__.py create mode 100644 padinfo/common/emoji_map.py create mode 100644 padinfo/common/padx.py create mode 100644 padinfo/view/__init__.py create mode 100644 padinfo/view/components/__init__.py create mode 100644 padinfo/view/components/base.py create mode 100644 padinfo/view/components/monster/__init__.py create mode 100644 padinfo/view/components/monster/header.py create mode 100644 padinfo/view/components/monster/image.py create mode 100644 padinfo/view/evos.py diff --git a/padinfo/common/__init__.py b/padinfo/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py new file mode 100644 index 00000000..f248828d --- /dev/null +++ b/padinfo/common/emoji_map.py @@ -0,0 +1,105 @@ +from discord import Emoji +from discordmenu.emoji_cache import emoji_cache + +AWAKENING_ID_TO_EMOJI_NAME_MAP = { + 1: 'boost_hp', + 2: 'boost_atk', + 3: 'boost_rcv', + 4: 'reduce_fire', + 5: 'reduce_water', + 6: 'reduce_wood', + 7: 'reduce_light', + 8: 'reduce_dark', + 9: 'misc_autoheal', + 10: 'res_bind', + 11: 'res_blind', + 12: 'res_jammer', + 13: 'res_poison', + 14: 'oe_fire', + 15: 'oe_water', + 16: 'oe_wood', + 17: 'oe_light', + 18: 'oe_dark', + 19: 'misc_te', + 20: 'misc_bindclear', + 21: 'misc_sb', + 22: 'row_fire', + 23: 'row_water', + 24: 'row_wood', + 25: 'row_light', + 26: 'row_dark', + 27: 'misc_tpa', + 28: 'res_skillbind', + 29: 'oe_heart', + 30: 'misc_multiboost', + 31: 'killer_dragon', + 32: 'killer_god', + 33: 'killer_devil', + 34: 'killer_machine', + 35: 'killer_balance', + 36: 'killer_attacker', + 37: 'killer_physical', + 38: 'killer_healer', + 39: 'killer_evomat', + 40: 'killer_awoken', + 41: 'killer_enhancemat', + 42: 'killer_vendor', + 43: 'misc_comboboost', + 44: 'misc_guardbreak', + 45: 'misc_extraattack', + 46: 'teamboost_hp', + 47: 'teamboost_rcv', + 48: 'misc_voidshield', + 49: 'misc_assist', + 50: 'misc_super_extraattack', + 51: 'misc_skillcharge', + 52: 'res_bind_super', + 53: 'misc_te_super', + 54: 'res_cloud', + 55: 'res_seal', + 56: 'misc_sb_super', + 57: 'attack_boost_high', + 58: 'attack_boost_low', + 59: 'l_shield', + 60: 'l_attack', + 61: 'misc_super_comboboost', + 62: 'orb_combo', + 63: 'misc_voice', + 64: 'misc_dungeonbonus', + 65: 'reduce_hp', + 66: 'reduce_atk', + 67: 'reduce_rcv', + 68: 'res_blind_super', + 69: 'res_jammer_super', + 70: 'res_poison_super', + 71: 'misc_jammerboost', + 72: 'misc_poisonboost', + 73: 'cc_fire', + 74: 'cc_water', + 75: 'cc_wood', + 76: 'cc_light', + 77: 'cc_dark', +} + +AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { + 606: 'unmatchable_clear', + 607: 'spinner_clear', + 608: 'absorb_pierce', +} + + +def awakening_restricted_latent_emoji(latent: "AwakeningRestrictedLatent"): + return str(get_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value]))) + + +def get_emoji(name): + if isinstance(name, int): + name = AWAKENING_ID_TO_EMOJI_NAME_MAP[name] + for e in emoji_cache.custom_emojis: + if e.name == name: + return e + return name + + +def format_emoji(emoji: Emoji): + return f"<:{emoji.name}:{emoji.id}>" diff --git a/padinfo/common/padx.py b/padinfo/common/padx.py new file mode 100644 index 00000000..ed2357ec --- /dev/null +++ b/padinfo/common/padx.py @@ -0,0 +1,12 @@ +from typing import TYPE_CHECKING + +import tsutils + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + +INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' + + +def monster_url(m: "MonsterModel"): + return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 9c2b0332..520669aa 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -4,22 +4,26 @@ import discord import prettytable -import tsutils +from discord import Color +from discordmenu.embed.components import EmbedMain, EmbedFooter, EmbedThumbnail from redbot.core.utils.chat_formatting import box +from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, awakening_restricted_latent_emoji +from padinfo.view.components.monster.header import MonsterHeader +from .common.padx import monster_url from .leader_skills import createMultiplierText from .leader_skills import createSingleMultiplierText +from .view.components.monster.image import MonsterImage +from .view.evos import EvosView if TYPE_CHECKING: from dadguide.database_context import DbContext from dadguide.models.monster_model import MonsterModel - from dadguide.models.enum_types import AwakeningRestrictedLatent INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' -RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' @@ -30,14 +34,6 @@ ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' -def get_pdx_url(m: "MonsterModel"): - return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) - - -def get_portrait_url(m: "MonsterModel"): - return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) - - def get_pic_url(m: "MonsterModel"): return RPAD_PIC_TEMPLATE.format(m.monster_id) @@ -56,54 +52,43 @@ def match_emoji(self, name): return e return name - @staticmethod - def monster_header(m: "MonsterModel", link=False): - # type_emojis = '{} '.format(''.join( - # [str(self.match_emoji('mons_type_{}'.format(t.name.lower()))) for t in m.types])) if show_types else '' - type_emojis = '' - msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - @staticmethod - def monster_ja_suffix(m: "MonsterModel", subname_on_override=True): - suffix = "" - if m.roma_subname and (subname_on_override or m.name_en_override is None): - suffix += ' [{}]'.format(m.roma_subname) - if not m.on_na: - suffix += ' (JP only)' - return suffix - - def monster_long_header(self, m: "MonsterModel", link=False): - msg = self.monster_header(m) + self.monster_ja_suffix(m) - return '[{}]({})'.format(msg, get_pdx_url(m)) if link else msg - - def monster_evo_header(self, m: "MonsterModel", link=True): - prefix = f" {self._get_monster_attr_emoji(m)} " - msg = f"{m.monster_no_na} - {m.name_en}" - suffix = self.monster_ja_suffix(m, False) - return prefix + ("[{}]({})".format(msg, get_pdx_url(m)) if link else msg) + suffix - - @staticmethod - def make_thumbnail_url(m: "MonsterModel"): - return get_portrait_url(m) - async def make_base_embed(self, m: "MonsterModel"): - header = self.monster_long_header(m) + header = MonsterHeader.long(m) embed = await self.make_custom_embed() - embed.set_thumbnail(url=self.make_thumbnail_url(m)) + embed.set_thumbnail(url=MonsterImage.icon(m)) embed.title = header - embed.url = get_pdx_url(m) + embed.url = monster_url(m) embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed + async def make_base_embed_v2(self, m: "MonsterModel"): + pdicog = self.ctx.bot.get_cog("PadInfo") + color = await self.get_user_embed_color(pdicog) + header = MonsterHeader.long_v2(m) + main = EmbedMain( + color=color, + title=header.to_markdown() + ) + footer = EmbedFooter('Requester may click the reactions below to switch tabs') + thumbnail = EmbedThumbnail(MonsterImage.icon(m)) + return main, footer, thumbnail + + async def get_user_embed_color(self, pdicog): + color = await pdicog.config.user(self.ctx.author).color() + if color is None: + return Color.default() + elif color == "random": + return Color(random.randint(0x000000, 0xffffff)) + else: + return discord.Color(color) + def _get_evo_list_fields(self, monsters, current_monster): if not len(monsters): return field_data = '' field_values = [] for ae in sorted(monsters, key=lambda x: int(x.monster_id)): - monster_header = self.monster_evo_header( - ae, link=ae.monster_id != current_monster.monster_id) + '\n' + monster_header = EvosView.evos(ae, link=ae.monster_id != current_monster.monster_id) + '\n' if len(field_data + monster_header) > 1024: field_values.append(field_data) field_data = "" @@ -111,11 +96,12 @@ def _get_evo_list_fields(self, monsters, current_monster): field_values.append(field_data) return field_values - def _get_monster_attr_emoji(self, monster: "MonsterModel"): - attr1 = monster.attr1.name.lower() - attr2 = monster.attr2.name.lower() - emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) - return self.match_emoji(emoji) + async def make_evo_embed_v2(self, m: "MonsterModel"): + alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) + gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + e = EvosView.embed(m, alt_versions, gem_versions, color) + return e.to_embed() async def make_evo_embed(self, m: "MonsterModel"): embed = await self.make_base_embed(m) @@ -154,7 +140,7 @@ def _add_mats_of_list(self, embed, monster_id_list, field_name): item_count = min(len(monster_id_list), 5) monster_list = [self.db_context.graph.get_monster(m) for m in monster_id_list] for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: - field_data += "{}\n".format(self.monster_long_header(ae, link=True)) + field_data += "{}\n".format(MonsterHeader.long(ae, link=True)) embed.add_field(name=field_name, value=field_data) async def make_evo_mats_embed(self, m: "MonsterModel"): @@ -166,7 +152,7 @@ async def make_evo_mats_embed(self, m: "MonsterModel"): field_data = '' if len(mats_for_evo) > 0: for ae in mats_for_evo: - field_data += "{}\n".format(self.monster_long_header(ae, link=True)) + field_data += "{}\n".format(MonsterHeader.long(ae, link=True)) else: field_data = 'None' embed.add_field(name=field_name, value=field_data) @@ -189,7 +175,7 @@ async def make_pantheon_embed(self, m: "MonsterModel"): field_name = 'Pantheon: ' + self.db_context.graph.get_monster(m.monster_no).series.name field_data = '' for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): - field_data += '\n' + self.monster_header(monster, link=True) + field_data += '\n' + MonsterHeader.short(monster, link=True) embed.add_field(name=field_name, value=field_data) return embed @@ -215,7 +201,7 @@ async def make_skillups_embed(self, m: "MonsterModel"): skillups_list = skillups_list[0:8] for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): - field_data += '\n' + self.monster_header(monster, link=True) + field_data += '\n' + MonsterHeader.short(monster, link=True) if len(field_data.strip()): embed.add_field(name=field_name, value=field_data) @@ -265,17 +251,17 @@ async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( - self.monster_header(left_m, link=True), + MonsterHeader.short(left_m, link=True), lls.desc if lls else 'None') description += '\n**{}**\n{}'.format( - self.monster_header(right_m, link=True), + MonsterHeader.short(right_m, link=True), rls.desc if rls else 'None') embed.description = description return embed async def make_header_embed(self, m: "MonsterModel"): - header = self.monster_long_header(m, link=True) + header = MonsterHeader.long(m, link=True) embed = await self.make_custom_embed() embed.description = header return embed @@ -367,7 +353,7 @@ async def make_embed(self, m: "MonsterModel"): stats_icons = [ self.match_emoji(63) if m.awakening_count(63) and not m.is_equip else '', self.match_emoji(1) if m.awakening_count(1) and not m.is_equip else '', - self.match_emoji(2) if m.awakening_count(2) and not m.is_equip else '', + self.match_emoji(2) if m.awakening_count(2) and not m.is_equip else '', self.match_emoji(3) if m.awakening_count(3) and not m.is_equip else '', ] stats_row_1 = '{} Stats (LB, +{}%)'.format(stats_icons[0], m.limit_mult) @@ -407,7 +393,7 @@ async def make_embed(self, m: "MonsterModel"): evos_header = "Alternate Evos" evos_body = ", ".join(f"**{m2.monster_id}**" if m2.monster_id == m.monster_id - else f"[{m2.monster_id}]({get_pdx_url(m2)})" + else f"[{m2.monster_id}]({monster_url(m2)})" for m2 in sorted({*self.db_context.graph.get_alt_monsters_by_id(m.monster_no)}, key=lambda x: x.monster_id)) @@ -436,15 +422,11 @@ def get_awakening_restricted_latents_text(self, m: "MonsterModel"): """ if not m.awakening_restricted_latents: return '' - return ' ' + ' '.join([self.awakening_restricted_latent_emoji(x) for x in m.awakening_restricted_latents]) + return ' ' + ' '.join([awakening_restricted_latent_emoji(x) for x in m.awakening_restricted_latents]) def killer_latent_emoji(self, latent: str): return str(self.match_emoji('latent_killer_{}'.format(latent.lower()))) - def awakening_restricted_latent_emoji(self, latent: "AwakeningRestrictedLatent"): - return str( - self.match_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value]))) - async def make_otherinfo_embed(self, m: "MonsterModel"): embed = await self.make_base_embed(m) # Clear the thumbnail, takes up too much space @@ -521,7 +503,7 @@ async def make_lssingle_embed(self, m: "MonsterModel"): embed.title = '{}\n\n'.format(multiplier_text) description = '' description += '\n**{}**\n{}'.format( - self.monster_header(m, link=True), + MonsterHeader.short(m, link=True), m.leader_skill.desc if m.leader_skill else 'None') embed.description = description @@ -536,90 +518,3 @@ async def make_custom_embed(self): return discord.Embed(color=random.randint(0x000000, 0xffffff)) else: return discord.Embed(color=discord.Color(color)) - - -AWAKENING_ID_TO_EMOJI_NAME_MAP = { - 1: 'boost_hp', - 2: 'boost_atk', - 3: 'boost_rcv', - 4: 'reduce_fire', - 5: 'reduce_water', - 6: 'reduce_wood', - 7: 'reduce_light', - 8: 'reduce_dark', - 9: 'misc_autoheal', - 10: 'res_bind', - 11: 'res_blind', - 12: 'res_jammer', - 13: 'res_poison', - 14: 'oe_fire', - 15: 'oe_water', - 16: 'oe_wood', - 17: 'oe_light', - 18: 'oe_dark', - 19: 'misc_te', - 20: 'misc_bindclear', - 21: 'misc_sb', - 22: 'row_fire', - 23: 'row_water', - 24: 'row_wood', - 25: 'row_light', - 26: 'row_dark', - 27: 'misc_tpa', - 28: 'res_skillbind', - 29: 'oe_heart', - 30: 'misc_multiboost', - 31: 'killer_dragon', - 32: 'killer_god', - 33: 'killer_devil', - 34: 'killer_machine', - 35: 'killer_balance', - 36: 'killer_attacker', - 37: 'killer_physical', - 38: 'killer_healer', - 39: 'killer_evomat', - 40: 'killer_awoken', - 41: 'killer_enhancemat', - 42: 'killer_vendor', - 43: 'misc_comboboost', - 44: 'misc_guardbreak', - 45: 'misc_extraattack', - 46: 'teamboost_hp', - 47: 'teamboost_rcv', - 48: 'misc_voidshield', - 49: 'misc_assist', - 50: 'misc_super_extraattack', - 51: 'misc_skillcharge', - 52: 'res_bind_super', - 53: 'misc_te_super', - 54: 'res_cloud', - 55: 'res_seal', - 56: 'misc_sb_super', - 57: 'attack_boost_high', - 58: 'attack_boost_low', - 59: 'l_shield', - 60: 'l_attack', - 61: 'misc_super_comboboost', - 62: 'orb_combo', - 63: 'misc_voice', - 64: 'misc_dungeonbonus', - 65: 'reduce_hp', - 66: 'reduce_atk', - 67: 'reduce_rcv', - 68: 'res_blind_super', - 69: 'res_jammer_super', - 70: 'res_poison_super', - 71: 'misc_jammerboost', - 72: 'misc_poisonboost', - 73: 'cc_fire', - 74: 'cc_water', - 75: 'cc_wood', - 76: 'cc_light', - 77: 'cc_dark', -} - -AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { - 606: 'unmatchable_clear', - 607: 'spinner_clear', - 608: 'absorb_pierce', -} diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4eca32e5..ec4c8ecc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -12,6 +12,7 @@ import discord import tsutils +from discordmenu.emoji_cache import emoji_cache from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline, pagify, text_to_file @@ -21,6 +22,7 @@ from .button_info import button_info from .find_monster import find_monster from .id_menu import IdMenu +from .view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dadguide.database_context import DbContext @@ -189,6 +191,9 @@ def __init__(self, bot, *args, **kwargs): self.config.register_user(survey_mode=0, color=None, beta_id3=False) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) + emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) + emoji_cache.refresh_from_discord_bot(self.bot) + def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.index_all = None @@ -252,7 +257,7 @@ async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: - await ctx.send(IdMenu.monster_header(m)) + await ctx.send(MonsterHeader.short(m)) await ctx.send(box(m.name_ja)) else: await self.makeFailureMsg(ctx, query, err) @@ -808,7 +813,7 @@ async def padsay(self, ctx, server, *, query: str = None): return base_dir = self.settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) - header = '{} ({})'.format(IdMenu.monster_header(m), server) + header = '{} ({})'.format(MonsterHeader.short(m), server) if not os.path.exists(voice_file): await ctx.send(inline('Could not find voice for ' + header)) return diff --git a/padinfo/view/__init__.py b/padinfo/view/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/view/components/__init__.py b/padinfo/view/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/view/components/base.py b/padinfo/view/components/base.py new file mode 100644 index 00000000..acd3b484 --- /dev/null +++ b/padinfo/view/components/base.py @@ -0,0 +1,18 @@ +from discordmenu.embed.components import EmbedFooter, EmbedMain + +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import monster_thumbnail + + +async def make_base_embed_v2(m: "MonsterModel", embed_color): + main = EmbedMain( + color=embed_color, + title=MonsterHeader.long_v2(m) + ) + footer = EmbedFooter('Requester may click the reactions below to switch tabs') + thumbnail = monster_thumbnail(m) + return main, footer, thumbnail + + +def pad_info_footer(): + return EmbedFooter('Requester may click the reactions below to switch tabs') diff --git a/padinfo/view/components/monster/__init__.py b/padinfo/view/components/monster/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py new file mode 100644 index 00000000..85e2f070 --- /dev/null +++ b/padinfo/view/components/monster/header.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.text import LinkedText, Text + +from padinfo.common.padx import monster_url + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class MonsterHeader: + @staticmethod + def ja_suffix(m: "MonsterModel", subname_on_override=True): + suffix = "" + if m.roma_subname and (subname_on_override or m.name_en_override is None): + suffix += ' [{}]'.format(m.roma_subname) + if not m.on_na: + suffix += ' (JP only)' + return suffix + + @staticmethod + def short(m: "MonsterModel", link=False): + type_emojis = '' + msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) + return '[{}]({})'.format(msg, monster_url(m)) if link else msg + + @staticmethod + def long(m: "MonsterModel", link=False): + msg = MonsterHeader.short(m) + MonsterHeader.ja_suffix(m) + return '[{}]({})'.format(msg, monster_url(m)) if link else msg + + @staticmethod + def short_v2(m: "MonsterModel", link=False): + msg = '[{}] {}'.format(m.monster_no_na, m.name_en) + return LinkedText(msg, monster_url(m)) if link else Text(msg) + + @staticmethod + def long_v2(m: "MonsterModel", link=False): + msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, MonsterHeader.ja_suffix(m)) + return LinkedText(msg, monster_url(m)) if link else Text(msg) diff --git a/padinfo/view/components/monster/image.py b/padinfo/view/components/monster/image.py new file mode 100644 index 00000000..4e901d06 --- /dev/null +++ b/padinfo/view/components/monster/image.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.components import EmbedThumbnail + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + +MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' +RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' + + +class MonsterImage: + @staticmethod + def icon(m: "MonsterModel"): + return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) + + +def monster_thumbnail(m: "MonsterModel"): + return EmbedThumbnail(MonsterImage.icon(m)) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py new file mode 100644 index 00000000..58d34467 --- /dev/null +++ b/padinfo/view/evos.py @@ -0,0 +1,74 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedThumbnail, EmbedFooter, EmbedMain, EmbedField +from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import Text, LinkedText +from discordmenu.emoji_cache import emoji_cache + +from padinfo.common.emoji_map import get_emoji, format_emoji +from padinfo.common.padx import monster_url +from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class EvosView: + @staticmethod + def _get_monster_attr_emoji(monster: "MonsterModel"): + attr1 = monster.attr1.name.lower() + attr2 = monster.attr2.name.lower() + emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) + return get_emoji(emoji) + + @staticmethod + def evos(m: "MonsterModel", link=True): + prefix = f" {EvosView._get_monster_attr_emoji(m)} " + msg = f"{m.monster_no_na} - {m.name_en}" + suffix = MonsterHeader.ja_suffix(m, False) + return prefix + ("[{}]({})".format(msg, monster_url(m)) if link else msg) + suffix + + @staticmethod + def _evo_line(m: "MonsterModel", link=True): + msg = f"{m.monster_no_na} - {m.name_en}" + return Box( + Text(format_emoji(EvosView._get_monster_attr_emoji(m))), + LinkedText(msg, monster_url(m)) if link else Text(msg), + Text(MonsterHeader.ja_suffix(m, False)), + delimiter=' ' + ) + + @staticmethod + def _evo_lines(monsters, current_monster): + if not len(monsters): + return [] + return [ + EvosView._evo_line(ae, link=ae.monster_id != current_monster.monster_id) + for ae in sorted(monsters, key=lambda x: int(x.monster_id)) + ] + + @staticmethod + def embed(m: "MonsterModel", alt_versions, gem_versions, color): + fields = [ + EmbedField( + "{} alternate evo(s)".format(len(alt_versions)), + Box(*EvosView._evo_lines(alt_versions, m)))] + + if gem_versions: + fields.append( + EmbedField( + "{} evolve gem(s)".format(len(gem_versions)), + Box(*EvosView._evo_lines(gem_versions, m)))) + + # todo: deal with field overflow + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + url=monster_url(m)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), + embed_footer=pad_info_footer(), + embed_fields=fields) diff --git a/requirements.txt b/requirements.txt index ba6bd09b..5c0f636b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ -# LEGACY DEPS - WILL UNCOMMENT AS THINGS ARE CONFIRMED NECESSARY -setuptools -wheel -Red-DiscordBot[postgres] -prettytable +aiofiles +aiohttp +aioodbc +discord-menu git+git://github.com/TsubakiBotPad/python-romkan -pymysql -ply +networkx opencv-python Pillow +ply +prettytable +pymysql pytz -aiohttp -aiofiles -aioodbc -networkx +Red-DiscordBot[postgres] +setuptools tabulate tsutils +wheel \ No newline at end of file From 696bfaeceeff1f31be7c1037cbba0a7b519e0438 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 12 Jan 2021 18:10:02 -0800 Subject: [PATCH 0409/1220] Fix incorrect typing (#473) also add discordmenu as requirement --- padinfo/info.json | 1 + padinfo/padinfo.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/info.json b/padinfo/info.json index 563f77df..b591fef3 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -21,6 +21,7 @@ }, "requirements": [ "tsutils>=3.0.1", + "discordmenu>=0.0.2", "prettytable" ], "tags": [ diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ec4c8ecc..2020acfd 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -974,7 +974,7 @@ def get_emojis(self): server_ids = [int(sid) for sid in self.settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] - async def makeFailureMsg(self, ctx: discord.abc.Messageable, query: str, err): + async def makeFailureMsg(self, ctx, query: str, err): if await self.config.user(ctx.author).beta_id3(): await self.idhelp(ctx, query=query, is_failed_query=True) return From 24402dc971c0af6afd7f53e2650e134999268e01 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 12 Jan 2021 20:18:41 -0600 Subject: [PATCH 0410/1220] Fix discord-menu requirement (#475) --- padinfo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/info.json b/padinfo/info.json index b591fef3..38bed8a5 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -21,7 +21,7 @@ }, "requirements": [ "tsutils>=3.0.1", - "discordmenu>=0.0.2", + "discord-menu>=0.0.2", "prettytable" ], "tags": [ From d19ddc12f80c0bbf6efce87cc5887ae631d563e9 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 12 Jan 2021 18:35:51 -0800 Subject: [PATCH 0411/1220] Change prefix to modifier in code (#474) * Change prefix to modifier in code * Requested Changes --- dadguide/monster_index.py | 110 ++++++++++++++++++------------------- dadguide/token_mappings.py | 32 +++++------ padinfo/find_monster.py | 33 ++++++----- padinfo/padinfo.py | 42 +++++++------- 4 files changed, 109 insertions(+), 108 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 6f0212d5..d3317e70 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -54,11 +54,11 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) - self.manual = self.tokens = self.monster_prefixes = defaultdict(set) + self.manual = self.tokens = self.modifiers = defaultdict(set) await self._build_monster_index(monsters) self.manual = combine_tokens(self.manual_nick, self.manual_tree) self.name_tokens = list(self.manual) + list(self.tokens) - self.all_prefixes = {p for ps in self.monster_prefixes.values() for p in ps} + self.all_modifiers = {p for ps in self.modifiers.values() for p in ps} __init__ = __ainit__ @@ -66,10 +66,10 @@ async def _build_monster_index(self, monsters): self.manual_nick = defaultdict(set) self.manual_tree = defaultdict(set) self.tokens = defaultdict(set) - self.monster_prefixes = defaultdict(set) + self.modifiers = defaultdict(set) async for m in AsyncIter(monsters): - self.monster_prefixes[m] = await self.get_prefixes(m) + self.modifiers[m] = await self.get_modifiers(m) # ID self.tokens[str(m.monster_id)].add(m) @@ -80,9 +80,9 @@ async def _build_monster_index(self, monsters): self.tokens[token.lower()].add(m) for repl in TOKEN_REPLACEMENTS[token.lower()]: self.tokens[repl].add(m) - for pas in PREFIX_MAPS.values(): + for pas in MODIFIER_MAPS.values(): if token in pas: - self.monster_prefixes[m].update(pas) + self.modifiers[m].update(pas) # Monster Nickname for nick in self.monster_id_to_nickname[m.monster_id]: @@ -100,44 +100,44 @@ def _name_to_tokens(oname): name = re.sub(r'[^a-z0-9 ]', '', name) return [t.strip() for t in set(name.split() + oname.split()) if t] - async def get_prefixes(self, m): - prefix = set() + async def get_modifiers(self, m): + modifiers = set() basemon = self.graph.get_base_monster(m) # Main Color for t in COLOR_MAP[m.attr1]: - prefix.add(t) + modifiers.add(t) # Sub Color for t in SUB_COLOR_MAP[m.attr2]: - prefix.add(t) + modifiers.add(t) if m.attr1.value == 6: for t in COLOR_MAP[m.attr2]: - prefix.add(t) + modifiers.add(t) # Both Colors for t in DUAL_COLOR_MAP[(m.attr1, m.attr2)]: - prefix.add(t) + modifiers.add(t) # Type for mt in m.types: for t in TYPE_MAP[mt]: - prefix.add(t) + modifiers.add(t) # Series if m.series_id in self.series_id_to_pantheon_nickname: for t in self.series_id_to_pantheon_nickname[m.series_id]: - prefix.add(t) + modifiers.add(t) # Rarity - prefix.add(str(m.rarity) + "*") - prefix.add(str(basemon.rarity) + "*b") + modifiers.add(str(m.rarity) + "*") + modifiers.add(str(basemon.rarity) + "*b") # Base if self.graph.monster_is_base(m): - for t in EVO_PREFIX_MAP[EvoTypes.BASE]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.BASE]: + modifiers.add(t) special_evo = ('覚醒' in m.name_ja or 'awoken' in m.name_en or '転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Reincarnated" or @@ -147,91 +147,91 @@ async def get_prefixes(self, m): # Evo if self.graph.cur_evo_type_by_monster(m).value == 1 and not special_evo: - for t in EVO_PREFIX_MAP[EvoTypes.EVO]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.EVO]: + modifiers.add(t) # Uvo if self.graph.cur_evo_type_by_monster(m).value == 2 and not special_evo: - for t in EVO_PREFIX_MAP[EvoTypes.UVO]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.UVO]: + modifiers.add(t) # UUvo if self.graph.cur_evo_type_by_monster(m).value == 3 and not special_evo: - for t in EVO_PREFIX_MAP[EvoTypes.UUVO]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.UUVO]: + modifiers.add(t) # Transform if not self.graph.monster_is_transform_base(m): - for t in EVO_PREFIX_MAP[EvoTypes.TRANS]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.TRANS]: + modifiers.add(t) # Awoken if '覚醒' in m.name_ja or 'awoken' in m.name_en.lower(): - for t in EVO_PREFIX_MAP[EvoTypes.AWOKEN]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.AWOKEN]: + modifiers.add(t) # Mega Awoken if '極醒' in m.name_ja or 'mega awoken' in m.name_en.lower(): - for t in EVO_PREFIX_MAP[EvoTypes.MEGA]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.MEGA]: + modifiers.add(t) # Reincarnated if '転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Reincarnated": - for t in EVO_PREFIX_MAP[EvoTypes.REVO]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.REVO]: + modifiers.add(t) # Super Reincarnated if '超転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated": - for t in EVO_PREFIX_MAP[EvoTypes.SREVO]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.SREVO]: + modifiers.add(t) # Pixel if (m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') or self.graph.true_evo_type_by_monster(m).value == "Pixel"): - for t in EVO_PREFIX_MAP[EvoTypes.PIXEL]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.PIXEL]: + modifiers.add(t) else: - for t in EVO_PREFIX_MAP[EvoTypes.NONPIXEL]: - prefix.add(t) + for t in EVO_MAP[EvoTypes.NONPIXEL]: + modifiers.add(t) # Awakenings for aw in m.awakenings: - for t in AWOKEN_PREFIX_MAP[Awakenings(aw.awoken_skill_id)]: - prefix.add(t) + for t in AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]: + modifiers.add(t) # Chibi if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or \ 'ミニ' in m.name_ja or '(chibi)' in m.name_en: - for t in MISC_PREFIX_MAP[MiscPrefixes.CHIBI]: - prefix.add(t) + for t in MISC_MAP[MiscModifiers.CHIBI]: + modifiers.add(t) # Method of Obtaining if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): - for t in MISC_PREFIX_MAP[MiscPrefixes.FARMABLE]: - prefix.add(t) + for t in MISC_MAP[MiscModifiers.FARMABLE]: + modifiers.add(t) if self.graph.monster_is_mp_evo(m): - for t in MISC_PREFIX_MAP[MiscPrefixes.MP]: - prefix.add(t) + for t in MISC_MAP[MiscModifiers.MP]: + modifiers.add(t) if self.graph.monster_is_rem_evo(m): - for t in MISC_PREFIX_MAP[MiscPrefixes.REM]: - prefix.add(t) + for t in MISC_MAP[MiscModifiers.REM]: + modifiers.add(t) # Server if m.on_jp: - for t in MISC_PREFIX_MAP[MiscPrefixes.INJP]: - prefix.add(t) + for t in MISC_MAP[MiscModifiers.INJP]: + modifiers.add(t) if m.on_na: - for t in MISC_PREFIX_MAP[MiscPrefixes.INNA]: - prefix.add(t) + for t in MISC_MAP[MiscModifiers.INNA]: + modifiers.add(t) if m.monster_id + 10000 in self.graph.nodes: - prefix.add("idjp") + modifiers.add("idjp") if m.monster_id > 10000: - prefix.add("idna") + modifiers.add("idna") - return prefix + return modifiers def calc_ratio(s1, s2): diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index a369c8a2..30cd0af6 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -59,7 +59,7 @@ class EvoTypes(Enum): NONPIXEL = 'Nonpixel' -EVO_PREFIX_MAP = { +EVO_MAP = { EvoTypes.BASE: ('base',), EvoTypes.EVO: ('evo', 'evolved'), EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), @@ -154,7 +154,7 @@ class Awakenings(Enum): DARKCOMBOCOUNT = 77 -AWOKEN_PREFIX_MAP = { +AWOKEN_MAP = { Awakenings.ENHANCEDHP: ('hp+', 'hp'), Awakenings.ENHANCEDATK: ('atk+', 'atk'), Awakenings.ENHANCEDRCV: ('rcv+', 'rcv'), @@ -235,7 +235,7 @@ class Awakenings(Enum): } -class MiscPrefixes(Enum): +class MiscModifiers(Enum): CHIBI = 'Chibi' NONCHIBI = 'Non-Chibi' FARMABLE = 'Farmable' @@ -245,23 +245,23 @@ class MiscPrefixes(Enum): INNA = 'In NA Server' -MISC_PREFIX_MAP = { - MiscPrefixes.CHIBI: ('chibi', 'mini'), - MiscPrefixes.FARMABLE: ('farmable',), - MiscPrefixes.REM: ('rem',), - MiscPrefixes.MP: ('mp',), - MiscPrefixes.INJP: ('jp',), - MiscPrefixes.INNA: ('na',) +MISC_MAP = { + MiscModifiers.CHIBI: ('chibi', 'mini'), + MiscModifiers.FARMABLE: ('farmable',), + MiscModifiers.REM: ('rem',), + MiscModifiers.MP: ('mp',), + MiscModifiers.INJP: ('jp',), + MiscModifiers.INNA: ('na',) } -PREFIX_MAPS = { +MODIFIER_MAPS = { **COLOR_MAP, **SUB_COLOR_MAP, **DUAL_COLOR_MAP, **TYPE_MAP, - **AWOKEN_PREFIX_MAP, - **EVO_PREFIX_MAP, - **MISC_PREFIX_MAP, + **AWOKEN_MAP, + **EVO_MAP, + **MISC_MAP, } COLOR_TOKENS = { @@ -270,8 +270,8 @@ class MiscPrefixes(Enum): *sum(DUAL_COLOR_MAP.values(), ()), } -AWAKENING_TOKENS = {*sum(AWOKEN_PREFIX_MAP.values(), ())} -EVO_TOKENS = {*sum(EVO_PREFIX_MAP.values(), ())} +AWAKENING_TOKENS = {*sum(AWOKEN_MAP.values(), ())} +EVO_TOKENS = {*sum(EVO_MAP.values(), ())} TYPE_TOKENS = {*sum(TYPE_MAP.values(), ())} diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index a9035a1c..4dcd9bdd 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -39,40 +39,40 @@ def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result.append(token) return result - def _monster_has_token(self, monster, token, monsterscore, prefixes_for_monster): + def _monster_has_token(self, monster, token, monsterscore, monster_mods): if len(token) < 6: - if token in prefixes_for_monster: + if token in monster_mods: monsterscore[monster] += 1 return True else: - dlm = difflib.get_close_matches(token, prefixes_for_monster, n=1, cutoff=.8) + dlm = difflib.get_close_matches(token, monster_mods, n=1, cutoff=.8) if dlm: monsterscore[monster] += max(calc_ratio(token, p) for p in dlm) return True return False - def interpret_query(self, raw_query: str, valid_multi_word_tokens, all_prefixes) -> (Set[str], Set[str]): + def interpret_query(self, raw_query: str, valid_multi_word_tokens, all_modifiers) -> (Set[str], Set[str]): tokenized_query = raw_query.split() tokenized_query = self._merge_multi_word_tokens(tokenized_query, valid_multi_word_tokens) - prefixes = set() - nprefixes = set() + modifiers = set() + negative_modifiers = set() name = set() - longer_prefixes = [p for p in all_prefixes if len(p) > 8] + longmods = [p for p in all_modifiers if len(p) > 8] for i, token in enumerate(tokenized_query): negated = token.startswith("-") token = token.lstrip('-') - if token in all_prefixes or difflib.get_close_matches(token, longer_prefixes, n=1, cutoff=.8): + if token in all_modifiers or difflib.get_close_matches(token, longmods, n=1, cutoff=.8): if negated: - nprefixes.add(token) + negative_modifiers.add(token) else: - prefixes.add(token) + modifiers.add(token) else: name.add(token) name.update(tokenized_query[i + 1:]) break - return prefixes, nprefixes, name + return modifiers, negative_modifiers, name def process_name_tokens(self, name_query_tokens, index2): monstergen = None @@ -102,16 +102,15 @@ def process_name_tokens(self, name_query_tokens, index2): return monstergen, monsterscore - def process_prefix_tokens(self, prefix_query_tokens, nprefix_query_tokens, monsterscore, potential_evos, - monster_prefixes): - for t in prefix_query_tokens: + def process_modifiers(self, mod_tokens, neg_mod_tokens, monsterscore, potential_evos, monster_mods): + for t in mod_tokens: potential_evos = {m for m in potential_evos if - self._monster_has_token(m, t, monsterscore, monster_prefixes[m])} + self._monster_has_token(m, t, monsterscore, monster_mods[m])} if not potential_evos: return None - for t in nprefix_query_tokens: + for t in neg_mod_tokens: potential_evos = {m for m in potential_evos if - not self._monster_has_token(m, t, monsterscore, monster_prefixes[m])} + not self._monster_has_token(m, t, monsterscore, monster_mods[m])} if not potential_evos: return None diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2020acfd..e532d2f6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1062,11 +1062,11 @@ async def _findMonster3(self, query): raise ValueError("Dadguide cog is not loaded") query = rmdiacritics(query).lower() - prefix_tokens, nprefix_tokens, name_query_tokens = find_monster.interpret_query(query, - DGCOG.index2.multi_word_tokens, - DGCOG.index2.all_prefixes) + mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, + DGCOG.index2.multi_word_tokens, + DGCOG.index2.all_modifiers) - print(prefix_tokens, name_query_tokens) + print(mod_tokens, name_query_tokens) if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) @@ -1080,10 +1080,10 @@ async def _findMonster3(self, query): # Expand search to the evo tree monster_gen = find_monster.get_monster_evos(DGCOG.database, monster_gen, monster_score) - monster_gen = find_monster.process_prefix_tokens(prefix_tokens, nprefix_tokens, monster_score, monster_gen, - DGCOG.index2.monster_prefixes) + monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_score, monster_gen, + DGCOG.index2.modifiers) if not monster_gen: - # no prefixes match any monster in the evo tree + # no modifiers match any monster in the evo tree return print(monster_gen) @@ -1108,7 +1108,7 @@ async def debugid(self, ctx, *, query): await ctx.send(box("Your query didn't match any monsters.")) return bm = DGCOG.database.graph.get_base_monster(m) - pfxs = DGCOG.index2.monster_prefixes[m] + pfxs = DGCOG.index2.modifiers[m] EVOANDTYPE = DGCOG.token_maps.EVO_TOKENS.union(DGCOG.token_maps.TYPE_TOKENS) o = (f"[{m.monster_id}] {m.name_en}\n" f"Base: [{bm.monster_id}] {bm.name_en}\n" @@ -1117,7 +1117,7 @@ async def debugid(self, ctx, *, query): f"[Manual Tokens]\n" f" Treenames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms))}\n" f" Nicknames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms))}\n\n" - f"[Prefix Tokens]\n" + f"[Modifier Tokens]\n" f" Attribute: {' '.join(sorted(t for t in pfxs if t in DGCOG.token_maps.COLOR_TOKENS))}\n" f" Awakening: {' '.join(sorted(t for t in pfxs if t in DGCOG.token_maps.AWAKENING_TOKENS))}\n" f" Evo & Type: {' '.join(sorted(t for t in pfxs if t in EVOANDTYPE))}\n" @@ -1129,7 +1129,8 @@ async def debugid(self, ctx, *, query): async def idhelp(self, ctx, *, query="", is_failed_query=False): """Get help with an id query""" query_display = '`{}` '.format(query) if query != '' else '' - failed_query_msg = "Sorry, your query {}didn't match any results :( ".format(query_display) if is_failed_query else "" + failed_query_msg = "Sorry, your query {}didn't match any results :( ".format( + query_display) if is_failed_query else "" if not is_failed_query: await ctx.send("See for " "documentation on {0.prefix}id!".format(ctx)) @@ -1140,7 +1141,7 @@ async def idhelp(self, ctx, *, query="", is_failed_query=False): "help with querying a specific monster.".format(failed_query_msg, ctx)) @commands.command() - async def exportprefixes(self, ctx): + async def exportmodifiers(self, ctx): DGCOG = self.bot.get_cog("Dadguide") tms = DGCOG.token_maps o = ("Jump to:\n\n" @@ -1152,21 +1153,22 @@ async def exportprefixes(self, ctx): anames = DGCOG.database.get_all_awoken_skills() - etable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.EVO_PREFIX_MAP.items()] - o += "\n\n### Evolutions\n\n"+tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") + etable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.EVO_MAP.items()] + o += "\n\n### Evolutions\n\n" + tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") ttable = [(k.name, ", ".join(map(inline, v))) for k, v in tms.TYPE_MAP.items()] - o += "\n\n### Types\n\n"+tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") - mtable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.MISC_PREFIX_MAP.items()] - o += "\n\n### Misc\n\n"+tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") - atable = [(anames[k.value - 1].name_en, ", ".join(map(inline, v))) for k, v in tms.AWOKEN_PREFIX_MAP.items()] - o += "\n\n### Awakenings\n\n"+tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") + o += "\n\n### Types\n\n" + tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") + mtable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.MISC_MAP.items()] + o += "\n\n### Misc\n\n" + tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") + atable = [(anames[k.value - 1].name_en, ", ".join(map(inline, v))) for k, v in tms.AWOKEN_MAP.items()] + o += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in tms.COLOR_MAP.items()] - ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in tms.SUB_COLOR_MAP.items()] + ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in + tms.SUB_COLOR_MAP.items()] for k, v in tms.DUAL_COLOR_MAP.items(): k0name = k[0].name.replace("Nil", "None") k1name = k[1].name.replace("Nil", "None") ctable.append((k0name + "/" + k1name, ", ".join(map(inline, v)))) - o += "### Attributes\n\n"+tabulate(ctable, headers=["Meaning", "Tokens"], tablefmt="github") + o += "### Attributes\n\n" + tabulate(ctable, headers=["Meaning", "Tokens"], tablefmt="github") await ctx.send(file=text_to_file(o, filename="table.md")) From fd3abf66e973dc210e4d5ec08b4b3d1ed1129f5f Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Tue, 12 Jan 2021 19:52:02 -0800 Subject: [PATCH 0412/1220] Swapping evos over to using discord-menu --- padinfo/common/emoji_map.py | 6 +++++- padinfo/info.json | 2 +- padinfo/padinfo.py | 2 +- padinfo/view/evos.py | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index f248828d..8ebb8d0b 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -1,3 +1,5 @@ +from typing import Union + from discord import Emoji from discordmenu.emoji_cache import emoji_cache @@ -101,5 +103,7 @@ def get_emoji(name): return name -def format_emoji(emoji: Emoji): +def format_emoji(emoji: Union[Emoji,str]): + if isinstance(emoji, str): + return f":{emoji}:" return f"<:{emoji.name}:{emoji.id}>" diff --git a/padinfo/info.json b/padinfo/info.json index 38bed8a5..f5bb4e4d 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -21,7 +21,7 @@ }, "requirements": [ "tsutils>=3.0.1", - "discord-menu>=0.0.2", + "discord-menu>=0.0.3", "prettytable" ], "tags": [ diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e532d2f6..c855a0c7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -465,7 +465,7 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) id_embed = await menu.make_embed(m) - evo_embed = await menu.make_evo_embed(m) + evo_embed = await menu.make_evo_embed_v2(m) mats_embed = await menu.make_evo_mats_embed(m) animated = m.has_animation pic_embed = await menu.make_picture_embed(m, animated=animated) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 58d34467..ef52e73f 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -54,13 +54,13 @@ def _evo_lines(monsters, current_monster): def embed(m: "MonsterModel", alt_versions, gem_versions, color): fields = [ EmbedField( - "{} alternate evo(s)".format(len(alt_versions)), + ("{} evolution" if len(alt_versions) == 1 else "{} evolutions").format(len(alt_versions)), Box(*EvosView._evo_lines(alt_versions, m)))] if gem_versions: fields.append( EmbedField( - "{} evolve gem(s)".format(len(gem_versions)), + ("{} evolve gem" if len(gem_versions) == 1 else "{} evolve gems").format(len(gem_versions)), Box(*EvosView._evo_lines(gem_versions, m)))) # todo: deal with field overflow From 5acaf64fa3ae4d8d49e8380614e8c9e6268fc321 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Tue, 12 Jan 2021 20:01:16 -0800 Subject: [PATCH 0413/1220] Removing old evos code in padinfo --- padinfo/id_menu.py | 53 -------------------------------------------- padinfo/view/evos.py | 7 ------ 2 files changed, 60 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 520669aa..4205f34e 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -61,18 +61,6 @@ async def make_base_embed(self, m: "MonsterModel"): embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed - async def make_base_embed_v2(self, m: "MonsterModel"): - pdicog = self.ctx.bot.get_cog("PadInfo") - color = await self.get_user_embed_color(pdicog) - header = MonsterHeader.long_v2(m) - main = EmbedMain( - color=color, - title=header.to_markdown() - ) - footer = EmbedFooter('Requester may click the reactions below to switch tabs') - thumbnail = EmbedThumbnail(MonsterImage.icon(m)) - return main, footer, thumbnail - async def get_user_embed_color(self, pdicog): color = await pdicog.config.user(self.ctx.author).color() if color is None: @@ -82,20 +70,6 @@ async def get_user_embed_color(self, pdicog): else: return discord.Color(color) - def _get_evo_list_fields(self, monsters, current_monster): - if not len(monsters): - return - field_data = '' - field_values = [] - for ae in sorted(monsters, key=lambda x: int(x.monster_id)): - monster_header = EvosView.evos(ae, link=ae.monster_id != current_monster.monster_id) + '\n' - if len(field_data + monster_header) > 1024: - field_values.append(field_data) - field_data = "" - field_data += monster_header - field_values.append(field_data) - return field_values - async def make_evo_embed_v2(self, m: "MonsterModel"): alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) @@ -103,33 +77,6 @@ async def make_evo_embed_v2(self, m: "MonsterModel"): e = EvosView.embed(m, alt_versions, gem_versions, color) return e.to_embed() - async def make_evo_embed(self, m: "MonsterModel"): - embed = await self.make_base_embed(m) - alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) - gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) - - if not len(alt_versions): - embed.description = 'No alternate evos or evo gem' - return embed - - evos = self._get_evo_list_fields(alt_versions, m) - if not gem_versions: - embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) - for f in evos[1:]: - embed.add_field(name="\u200b", value=f) - return embed - gems = self._get_evo_list_fields(gem_versions, m) - - embed.add_field(name="{} alternate evo(s)".format(len(alt_versions)), value=evos[0], inline=False) - for e in evos[1:]: - embed.add_field(name="\u200b", value=e, inline=False) - - embed.add_field(name="{} evolve gem(s)".format(len(gem_versions)), value=gems[0], inline=False) - for e in gems[1:]: - embed.add_field(name="\u200b", value=e, inline=False) - - return embed - def _add_mats_of_list(self, embed, monster_id_list, field_name): if not len(monster_id_list): return diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index ef52e73f..81a682bf 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -24,13 +24,6 @@ def _get_monster_attr_emoji(monster: "MonsterModel"): emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) return get_emoji(emoji) - @staticmethod - def evos(m: "MonsterModel", link=True): - prefix = f" {EvosView._get_monster_attr_emoji(m)} " - msg = f"{m.monster_no_na} - {m.name_en}" - suffix = MonsterHeader.ja_suffix(m, False) - return prefix + ("[{}]({})".format(msg, monster_url(m)) if link else msg) + suffix - @staticmethod def _evo_line(m: "MonsterModel", link=True): msg = f"{m.monster_no_na} - {m.name_en}" From 6b08954110e3177240cb784c18b9c99719026869 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 12 Jan 2021 23:10:14 -0600 Subject: [PATCH 0414/1220] Remove todo in padinfo evos (#477) --- padinfo/view/evos.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 81a682bf..79171f5f 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -56,7 +56,6 @@ def embed(m: "MonsterModel", alt_versions, gem_versions, color): ("{} evolve gem" if len(gem_versions) == 1 else "{} evolve gems").format(len(gem_versions)), Box(*EvosView._evo_lines(gem_versions, m)))) - # todo: deal with field overflow return EmbedView( EmbedMain( color=color, From cb1a8dc9f4e3d488e96943940b8a3a9ba1a59fde Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Wed, 13 Jan 2021 03:07:30 -0600 Subject: [PATCH 0415/1220] Moving LS embed to use discord-menu (#478) * Rewriting format_ls_text to be clearer * Rewriting LS embed with discord-menu --- padinfo/id_menu.py | 30 +++++++++++------------ padinfo/info.json | 2 +- padinfo/leader_skills.py | 26 +++++++++----------- padinfo/view/components/monster/header.py | 7 ++++-- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 4205f34e..9f668f9d 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -5,7 +5,10 @@ import discord import prettytable from discord import Color -from discordmenu.embed.components import EmbedMain, EmbedFooter, EmbedThumbnail +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain +from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import Text, BoldText from redbot.core.utils.chat_formatting import box from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, awakening_restricted_latent_emoji @@ -191,21 +194,18 @@ def monster_orb_skin_cb_url(m: "MonsterModel", link_text='Color Blind'): async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): lls = left_m.leader_skill rls = right_m.leader_skill + pdicog = self.ctx.bot.get_cog("PadInfo") - multiplier_text = createMultiplierText(lls, rls) - - embed = await self.make_custom_embed() - embed.title = '{}\n\n'.format(multiplier_text) - description = '' - description += '\n**{}**\n{}'.format( - MonsterHeader.short(left_m, link=True), - lls.desc if lls else 'None') - description += '\n**{}**\n{}'.format( - MonsterHeader.short(right_m, link=True), - rls.desc if rls else 'None') - embed.description = description - - return embed + return EmbedView( + EmbedMain( + title=createMultiplierText(lls, rls), + description=Box( + BoldText(MonsterHeader.name(left_m, link=True, show_jp=True)), + Text(lls.desc if lls else 'None'), + BoldText(MonsterHeader.name(right_m, link=True, show_jp=True)), + Text(rls.desc if rls else 'None')), + color=await self.get_user_embed_color(pdicog) + )).to_embed() async def make_header_embed(self, m: "MonsterModel"): header = MonsterHeader.long(m, link=True) diff --git a/padinfo/info.json b/padinfo/info.json index f5bb4e4d..d29d4207 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -21,7 +21,7 @@ }, "requirements": [ "tsutils>=3.0.1", - "discord-menu>=0.0.3", + "discord-menu>=0.1.0", "prettytable" ], "tags": [ diff --git a/padinfo/leader_skills.py b/padinfo/leader_skills.py index ee5b2000..23cb66b7 100644 --- a/padinfo/leader_skills.py +++ b/padinfo/leader_skills.py @@ -45,22 +45,18 @@ def createSingleMultiplierText(ls=None): return format_ls_text(hp, atk, rcv, resist, combo, fua, mfua, te) +def format_number(val): + return '{:.2f}'.format(val).strip('0').rstrip('.') + + def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): - def fmtNum(val): - return '{:.2f}'.format(val).strip('0').rstrip('.') + resist = ' Resist {}%'.format(format_number(100 * resist)) if resist else '' - text = "{}/{}/{}".format(fmtNum(hp), fmtNum(atk), fmtNum(rcv)) - if resist != 0: - text += ' Resist {}%'.format(fmtNum(100 * resist)) + combos = '+{}c'.format(combo) if combo else '' + true_damage = '{}'.format(humanize_number(fua, 2)) if fua else '' + any_fua = 'fua' if fua or mfua else '' - extras = [] - if combo: - extras.append('+{}c'.format(combo)) - if fua: - extras.append('{} fua'.format(humanize_number(fua, 2))) - elif mfua: - extras.append('fua') + joined = ' '.join((a for a in [combos, true_damage, any_fua] if a)) + extras = f"[{joined}]" if joined else '' - if extras: - return '[{}] [{}]'.format(text, ' '.join(extras)) - return '[{}]'.format(text) + return f"[{format_number(hp)}/{format_number(atk)}/{format_number(rcv)}{resist}] {extras}" diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 85e2f070..39ffc095 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -30,8 +30,11 @@ def long(m: "MonsterModel", link=False): return '[{}]({})'.format(msg, monster_url(m)) if link else msg @staticmethod - def short_v2(m: "MonsterModel", link=False): - msg = '[{}] {}'.format(m.monster_no_na, m.name_en) + def name(m: "MonsterModel", link=False, show_jp=False): + msg = '[{}] {}{}'.format( + m.monster_no_na, + m.name_en, + MonsterHeader.ja_suffix(m) if show_jp else '') return LinkedText(msg, monster_url(m)) if link else Text(msg) @staticmethod From 43f889569490bd22206ad50583d8275f2f5f1ce0 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 13 Jan 2021 20:37:09 -0700 Subject: [PATCH 0416/1220] [FindMonster3] Allow some tokens at the end of queries (#481) * [FindMonster3] Allow some tokens at the end of queries * Add else * Add name tokens correctly --- dadguide/monster_index.py | 1 + dadguide/token_mappings.py | 6 ++++++ padinfo/find_monster.py | 25 +++++++++++++++++++------ padinfo/padinfo.py | 4 +--- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index d3317e70..f8702c02 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -59,6 +59,7 @@ async def __ainit__(self, monsters, db): self.manual = combine_tokens(self.manual_nick, self.manual_tree) self.name_tokens = list(self.manual) + list(self.tokens) self.all_modifiers = {p for ps in self.modifiers.values() for p in ps} + self.suffixes = LEGAL_END_TOKENS __init__ = __ainit__ diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 30cd0af6..108728c4 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -285,3 +285,9 @@ class MiscModifiers(Enum): 'tamadra': ('tama',), 'evolution': ('evo',), }) + +LEGAL_END_TOKENS = { + "equip", + "assist", + "eq", +} diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 4dcd9bdd..831b1ddd 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -51,25 +51,38 @@ def _monster_has_token(self, monster, token, monsterscore, monster_mods): return True return False - def interpret_query(self, raw_query: str, valid_multi_word_tokens, all_modifiers) -> (Set[str], Set[str]): + def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): tokenized_query = raw_query.split() - tokenized_query = self._merge_multi_word_tokens(tokenized_query, valid_multi_word_tokens) + tokenized_query = self._merge_multi_word_tokens(tokenized_query, index2.multi_word_tokens) modifiers = set() negative_modifiers = set() name = set() - longmods = [p for p in all_modifiers if len(p) > 8] + longmods = [p for p in index2.all_modifiers if len(p) > 8] for i, token in enumerate(tokenized_query): negated = token.startswith("-") token = token.lstrip('-') - if token in all_modifiers or difflib.get_close_matches(token, longmods, n=1, cutoff=.8): + if token in index2.all_modifiers or difflib.get_close_matches(token, longmods, n=1, cutoff=.8): if negated: negative_modifiers.add(token) else: modifiers.add(token) else: - name.add(token) - name.update(tokenized_query[i + 1:]) + tokenized_query = tokenized_query[i:] + break + else: + tokenized_query = [] + + for i, token in enumerate(tokenized_query[::-1]): + negated = token.startswith("-") + token = token.lstrip('-') + if difflib.get_close_matches(token, index2.suffixes, n=1, cutoff=.8): + if negated: + negative_modifiers.add(token) + else: + modifiers.add(token) + else: + name.update(tokenized_query[i:]) break return modifiers, negative_modifiers, name diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c855a0c7..c7a86caf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1062,9 +1062,7 @@ async def _findMonster3(self, query): raise ValueError("Dadguide cog is not loaded") query = rmdiacritics(query).lower() - mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, - DGCOG.index2.multi_word_tokens, - DGCOG.index2.all_modifiers) + mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, DGCOG.index2) print(mod_tokens, name_query_tokens) From 73efbb069f47b7965f857426f0c1aec4d8090a15 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 13 Jan 2021 20:45:29 -0700 Subject: [PATCH 0417/1220] Reverse tokens in place (#483) --- padinfo/find_monster.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 831b1ddd..e77ee62c 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -73,7 +73,9 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): else: tokenized_query = [] - for i, token in enumerate(tokenized_query[::-1]): + tokenized_query = tokenized_query[::-1] + + for i, token in enumerate(tokenized_query): negated = token.startswith("-") token = token.lstrip('-') if difflib.get_close_matches(token, index2.suffixes, n=1, cutoff=.8): From 4d93358ad4ddc302db7b65d029d870cabc6b9fc0 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 13 Jan 2021 21:43:06 -0700 Subject: [PATCH 0418/1220] Treat "last" token as a name token (#484) - "Last" excludes suffix tokens such as equip/assist. - This allows overlap of nicknames & modifier tokens, such as hel, xm, cloud, etc --- padinfo/find_monster.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index e77ee62c..d4d14503 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -55,39 +55,45 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): tokenized_query = raw_query.split() tokenized_query = self._merge_multi_word_tokens(tokenized_query, index2.multi_word_tokens) - modifiers = set() + modifiers = [] negative_modifiers = set() name = set() longmods = [p for p in index2.all_modifiers if len(p) > 8] - for i, token in enumerate(tokenized_query): + lastmodpos = False + + for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if token in index2.all_modifiers or difflib.get_close_matches(token, longmods, n=1, cutoff=.8): + if difflib.get_close_matches(token, index2.suffixes, n=1, cutoff=.8): if negated: negative_modifiers.add(token) else: - modifiers.add(token) + modifiers.append(token) else: - tokenized_query = tokenized_query[i:] + if i: + tokenized_query = tokenized_query[:-i] break - else: - tokenized_query = [] - - tokenized_query = tokenized_query[::-1] for i, token in enumerate(tokenized_query): negated = token.startswith("-") token = token.lstrip('-') - if difflib.get_close_matches(token, index2.suffixes, n=1, cutoff=.8): + if token in index2.all_modifiers or difflib.get_close_matches(token, longmods, n=1, cutoff=.8): if negated: + lastmodpos = False negative_modifiers.add(token) else: - modifiers.add(token) + lastmodpos = True + modifiers.append(token) else: name.update(tokenized_query[i:]) break - return modifiers, negative_modifiers, name + if not name and modifiers and lastmodpos: + if modifiers[-1] in index2.name_tokens: + name.add(modifiers[-1]) + modifiers = modifiers[:-1] + + return set(modifiers), negative_modifiers, name def process_name_tokens(self, name_query_tokens, index2): monstergen = None From 33faf82034cb1021a66ca12c3a89a3abc98a2f12 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 13 Jan 2021 21:57:21 -0700 Subject: [PATCH 0419/1220] idtest import (#486) --- padinfo/padinfo.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c7a86caf..317a0e89 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -734,6 +734,14 @@ async def idt_add(self, ctx, id: int, *, query): suite[query] = {'result': id} await ctx.tick() + @idtest.command(name="import") + async def idt_import(self, ctx, *, queries): + cases = re.findall(r'\s*\d+. (.+?) + - (\d+) *(.*)', queries) + async with self.config.test_suite() as suite: + for query, result, reason in cases: + suite[query] = {'result': int(result), 'reason': reason} + await ctx.tick() + @idtest.command(name="remove", aliases=["delete", "rm"]) async def idt_remove(self, ctx, number: int): async with self.config.test_suite() as suite: From 0b7d0908fde32438131d63061fa407f45ea0c9ec Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 13 Jan 2021 22:24:13 -0700 Subject: [PATCH 0420/1220] Only change last token when it matches a manual (#485) * Only change last token when it matches a manual * Defaultdicts suck --- padinfo/find_monster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index d4d14503..f521d895 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -89,7 +89,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): break if not name and modifiers and lastmodpos: - if modifiers[-1] in index2.name_tokens: + if index2.manual[modifiers[-1]]: name.add(modifiers[-1]) modifiers = modifiers[:-1] From 4651455b7cd73bef2542d5fde10080391b44669b Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 13 Jan 2021 22:58:06 -0700 Subject: [PATCH 0421/1220] IDTest remove element by name (#491) --- padinfo/padinfo.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 317a0e89..cf30e561 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -743,12 +743,15 @@ async def idt_import(self, ctx, *, queries): await ctx.tick() @idtest.command(name="remove", aliases=["delete", "rm"]) - async def idt_remove(self, ctx, number: int): + async def idt_remove(self, ctx, *, item): async with self.config.test_suite() as suite: - if number >= len(suite): + if item in suite: + del suite[item] + elif item.isdigit() and int(item) < len(suite): + del suite[sorted(suite)[int(item)]] + else: await ctx.react_quietly("\N{CROSS MARK}") return - del suite[sorted(suite)[number]] await ctx.tick() @idtest.command(name="setreason", aliases=["addreason"]) From 08460f310fa6cbdfc98f2fe70f172df133cb7734 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 13 Jan 2021 23:15:47 -0700 Subject: [PATCH 0422/1220] idtest listrecent (#492) --- padinfo/padinfo.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index cf30e561..3a790d7e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,4 +1,5 @@ import asyncio +from datetime import datetime import json import logging import os @@ -731,15 +732,15 @@ async def idtest(self, ctx): @idtest.command(name="add") async def idt_add(self, ctx, id: int, *, query): async with self.config.test_suite() as suite: - suite[query] = {'result': id} + suite[query] = {'result': id, 'ts': datetime.now().timestamp()} await ctx.tick() @idtest.command(name="import") async def idt_import(self, ctx, *, queries): - cases = re.findall(r'\s*\d+. (.+?) + - (\d+) *(.*)', queries) + cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) async with self.config.test_suite() as suite: for query, result, reason in cases: - suite[query] = {'result': int(result), 'reason': reason} + suite[query] = {'result': int(result), 'reason': reason, 'ts': datetime.now().timestamp()} await ctx.tick() @idtest.command(name="remove", aliases=["delete", "rm"]) @@ -775,6 +776,20 @@ async def idt_list(self, ctx): for page in pagify(o): await ctx.send(box(page)) + @idtest.command(name="listrecent") + async def idt_listrecent(self, ctx, count: int = 0): + suite = await self.config.test_suite() + if count == 0: + count = len(suite) + o = "" + ml = len(max(suite, key=len)) + for c, kv in enumerate(sorted(suite.items(), key=lambda kv: kv[1].get('ts', 0), reverse=True)[:count]): + o += f"{kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + @idtest.command(name="run", aliases=["test"]) async def idt_run(self, ctx): suite = await self.config.test_suite() From b142ba05511cc1421e8d73fdd54d1c6364b19591 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 10:57:17 -0700 Subject: [PATCH 0423/1220] Make AED and AEP case insensitive (#494) --- padevents/padevents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index dad6d365..718c9dc8 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -109,7 +109,7 @@ async def check_started(self): elif aep['regex']: matches = re.search(aep['searchstr'], event.clean_dungeon_name) else: - matches = aep['searchstr'] in event.clean_dungeon_name + matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() self.rolepinged_events.add((key, event.key)) if matches: @@ -138,7 +138,7 @@ async def check_started(self): or (aed['key'], event.key) in self.rolepinged_events: continue self.rolepinged_events.add((aed['key'], event.key)) - if aed['searchstr'] in event.clean_dungeon_name: + if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): offsetstr = " starts now!" if aed['offset']: offsetstr = " starts in {} minute(s)!".format(aed['offset']) From 97925795eded202bcefe9b7c7ba840f7a5f82a46 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 11:15:19 -0700 Subject: [PATCH 0424/1220] Ungate AEP add (#496) --- padevents/padevents.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 718c9dc8..085ed25f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -20,6 +20,14 @@ if TYPE_CHECKING: from dadguide.models.scheduled_event_model import ScheduledEventModel +def user_is_donor(ctx, only_patron=False): + if ctx.author.id in ctx.bot.owner_ids: + return True + donationcog = ctx.bot.get_cog("Donations") + if not donationcog: + return False + return donationcog.is_donor(ctx, only_patron) + logger = logging.getLogger('red.padbot-cogs.padevents') SUPPORTED_SERVERS = ["JP", "NA", "KR"] @@ -517,9 +525,8 @@ async def aep_s_disabled(self, ctx, key, offset: int): async def autoeventdm(self, ctx): """Auto Event DMs""" - @is_donor() @autoeventdm.command(name="add") - async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): + async def aed_add(self, ctx, server, searchstr, group, time_offset: int = 0): """Add a new autoeventdm""" server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: @@ -529,7 +536,10 @@ async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): if group not in GROUPS: await ctx.send("Unsupported group, pick one of red, blue, green") return - if offset < 0: + if time_offset and user_is_donor(ctx): + await ctx.send("You must be a donor to set a time offset!") + return + if time_offset < 0: await ctx.send("Offset cannot be negative") return @@ -538,7 +548,7 @@ async def aed_add(self, ctx, server, searchstr, group, offset: int = 0): 'server': server, 'group': group, 'searchstr': searchstr, - 'offset': offset, + 'offset': time_offset, } async with self.config.user(ctx.author).dmevents() as dmevents: @@ -586,7 +596,7 @@ async def aed_e(self, ctx): @is_donor() @aed_e.command(name="offset") async def aed_e_offset(self, ctx, index, offset): - """Set offset of an autoeventdm (Donor only)""" + """(DONOR ONLY) Set time offset to an AED to allow you to prepare for a dungeon""" if offset < 0: await ctx.send("Offset cannot be negative") return From 7b1e20511eba7a788cce91416d1195b0bf700c70 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 11:18:38 -0700 Subject: [PATCH 0425/1220] Gate offset to donors only (#497) --- padevents/padevents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 085ed25f..aad67d83 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -536,7 +536,7 @@ async def aed_add(self, ctx, server, searchstr, group, time_offset: int = 0): if group not in GROUPS: await ctx.send("Unsupported group, pick one of red, blue, green") return - if time_offset and user_is_donor(ctx): + if time_offset and not user_is_donor(ctx): await ctx.send("You must be a donor to set a time offset!") return if time_offset < 0: From 0686bf2b47f9d48a61b6f367341083d1495894bb Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 11:34:16 -0700 Subject: [PATCH 0426/1220] Fix buttoninfo (#498) --- padinfo/padinfo.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3a790d7e..3355bff4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -581,7 +581,10 @@ async def otherinfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" - monster = await self.findMonster3(query) + monster, err, _ = await self.findMonsterCustom(ctx, query) + if monster is None: + await self.makeFailureMsg(ctx, query, err) + return DGCOG = self.bot.get_cog("Dadguide") info = button_info.get_info(DGCOG, monster) info_str = button_info.to_string(monster, info) @@ -1153,7 +1156,7 @@ async def debugid(self, ctx, *, query): async def idhelp(self, ctx, *, query="", is_failed_query=False): """Get help with an id query""" query_display = '`{}` '.format(query) if query != '' else '' - failed_query_msg = "Sorry, your query {}didn't match any results :( ".format( + failed_query_msg = "Sorry, your query {}didn't match any results :(\n".format( query_display) if is_failed_query else "" if not is_failed_query: await ctx.send("See for " From bcbb2a5283a4e0dc2851c9d7a082dab186d783f2 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 12:23:09 -0700 Subject: [PATCH 0427/1220] Fix idhelp with no arguments (#500) * Fix buttoninfo * Fix idhelp with no arguments * Make ID Guide a constant --- padinfo/padinfo.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3355bff4..467468f4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -34,6 +34,7 @@ EMBED_NOT_GENERATED = -1 +IDGUIDE = "https://github.com/TsubakiBotPad/pad-cogs/wiki/%5Eid-user-guide" class ServerFilter(Enum): any = 0 @@ -1005,7 +1006,10 @@ def get_emojis(self): async def makeFailureMsg(self, ctx, query: str, err): if await self.config.user(ctx.author).beta_id3(): - await self.idhelp(ctx, query=query, is_failed_query=True) + await ctx.send("Sorry, your query {0} didn't match any results :(\n" + "See <{2}> for " + "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " + "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) return msg = ('Lookup failed: {0}.\n' 'Try one of , , [argbld]/[rgbld] . ' @@ -1153,19 +1157,12 @@ async def debugid(self, ctx, *, query): await ctx.send(box(page)) @commands.command(aliases=['helpid']) - async def idhelp(self, ctx, *, query="", is_failed_query=False): + async def idhelp(self, ctx, *, query=""): """Get help with an id query""" - query_display = '`{}` '.format(query) if query != '' else '' - failed_query_msg = "Sorry, your query {}didn't match any results :(\n".format( - query_display) if is_failed_query else "" - if not is_failed_query: - await ctx.send("See for " - "documentation on {0.prefix}id!".format(ctx)) + await ctx.send("See <{0}> for documentation on {1.prefix}id!".format(IDGUIDE, ctx)) + if query: await self.debugid(ctx, query=query) - else: - await ctx.send("{0}See for " - "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " - "help with querying a specific monster.".format(failed_query_msg, ctx)) + @commands.command() async def exportmodifiers(self, ctx): From 1294b1209e5f9545a1412a025b58cb150e64584b Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 17:00:14 -0700 Subject: [PATCH 0428/1220] Add series type to crud (#501) * Add series type to tsubaki * Add valid series types --- crud/crud.py | 45 +++++++++++++++++++++++---------- dadguide/models/series_model.py | 1 + dadguide/monster_graph.py | 20 +++++++++------ 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 3c6760f6..3a84102f 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -1,13 +1,12 @@ -import asyncio -import aiofiles -import csv -import discord -import os import json import logging -import pymysql -from io import BytesIO +import os from datetime import datetime +from io import BytesIO + +import aiofiles +import discord +import pymysql from redbot.core import checks, commands, Config, errors from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check, confirm_message @@ -18,12 +17,23 @@ "name_en": 'Untranslated', "name_ja": 'Untranslated', "name_ko": 'Untranslated', + "series_type": None, } + +SERIES_TYPES = [ + "default", + "event", + "seasonal", + "ghcollab", + "collab" +] + async def check_crud_channel(ctx): chan = await ctx.bot.get_cog("Crud").config.chan() return chan is None or chan == ctx.channel.id or ctx.author.id in ctx.bot.owner_ids + class Crud(commands.Cog): """PadGuide CRUD""" @@ -111,7 +121,7 @@ async def series_search(self, ctx, *, search_text): async def series_add(self, ctx, *elements): """Add a new series. - Valid element keys are: `name_en`, `name_ko`, `name_ja` + Valid element keys are: `name_en`, `name_ko`, `name_ja`, `series_type` Example Usage: [p]crud series add key1 "Value1" key2 "Value2" @@ -119,12 +129,16 @@ async def series_add(self, ctx, *elements): if len(elements) % 2 != 0: await ctx.send_help() return - elements = {elements[i]: elements[i+1] for i in range(0, len(elements), 2)} + elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} - if not all(x in SERIES_KEYS for x in elements): + if not (elements and all(x in SERIES_KEYS for x in elements)): await ctx.send_help() return + if "series_type" in elements and elements['series_type'] not in SERIES_TYPES: + await ctx.send("`series_type` must be one of: " + ", ".join(SERIES_TYPES)) + return + EXTRAS = {} with await self.get_cursor() as cursor: cursor.execute("SELECT MAX(series_id) AS max_id FROM series") @@ -152,12 +166,11 @@ async def series_add(self, ctx, *elements): async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *elements): """Edit an existing series series. - Valid element keys are: `name_en`, `name_ko`, `name_ja` + Valid element keys are: `name_en`, `name_ko`, `name_ja`, `series_type` Example Usage: [p]crud series edit 100 key1 "Value1" key2 "Value2" @@ -165,12 +178,16 @@ async def series_edit(self, ctx, series_id: int, *elements): if len(elements) % 2 != 0: await ctx.send_help() return - elements = {elements[i]: elements[i+1] for i in range(0, len(elements), 2)} + elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} - if not all(x in SERIES_KEYS for x in elements): + if not (elements and all(x in SERIES_KEYS for x in elements)): await ctx.send_help() return + if "series_type" in elements and elements['series_type'] not in SERIES_TYPES: + await ctx.send("`series_type` must be one of: " + ", ".join(SERIES_TYPES)) + return + replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) sql = ('UPDATE series' ' SET {}' diff --git a/dadguide/models/series_model.py b/dadguide/models/series_model.py index ee10780b..cb05a28c 100644 --- a/dadguide/models/series_model.py +++ b/dadguide/models/series_model.py @@ -7,6 +7,7 @@ def __init__(self, **kwargs): self.name_ja = kwargs['name_ja'] self.name_en = kwargs['name_en'] self.name_ko = kwargs['name_ko'] + self.series_type = kwargs['series_type'] @property def name(self): diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 50ad1a5b..08e94809 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,16 +1,18 @@ -import networkx import json -from typing import Optional from collections import defaultdict +from typing import Optional + +import networkx + from .database_manager import DadguideDatabase +from .models.active_skill_model import ActiveSkillModel +from .models.awakening_model import AwakeningModel +from .models.awoken_skill_model import AwokenSkillModel from .models.enum_types import EvoType, InternalEvoType -from .models.monster_model import MonsterModel +from .models.evolution_model import EvolutionModel from .models.leader_skill_model import LeaderSkillModel -from .models.active_skill_model import ActiveSkillModel +from .models.monster_model import MonsterModel from .models.series_model import SeriesModel -from .models.evolution_model import EvolutionModel -from .models.awoken_skill_model import AwokenSkillModel -from .models.awakening_model import AwakeningModel MONSTER_QUERY = """SELECT monsters.*, @@ -40,6 +42,7 @@ series.name_ja AS s_name_ja, series.name_en AS s_name_en, series.name_ko AS s_name_ko, + series.series_type AS s_series_type, exchanges.target_monster_id AS evo_gem_id, drops.drop_id FROM @@ -155,7 +158,8 @@ def build_graph(self): s_model = SeriesModel(series_id=m.series_id, name_ja=m.s_name_ja, name_en=m.s_name_en, - name_ko=m.s_name_ko + name_ko=m.s_name_ko, + series_type=m.s_series_type ) m_model = MonsterModel(monster_id=m.monster_id, From 7c65fb237392e4d3878af4e6ab5072b1040f25f0 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 21:04:49 -0700 Subject: [PATCH 0429/1220] Rename default to regular as series_type (#503) --- crud/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crud/crud.py b/crud/crud.py index 3a84102f..cdbc6278 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -22,7 +22,7 @@ SERIES_TYPES = [ - "default", + "regular", "event", "seasonal", "ghcollab", From 3266f1f6586fa8407920de47eaeb133b8926c780 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Jan 2021 23:44:02 -0700 Subject: [PATCH 0430/1220] Add series type priority in id3 (#509) * series_id integration * Transforms are in tree * ids --- crud/crud.py | 3 ++- dadguide/monster_graph.py | 8 ++++---- padinfo/find_monster.py | 8 ++++++++ padinfo/padinfo.py | 20 +++++++++++--------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index cdbc6278..23f203cf 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -161,7 +161,8 @@ async def series_add(self, ctx, *elements): 'name_ja': elements['name_ja'], 'name_en': elements['name_en'], 'name_ko': elements['name_ko'], - 'series_id': elements['series_id'] + 'series_id': elements['series_id'], + 'series_type': elements['series_id'] }) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 08e94809..1c0a2389 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -461,7 +461,7 @@ def monster_is_farmable(self, monster: MonsterModel): def monster_is_farmable_evo_by_id(self, monster_id): return any( - m for m in self.get_evo_tree(monster_id) if self.monster_is_farmable_by_id(m)) + m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_farmable_by_id(m)) def monster_is_farmable_evo(self, monster: MonsterModel): return self.monster_is_farmable_evo_by_id(monster.monster_no) @@ -475,7 +475,7 @@ def monster_is_mp(self, monster: MonsterModel): def monster_is_mp_evo_by_id(self, monster_id): return any( - m for m in self.get_evo_tree(monster_id) if self.monster_is_mp_by_id(m)) + m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_mp_by_id(m)) def monster_is_mp_evo(self, monster: MonsterModel): return self.monster_is_mp_evo_by_id(monster.monster_no) @@ -489,7 +489,7 @@ def monster_is_pem(self, monster: MonsterModel): def monster_is_pem_evo_by_id(self, monster_id): return any( - m for m in self.get_evo_tree(monster_id) if self.monster_is_pem_by_id(m)) + m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_pem_by_id(m)) def monster_is_pem_evo(self, monster: MonsterModel): return self.monster_is_pem_evo_by_id(monster.monster_no) @@ -503,7 +503,7 @@ def monster_is_rem(self, monster: MonsterModel): def monster_is_rem_evo_by_id(self, monster_id): return any( - m for m in self.get_evo_tree(monster_id) if self.monster_is_rem_by_id(m)) + m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_rem_by_id(m)) def monster_is_rem_evo(self, monster: MonsterModel): return self.monster_is_rem_evo_by_id(monster.monster_no) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index f521d895..cbcbc156 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -2,6 +2,14 @@ from collections import defaultdict from typing import Set +SERIES_TYPE_PRIORITY = [ + "regular", + "event", + "seasonal", + "ghcollab", + "collab", + None +][::-1] def calc_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 467468f4..10335397 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -21,7 +21,7 @@ from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor from .button_info import button_info -from .find_monster import find_monster +from .find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority from .id_menu import IdMenu from .view.components.monster.header import MonsterHeader @@ -1117,16 +1117,18 @@ async def _findMonster3(self, query): # no modifiers match any monster in the evo tree return - print(monster_gen) + print({k: v for k, v in monster_score.items() if k in monster_gen}) # Return most likely candidate based on query. - mon = max(monster_gen, key=lambda m: (monster_score[m], - not m.is_equip, - bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), - # Match na on id overlap - -DGCOG.database.graph.get_base_id(m), - m.rarity, - m.monster_no_na)) + mon = max(monster_gen, + key=lambda m: (monster_score[m], + not m.is_equip, + bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap + series_priority.index(m.series.series_type), + DGCOG.database.graph.monster_is_rem_evo(m), + -DGCOG.database.graph.get_base_id(m), + m.rarity, + m.monster_no_na)) return mon From ffd3b6619480c658a763ca0477c9be383a151f92 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 15 Jan 2021 00:52:39 -0700 Subject: [PATCH 0431/1220] Prioritize not jp-only collabs (#513) --- padinfo/padinfo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 10335397..eb6c48bf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1125,6 +1125,7 @@ async def _findMonster3(self, query): not m.is_equip, bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap series_priority.index(m.series.series_type), + m.on_na if m.series.series_type == "collab" else 0, DGCOG.database.graph.monster_is_rem_evo(m), -DGCOG.database.graph.get_base_id(m), m.rarity, From 4d517b3444c8aab01a54c167e4f89edcb90cbde9 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 15 Jan 2021 01:21:26 -0700 Subject: [PATCH 0432/1220] Exact name match > modifier fuzzy match (#514) * Prioritize NA collabs * Prefixes no longer fuzzy match when there's an exact name match * Token length check --- padinfo/find_monster.py | 11 ++++++++--- padinfo/padinfo.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index cbcbc156..d36ed528 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -9,16 +9,18 @@ "ghcollab", "collab", None -][::-1] +] + def calc_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() + def calc_ratio_prefix(token, full_word): if full_word == token: return 1 elif full_word.startswith(token): - return 1 - len(full_word)/100 + return 1 - len(full_word) / 100 return difflib.SequenceMatcher(None, token, full_word).ratio() @@ -85,7 +87,10 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): for i, token in enumerate(tokenized_query): negated = token.startswith("-") token = token.lstrip('-') - if token in index2.all_modifiers or difflib.get_close_matches(token, longmods, n=1, cutoff=.8): + if token in index2.all_modifiers or ( + difflib.get_close_matches(token, longmods, n=1, cutoff=.8) + and token not in index2.name_tokens + and len(token) >= 8): if negated: lastmodpos = False negative_modifiers.add(token) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index eb6c48bf..273560c2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1124,7 +1124,7 @@ async def _findMonster3(self, query): key=lambda m: (monster_score[m], not m.is_equip, bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap - series_priority.index(m.series.series_type), + -series_priority.index(m.series.series_type), m.on_na if m.series.series_type == "collab" else 0, DGCOG.database.graph.monster_is_rem_evo(m), -DGCOG.database.graph.get_base_id(m), From e5202ffcb834cfd1c9524cdb8f114052ff9a8586 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 15 Jan 2021 01:34:13 -0700 Subject: [PATCH 0433/1220] Fix multi-word token processing (#515) --- padinfo/find_monster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index d36ed528..f5d637ee 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -42,7 +42,7 @@ def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): if (tokens[c1 + c2] != t and len(t) < 5) or calc_ratio(tokens[c1 + c2], t) < .8: break else: - s = len(mwt) + s = len(mwt) - 1 result.append("".join(mwt)) break else: From 3df92162ac3ea8dc5bccb8f3e45687059cc0a525 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 15 Jan 2021 15:34:49 -0700 Subject: [PATCH 0434/1220] Add series tokens to modifiers if they appear in name like with other tokens (#516) This should fix ^id gh knight --- dadguide/monster_index.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index f8702c02..791e5777 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -69,6 +69,8 @@ async def _build_monster_index(self, monsters): self.tokens = defaultdict(set) self.modifiers = defaultdict(set) + mod_maps = list(MODIFIER_MAPS.values()) + list(self.series_id_to_pantheon_nickname.values()) + async for m in AsyncIter(monsters): self.modifiers[m] = await self.get_modifiers(m) @@ -81,7 +83,7 @@ async def _build_monster_index(self, monsters): self.tokens[token.lower()].add(m) for repl in TOKEN_REPLACEMENTS[token.lower()]: self.tokens[repl].add(m) - for pas in MODIFIER_MAPS.values(): + for pas in mod_maps: if token in pas: self.modifiers[m].update(pas) From bb7f715c5ad1e0df08d441637491e3a14ef907d7 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 15 Jan 2021 15:41:25 -0700 Subject: [PATCH 0435/1220] Prioritize events and regular monsters equally (#517) --- padinfo/find_monster.py | 16 ++++++++-------- padinfo/padinfo.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index f5d637ee..7b27f118 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -2,14 +2,14 @@ from collections import defaultdict from typing import Set -SERIES_TYPE_PRIORITY = [ - "regular", - "event", - "seasonal", - "ghcollab", - "collab", - None -] +SERIES_TYPE_PRIORITY = { + "regular": 4, + "event": 4, + "seasonal": 3, + "ghcollab": 2, + "collab": 1, + None: 0 +} def calc_ratio(s1, s2): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 273560c2..ebc8c48d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1124,7 +1124,7 @@ async def _findMonster3(self, query): key=lambda m: (monster_score[m], not m.is_equip, bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap - -series_priority.index(m.series.series_type), + series_priority.get(m.series.series_type), m.on_na if m.series.series_type == "collab" else 0, DGCOG.database.graph.monster_is_rem_evo(m), -DGCOG.database.graph.get_base_id(m), From cb3f4e42afb822460f03667332d77864519cb558 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 15 Jan 2021 20:31:15 -0700 Subject: [PATCH 0436/1220] Move from difflib to Jaro Winkler (#519) * Move from difflib to Jaro Winkler * Remove unnecessary function * Add levenshtein to info.json and requirements.txt --- dadguide/monster_index.py | 5 ----- padinfo/find_monster.py | 23 +++++++++++++---------- padinfo/info.json | 3 ++- padinfo/padinfo.py | 11 ++++++----- requirements.txt | 3 ++- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 791e5777..0c9c064a 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,5 +1,4 @@ import csv -import difflib import io import re @@ -237,10 +236,6 @@ async def get_modifiers(self, m): return modifiers -def calc_ratio(s1, s2): - return difflib.SequenceMatcher(None, s1, s2).ratio() - - # TODO: Move this to TSUtils async def sheet_to_reader(url): async with aiohttp.ClientSession() as session: diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 7b27f118..c196e70a 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -1,7 +1,8 @@ -import difflib from collections import defaultdict from typing import Set +from Levenshtein import jaro_winkler + SERIES_TYPE_PRIORITY = { "regular": 4, "event": 4, @@ -13,15 +14,16 @@ def calc_ratio(s1, s2): - return difflib.SequenceMatcher(None, s1, s2).ratio() + return jaro_winkler(s1, s2, .05) def calc_ratio_prefix(token, full_word): + print(token, full_word) if full_word == token: return 1 elif full_word.startswith(token): - return 1 - len(full_word) / 100 - return difflib.SequenceMatcher(None, token, full_word).ratio() + return 1 - len(full_word) / 1000 + return jaro_winkler(token, full_word, .05) class FindMonster: @@ -55,9 +57,9 @@ def _monster_has_token(self, monster, token, monsterscore, monster_mods): monsterscore[monster] += 1 return True else: - dlm = difflib.get_close_matches(token, monster_mods, n=1, cutoff=.8) - if dlm: - monsterscore[monster] += max(calc_ratio(token, p) for p in dlm) + closest = max(jaro_winkler(m, token, .05) for m in monster_mods) + if closest > .8: + monsterscore[monster] += closest return True return False @@ -74,7 +76,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if difflib.get_close_matches(token, index2.suffixes, n=1, cutoff=.8): + if any(jaro_winkler(m, token) > .8 for m in index2.suffixes): if negated: negative_modifiers.add(token) else: @@ -88,7 +90,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): negated = token.startswith("-") token = token.lstrip('-') if token in index2.all_modifiers or ( - difflib.get_close_matches(token, longmods, n=1, cutoff=.8) + any(jaro_winkler(m, token) > .8 for m in longmods) and token not in index2.name_tokens and len(token) >= 8): if negated: @@ -115,7 +117,8 @@ def process_name_tokens(self, name_query_tokens, index2): for t in name_query_tokens: valid = set() - ms = difflib.get_close_matches(t, index2.name_tokens, n=10000, cutoff=.8) + ms = sorted([nt for nt in index2.name_tokens if jaro_winkler(t, nt, .05) > .8], + key=lambda nt: jaro_winkler(t, nt, .05), reverse=True) ms += [token for token in index2.name_tokens if token.startswith(t)] if not ms: return None, None diff --git a/padinfo/info.json b/padinfo/info.json index d29d4207..12935692 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -22,7 +22,8 @@ "requirements": [ "tsutils>=3.0.1", "discord-menu>=0.1.0", - "prettytable" + "prettytable", + "python-levenshtein" ], "tags": [ "PAD" diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ebc8c48d..5c86accc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,5 +1,4 @@ import asyncio -from datetime import datetime import json import logging import os @@ -7,6 +6,7 @@ import re import urllib.parse from collections import OrderedDict, defaultdict +from datetime import datetime from enum import Enum from io import BytesIO from typing import TYPE_CHECKING @@ -36,6 +36,7 @@ IDGUIDE = "https://github.com/TsubakiBotPad/pad-cogs/wiki/%5Eid-user-guide" + class ServerFilter(Enum): any = 0 na = 1 @@ -803,10 +804,11 @@ async def idt_run(self, ctx): async with ctx.typing(): for q, r in suite.items(): m = await self.findMonster3(q) + mid = m and m.monster_id if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{q.ljust(ml)} - Ex: {r['result']}, Ac: {m and m.monster_id}{reason}\n" + o += f"{q.ljust(ml)} - Ex: {r['result']}, Ac: {mid}{reason}\n" else: c += 1 if c: @@ -1117,13 +1119,13 @@ async def _findMonster3(self, query): # no modifiers match any monster in the evo tree return - print({k: v for k, v in monster_score.items() if k in monster_gen}) + print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. mon = max(monster_gen, key=lambda m: (monster_score[m], not m.is_equip, - bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap + bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap series_priority.get(m.series.series_type), m.on_na if m.series.series_type == "collab" else 0, DGCOG.database.graph.monster_is_rem_evo(m), @@ -1166,7 +1168,6 @@ async def idhelp(self, ctx, *, query=""): if query: await self.debugid(ctx, query=query) - @commands.command() async def exportmodifiers(self, ctx): DGCOG = self.bot.get_cog("Dadguide") diff --git a/requirements.txt b/requirements.txt index 5c0f636b..69c03afb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,9 +9,10 @@ Pillow ply prettytable pymysql +python-levenshtein pytz Red-DiscordBot[postgres] setuptools tabulate tsutils -wheel \ No newline at end of file +wheel From be068c1f797ce87bcdcc2403bf8a0c6c4394f949 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 16 Jan 2021 01:41:45 -0700 Subject: [PATCH 0437/1220] Split out fluffy tokens (#520) * Save * Finish * Allow for name tokens to be added via google sheet * Update to set * Remove some pesky print functions --- dadguide/monster_index.py | 60 +++++++++++++++++++++++++++++++-------- padinfo/find_monster.py | 14 +++++---- padinfo/padinfo.py | 9 ++++-- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 0c9c064a..3bfb86a3 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -20,6 +20,7 @@ async def __ainit__(self, monsters, db): self.graph = db.graph self.monster_id_to_nickname = defaultdict(set) + self.monster_id_to_nametokens = defaultdict(set) self.monster_id_to_treename = defaultdict(set) self.series_id_to_pantheon_nickname = defaultdict(set, {m.series_id: {m.series.name_en.lower().replace(" ", "")} for m @@ -32,11 +33,14 @@ async def __ainit__(self, monsters, db): nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) for name, m_id, *data in nickname_data: - _, i, *_ = data + [None, None] + lp, i, *_ = data + [None, None] if m_id.isdigit() and not i: - if " " in name: - self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.monster_id_to_nickname[int(m_id)].add(name.lower().replace(" ", "")) + if lp: + self.monster_id_to_nametokens[int(m_id)].update(self._name_to_tokens(name)) + else: + if " " in name: + self.multi_word_tokens.add(tuple(name.lower().split(" "))) + self.monster_id_to_nickname[int(m_id)].add(name.lower().replace(" ", "")) treenames_data = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) for m_id, name, *data in treenames_data: @@ -53,10 +57,10 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) - self.manual = self.tokens = self.modifiers = defaultdict(set) + self.manual = self.name_tokens = self.fluff_tokens = self.modifiers = defaultdict(set) await self._build_monster_index(monsters) self.manual = combine_tokens(self.manual_nick, self.manual_tree) - self.name_tokens = list(self.manual) + list(self.tokens) + self.all_name_tokens = list(self.manual) + list(self.fluff_tokens) + list(self.name_tokens) self.all_modifiers = {p for ps in self.modifiers.values() for p in ps} self.suffixes = LEGAL_END_TOKENS @@ -65,7 +69,8 @@ async def __ainit__(self, monsters, db): async def _build_monster_index(self, monsters): self.manual_nick = defaultdict(set) self.manual_tree = defaultdict(set) - self.tokens = defaultdict(set) + self.name_tokens = defaultdict(set) + self.fluff_tokens = defaultdict(set) self.modifiers = defaultdict(set) mod_maps = list(MODIFIER_MAPS.values()) + list(self.series_id_to_pantheon_nickname.values()) @@ -74,14 +79,27 @@ async def _build_monster_index(self, monsters): self.modifiers[m] = await self.get_modifiers(m) # ID - self.tokens[str(m.monster_id)].add(m) - self.tokens[str(m.monster_id % 10000)].add(m) + self.name_tokens[str(m.monster_id)].add(m) + self.name_tokens[str(m.monster_id % 10000)].add(m) # Name Tokens + if self.monster_id_to_nametokens[m.monster_id]: + for t in self.monster_id_to_nametokens[m.monster_id]: + self.name_tokens[t].add(m) + else: + for token in self._get_important_tokens(m.name_en): + self.name_tokens[token.lower()].add(m) + for repl in TOKEN_REPLACEMENTS[token.lower()]: + self.name_tokens[repl].add(m) + for pas in mod_maps: + if token in pas: + self.modifiers[m].update(pas) for token in self._name_to_tokens(m.name_en): - self.tokens[token.lower()].add(m) + if m in self.name_tokens[token.lower()]: + continue + self.fluff_tokens[token.lower()].add(m) for repl in TOKEN_REPLACEMENTS[token.lower()]: - self.tokens[repl].add(m) + self.fluff_tokens[repl].add(m) for pas in mod_maps: if token in pas: self.modifiers[m].update(pas) @@ -89,19 +107,37 @@ async def _build_monster_index(self, monsters): # Monster Nickname for nick in self.monster_id_to_nickname[m.monster_id]: self.manual_nick[nick].add(m) + for pas in mod_maps: + if nick in pas: + self.modifiers[m].update(pas) # Tree Nickname base_id = self.graph.get_base_id(m) for nick in self.monster_id_to_treename[base_id]: self.manual_tree[nick].add(m) + for pas in mod_maps: + if nick in pas: + self.modifiers[m].update(pas) @staticmethod def _name_to_tokens(oname): oname = oname.lower() - name = re.sub(r'[\-+]', ' ', oname) + name = re.sub(r'[\-+\']', ' ', oname) name = re.sub(r'[^a-z0-9 ]', '', name) return [t.strip() for t in set(name.split() + oname.split()) if t] + @classmethod + def _get_important_tokens(cls, oname): + name = oname.split(", ") + if len(name) == 1: + return cls._name_to_tokens(oname) + *n1, n2 = name + n1 = ", ".join(n1) + if n1.count(" ") == n2.count(" ") or max(n1.count(" "), n2.count(" ")) >= 2: + return cls._name_to_tokens(oname) + else: + return cls._name_to_tokens(min(n1, n2, key=lambda n: n.count(" "))) + async def get_modifiers(self, m): modifiers = set() diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index c196e70a..a49728cb 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -18,7 +18,6 @@ def calc_ratio(s1, s2): def calc_ratio_prefix(token, full_word): - print(token, full_word) if full_word == token: return 1 elif full_word.startswith(token): @@ -91,7 +90,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): token = token.lstrip('-') if token in index2.all_modifiers or ( any(jaro_winkler(m, token) > .8 for m in longmods) - and token not in index2.name_tokens + and token not in index2.all_name_tokens and len(token) >= 8): if negated: lastmodpos = False @@ -116,10 +115,9 @@ def process_name_tokens(self, name_query_tokens, index2): for t in name_query_tokens: valid = set() - - ms = sorted([nt for nt in index2.name_tokens if jaro_winkler(t, nt, .05) > .8], + ms = sorted([nt for nt in index2.all_name_tokens if jaro_winkler(t, nt, .05) > .8], key=lambda nt: jaro_winkler(t, nt, .05), reverse=True) - ms += [token for token in index2.name_tokens if token.startswith(t)] + ms += [token for token in index2.all_name_tokens if token.startswith(t)] if not ms: return None, None for match in ms: @@ -127,10 +125,14 @@ def process_name_tokens(self, name_query_tokens, index2): if m not in valid: monsterscore[m] += calc_ratio_prefix(t, match) + .001 valid.add(m) - for m in index2.tokens[match]: + for m in index2.name_tokens[match]: if m not in valid: monsterscore[m] += calc_ratio_prefix(t, match) valid.add(m) + for m in index2.fluff_tokens[match]: + if m not in valid: + monsterscore[m] += calc_ratio_prefix(t, match) / 2 + valid.add(m) if monstergen is not None: monstergen.intersection_update(valid) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5c86accc..dfde36c2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1099,7 +1099,7 @@ async def _findMonster3(self, query): query = rmdiacritics(query).lower() mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, DGCOG.index2) - print(mod_tokens, name_query_tokens) + # print(mod_tokens, name_query_tokens) if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) @@ -1119,7 +1119,7 @@ async def _findMonster3(self, query): # no modifiers match any monster in the evo tree return - print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. mon = max(monster_gen, @@ -1129,6 +1129,8 @@ async def _findMonster3(self, query): series_priority.get(m.series.series_type), m.on_na if m.series.series_type == "collab" else 0, DGCOG.database.graph.monster_is_rem_evo(m), + not all(t.value in [0, 12, 14, 15] for t in m.types), + not any(t.value in [0, 12, 14, 15] for t in m.types), -DGCOG.database.graph.get_base_id(m), m.rarity, m.monster_no_na)) @@ -1149,7 +1151,8 @@ async def debugid(self, ctx, *, query): o = (f"[{m.monster_id}] {m.name_en}\n" f"Base: [{bm.monster_id}] {bm.name_en}\n" f"Series: {m.series.name_en} ({m.series_id})\n\n" - f"[Name Tokens] {' '.join(sorted(t for t, ms in DGCOG.index2.tokens.items() if m in ms))}\n\n" + f"[Name Tokens] {' '.join(sorted(t for t, ms in DGCOG.index2.name_tokens.items() if m in ms))}\n" + f"[Fluff Tokens] {' '.join(sorted(t for t, ms in DGCOG.index2.fluff_tokens.items() if m in ms))}\n\n" f"[Manual Tokens]\n" f" Treenames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms))}\n" f" Nicknames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms))}\n\n" From 1978f41567178edd01b69e0f3404dbe0598ca4b4 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 16 Jan 2021 01:46:54 -0700 Subject: [PATCH 0438/1220] Fix fluff tokens (equality sign was backwards) (#523) --- dadguide/monster_index.py | 2 +- padinfo/padinfo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 3bfb86a3..d680975e 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -133,7 +133,7 @@ def _get_important_tokens(cls, oname): return cls._name_to_tokens(oname) *n1, n2 = name n1 = ", ".join(n1) - if n1.count(" ") == n2.count(" ") or max(n1.count(" "), n2.count(" ")) >= 2: + if n1.count(" ") == n2.count(" ") or max(n1.count(" "), n2.count(" ")) < 2: return cls._name_to_tokens(oname) else: return cls._name_to_tokens(min(n1, n2, key=lambda n: n.count(" "))) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index dfde36c2..cfe3edc4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -811,7 +811,7 @@ async def idt_run(self, ctx): o += f"{q.ljust(ml)} - Ex: {r['result']}, Ac: {mid}{reason}\n" else: c += 1 - if c: + if c != len(suite): o += f"\n\nTests complete. {c}/{len(suite)} succeeded." else: o += "\n\nAll tests succeeded." From 83bc2421a9d5aa67f918048ec9a13785aefe961e Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 16 Jan 2021 23:14:20 -0700 Subject: [PATCH 0439/1220] Allow name tokens to be inferred by the index (#527) * Cannot be inferred from equips * Unless the equip has an 's in the name Fixes Bride Roche as test case --- dadguide/monster_index.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index d680975e..f7d8850e 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -82,19 +82,34 @@ async def _build_monster_index(self, monsters): self.name_tokens[str(m.monster_id)].add(m) self.name_tokens[str(m.monster_id % 10000)].add(m) - # Name Tokens - if self.monster_id_to_nametokens[m.monster_id]: - for t in self.monster_id_to_nametokens[m.monster_id]: - self.name_tokens[t].add(m) - else: + # Name and Fluff Tokens + manual = False + nametokens = self._name_to_tokens(m.name_en) + for me in self.graph.get_alt_ids_by_id(m.monster_id): + for t in self.monster_id_to_nametokens[me]: + if t in nametokens: + self.name_tokens[t].add(m) + if not manual: for token in self._get_important_tokens(m.name_en): self.name_tokens[token.lower()].add(m) for repl in TOKEN_REPLACEMENTS[token.lower()]: self.name_tokens[repl].add(m) + if m.is_equip: + ts = re.findall(r"(\w+)'s", m.name_en.lower()) + for me in self.graph.get_alt_monsters(m): + for t2 in ts: + if t2 in me.name_en.lower(): + self.name_tokens[t2].add(me) + else: + for me in self.graph.get_alt_monsters(m): + if token in self._name_to_tokens(me.name_en): + self.name_tokens[token].add(me) + for repl in TOKEN_REPLACEMENTS[token.lower()]: + self.name_tokens[repl].add(me) for pas in mod_maps: if token in pas: self.modifiers[m].update(pas) - for token in self._name_to_tokens(m.name_en): + for token in nametokens: if m in self.name_tokens[token.lower()]: continue self.fluff_tokens[token.lower()].add(m) @@ -133,10 +148,10 @@ def _get_important_tokens(cls, oname): return cls._name_to_tokens(oname) *n1, n2 = name n1 = ", ".join(n1) - if n1.count(" ") == n2.count(" ") or max(n1.count(" "), n2.count(" ")) < 2: + if tcount(n1) == tcount(n2) or max(tcount(n1), tcount(n2)) < 3: return cls._name_to_tokens(oname) else: - return cls._name_to_tokens(min(n1, n2, key=lambda n: n.count(" "))) + return cls._name_to_tokens(min(n1, n2, key=tcount)) async def get_modifiers(self, m): modifiers = set() @@ -285,3 +300,9 @@ def combine_tokens(d1, d2): for k, v in d2.items(): do[k].update(v) return do + + +def tcount(tstr): + tstr = re.sub(r"[^\w ]", "", tstr) + tstr = re.sub(r"\(.+\)", "", tstr) + return len([*filter(None, tstr.split())]) From 1e8ab0ffe91dc94bbb18c58f3e5400a228091254 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 17 Jan 2021 00:34:02 -0700 Subject: [PATCH 0440/1220] Prepare for nickname sheet column order switching (#529) The nicknames & treenames sheets have id & name in opposite order currently, we want to fix this. We will also standardize the pantheon names sheet. --- dadguide/dadguide.py | 4 ++-- dadguide/monster_index.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index c7c379ea..eda9ea06 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -181,7 +181,7 @@ async def download_and_refresh_nicknames(self): panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) self.nickname_overrides = defaultdict(set) - for nick, id, _, _, i in nickname_overrides: + for id, nick, _, _, i in nickname_overrides: if id.isdigit() and not i: self.nickname_overrides[int(id)].add(nick.lower()) @@ -190,7 +190,7 @@ async def download_and_refresh_nicknames(self): if id.isdigit() and not i: self.treename_overrides[int(id)].add(treename.lower()) - self.panthname_overrides = {x[0].lower(): x[1].lower() for x in panthname_overrides} + self.panthname_overrides = {x[1].lower(): x[2].lower() for x in panthname_overrides} self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) logger.info('Loading dg database') diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index f7d8850e..92a2b99b 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -32,7 +32,7 @@ async def __ainit__(self, monsters, db): if " " in m.series.name_en} nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) - for name, m_id, *data in nickname_data: + for m_id, name, *data in nickname_data: lp, i, *_ = data + [None, None] if m_id.isdigit() and not i: if lp: @@ -51,7 +51,7 @@ async def __ainit__(self, monsters, db): self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) pantheon_data = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) - for name, _, sid, *_ in pantheon_data: + for sid, name, *_ in pantheon_data: if sid.isdigit(): if " " in name: self.multi_word_tokens.add(tuple(name.lower().split(" "))) From 19ee0e4ee4926bba8b42c573d064e4360198bcd2 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 17 Jan 2021 00:37:41 -0700 Subject: [PATCH 0441/1220] Increase pantheon sheet reader size (#530) * Prepare for column order switching * Increase pantheon reader size --- dadguide/dadguide.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index eda9ea06..b4f46392 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -178,7 +178,7 @@ async def download_and_refresh_nicknames(self): logger.info('Loading dg name overrides') nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN, 5) treename_overrides = self._csv_to_tuples(TREENAME_FILE_PATTERN, 5) - panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN) + panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN, 3) self.nickname_overrides = defaultdict(set) for id, nick, _, _, i in nickname_overrides: From 8845e26fad785ce4ee6fcab0855a81a9358ad388 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Sun, 17 Jan 2021 13:37:46 -0800 Subject: [PATCH 0442/1220] Rewriting id menu to use discord-menu --- padinfo/common/emoji_map.py | 6 +- padinfo/id_menu.py | 12 +++ padinfo/info.json | 4 +- padinfo/padinfo.py | 3 +- padinfo/view/id.py | 176 ++++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 padinfo/view/id.py diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 8ebb8d0b..088217a8 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -94,6 +94,10 @@ def awakening_restricted_latent_emoji(latent: "AwakeningRestrictedLatent"): return str(get_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value]))) +def emoji_markdown(name_or_number: Union[str, int]): + return format_emoji(get_emoji(name_or_number)) + + def get_emoji(name): if isinstance(name, int): name = AWAKENING_ID_TO_EMOJI_NAME_MAP[name] @@ -103,7 +107,7 @@ def get_emoji(name): return name -def format_emoji(emoji: Union[Emoji,str]): +def format_emoji(emoji: Union[Emoji, str]): if isinstance(emoji, str): return f":{emoji}:" return f"<:{emoji.name}:{emoji.id}>" diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 9f668f9d..e5874865 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -18,6 +18,7 @@ from .leader_skills import createSingleMultiplierText from .view.components.monster.image import MonsterImage from .view.evos import EvosView +from .view.id import IdView if TYPE_CHECKING: from dadguide.database_context import DbContext @@ -73,6 +74,17 @@ async def get_user_embed_color(self, pdicog): else: return discord.Color(color) + async def make_id_embed_v2(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + is_transform_base = self.db_context.graph.monster_is_transform_base(m) + true_evo_type_raw = self.db_context.graph.true_evo_type_by_monster(m).value + acquire_raw = self._monster_acquisition_string(m) + base_rarity = self.db_context.graph.get_base_monster_by_id(m.monster_no).rarity + alt_monsters = sorted({*self.db_context.graph.get_alt_monsters_by_id(m.monster_no)}, + key=lambda x: x.monster_id) + e = IdView.embed(m, color, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters) + return e.to_embed() + async def make_evo_embed_v2(self, m: "MonsterModel"): alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) diff --git a/padinfo/info.json b/padinfo/info.json index 12935692..c7c88eef 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -21,9 +21,9 @@ }, "requirements": [ "tsutils>=3.0.1", - "discord-menu>=0.1.0", "prettytable", - "python-levenshtein" + "python-levenshtein", + "discord-menu>=0.4.0" ], "tags": [ "PAD" diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index cfe3edc4..91d200d9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -467,7 +467,8 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) - id_embed = await menu.make_embed(m) + # id_embed = await menu.make_embed(m) + id_embed = await menu.make_id_embed_v2(m) evo_embed = await menu.make_evo_embed_v2(m) mats_embed = await menu.make_evo_mats_embed(m) animated = m.has_animation diff --git a/padinfo/view/id.py b/padinfo/view/id.py new file mode 100644 index 00000000..6821427d --- /dev/null +++ b/padinfo/view/id.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING, List + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField +from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText + +from padinfo.common.emoji_map import get_emoji, format_emoji, AWAKENING_ID_TO_EMOJI_NAME_MAP, emoji_markdown +from padinfo.common.padx import monster_url +from padinfo.leader_skills import createMultiplierText +from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + from dadguide.models.awakening_model import AwakeningModel + + +def _get_awakening_text(awakening: "AwakeningModel"): + return format_emoji(get_emoji( + AWAKENING_ID_TO_EMOJI_NAME_MAP.get(awakening.awoken_skill_id, awakening.name))) + + +def _killer_latent_emoji(latent_name: str): + return emoji_markdown('latent_killer_{}'.format(latent_name.lower())) + + +def _get_awakening_emoji_for_stats(m: "MonsterModel", i: int): + return emoji_markdown(i) if m.awakening_count(i) and not m.is_equip else '' + + +def _get_stat_text(stat, lb_stat, icon): + return Box( + Text(str(stat)), + Text("({})".format(lb_stat)) if lb_stat else None, + Text(icon) if icon else None, + delimiter=' ' + ) + + +def _monster_is_enhance(m: "MonsterModel"): + return any(x if x.name == 'Enhance' else None for x in m.types) + + +class IdView: + @staticmethod + def awakenings_row(m: "MonsterModel"): + if len(m.awakenings) == 0: + return Box(Text('No Awakenings')) + + normal_awakenings = len(m.awakenings) - m.superawakening_count + normal_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[:normal_awakenings]] + super_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[normal_awakenings:]] + + return Box( + Box(*[Text(e) for e in normal_awakenings_emojis], delimiter=' '), + Box( + Text(emoji_markdown('sa_questionmark')), + *[Text(e) for e in super_awakenings_emojis], + delimiter=' ') + ) + + @staticmethod + def killers_row(m: "MonsterModel", is_transform_base): + killers_text = 'Any' if 'Any' in m.killers else ' '.join([_killer_latent_emoji(k) for k in m.killers]) + available_killer_text = 'Available killers:' if is_transform_base else 'Avail. killers (pre-xform):' + return Box( + BoldText(available_killer_text), + Text('[{} slots]'.format(m.latent_slots)), + Text(killers_text), + delimiter=' ' + ) + + @staticmethod + def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_rarity: str): + rarity = Box( + LabeledText('Rarity', str(m.rarity)), + Text('({})'.format(LabeledText('Base', str(base_rarity)).to_markdown())), + Text("" if m.orb_skin_id is None else "(Orb Skin)"), + delimiter=' ' + ) + + cost = LabeledText('Cost', str(m.cost)) + acquire = BoldText(acquire_raw) if acquire_raw else None + valid_true_evo_types = ("Reincarnated", "Assist", "Pixel", "Super Reincarnated") + true_evo_type = BoldText(true_evo_type_raw) if true_evo_type_raw in valid_true_evo_types else None + + return Box(rarity, cost, acquire, true_evo_type) + + @staticmethod + def stats(m: "MonsterModel"): + hp, atk, rcv, weighted = m.stats() + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) if m.limit_mult > 0 else (None, None, None, None) + return Box( + LabeledText('HP', _get_stat_text(hp, lb_hp, _get_awakening_emoji_for_stats(m, 1))), + LabeledText('ATK', _get_stat_text(atk, lb_atk, _get_awakening_emoji_for_stats(m, 2))), + LabeledText('RCV', _get_stat_text(rcv, lb_rcv, _get_awakening_emoji_for_stats(m, 3))), + LabeledText('Fodder EXP', "{:,}".format(m.fodder_exp)) if _monster_is_enhance(m) else None + ) + + @staticmethod + def stats_header(m: "MonsterModel"): + voice = emoji_markdown(63) if m.awakening_count(63) and not m.is_equip else '' + header = Box( + Text(voice), + Text('Stats'), + Text('(LB, +{}%)'.format(m.limit_mult)) if m.limit_mult else None, + delimiter=' ' + ) + return header + + @staticmethod + def active_skill_header(m: "MonsterModel"): + active_skill = m.active_skill + active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) if active_skill else 'None' + return Box( + BoldText('Active Skill'), + BoldText(active_cd), + delimiter=' ' + ) + + @staticmethod + def leader_skill_header(m: "MonsterModel"): + return Box( + BoldText('Leader Skill'), + BoldText(createMultiplierText(m.leader_skill)), + delimiter=' ' + ) + + @staticmethod + def embed(m: "MonsterModel", color, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, + alt_monsters: List["MonsterModel"]): + fields = [ + EmbedField( + '/'.join(['{}'.format(t.name) for t in m.types]), + Box( + IdView.awakenings_row(m), + IdView.killers_row(m, is_transform_base) + ) + ), + EmbedField( + 'Inheritable' if m.is_inheritable else 'Not inheritable', + IdView.misc_info(m, true_evo_type_raw, acquire_raw, base_rarity), + inline=True + ), + EmbedField( + IdView.stats_header(m).to_markdown(), + IdView.stats(m), + inline=True + ), + EmbedField( + IdView.active_skill_header(m).to_markdown(), + Text(m.active_skill.desc if m.active_skill else 'None') + ), + EmbedField( + IdView.leader_skill_header(m).to_markdown(), + Text(m.leader_skill.desc if m.leader_skill else 'None') + ), + EmbedField( + "Alternate Evos", + HighlightableLinks( + links=[LinkedText(str(m.monster_id), monster_url(m)) for m in alt_monsters], + highlighted=next(i for i, mon in enumerate(alt_monsters) if m.monster_id == mon.monster_id) + ) + ) + ] + + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + url=monster_url(m)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), + embed_footer=pad_info_footer(), + embed_fields=fields) From dcc9a88c52ea560ded3ef207f5b2086ef148cb44 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Sun, 17 Jan 2021 14:42:35 -0800 Subject: [PATCH 0443/1220] Removing old ^id embed code --- padinfo/id_menu.py | 142 +-------------------------------------------- padinfo/info.json | 2 +- padinfo/padinfo.py | 9 ++- 3 files changed, 6 insertions(+), 147 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index e5874865..2d45da3a 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -11,7 +11,7 @@ from discordmenu.embed.text import Text, BoldText from redbot.core.utils.chat_formatting import box -from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, awakening_restricted_latent_emoji +from padinfo.common.emoji_map import awakening_restricted_latent_emoji from padinfo.view.components.monster.header import MonsterHeader from .common.padx import monster_url from .leader_skills import createMultiplierText @@ -48,14 +48,6 @@ def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = N self.db_context = db_context self.allowed_emojis = allowed_emojis - def match_emoji(self, name): - if isinstance(name, int): - name = AWAKENING_ID_TO_EMOJI_NAME_MAP[name] - for e in self.allowed_emojis: - if e.name == name: - return e - return name - async def make_base_embed(self, m: "MonsterModel"): header = MonsterHeader.long(m) embed = await self.make_custom_embed() @@ -246,135 +238,6 @@ def _monster_acquisition_string(self, m: "MonsterModel"): acquire_text = 'MP Shop Evo' return acquire_text - async def make_embed(self, m: "MonsterModel"): - embed = await self.make_base_embed(m) - - # in case we want to readd the type emojis later - # types_row = ' '.join(['{} {}'.format(str(match_emoji(allowed_emojis, 'mons_type_{}'.format(t.name.lower()))), t.name) for t in m.types]) - types_row = '/'.join(['{}'.format(t.name) for t in m.types]) - - awakenings_row = '' - for idx, a in enumerate(m.awakenings): - as_id = a.awoken_skill_id - as_name = a.name - mapped_awakening = AWAKENING_ID_TO_EMOJI_NAME_MAP.get(as_id, as_name) - mapped_awakening = self.match_emoji(mapped_awakening) - - # Wrap superawakenings to the next line - if len(m.awakenings) - idx == m.superawakening_count: - awakenings_row += '\n{}'.format(self.match_emoji('sa_questionmark')) - awakenings_row += ' {}'.format(mapped_awakening) - else: - awakenings_row += ' {}'.format(mapped_awakening) - - is_transform_base = self.db_context.graph.monster_is_transform_base(m) - transform_base = self.db_context.graph.get_transform_base_by_id(m.monster_id) if not is_transform_base else None - - awakenings_row = awakenings_row.strip() - - if not len(awakenings_row): - awakenings_row = 'No Awakenings' - - if is_transform_base: - - killers_row = '**Available killers:** [{} slots] {}'.format(m.latent_slots, - self.get_killers_text(m)) - else: - killers_row = '**Avail. killers (pre-xform):** [{} slots] {}'.format( - transform_base.latent_slots, - self.get_killers_text(transform_base)) - - embed.add_field(name=types_row, value='{}\n{}'.format(awakenings_row, killers_row), inline=False) - - info_row_1 = 'Inheritable' if m.is_inheritable else 'Not inheritable' - acquire_text = self._monster_acquisition_string(m) - tet_text = self.db_context.graph.true_evo_type_by_monster(m).value - - orb_skin = "" if m.orb_skin_id is None else " (Orb Skin)" - - info_row_2 = '**Rarity** {} (**Base** {}){}\n**Cost** {}'.format( - m.rarity, - self.db_context.graph.get_base_monster_by_id(m.monster_no).rarity, - orb_skin, - m.cost - ) - - if acquire_text: - info_row_2 += '\n**{}**'.format(acquire_text) - if tet_text in ("Reincarnated", "Assist", "Pixel", "Super Reincarnated"): - info_row_2 += '\n**{}**'.format(tet_text) - - embed.add_field(name=info_row_1, value=info_row_2) - - hp, atk, rcv, weighted = m.stats() - if m.limit_mult > 0: - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - stats_icons = [ - self.match_emoji(63) if m.awakening_count(63) and not m.is_equip else '', - self.match_emoji(1) if m.awakening_count(1) and not m.is_equip else '', - self.match_emoji(2) if m.awakening_count(2) and not m.is_equip else '', - self.match_emoji(3) if m.awakening_count(3) and not m.is_equip else '', - ] - stats_row_1 = '{} Stats (LB, +{}%)'.format(stats_icons[0], m.limit_mult) - stats_row_2 = '**HP** {} ({}) {stats[1]}\n**ATK** {} ({}) {stats[2]}\n**RCV** {} ({}) {stats[3]}'.format( - hp, lb_hp, atk, lb_atk, rcv, lb_rcv, stats=stats_icons) - else: - stats_icons = [ - '', - self.match_emoji(1) if m.awakening_count(1) and not m.is_equip else '', - self.match_emoji(2) if m.awakening_count(2) and not m.is_equip else '', - self.match_emoji(3) if m.awakening_count(3) and not m.is_equip else '', - ] - stats_row_1 = '{} Stats'.format(stats_icons[0]) - stats_row_2 = '**HP** {} {stats[1]}\n**ATK** {} {stats[2]}\n**RCV** {} {stats[3]}'.format( - hp, atk, rcv, stats=stats_icons) - if any(x if x.name == 'Enhance' else None for x in m.types): - stats_row_2 += '\n**Fodder EXP** {:,}'.format(m.fodder_exp) - embed.add_field(name=stats_row_1, value=stats_row_2) - - active_header = '**Active Skill**' - active_body = 'None' - active_skill = m.active_skill - if active_skill: - active_header = '**Active Skill ({} -> {})**'.format(active_skill.turn_max, - active_skill.turn_min) - active_body = active_skill.desc - embed.add_field(name=active_header, value=active_body, inline=False) - - leader_skill = m.leader_skill - ls_row = m.leader_skill.desc if leader_skill else 'None' - ls_header = '**Leader Skill**' - if leader_skill: - multiplier_text = createMultiplierText(leader_skill) - ls_header += " **{}**".format(multiplier_text) - embed.add_field(name=ls_header, value=ls_row, inline=False) - - evos_header = "Alternate Evos" - evos_body = ", ".join(f"**{m2.monster_id}**" - if m2.monster_id == m.monster_id - else f"[{m2.monster_id}]({monster_url(m2)})" - for m2 in - sorted({*self.db_context.graph.get_alt_monsters_by_id(m.monster_no)}, - key=lambda x: x.monster_id)) - embed.add_field(name=evos_header, value=evos_body, inline=False) - - return embed - - def get_killers_text(self, m: "MonsterModel"): - return_text = '' - if 'Any' in m.killers: - return_text += 'Any' - if m.awakening_restricted_latents: - pass - # return_text += ', ' - else: - return_text += ' '.join([self.killer_latent_emoji(k) for k in m.killers]) - if m.awakening_restricted_latents: - pass - # return_text += ' ' - # return_text += self.get_awakening_restricted_latents_text(m) - return return_text - def get_awakening_restricted_latents_text(self, m: "MonsterModel"): """Not currently in use, but potentially could be in the future if more ARLatents are added later @@ -383,9 +246,6 @@ def get_awakening_restricted_latents_text(self, m: "MonsterModel"): return '' return ' ' + ' '.join([awakening_restricted_latent_emoji(x) for x in m.awakening_restricted_latents]) - def killer_latent_emoji(self, latent: str): - return str(self.match_emoji('latent_killer_{}'.format(latent.lower()))) - async def make_otherinfo_embed(self, m: "MonsterModel"): embed = await self.make_base_embed(m) # Clear the thumbnail, takes up too much space diff --git a/padinfo/info.json b/padinfo/info.json index c7c88eef..05468be5 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -23,7 +23,7 @@ "tsutils>=3.0.1", "prettytable", "python-levenshtein", - "discord-menu>=0.4.0" + "discord-menu>=0.5.0" ], "tags": [ "PAD" diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 91d200d9..ffb06bbf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -467,7 +467,6 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) - # id_embed = await menu.make_embed(m) id_embed = await menu.make_id_embed_v2(m) evo_embed = await menu.make_evo_embed_v2(m) mats_embed = await menu.make_evo_mats_embed(m) @@ -524,7 +523,7 @@ async def _do_evolistmenu(self, ctx, sm): return else: emoji = char_to_emoji(chars[idx]) - emoji_to_embed[emoji] = await menu.make_embed(m) + emoji_to_embed[emoji] = await menu.make_id_embed_v2(m) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji @@ -708,8 +707,8 @@ async def leaderskill(self, ctx, *, whole_query): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = await menu.make_ls_embed(left_m, right_m) - emoji_to_embed[self.left_emoji] = await menu.make_embed(left_m) - emoji_to_embed[self.right_emoji] = await menu.make_embed(right_m) + emoji_to_embed[self.left_emoji] = await menu.make_id_embed_v2(left_m) + emoji_to_embed[self.right_emoji] = await menu.make_id_embed_v2(right_m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -726,7 +725,7 @@ async def leaderskillsingle(self, ctx, *, query): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = await menu.make_lssingle_embed(m) - emoji_to_embed[self.left_emoji] = await menu.make_embed(m) + emoji_to_embed[self.left_emoji] = await menu.make_id_embed_v2(m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) From f08a05ceb89a77a749a7d9f6b5f017f79cb150c8 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Sun, 17 Jan 2021 15:16:33 -0800 Subject: [PATCH 0444/1220] Refactoring lookup and leader_skill --- padinfo/id_menu.py | 26 +++++++------------------- padinfo/padinfo.py | 2 +- padinfo/view/leader_skill.py | 29 +++++++++++++++++++++++++++++ padinfo/view/lookup.py | 20 ++++++++++++++++++++ 4 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 padinfo/view/leader_skill.py create mode 100644 padinfo/view/lookup.py diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 2d45da3a..b28c6520 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -19,6 +19,8 @@ from .view.components.monster.image import MonsterImage from .view.evos import EvosView from .view.id import IdView +from .view.leader_skill import LeaderSkillView +from .view.lookup import LookupView if TYPE_CHECKING: from dadguide.database_context import DbContext @@ -196,26 +198,12 @@ def monster_orb_skin_cb_url(m: "MonsterModel", link_text='Color Blind'): return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): - lls = left_m.leader_skill - rls = right_m.leader_skill - pdicog = self.ctx.bot.get_cog("PadInfo") + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return LeaderSkillView.embed(left_m, right_m, color).to_embed() - return EmbedView( - EmbedMain( - title=createMultiplierText(lls, rls), - description=Box( - BoldText(MonsterHeader.name(left_m, link=True, show_jp=True)), - Text(lls.desc if lls else 'None'), - BoldText(MonsterHeader.name(right_m, link=True, show_jp=True)), - Text(rls.desc if rls else 'None')), - color=await self.get_user_embed_color(pdicog) - )).to_embed() - - async def make_header_embed(self, m: "MonsterModel"): - header = MonsterHeader.long(m, link=True) - embed = await self.make_custom_embed() - embed.description = header - return embed + async def make_lookup_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return LookupView.embed(m, color).to_embed() def _monster_acquisition_string(self, m: "MonsterModel"): acquire_text = None diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ffb06bbf..34eac4ab 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -600,7 +600,7 @@ async def lookup(self, ctx, *, query: str): m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: menu = IdMenu(ctx, allowed_emojis=self.get_emojis()) - embed = await menu.make_header_embed(m) + embed = await menu.make_lookup_embed(m) await ctx.send(embed=embed) else: await self.makeFailureMsg(ctx, query, err) diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py new file mode 100644 index 00000000..65627035 --- /dev/null +++ b/padinfo/view/leader_skill.py @@ -0,0 +1,29 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain +from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import BoldText, Text + +from padinfo.leader_skills import createMultiplierText +from padinfo.view.components.monster.header import MonsterHeader + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class LeaderSkillView: + @staticmethod + def embed(left_m: "MonsterModel", right_m: "MonsterModel", color): + lls = left_m.leader_skill + rls = right_m.leader_skill + return EmbedView( + EmbedMain( + title=createMultiplierText(lls, rls), + description=Box( + BoldText(MonsterHeader.name(left_m, link=True, show_jp=True)), + Text(lls.desc if lls else 'None'), + BoldText(MonsterHeader.name(right_m, link=True, show_jp=True)), + Text(rls.desc if rls else 'None')), + color=color + )) diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py new file mode 100644 index 00000000..abb01cda --- /dev/null +++ b/padinfo/view/lookup.py @@ -0,0 +1,20 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.components import EmbedMain +from discordmenu.embed.menu import EmbedView + +from padinfo.common.padx import monster_url +from padinfo.view.components.monster.header import MonsterHeader + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class LookupView: + @staticmethod + def embed(m: "MonsterModel", color): + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + url=monster_url(m))) From 08e1dbf1b97127d17f17667e1a02cd868d3ff240 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 18 Jan 2021 01:30:33 -0700 Subject: [PATCH 0445/1220] Increase prefix matching threshold (#534) --- padinfo/find_monster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index a49728cb..7c873faf 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -75,7 +75,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if any(jaro_winkler(m, token) > .8 for m in index2.suffixes): + if any(jaro_winkler(m, token) > .95 for m in index2.suffixes): if negated: negative_modifiers.add(token) else: @@ -89,7 +89,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): negated = token.startswith("-") token = token.lstrip('-') if token in index2.all_modifiers or ( - any(jaro_winkler(m, token) > .8 for m in longmods) + any(jaro_winkler(m, token) > .95 for m in longmods) and token not in index2.all_name_tokens and len(token) >= 8): if negated: From 575f49494db23923ac6d0cbf9920dc79b69a2fa1 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 18 Jan 2021 02:05:54 -0700 Subject: [PATCH 0446/1220] Write debugiddist (#535) --- padinfo/find_monster.py | 13 ++++++++----- padinfo/padinfo.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/padinfo/find_monster.py b/padinfo/find_monster.py index 7c873faf..49251371 100644 --- a/padinfo/find_monster.py +++ b/padinfo/find_monster.py @@ -26,6 +26,9 @@ def calc_ratio_prefix(token, full_word): class FindMonster: + MODIFIER_JW_DISTANCE = .95 + TOKEN_JW_DISTANCE = .8 + def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result = [] s = 0 @@ -40,7 +43,7 @@ def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): if len(mwt) > len(tokens) - c1: continue for c2, t in enumerate(mwt): - if (tokens[c1 + c2] != t and len(t) < 5) or calc_ratio(tokens[c1 + c2], t) < .8: + if (tokens[c1 + c2] != t and len(t) < 5) or calc_ratio(tokens[c1 + c2], t) < self.TOKEN_JW_DISTANCE: break else: s = len(mwt) - 1 @@ -57,7 +60,7 @@ def _monster_has_token(self, monster, token, monsterscore, monster_mods): return True else: closest = max(jaro_winkler(m, token, .05) for m in monster_mods) - if closest > .8: + if closest > self.TOKEN_JW_DISTANCE: monsterscore[monster] += closest return True return False @@ -75,7 +78,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if any(jaro_winkler(m, token) > .95 for m in index2.suffixes): + if any(jaro_winkler(m, token) > self.MODIFIER_JW_DISTANCE for m in index2.suffixes): if negated: negative_modifiers.add(token) else: @@ -89,7 +92,7 @@ def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): negated = token.startswith("-") token = token.lstrip('-') if token in index2.all_modifiers or ( - any(jaro_winkler(m, token) > .95 for m in longmods) + any(jaro_winkler(m, token) > self.MODIFIER_JW_DISTANCE for m in longmods) and token not in index2.all_name_tokens and len(token) >= 8): if negated: @@ -115,7 +118,7 @@ def process_name_tokens(self, name_query_tokens, index2): for t in name_query_tokens: valid = set() - ms = sorted([nt for nt in index2.all_name_tokens if jaro_winkler(t, nt, .05) > .8], + ms = sorted([nt for nt in index2.all_name_tokens if jaro_winkler(t, nt, .05) > self.TOKEN_JW_DISTANCE], key=lambda nt: jaro_winkler(t, nt, .05), reverse=True) ms += [token for token in index2.all_name_tokens if token.startswith(t)] if not ms: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 34eac4ab..52fddfce 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -13,6 +13,7 @@ import discord import tsutils +from Levenshtein import jaro_winkler from discordmenu.emoji_cache import emoji_cache from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter @@ -1164,6 +1165,17 @@ async def debugid(self, ctx, *, query): for page in pagify(o): await ctx.send(box(page)) + @commands.command() + async def debugiddist(self, ctx, s1, s2): + dist = jaro_winkler(s1, s2) + yes = '\N{WHITE HEAVY CHECK MARK}' + no = '\N{CROSS MARK}' + await ctx.send(box(f"Jaro-Winkler Distance: {dist}\n" + f"Modifier token threshold: {find_monster.MODIFIER_JW_DISTANCE} " + f"{yes if dist >= find_monster.MODIFIER_JW_DISTANCE else no}\n" + f"Name token threshold: {find_monster.TOKEN_JW_DISTANCE} " + f"{yes if dist >= find_monster.TOKEN_JW_DISTANCE else no}")) + @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query=""): """Get help with an id query""" From 4ce4849e63443779455be05df166e08bddfa95fc Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 18 Jan 2021 02:23:07 -0700 Subject: [PATCH 0447/1220] Make debugiddist look nicer (#536) * Write debugiddist * Make debugiddist look nicer --- padinfo/padinfo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 52fddfce..7d9bae21 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1170,10 +1170,11 @@ async def debugiddist(self, ctx, s1, s2): dist = jaro_winkler(s1, s2) yes = '\N{WHITE HEAVY CHECK MARK}' no = '\N{CROSS MARK}' - await ctx.send(box(f"Jaro-Winkler Distance: {dist}\n" + await ctx.send(f"Printing info for {inline(s1)}, {inline(s2)}\n" + + box(f"Jaro-Winkler Distance: {round(dist, 4)}\n" f"Modifier token threshold: {find_monster.MODIFIER_JW_DISTANCE} " f"{yes if dist >= find_monster.MODIFIER_JW_DISTANCE else no}\n" - f"Name token threshold: {find_monster.TOKEN_JW_DISTANCE} " + f"Name token threshold: {find_monster.TOKEN_JW_DISTANCE} " f"{yes if dist >= find_monster.TOKEN_JW_DISTANCE else no}")) @commands.command(aliases=['helpid']) From c9d8f94f56576151e854e4870f4f320b711f006a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 18 Jan 2021 02:50:09 -0700 Subject: [PATCH 0448/1220] Add romaji subnames (#537) --- dadguide/monster_index.py | 4 +++- padinfo/padinfo.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 92a2b99b..fafe98b9 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -90,7 +90,7 @@ async def _build_monster_index(self, monsters): if t in nametokens: self.name_tokens[t].add(m) if not manual: - for token in self._get_important_tokens(m.name_en): + for token in self._get_important_tokens(m.name_en) + self._name_to_tokens(m.roma_subname): self.name_tokens[token.lower()].add(m) for repl in TOKEN_REPLACEMENTS[token.lower()]: self.name_tokens[repl].add(m) @@ -136,6 +136,8 @@ async def _build_monster_index(self, monsters): @staticmethod def _name_to_tokens(oname): + if not oname: + return [] oname = oname.lower() name = re.sub(r'[\-+\']', ' ', oname) name = re.sub(r'[^a-z0-9 ]', '', name) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7d9bae21..4b753908 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1093,9 +1093,9 @@ async def findMonster3(self, query): async def _findMonster3(self, query): DGCOG = self.bot.get_cog("Dadguide") - await DGCOG.wait_until_ready() if DGCOG is None: raise ValueError("Dadguide cog is not loaded") + await DGCOG.wait_until_ready() query = rmdiacritics(query).lower() mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, DGCOG.index2) From 827e30b227f1de7bdf1a74f95fee68d639490ed8 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 18 Jan 2021 23:13:31 -0600 Subject: [PATCH 0449/1220] Stop printing sa questionmark when no SAs (#541) --- padinfo/view/id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 6821427d..772c9786 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -58,7 +58,7 @@ def awakenings_row(m: "MonsterModel"): Box( Text(emoji_markdown('sa_questionmark')), *[Text(e) for e in super_awakenings_emojis], - delimiter=' ') + delimiter=' ') if len(super_awakenings_emojis) > 0 else None ) @staticmethod From d2fb1df5351906819a575fc4b215003efad8c8de Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 19 Jan 2021 23:53:39 -0700 Subject: [PATCH 0450/1220] Move name token aliases to gdoc from hardcoded (#543) --- dadguide/monster_index.py | 15 ++++++++++++--- dadguide/token_mappings.py | 20 +++++++------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index fafe98b9..b052835b 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,6 +1,7 @@ import csv import io import re +from collections import defaultdict import aiohttp from redbot.core.utils import AsyncIter @@ -13,6 +14,7 @@ NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') GROUP_TREENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') +NAME_TOKEN_ALIAS_SHEET = SHEETS_PATTERN.format('1229125459') class MonsterIndex2(aobject): @@ -31,6 +33,8 @@ async def __ainit__(self, monsters, db): in db.get_all_monsters() if " " in m.series.name_en} + self.replacement_tokens = defaultdict(set) + nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) for m_id, name, *data in nickname_data: lp, i, *_ = data + [None, None] @@ -57,6 +61,11 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) + nt_alias_data = await sheet_to_reader(NAME_TOKEN_ALIAS_SHEET) + next(nt_alias_data) # Skip over heading + for token, alias, *_ in nt_alias_data: + self.replacement_tokens[token].add(alias) + self.manual = self.name_tokens = self.fluff_tokens = self.modifiers = defaultdict(set) await self._build_monster_index(monsters) self.manual = combine_tokens(self.manual_nick, self.manual_tree) @@ -92,7 +101,7 @@ async def _build_monster_index(self, monsters): if not manual: for token in self._get_important_tokens(m.name_en) + self._name_to_tokens(m.roma_subname): self.name_tokens[token.lower()].add(m) - for repl in TOKEN_REPLACEMENTS[token.lower()]: + for repl in self.replacement_tokens[token.lower()]: self.name_tokens[repl].add(m) if m.is_equip: ts = re.findall(r"(\w+)'s", m.name_en.lower()) @@ -104,7 +113,7 @@ async def _build_monster_index(self, monsters): for me in self.graph.get_alt_monsters(m): if token in self._name_to_tokens(me.name_en): self.name_tokens[token].add(me) - for repl in TOKEN_REPLACEMENTS[token.lower()]: + for repl in self.replacement_tokens[token.lower()]: self.name_tokens[repl].add(me) for pas in mod_maps: if token in pas: @@ -113,7 +122,7 @@ async def _build_monster_index(self, monsters): if m in self.name_tokens[token.lower()]: continue self.fluff_tokens[token.lower()].add(m) - for repl in TOKEN_REPLACEMENTS[token.lower()]: + for repl in self.replacement_tokens[token.lower()]: self.fluff_tokens[repl].add(m) for pas in mod_maps: if token in pas: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 108728c4..5f8b76d4 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -1,7 +1,8 @@ -from collections import defaultdict from enum import Enum from .models.enum_types import Attribute, MonsterType +from enum import Enum +from .models.enum_types import Attribute, MonsterType COLOR_MAP = { Attribute.Fire: ('r', 'red', 'fire'), @@ -12,7 +13,7 @@ Attribute.Nil: ('nil', 'x', 'none', 'null', 'white') } -SUB_COLOR_MAP = {k: tuple('?'+t for t in v if t != "white") for k, v in COLOR_MAP.items()} +SUB_COLOR_MAP = {k: tuple('?' + t for t in v if t != "white") for k, v in COLOR_MAP.items()} DUAL_COLOR_MAP = {} for cid1, cns1 in COLOR_MAP.items(): @@ -28,7 +29,6 @@ ts += (t1 + "/" + t2,) DUAL_COLOR_MAP[(cid1, cid2)] = ts - TYPE_MAP = { MonsterType.Evolve: ('evolve',), MonsterType.Balance: ('balanced', 'bal'), @@ -274,18 +274,12 @@ class MiscModifiers(Enum): EVO_TOKENS = {*sum(EVO_MAP.values(), ())} TYPE_TOKENS = {*sum(TYPE_MAP.values(), ())} - -OTHER_HIDDEN_TOKENS = set()\ - .union(COLOR_TOKENS)\ - .union(AWAKENING_TOKENS)\ - .union(EVO_TOKENS)\ +OTHER_HIDDEN_TOKENS = set() \ + .union(COLOR_TOKENS) \ + .union(AWAKENING_TOKENS) \ + .union(EVO_TOKENS) \ .union(TYPE_TOKENS) -TOKEN_REPLACEMENTS = defaultdict(tuple, { - 'tamadra': ('tama',), - 'evolution': ('evo',), -}) - LEGAL_END_TOKENS = { "equip", "assist", From 2bee9881e66cbf6c6915ad6f223020440d3a762a Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jan 2021 03:22:37 -0700 Subject: [PATCH 0451/1220] Fix reincarnated pulling up srevo monsters (#548) --- dadguide/monster_index.py | 11 ++++++----- dadguide/token_mappings.py | 17 ++++++++++++----- padinfo/id_menu.py | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index b052835b..fcc1e79a 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -31,7 +31,7 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens = {tuple(m.series.name_en.lower().split()) for m in db.get_all_monsters() - if " " in m.series.name_en} + if " " in m.series.name_en}.union(MULTI_WORD_TOKENS) self.replacement_tokens = defaultdict(set) @@ -115,9 +115,10 @@ async def _build_monster_index(self, monsters): self.name_tokens[token].add(me) for repl in self.replacement_tokens[token.lower()]: self.name_tokens[repl].add(me) - for pas in mod_maps: - if token in pas: - self.modifiers[m].update(pas) + if token not in HAZARDOUS_IN_NAME_PREFIXES: + for pas in mod_maps: + if token in pas: + self.modifiers[m].update(pas) for token in nametokens: if m in self.name_tokens[token.lower()]: continue @@ -240,7 +241,7 @@ async def get_modifiers(self, m): modifiers.add(t) # Reincarnated - if '転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Reincarnated": + if self.graph.true_evo_type_by_monster(m).value == "Reincarnated": for t in EVO_MAP[EvoTypes.REVO]: modifiers.add(t) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 5f8b76d4..0767e501 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -1,6 +1,4 @@ from enum import Enum -from .models.enum_types import Attribute, MonsterType -from enum import Enum from .models.enum_types import Attribute, MonsterType @@ -66,9 +64,9 @@ class EvoTypes(Enum): EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo', 'suvo'), EvoTypes.TRANS: ('transform', 'trans', 'transformed'), EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), - EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'mega awoken'), - EvoTypes.REVO: ('revo', 'reincarnated'), - EvoTypes.SREVO: ('srevo', 'super', 'sr', 'super reincarnated'), + EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'megaawoken'), + EvoTypes.REVO: ('revo', 'reincarnated', 'rv'), + EvoTypes.SREVO: ('srevo', 'super', 'sr', 'superreincarnated'), EvoTypes.PIXEL: ('pixel', 'p', 'dot', 'px'), EvoTypes.NONPIXEL: ('nonpixel', 'np'), } @@ -254,6 +252,11 @@ class MiscModifiers(Enum): MiscModifiers.INNA: ('na',) } +MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { + 'super reincarnated', + 'mega awoken' +}} + MODIFIER_MAPS = { **COLOR_MAP, **SUB_COLOR_MAP, @@ -285,3 +288,7 @@ class MiscModifiers(Enum): "assist", "eq", } + +HAZARDOUS_IN_NAME_PREFIXES = { + "reincarnated" +} diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index b28c6520..e209515d 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -123,7 +123,7 @@ async def make_evo_mats_embed(self, m: "MonsterModel"): async def make_pantheon_embed(self, m: "MonsterModel"): full_pantheon = self.db_context.get_monsters_by_series(m.series_id) pantheon_list = list(filter(lambda x: self.db_context.graph.monster_is_base(x), full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 6: + if len(pantheon_list) == 0 or len(pantheon_list) > 20: return None embed = await self.make_base_embed(m) From a2df25f57da9c775ca8a91b08b10d5b8f3644856 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jan 2021 03:37:31 -0700 Subject: [PATCH 0452/1220] Fix na and jp modifiers (#550) In JP: injp In NA: inna JP Only: jp NA Only: na JP ID: jpid NA ID: naid --- dadguide/monster_index.py | 6 ++++++ dadguide/token_mappings.py | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index fcc1e79a..af2c2781 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -288,9 +288,15 @@ async def get_modifiers(self, m): if m.on_jp: for t in MISC_MAP[MiscModifiers.INJP]: modifiers.add(t) + if not m.on_na: + for t in MISC_MAP[MiscModifiers.ONLYJP]: + modifiers.add(t) if m.on_na: for t in MISC_MAP[MiscModifiers.INNA]: modifiers.add(t) + if not m.on_jp: + for t in MISC_MAP[MiscModifiers.ONLYNA]: + modifiers.add(t) if m.monster_id + 10000 in self.graph.nodes: modifiers.add("idjp") if m.monster_id > 10000: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 0767e501..f66b0c77 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -240,7 +240,9 @@ class MiscModifiers(Enum): REM = 'REM' MP = 'MP' INJP = 'In JP Server' + ONLYJP = 'Only in JP Server' INNA = 'In NA Server' + ONLYNA = 'Only in NA Server' MISC_MAP = { @@ -248,8 +250,10 @@ class MiscModifiers(Enum): MiscModifiers.FARMABLE: ('farmable',), MiscModifiers.REM: ('rem',), MiscModifiers.MP: ('mp',), - MiscModifiers.INJP: ('jp',), - MiscModifiers.INNA: ('na',) + MiscModifiers.INJP: ('injp',), + MiscModifiers.INNA: ('inna',), + MiscModifiers.ONLYJP: ('jp',), + MiscModifiers.ONLYNA: ('na',) } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { From 84632d6c56ae04ff707fedfc1f894f44a853c091 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jan 2021 03:55:01 -0700 Subject: [PATCH 0453/1220] make idna/idjp use id3 (by user pref) (#551) --- padinfo/padinfo.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4b753908..9687c19b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -285,13 +285,19 @@ async def id1(self, ctx, *, query): @checks.bot_has_permissions(embed_links=True) async def idna(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" - await self._do_id(ctx, query, server_filter=ServerFilter.na) + if await self.config.user(ctx.author).beta_id3(): + await self._do_id3(ctx, "inna " + query) + else: + await self._do_id(ctx, query, server_filter=ServerFilter.na) @commands.command() @checks.bot_has_permissions(embed_links=True) async def idjp(self, ctx, *, query: str): """Monster info (limited to JP monsters ONLY)""" - await self._do_id(ctx, query, server_filter=ServerFilter.jp) + if await self.config.user(ctx.author).beta_id3(): + await self._do_id3(ctx, "injp " + query) + else: + await self._do_id(ctx, query, server_filter=ServerFilter.jp) async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): m, err, debug_info = await self.findMonster1(query, server_filter=server_filter) From 2f5747786ab7c5125de964fe51bc53bbb4f825a9 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jan 2021 04:12:17 -0700 Subject: [PATCH 0454/1220] Log misspelled modifiers (#554) --- padinfo/padinfo.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9687c19b..cb7e1efc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1106,6 +1106,10 @@ async def _findMonster3(self, query): query = rmdiacritics(query).lower() mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, DGCOG.index2) + for t in mod_tokens.union(neg_mod_tokens): + if t not in DGCOG.index2.all_modifiers: + self.settings.add_typo_mod(t) + # print(mod_tokens, name_query_tokens) if name_query_tokens: @@ -1230,6 +1234,7 @@ def make_default_settings(self): 'alt_id_optout': [], 'voice_dir_path': '', 'emoji_use': {}, + 'typo_mods': [], } return config @@ -1272,3 +1277,7 @@ def voiceDir(self): def log_emoji(self, emote): self.bot_settings['emoji_use'][emote] = self.bot_settings['emoji_use'].get(emote, 0) + 1 self.save_settings() + + def add_typo_mod(self, typo): + self.bot_settings['typo_mods'].append(typo) + self.save_settings() From 42b3b0a0d2cf59a09c28b6ec449628571ad0365c Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Wed, 20 Jan 2021 18:05:54 -0800 Subject: [PATCH 0455/1220] Moving ^pic to new embed mechanism --- padinfo/common/external_links.py | 39 ++++++++++++++++ padinfo/common/padx.py | 12 ----- padinfo/id_menu.py | 54 +++-------------------- padinfo/padinfo.py | 3 +- padinfo/view/components/monster/header.py | 2 +- padinfo/view/evos.py | 2 +- padinfo/view/id.py | 2 +- padinfo/view/lookup.py | 2 +- padinfo/view/pic.py | 50 +++++++++++++++++++++ 9 files changed, 99 insertions(+), 67 deletions(-) create mode 100644 padinfo/common/external_links.py delete mode 100644 padinfo/common/padx.py create mode 100644 padinfo/view/pic.py diff --git a/padinfo/common/external_links.py b/padinfo/common/external_links.py new file mode 100644 index 00000000..481e25df --- /dev/null +++ b/padinfo/common/external_links.py @@ -0,0 +1,39 @@ +from typing import TYPE_CHECKING + +import tsutils + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + +INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' + +MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' +RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' +VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' +GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' +ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' +ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' + + +def monster_url(m: "MonsterModel"): + return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) + + +def get_pic_url(m: "MonsterModel"): + return RPAD_PIC_TEMPLATE.format(m.monster_id) + + +def monster_video_url(m: "MonsterModel"): + return VIDEO_TEMPLATE.format(m.monster_no_jp) + + +def monster_gif_url(m: "MonsterModel"): + return GIF_TEMPLATE.format(m.monster_no_jp) + + +def monster_orb_skin_url(m: "MonsterModel"): + return ORB_SKIN_TEMPLATE.format(m.orb_skin_id) + + +def monster_orb_skin_cb_url(m: "MonsterModel"): + return ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id) diff --git a/padinfo/common/padx.py b/padinfo/common/padx.py deleted file mode 100644 index ed2357ec..00000000 --- a/padinfo/common/padx.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import TYPE_CHECKING - -import tsutils - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - -INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' - - -def monster_url(m: "MonsterModel"): - return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index e209515d..e7c8a5fd 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -5,22 +5,18 @@ import discord import prettytable from discord import Color -from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain -from discordmenu.embed.menu import EmbedView -from discordmenu.embed.text import Text, BoldText from redbot.core.utils.chat_formatting import box from padinfo.common.emoji_map import awakening_restricted_latent_emoji from padinfo.view.components.monster.header import MonsterHeader -from .common.padx import monster_url -from .leader_skills import createMultiplierText +from .common.external_links import monster_url from .leader_skills import createSingleMultiplierText from .view.components.monster.image import MonsterImage from .view.evos import EvosView from .view.id import IdView from .view.leader_skill import LeaderSkillView from .view.lookup import LookupView +from .view.pic import PicsView if TYPE_CHECKING: from dadguide.database_context import DbContext @@ -28,22 +24,11 @@ INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' -MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' -RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' -VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' -GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' - YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' -def get_pic_url(m: "MonsterModel"): - return RPAD_PIC_TEMPLATE.format(m.monster_id) - - class IdMenu: def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = None): self.ctx = ctx @@ -164,38 +149,9 @@ async def make_skillups_embed(self, m: "MonsterModel"): return embed - async def make_picture_embed(self, m: "MonsterModel", animated=False): - embed = await self.make_base_embed(m) - url = get_pic_url(m) - embed.set_image(url=url) - # Clear the thumbnail, don't need it on pic - embed.set_thumbnail(url='') - extra_links = [] - if animated: - extra_links.append('Animation: {} -- {}'.format(self.monster_video_url(m), self.monster_gif_url(m))) - if m.orb_skin_id is not None: - extra_links.append( - 'Orb Skin: {} -- {}'.format(self.monster_orb_skin_url(m), self.monster_orb_skin_cb_url(m))) - if len(extra_links) > 0: - embed.add_field(name='Extra Links', value='\n'.join(extra_links)) - - return embed - - @staticmethod - def monster_video_url(m: "MonsterModel", link_text='(MP4)'): - return '[{}]({})'.format(link_text, VIDEO_TEMPLATE.format(m.monster_no_jp)) - - @staticmethod - def monster_gif_url(m: "MonsterModel", link_text='(GIF)'): - return '[{}]({})'.format(link_text, GIF_TEMPLATE.format(m.monster_no_jp)) - - @staticmethod - def monster_orb_skin_url(m: "MonsterModel", link_text='Regular'): - return '[{}]({})'.format(link_text, ORB_SKIN_TEMPLATE.format(m.orb_skin_id)) - - @staticmethod - def monster_orb_skin_cb_url(m: "MonsterModel", link_text='Color Blind'): - return '[{}]({})'.format(link_text, ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id)) + async def make_picture_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return PicsView.embed(m, color).to_embed() async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index cb7e1efc..2d07d423 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -477,8 +477,7 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): id_embed = await menu.make_id_embed_v2(m) evo_embed = await menu.make_evo_embed_v2(m) mats_embed = await menu.make_evo_mats_embed(m) - animated = m.has_animation - pic_embed = await menu.make_picture_embed(m, animated=animated) + pic_embed = await menu.make_picture_embed(m) other_info_embed = await menu.make_otherinfo_embed(m) emoji_to_embed = OrderedDict() diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 39ffc095..e2bbc8bc 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -2,7 +2,7 @@ from discordmenu.embed.text import LinkedText, Text -from padinfo.common.padx import monster_url +from padinfo.common.external_links import monster_url if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 79171f5f..11b3bd40 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -7,7 +7,7 @@ from discordmenu.emoji_cache import emoji_cache from padinfo.common.emoji_map import get_emoji, format_emoji -from padinfo.common.padx import monster_url +from padinfo.common.external_links import monster_url from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 772c9786..d4bde299 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -6,7 +6,7 @@ from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from padinfo.common.emoji_map import get_emoji, format_emoji, AWAKENING_ID_TO_EMOJI_NAME_MAP, emoji_markdown -from padinfo.common.padx import monster_url +from padinfo.common.external_links import monster_url from padinfo.leader_skills import createMultiplierText from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index abb01cda..c3bcdf50 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -3,7 +3,7 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.menu import EmbedView -from padinfo.common.padx import monster_url +from padinfo.common.external_links import monster_url from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py new file mode 100644 index 00000000..8cc7e2a7 --- /dev/null +++ b/padinfo/view/pic.py @@ -0,0 +1,50 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage +from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import LinkedText, Text + +from padinfo.common.external_links import monster_url, monster_video_url, monster_gif_url, monster_orb_skin_url, \ + monster_orb_skin_cb_url, get_pic_url +from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.monster.header import MonsterHeader + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class PicsView: + @staticmethod + def embed(m: "MonsterModel", color): + url = get_pic_url(m) + animated = m.has_animation + fields = [EmbedField( + 'Extra Links', + Box( + Box( + Text('Animation:'), + LinkedText('(MP4)', monster_video_url(m)), + Text('|'), + LinkedText('(GIF)', monster_gif_url(m)), + delimiter=' ' + ) if animated else None, + Box( + Text('Orb Skin:'), + LinkedText('Regular', monster_orb_skin_url(m)), + Text('|'), + LinkedText('Color Blind', monster_orb_skin_cb_url(m)), + delimiter=' ' + ) if m.orb_skin_id else None, + ) + )] + + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + url=monster_url(m)), + embed_footer=pad_info_footer(), + embed_fields=fields, + embed_body_image=EmbedBodyImage(url), + ) From 2e311fbd4b44d0d807f7e7e57d716df065b8079c Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jan 2021 20:37:39 -0700 Subject: [PATCH 0456/1220] Improve error message when loading crud before gadmin (#555) --- crud/crud.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 23f203cf..7e1b78dc 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -20,7 +20,6 @@ "series_type": None, } - SERIES_TYPES = [ "regular", "event", @@ -29,6 +28,7 @@ "collab" ] + async def check_crud_channel(ctx): chan = await ctx.bot.get_cog("Crud").config.chan() return chan is None or chan == ctx.channel.id or ctx.author.id in ctx.bot.owner_ids @@ -47,7 +47,8 @@ def __init__(self, bot, *args, **kwargs): if GADMIN_COG: GADMIN_COG.register_perm("crud") else: - raise errors.CogLoadError("Global Administration cog must be loaded.") + raise errors.CogLoadError("Global Administration cog must be loaded. Make sure it's " + "installed from misc-cogs, and load it via `^load globaladmin`") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" From 91a03d489936fe3f0a1129856e5026e48d05b869 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jan 2021 21:25:05 -0700 Subject: [PATCH 0457/1220] Bump TSUtils version in relevant cogs (#556) --- padglobal/info.json | 2 +- padinfo/info.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/padglobal/info.json b/padglobal/info.json index 9d9bea25..5cd0a349 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -20,7 +20,7 @@ "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils", + "tsutils>=3.0.2", "aiohttp", "prettytable", "pytz" diff --git a/padinfo/info.json b/padinfo/info.json index 05468be5..7f1250fb 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.0.1", + "tsutils>=3.0.2", "prettytable", "python-levenshtein", "discord-menu>=0.5.0" From 097b89f072fe69c3df26e90d07a07cd54168b92d Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jan 2021 22:09:39 -0700 Subject: [PATCH 0458/1220] Fix ^guide (#558) --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index b2e419f4..3b7ec57e 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1141,7 +1141,7 @@ async def get_guide_text(self, term: str): return None, None, 'No dungeon or monster matched that query' name = nm.group_computed_treename.title() - definition = self.settings.leaderGuide().get(str(nm.base_monster_no), None) + definition = self.settings.leaderGuide().get(nm.base_monster_no, None) if definition is None: return None, None, 'A monster matched that query but has no guide' From 7ab4e250914dd521211d8a698523aa96ee2e4027 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Wed, 20 Jan 2021 23:09:45 -0800 Subject: [PATCH 0459/1220] Migrating ^pantheon to new embed mechanism --- padinfo/common/external_links.py | 27 --------------- padinfo/id_menu.py | 13 +++---- padinfo/view/components/monster/emoji.py | 15 ++++++++ padinfo/view/components/monster/header.py | 13 +++++++ padinfo/view/components/monster/image.py | 29 ++++++++++++++-- padinfo/view/evos.py | 42 ++++++----------------- padinfo/view/pantheon.py | 35 +++++++++++++++++++ padinfo/view/pic.py | 14 ++++---- 8 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 padinfo/view/components/monster/emoji.py create mode 100644 padinfo/view/pantheon.py diff --git a/padinfo/common/external_links.py b/padinfo/common/external_links.py index 481e25df..ed2357ec 100644 --- a/padinfo/common/external_links.py +++ b/padinfo/common/external_links.py @@ -7,33 +7,6 @@ INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' -MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' -RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' -VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' -GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' - def monster_url(m: "MonsterModel"): return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) - - -def get_pic_url(m: "MonsterModel"): - return RPAD_PIC_TEMPLATE.format(m.monster_id) - - -def monster_video_url(m: "MonsterModel"): - return VIDEO_TEMPLATE.format(m.monster_no_jp) - - -def monster_gif_url(m: "MonsterModel"): - return GIF_TEMPLATE.format(m.monster_no_jp) - - -def monster_orb_skin_url(m: "MonsterModel"): - return ORB_SKIN_TEMPLATE.format(m.orb_skin_id) - - -def monster_orb_skin_cb_url(m: "MonsterModel"): - return ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index e7c8a5fd..b2f7f955 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -16,6 +16,7 @@ from .view.id import IdView from .view.leader_skill import LeaderSkillView from .view.lookup import LookupView +from .view.pantheon import PantheonView from .view.pic import PicsView if TYPE_CHECKING: @@ -111,15 +112,9 @@ async def make_pantheon_embed(self, m: "MonsterModel"): if len(pantheon_list) == 0 or len(pantheon_list) > 20: return None - embed = await self.make_base_embed(m) - - field_name = 'Pantheon: ' + self.db_context.graph.get_monster(m.monster_no).series.name - field_data = '' - for monster in sorted(pantheon_list, key=lambda x: x.monster_no_na): - field_data += '\n' + MonsterHeader.short(monster, link=True) - embed.add_field(name=field_name, value=field_data) - - return embed + series_name = self.db_context.graph.get_monster(m.monster_no).series.name + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return PantheonView.embed(m, color, pantheon_list, series_name).to_embed() async def make_skillups_embed(self, m: "MonsterModel"): if m.active_skill is None: diff --git a/padinfo/view/components/monster/emoji.py b/padinfo/view/components/monster/emoji.py new file mode 100644 index 00000000..2aef29a8 --- /dev/null +++ b/padinfo/view/components/monster/emoji.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING + +from padinfo.common.emoji_map import get_emoji + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class MonsterEmoji: + @staticmethod + def attribute(monster: "MonsterModel"): + attr1 = monster.attr1.name.lower() + attr2 = monster.attr2.name.lower() + emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) + return get_emoji(emoji) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index e2bbc8bc..e12de9f9 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -1,8 +1,11 @@ from typing import TYPE_CHECKING +from discordmenu.embed.base import Box from discordmenu.embed.text import LinkedText, Text +from padinfo.common.emoji_map import format_emoji from padinfo.common.external_links import monster_url +from padinfo.view.components.monster.emoji import MonsterEmoji if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -41,3 +44,13 @@ def name(m: "MonsterModel", link=False, show_jp=False): def long_v2(m: "MonsterModel", link=False): msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, MonsterHeader.ja_suffix(m)) return LinkedText(msg, monster_url(m)) if link else Text(msg) + + @staticmethod + def short_with_emoji(m: "MonsterModel", link=True): + msg = f"{m.monster_no_na} - {m.name_en}" + return Box( + Text(format_emoji(MonsterEmoji.attribute(m))), + LinkedText(msg, monster_url(m)) if link else Text(msg), + Text(MonsterHeader.ja_suffix(m, False)), + delimiter=' ' + ) \ No newline at end of file diff --git a/padinfo/view/components/monster/image.py b/padinfo/view/components/monster/image.py index 4e901d06..03a548cf 100644 --- a/padinfo/view/components/monster/image.py +++ b/padinfo/view/components/monster/image.py @@ -6,13 +6,38 @@ from dadguide.models.monster_model import MonsterModel MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' -RPAD_PORTRAIT_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' +ICON_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' +RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' +VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' +GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' +ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' +ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' class MonsterImage: @staticmethod def icon(m: "MonsterModel"): - return RPAD_PORTRAIT_TEMPLATE.format(m.monster_id) + return ICON_TEMPLATE.format(m.monster_id) + + @staticmethod + def picture(m: "MonsterModel"): + return RPAD_PIC_TEMPLATE.format(m.monster_id) + + @staticmethod + def video(m: "MonsterModel"): + return VIDEO_TEMPLATE.format(m.monster_no_jp) + + @staticmethod + def gif(m: "MonsterModel"): + return GIF_TEMPLATE.format(m.monster_no_jp) + + @staticmethod + def orb_skin(m: "MonsterModel"): + return ORB_SKIN_TEMPLATE.format(m.orb_skin_id) + + @staticmethod + def orb_skin_colorblind(m: "MonsterModel"): + return ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id) def monster_thumbnail(m: "MonsterModel"): diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 11b3bd40..7222a519 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -1,12 +1,9 @@ from typing import TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedThumbnail, EmbedFooter, EmbedMain, EmbedField +from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.menu import EmbedView -from discordmenu.embed.text import Text, LinkedText -from discordmenu.emoji_cache import emoji_cache -from padinfo.common.emoji_map import get_emoji, format_emoji from padinfo.common.external_links import monster_url from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader @@ -16,45 +13,28 @@ from dadguide.models.monster_model import MonsterModel -class EvosView: - @staticmethod - def _get_monster_attr_emoji(monster: "MonsterModel"): - attr1 = monster.attr1.name.lower() - attr2 = monster.attr2.name.lower() - emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) - return get_emoji(emoji) - - @staticmethod - def _evo_line(m: "MonsterModel", link=True): - msg = f"{m.monster_no_na} - {m.name_en}" - return Box( - Text(format_emoji(EvosView._get_monster_attr_emoji(m))), - LinkedText(msg, monster_url(m)) if link else Text(msg), - Text(MonsterHeader.ja_suffix(m, False)), - delimiter=' ' - ) +def _evo_lines(monsters, current_monster): + if not len(monsters): + return [] + return [ + MonsterHeader.short_with_emoji(ae, link=ae.monster_id != current_monster.monster_id) + for ae in sorted(monsters, key=lambda x: int(x.monster_id)) + ] - @staticmethod - def _evo_lines(monsters, current_monster): - if not len(monsters): - return [] - return [ - EvosView._evo_line(ae, link=ae.monster_id != current_monster.monster_id) - for ae in sorted(monsters, key=lambda x: int(x.monster_id)) - ] +class EvosView: @staticmethod def embed(m: "MonsterModel", alt_versions, gem_versions, color): fields = [ EmbedField( ("{} evolution" if len(alt_versions) == 1 else "{} evolutions").format(len(alt_versions)), - Box(*EvosView._evo_lines(alt_versions, m)))] + Box(*_evo_lines(alt_versions, m)))] if gem_versions: fields.append( EmbedField( ("{} evolve gem" if len(gem_versions) == 1 else "{} evolve gems").format(len(gem_versions)), - Box(*EvosView._evo_lines(gem_versions, m)))) + Box(*_evo_lines(gem_versions, m)))) return EmbedView( EmbedMain( diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py new file mode 100644 index 00000000..56390e9c --- /dev/null +++ b/padinfo/view/pantheon.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail +from discordmenu.embed.menu import EmbedView + +from padinfo.common.external_links import monster_url +from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class PantheonView: + @staticmethod + def embed(m: "MonsterModel", color, pantheon_list, series_name): + fields = [EmbedField( + 'Pantheon: {}'.format(series_name), + Box( + *[MonsterHeader.short_with_emoji(m) + for m in sorted(pantheon_list, key=lambda x: x.monster_no_na)] + ) + )] + + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + url=monster_url(m)), + embed_footer=pad_info_footer(), + embed_fields=fields, + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), + ) diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 8cc7e2a7..6904d752 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -5,10 +5,10 @@ from discordmenu.embed.menu import EmbedView from discordmenu.embed.text import LinkedText, Text -from padinfo.common.external_links import monster_url, monster_video_url, monster_gif_url, monster_orb_skin_url, \ - monster_orb_skin_cb_url, get_pic_url +from padinfo.common.external_links import monster_url from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -17,23 +17,23 @@ class PicsView: @staticmethod def embed(m: "MonsterModel", color): - url = get_pic_url(m) + url = MonsterImage.picture(m) animated = m.has_animation fields = [EmbedField( 'Extra Links', Box( Box( Text('Animation:'), - LinkedText('(MP4)', monster_video_url(m)), + LinkedText('(MP4)', MonsterImage.video(m)), Text('|'), - LinkedText('(GIF)', monster_gif_url(m)), + LinkedText('(GIF)', MonsterImage.gif(m)), delimiter=' ' ) if animated else None, Box( Text('Orb Skin:'), - LinkedText('Regular', monster_orb_skin_url(m)), + LinkedText('Regular', MonsterImage.orb_skin(m)), Text('|'), - LinkedText('Color Blind', monster_orb_skin_cb_url(m)), + LinkedText('Color Blind', MonsterImage.orb_skin_colorblind(m)), delimiter=' ' ) if m.orb_skin_id else None, ) From 19569090b257029b2d1b7716f1d65eb2ff57197e Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Wed, 20 Jan 2021 23:40:34 -0800 Subject: [PATCH 0460/1220] Migrating ^links to new embed --- padinfo/common/external_links.py | 18 ++++++++++++- padinfo/id_menu.py | 15 ++++------- padinfo/view/components/monster/header.py | 12 ++++----- padinfo/view/evos.py | 4 +-- padinfo/view/id.py | 6 ++--- padinfo/view/links.py | 31 +++++++++++++++++++++++ padinfo/view/lookup.py | 4 +-- padinfo/view/pantheon.py | 4 +-- padinfo/view/pic.py | 4 +-- 9 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 padinfo/view/links.py diff --git a/padinfo/common/external_links.py b/padinfo/common/external_links.py index ed2357ec..ad4254e6 100644 --- a/padinfo/common/external_links.py +++ b/padinfo/common/external_links.py @@ -1,3 +1,4 @@ +import urllib.parse from typing import TYPE_CHECKING import tsutils @@ -6,7 +7,22 @@ from dadguide.models.monster_model import MonsterModel INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' +YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' +SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' +ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' -def monster_url(m: "MonsterModel"): +def puzzledragonx(m: "MonsterModel"): return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) + + +def youtube_search(m: "MonsterModel"): + return YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)) + + +def skyozora(m: "MonsterModel"): + return SKYOZORA_TEMPLATE.format(m.monster_no_jp) + + +def ilmina(m: "MonsterModel"): + return ILMINA_TEMPLATE.format(m.monster_no_jp) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index b2f7f955..2a60c4a6 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -9,12 +9,13 @@ from padinfo.common.emoji_map import awakening_restricted_latent_emoji from padinfo.view.components.monster.header import MonsterHeader -from .common.external_links import monster_url +from .common.external_links import puzzledragonx from .leader_skills import createSingleMultiplierText from .view.components.monster.image import MonsterImage from .view.evos import EvosView from .view.id import IdView from .view.leader_skill import LeaderSkillView +from .view.links import LinksView from .view.lookup import LookupView from .view.pantheon import PantheonView from .view.pic import PicsView @@ -41,7 +42,7 @@ async def make_base_embed(self, m: "MonsterModel"): embed = await self.make_custom_embed() embed.set_thumbnail(url=MonsterImage.icon(m)) embed.title = header - embed.url = monster_url(m) + embed.url = puzzledragonx(m) embed.set_footer(text='Requester may click the reactions below to switch tabs') return embed @@ -245,14 +246,8 @@ async def make_otherinfo_embed(self, m: "MonsterModel"): return embed async def make_links_embed(self, m: "MonsterModel"): - embed = await self.make_base_embed(m) - embed.description = "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - embed.set_footer(text='') - return embed + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return LinksView.embed(m, color).to_embed() async def make_lssingle_embed(self, m: "MonsterModel"): multiplier_text = createSingleMultiplierText(m.leader_skill) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index e12de9f9..4ef9f514 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -4,7 +4,7 @@ from discordmenu.embed.text import LinkedText, Text from padinfo.common.emoji_map import format_emoji -from padinfo.common.external_links import monster_url +from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.emoji import MonsterEmoji if TYPE_CHECKING: @@ -25,12 +25,12 @@ def ja_suffix(m: "MonsterModel", subname_on_override=True): def short(m: "MonsterModel", link=False): type_emojis = '' msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) - return '[{}]({})'.format(msg, monster_url(m)) if link else msg + return '[{}]({})'.format(msg, puzzledragonx(m)) if link else msg @staticmethod def long(m: "MonsterModel", link=False): msg = MonsterHeader.short(m) + MonsterHeader.ja_suffix(m) - return '[{}]({})'.format(msg, monster_url(m)) if link else msg + return '[{}]({})'.format(msg, puzzledragonx(m)) if link else msg @staticmethod def name(m: "MonsterModel", link=False, show_jp=False): @@ -38,19 +38,19 @@ def name(m: "MonsterModel", link=False, show_jp=False): m.monster_no_na, m.name_en, MonsterHeader.ja_suffix(m) if show_jp else '') - return LinkedText(msg, monster_url(m)) if link else Text(msg) + return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) @staticmethod def long_v2(m: "MonsterModel", link=False): msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, MonsterHeader.ja_suffix(m)) - return LinkedText(msg, monster_url(m)) if link else Text(msg) + return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) @staticmethod def short_with_emoji(m: "MonsterModel", link=True): msg = f"{m.monster_no_na} - {m.name_en}" return Box( Text(format_emoji(MonsterEmoji.attribute(m))), - LinkedText(msg, monster_url(m)) if link else Text(msg), + LinkedText(msg, puzzledragonx(m)) if link else Text(msg), Text(MonsterHeader.ja_suffix(m, False)), delimiter=' ' ) \ No newline at end of file diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 7222a519..e4a80f70 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.menu import EmbedView -from padinfo.common.external_links import monster_url +from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -40,7 +40,7 @@ def embed(m: "MonsterModel", alt_versions, gem_versions, color): EmbedMain( color=color, title=MonsterHeader.long_v2(m).to_markdown(), - url=monster_url(m)), + url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=pad_info_footer(), embed_fields=fields) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index d4bde299..2dab1ed7 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -6,7 +6,7 @@ from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from padinfo.common.emoji_map import get_emoji, format_emoji, AWAKENING_ID_TO_EMOJI_NAME_MAP, emoji_markdown -from padinfo.common.external_links import monster_url +from padinfo.common.external_links import puzzledragonx from padinfo.leader_skills import createMultiplierText from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader @@ -160,7 +160,7 @@ def embed(m: "MonsterModel", color, is_transform_base, true_evo_type_raw, acquir EmbedField( "Alternate Evos", HighlightableLinks( - links=[LinkedText(str(m.monster_id), monster_url(m)) for m in alt_monsters], + links=[LinkedText(str(m.monster_id), puzzledragonx(m)) for m in alt_monsters], highlighted=next(i for i, mon in enumerate(alt_monsters) if m.monster_id == mon.monster_id) ) ) @@ -170,7 +170,7 @@ def embed(m: "MonsterModel", color, is_transform_base, true_evo_type_raw, acquir EmbedMain( color=color, title=MonsterHeader.long_v2(m).to_markdown(), - url=monster_url(m)), + url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=pad_info_footer(), embed_fields=fields) diff --git a/padinfo/view/links.py b/padinfo/view/links.py new file mode 100644 index 00000000..5a418006 --- /dev/null +++ b/padinfo/view/links.py @@ -0,0 +1,31 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedThumbnail, EmbedMain +from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import LinkedText + +from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class LinksView: + @staticmethod + def embed(m: "MonsterModel", color): + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + description=Box( + LinkedText('YouTube', youtube_search(m)), + LinkedText('Skyozora', skyozora(m)), + LinkedText('PDX', puzzledragonx(m)), + LinkedText('Ilmina', ilmina(m)), + delimiter=' | ' + ), + url=puzzledragonx(m)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m))) diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index c3bcdf50..0449aeb9 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -3,7 +3,7 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.menu import EmbedView -from padinfo.common.external_links import monster_url +from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: @@ -17,4 +17,4 @@ def embed(m: "MonsterModel", color): EmbedMain( color=color, title=MonsterHeader.long_v2(m).to_markdown(), - url=monster_url(m))) + url=puzzledragonx(m))) diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 56390e9c..40fbeb05 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.menu import EmbedView -from padinfo.common.external_links import monster_url +from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -28,7 +28,7 @@ def embed(m: "MonsterModel", color, pantheon_list, series_name): EmbedMain( color=color, title=MonsterHeader.long_v2(m).to_markdown(), - url=monster_url(m)), + url=puzzledragonx(m)), embed_footer=pad_info_footer(), embed_fields=fields, embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 6904d752..f3f8e2c5 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -5,7 +5,7 @@ from discordmenu.embed.menu import EmbedView from discordmenu.embed.text import LinkedText, Text -from padinfo.common.external_links import monster_url +from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -43,7 +43,7 @@ def embed(m: "MonsterModel", color): EmbedMain( color=color, title=MonsterHeader.long_v2(m).to_markdown(), - url=monster_url(m)), + url=puzzledragonx(m)), embed_footer=pad_info_footer(), embed_fields=fields, embed_body_image=EmbedBodyImage(url), From 618549d8daced9e2a59c3ce734c782923dc59028 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 21 Jan 2021 00:40:56 -0700 Subject: [PATCH 0461/1220] Move arrows to front of idmenu (#559) --- padinfo/info.json | 2 +- padinfo/padinfo.py | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/padinfo/info.json b/padinfo/info.json index 7f1250fb..72a567ac 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.0.2", + "tsutils>=3.1.0", "prettytable", "python-levenshtein", "discord-menu>=0.5.0" diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2d07d423..956e26f8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -481,6 +481,19 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): other_info_embed = await menu.make_otherinfo_embed(m) emoji_to_embed = OrderedDict() + + # it's impossible for the previous/next ones to be accessed because + # IdEmojiUpdater won't allow it, however they have to be defined + # so that the buttons display in the first place + + if len(scroll) > 1 and menu_type != 1: + emoji_to_embed[self.first_monster_emoji] = None + if len(scroll) != 1: + emoji_to_embed[self.previous_monster_emoji] = None + emoji_to_embed[self.next_monster_emoji] = None + if len(scroll) > 1 and menu_type != 1: + emoji_to_embed[self.last_monster_emoji] = None + emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed emoji_to_embed[self.mats_emoji] = mats_embed @@ -495,18 +508,6 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): emoji_to_embed[self.other_info_emoji] = other_info_embed - # it's impossible for the previous/next ones to be accessed because - # IdEmojiUpdater won't allow it, however they have to be defined - # so that the buttons display in the first place - - if len(scroll) > 1 and menu_type != 1: - emoji_to_embed[self.first_monster_emoji] = None - if len(scroll) != 1: - emoji_to_embed[self.previous_monster_emoji] = None - emoji_to_embed[self.next_monster_emoji] = None - if len(scroll) > 1 and menu_type != 1: - emoji_to_embed[self.last_monster_emoji] = None - # remove emoji needs to be last emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message return emoji_to_embed From 351753b1726585e1726e1e02ad66bb2aa28ec7e0 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Thu, 21 Jan 2021 00:03:00 -0800 Subject: [PATCH 0462/1220] Refactoring padinfo monster_acquisition string --- dadguide/monster_graph.py | 21 +++++++++++++++ padinfo/id_menu.py | 32 +---------------------- padinfo/view/components/monster/header.py | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 1c0a2389..104665ce 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -507,6 +507,27 @@ def monster_is_rem_evo_by_id(self, monster_id): def monster_is_rem_evo(self, monster: MonsterModel): return self.monster_is_rem_evo_by_id(monster.monster_no) + + def monster_acquisition(self, monster: MonsterModel): + acquire_text = None + if self.monster_is_farmable(monster) and not self.monster_is_mp_evo(monster): + # Some MP shop monsters 'drop' in PADR + acquire_text = 'Farmable' + elif self.monster_is_farmable_evo(monster) and not self.monster_is_mp_evo(monster): + acquire_text = 'Farmable Evo' + elif monster.in_pem: + acquire_text = 'In PEM' + elif self.monster_is_pem_evo(monster): + acquire_text = 'PEM Evo' + elif monster.in_rem: + acquire_text = 'In REM' + elif self.monster_is_rem_evo(monster): + acquire_text = 'REM Evo' + elif monster.in_mpshop: + acquire_text = 'MP Shop' + elif self.monster_is_mp_evo(monster): + acquire_text = 'MP Shop Evo' + return acquire_text def numeric_next_monster_id_by_id(self, monster_id: int) -> Optional[int]: next_monster = None diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 2a60c4a6..4de6101e 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -7,7 +7,6 @@ from discord import Color from redbot.core.utils.chat_formatting import box -from padinfo.common.emoji_map import awakening_restricted_latent_emoji from padinfo.view.components.monster.header import MonsterHeader from .common.external_links import puzzledragonx from .leader_skills import createSingleMultiplierText @@ -59,7 +58,7 @@ async def make_id_embed_v2(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) is_transform_base = self.db_context.graph.monster_is_transform_base(m) true_evo_type_raw = self.db_context.graph.true_evo_type_by_monster(m).value - acquire_raw = self._monster_acquisition_string(m) + acquire_raw = self.db_context.graph.monster_acquisition(m) base_rarity = self.db_context.graph.get_base_monster_by_id(m.monster_no).rarity alt_monsters = sorted({*self.db_context.graph.get_alt_monsters_by_id(m.monster_no)}, key=lambda x: x.monster_id) @@ -157,35 +156,6 @@ async def make_lookup_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return LookupView.embed(m, color).to_embed() - def _monster_acquisition_string(self, m: "MonsterModel"): - acquire_text = None - if self.db_context.graph.monster_is_farmable(m) and not self.db_context.graph.monster_is_mp_evo(m): - # Some MP shop monsters 'drop' in PADR - acquire_text = 'Farmable' - elif self.db_context.graph.monster_is_farmable_evo(m) and not self.db_context.graph.monster_is_mp_evo(m): - acquire_text = 'Farmable Evo' - elif m.in_pem: - acquire_text = 'In PEM' - elif self.db_context.graph.monster_is_pem_evo(m): - acquire_text = 'PEM Evo' - elif m.in_rem: - acquire_text = 'In REM' - elif self.db_context.graph.monster_is_rem_evo(m): - acquire_text = 'REM Evo' - elif m.in_mpshop: - acquire_text = 'MP Shop' - elif self.db_context.graph.monster_is_mp_evo(m): - acquire_text = 'MP Shop Evo' - return acquire_text - - def get_awakening_restricted_latents_text(self, m: "MonsterModel"): - """Not currently in use, but potentially could be in the future - if more ARLatents are added later - """ - if not m.awakening_restricted_latents: - return '' - return ' ' + ' '.join([awakening_restricted_latent_emoji(x) for x in m.awakening_restricted_latents]) - async def make_otherinfo_embed(self, m: "MonsterModel"): embed = await self.make_base_embed(m) # Clear the thumbnail, takes up too much space diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 4ef9f514..2ef9f43c 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -53,4 +53,4 @@ def short_with_emoji(m: "MonsterModel", link=True): LinkedText(msg, puzzledragonx(m)) if link else Text(msg), Text(MonsterHeader.ja_suffix(m, False)), delimiter=' ' - ) \ No newline at end of file + ) From 5af1f2fbe5cefb2fe6012213d939b76eb5a65772 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Thu, 21 Jan 2021 17:49:39 -0800 Subject: [PATCH 0463/1220] Moving code around padinfo --- padinfo/core/__init__.py | 0 padinfo/{ => core}/button_info.py | 0 padinfo/{ => core}/find_monster.py | 0 padinfo/{ => core}/leader_skills.py | 0 padinfo/id_menu.py | 20 ++++++++++---------- padinfo/padinfo.py | 8 ++++---- padinfo/view/id.py | 2 +- padinfo/view/leader_skill.py | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 padinfo/core/__init__.py rename padinfo/{ => core}/button_info.py (100%) rename padinfo/{ => core}/find_monster.py (100%) rename padinfo/{ => core}/leader_skills.py (100%) diff --git a/padinfo/core/__init__.py b/padinfo/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/button_info.py b/padinfo/core/button_info.py similarity index 100% rename from padinfo/button_info.py rename to padinfo/core/button_info.py diff --git a/padinfo/find_monster.py b/padinfo/core/find_monster.py similarity index 100% rename from padinfo/find_monster.py rename to padinfo/core/find_monster.py diff --git a/padinfo/leader_skills.py b/padinfo/core/leader_skills.py similarity index 100% rename from padinfo/leader_skills.py rename to padinfo/core/leader_skills.py diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 4de6101e..2b55fcae 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -8,16 +8,16 @@ from redbot.core.utils.chat_formatting import box from padinfo.view.components.monster.header import MonsterHeader -from .common.external_links import puzzledragonx -from .leader_skills import createSingleMultiplierText -from .view.components.monster.image import MonsterImage -from .view.evos import EvosView -from .view.id import IdView -from .view.leader_skill import LeaderSkillView -from .view.links import LinksView -from .view.lookup import LookupView -from .view.pantheon import PantheonView -from .view.pic import PicsView +from padinfo.common.external_links import puzzledragonx +from padinfo.lib.leader_skills import createSingleMultiplierText +from padinfo.view.components.monster.image import MonsterImage +from padinfo.view.evos import EvosView +from padinfo.view.id import IdView +from padinfo.view.leader_skill import LeaderSkillView +from padinfo.view.links import LinksView +from padinfo.view.lookup import LookupView +from padinfo.view.pantheon import PantheonView +from padinfo.view.pic import PicsView if TYPE_CHECKING: from dadguide.database_context import DbContext diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 956e26f8..90eecc49 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -21,10 +21,10 @@ from tabulate import tabulate from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor -from .button_info import button_info -from .find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority -from .id_menu import IdMenu -from .view.components.monster.header import MonsterHeader +from padinfo.lib.button_info import button_info +from padinfo.lib.find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority +from padinfo.id_menu import IdMenu +from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dadguide.database_context import DbContext diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 2dab1ed7..49017bf9 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -7,7 +7,7 @@ from padinfo.common.emoji_map import get_emoji, format_emoji, AWAKENING_ID_TO_EMOJI_NAME_MAP, emoji_markdown from padinfo.common.external_links import puzzledragonx -from padinfo.leader_skills import createMultiplierText +from padinfo.lib.leader_skills import createMultiplierText from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 65627035..af786d15 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -5,7 +5,7 @@ from discordmenu.embed.menu import EmbedView from discordmenu.embed.text import BoldText, Text -from padinfo.leader_skills import createMultiplierText +from padinfo.lib.leader_skills import createMultiplierText from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: From 51549d31ad30ec76ef1a5b30a7b8883586f4cf79 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Thu, 21 Jan 2021 17:51:44 -0800 Subject: [PATCH 0464/1220] Fixing lib -> core --- padinfo/id_menu.py | 2 +- padinfo/padinfo.py | 4 ++-- padinfo/view/id.py | 2 +- padinfo/view/leader_skill.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 2b55fcae..6471297d 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -9,7 +9,7 @@ from padinfo.view.components.monster.header import MonsterHeader from padinfo.common.external_links import puzzledragonx -from padinfo.lib.leader_skills import createSingleMultiplierText +from padinfo.core.leader_skills import createSingleMultiplierText from padinfo.view.components.monster.image import MonsterImage from padinfo.view.evos import EvosView from padinfo.view.id import IdView diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 90eecc49..5da21630 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -21,8 +21,8 @@ from tabulate import tabulate from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor -from padinfo.lib.button_info import button_info -from padinfo.lib.find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority +from padinfo.core.button_info import button_info +from padinfo.core.find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority from padinfo.id_menu import IdMenu from padinfo.view.components.monster.header import MonsterHeader diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 49017bf9..19c8c185 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -7,7 +7,7 @@ from padinfo.common.emoji_map import get_emoji, format_emoji, AWAKENING_ID_TO_EMOJI_NAME_MAP, emoji_markdown from padinfo.common.external_links import puzzledragonx -from padinfo.lib.leader_skills import createMultiplierText +from padinfo.core.leader_skills import createMultiplierText from padinfo.view.components.base import pad_info_footer from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index af786d15..993d9195 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -5,7 +5,7 @@ from discordmenu.embed.menu import EmbedView from discordmenu.embed.text import BoldText, Text -from padinfo.lib.leader_skills import createMultiplierText +from padinfo.core.leader_skills import createMultiplierText from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: From 482676f79be1c58940dbe92c012ffe234e88cb33 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 21 Jan 2021 23:26:28 -0700 Subject: [PATCH 0465/1220] Make prefix matching word-length independent (#568) (remember, prefix != modifier!!) --- padinfo/core/find_monster.py | 4 ++-- padinfo/padinfo.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 49251371..95d62c12 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -20,8 +20,8 @@ def calc_ratio(s1, s2): def calc_ratio_prefix(token, full_word): if full_word == token: return 1 - elif full_word.startswith(token): - return 1 - len(full_word) / 1000 + elif len(token) >= 3 and full_word.startswith(token): + return .995 return jaro_winkler(token, full_word, .05) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5da21630..f34977ca 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1106,6 +1106,8 @@ async def _findMonster3(self, query): query = rmdiacritics(query).lower() mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, DGCOG.index2) + name_query_tokens.difference_update({'|'}) + for t in mod_tokens.union(neg_mod_tokens): if t not in DGCOG.index2.all_modifiers: self.settings.add_typo_mod(t) From d74091a029c2eecf7a0d68b3d91364012a092b58 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 21 Jan 2021 23:40:23 -0700 Subject: [PATCH 0466/1220] Add lowpriority as a series type (#569) Fixes chibi urd being matched over gh chibi urd --- crud/crud.py | 3 ++- padinfo/core/find_monster.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crud/crud.py b/crud/crud.py index 7e1b78dc..c3e10ae9 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -25,7 +25,8 @@ "event", "seasonal", "ghcollab", - "collab" + "collab", + "lowpriority", ] diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 95d62c12..d1c02be8 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -9,6 +9,7 @@ "seasonal": 3, "ghcollab": 2, "collab": 1, + "lowpriority": 0, None: 0 } From 90b1c2c116e6d54bed1e7118f5a109ad67baa02e Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 03:21:46 -0700 Subject: [PATCH 0467/1220] Create EvoMats embed via new syntax (#571) --- dadguide/monster_graph.py | 14 ++++++---- padinfo/id_menu.py | 57 +++++++-------------------------------- padinfo/padinfo.py | 21 ++------------- padinfo/view/materials.py | 54 +++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 71 deletions(-) create mode 100644 padinfo/view/materials.py diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 104665ce..3230b2cc 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,6 +1,6 @@ import json from collections import defaultdict -from typing import Optional +from typing import Optional, List import networkx @@ -443,13 +443,13 @@ def get_next_transform_by_monster_id(self, monster_id): def get_next_transform_by_monster(self, monster: MonsterModel): return self.get_next_evolutions_by_monster_id(monster.monster_no) - def evo_mats_by_monster_id(self, monster_id: int) -> list: + def evo_mats_by_monster_id(self, monster_id: int) -> List[MonsterModel]: evo = self.get_evo_by_monster_id(monster_id) if evo is None: return [] return [self.get_monster(mat) for mat in evo.mats] - def evo_mats_by_monster(self, monster: MonsterModel) -> list: + def evo_mats_by_monster(self, monster: MonsterModel) -> List[MonsterModel]: return self.evo_mats_by_monster_id(monster.monster_no) # farmable @@ -570,8 +570,12 @@ def evo_gem_monster_by_id(self, monster_id) -> Optional[MonsterModel]: def evo_gem_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: return self.evo_gem_monster_by_id(monster.monster_no) - def material_of_ids_by_id(self, monster_id: int) -> list: + def material_of_ids_by_id(self, monster_id: int) -> List[int]: return sorted(self._get_edges(self.graph[monster_id], 'material_of')) - def material_of_ids(self, monster: MonsterModel) -> list: + def material_of_ids(self, monster: MonsterModel) -> List[int]: return self.material_of_ids_by_id(monster.monster_no) + + def material_of_monsters(self, monster: MonsterModel) -> List[MonsterModel]: + return [self.get_monster(m) + for m in self.material_of_ids_by_id(monster.monster_no)] diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 6471297d..0bc266da 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -7,10 +7,11 @@ from discord import Color from redbot.core.utils.chat_formatting import box -from padinfo.view.components.monster.header import MonsterHeader from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createSingleMultiplierText +from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage +from padinfo.view.materials import MaterialView from padinfo.view.evos import EvosView from padinfo.view.id import IdView from padinfo.view.leader_skill import LeaderSkillView @@ -86,25 +87,15 @@ def _add_mats_of_list(self, embed, monster_id_list, field_name): embed.add_field(name=field_name, value=field_data) async def make_evo_mats_embed(self, m: "MonsterModel"): - embed = await self.make_base_embed(m) - - mats_for_evo = self.db_context.graph.evo_mats_by_monster(m) - - field_name = 'Evo materials' - field_data = '' - if len(mats_for_evo) > 0: - for ae in mats_for_evo: - field_data += "{}\n".format(MonsterHeader.long(ae, link=True)) - else: - field_data = 'None' - embed.add_field(name=field_name, value=field_data) - - self._add_mats_of_list(embed, self.db_context.graph.material_of_ids(m), 'Material for') + mats = self.db_context.graph.evo_mats_by_monster(m) + usedin = self.db_context.graph.material_of_monsters(m) evo_gem = self.db_context.graph.evo_gem_monster(m) - if not evo_gem: - return embed - self._add_mats_of_list(embed, self.db_context.graph.material_of_ids(evo_gem), "Evo gem is mat for") - return embed + gemusedin = self.db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] + skillups = [su for su in self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) + if self.db_context.graph.monster_is_farmable_evo(su) and su != m] if m.active_skill else [] + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + + return MaterialView.embed(m, mats, usedin, gemusedin, skillups, color).to_embed() async def make_pantheon_embed(self, m: "MonsterModel"): full_pantheon = self.db_context.get_monsters_by_series(m.series_id) @@ -116,34 +107,6 @@ async def make_pantheon_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return PantheonView.embed(m, color, pantheon_list, series_name).to_embed() - async def make_skillups_embed(self, m: "MonsterModel"): - if m.active_skill is None: - return None - possible_skillups_list = self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) - skillups_list = list(filter( - lambda x: self.db_context.graph.monster_is_farmable_evo(x), possible_skillups_list)) - - if len(skillups_list) == 0: - return None - - embed = await self.make_base_embed(m) - - field_name = 'Skillups' - field_data = '' - - # Prevent huge skillup lists - if len(skillups_list) > 8: - field_data = '({} skillups omitted)'.format(len(skillups_list) - 8) - skillups_list = skillups_list[0:8] - - for monster in sorted(skillups_list, key=lambda x: x.monster_no_na): - field_data += '\n' + MonsterHeader.short(monster, link=True) - - if len(field_data.strip()): - embed.add_field(name=field_name, value=field_data) - - return embed - async def make_picture_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return PicsView.embed(m, color).to_embed() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f34977ca..8e7dd34c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -168,12 +168,11 @@ def __init__(self, bot, *args, **kwargs): # These emojis are the keys into the idmenu submenus self.id_emoji = '\N{HOUSE BUILDING}' self.evo_emoji = char_to_emoji('e') - self.mats_emoji = char_to_emoji('m') + self.mats_emoji = '\N{MEAT ON BONE}' self.ls_emoji = '\N{HOUSE BUILDING}' self.left_emoji = char_to_emoji('l') self.right_emoji = char_to_emoji('r') self.pantheon_emoji = '\N{CLASSICAL BUILDING}' - self.skillups_emoji = '\N{MEAT ON BONE}' self.pic_emoji = '\N{FRAME WITH PICTURE}' self.other_info_emoji = '\N{SCROLL}' self.first_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' @@ -405,7 +404,7 @@ async def evos(self, ctx, *, query: str): else: await self.makeFailureMsg(ctx, query, err) - @commands.command(name="mats", aliases=['evomats', 'evomat']) + @commands.command(name="mats", aliases=['evomats', 'evomat', 'skillups']) @checks.bot_has_permissions(embed_links=True) async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" @@ -427,18 +426,6 @@ async def pantheon(self, ctx, *, query: str): else: await self.makeFailureMsg(ctx, query, err) - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def skillups(self, ctx, *, query: str): - """Monster info (evolutions tab)""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) - if m is not None: - menu = await self._do_idmenu(ctx, m, self.skillups_emoji) - if menu == EMBED_NOT_GENERATED: - await ctx.send(inline('No skillups available')) - else: - await self.makeFailureMsg(ctx, query, err) - async def _do_idmenu(self, ctx, m, starting_menu_emoji): DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database @@ -502,10 +489,6 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed - skillups_embed = await menu.make_skillups_embed(m) - if skillups_embed: - emoji_to_embed[self.skillups_emoji] = skillups_embed - emoji_to_embed[self.other_info_emoji] = other_info_embed # remove emoji needs to be last diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py new file mode 100644 index 00000000..2f505536 --- /dev/null +++ b/padinfo/view/materials.py @@ -0,0 +1,54 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField +from discordmenu.embed.menu import EmbedView + +from padinfo.common.external_links import puzzledragonx +from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + +MAX_MONS_TO_SHOW = 5 + + +def mat_use_field(mons, title): + return EmbedField( + title, + Box(*((MonsterHeader.short_with_emoji(em) for em in mons) + if len(mons) <= MAX_MONS_TO_SHOW + else [f"{len(mons)} monsters"]), + "None" if not mons else None)) + +def skillup_field(mons): + text = None + if len(mons) == 0: + text = "None" + elif len(mons) > MAX_MONS_TO_SHOW: + text = f"({len(mons) - MAX_MONS_TO_SHOW} more monsters)" + + return EmbedField( + "Skillups", + Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:MAX_MONS_TO_SHOW]), text)) + +class MaterialView: + @staticmethod + def embed(m: "MonsterModel", mats, usedin, gemusedin, skillups, color): + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + url=puzzledragonx(m) + ), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), + embed_footer=pad_info_footer(), + embed_fields=[f for f in [ + mat_use_field(mats, "Evo materials"), + mat_use_field(usedin, "Material for") if usedin else None, + mat_use_field(gemusedin, "Evo gem is mat for") if gemusedin else None, + skillup_field(skillups) + ] if f is not None] + ) From 997576c30fbc18c7e8296113764203db485c9fc1 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 06:32:26 -0700 Subject: [PATCH 0468/1220] Support normal priority treenames from gdoc (#576) --- dadguide/monster_index.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index af2c2781..79ded4d1 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -48,11 +48,15 @@ async def __ainit__(self, monsters, db): treenames_data = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) for m_id, name, *data in treenames_data: - _, i, *_ = data + [None, None] + lp, i, *_ = data + [None, None] if m_id.isdigit() and not i: - if " " in name: - self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) + if lp: + for em_id in self.graph.get_alt_ids_by_id(int(m_id)): + self.monster_id_to_nametokens[em_id].update(self._name_to_tokens(name)) + else: + if " " in name: + self.multi_word_tokens.add(tuple(name.lower().split(" "))) + self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) pantheon_data = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) for sid, name, *_ in pantheon_data: @@ -93,12 +97,12 @@ async def _build_monster_index(self, monsters): # Name and Fluff Tokens manual = False - nametokens = self._name_to_tokens(m.name_en) + nametokens = self._name_to_tokens(m.name_en) + list(self.monster_id_to_nametokens[m.monster_id]) for me in self.graph.get_alt_ids_by_id(m.monster_id): for t in self.monster_id_to_nametokens[me]: if t in nametokens: self.name_tokens[t].add(m) - if not manual: + if not self.monster_id_to_nametokens[m.monster_id]: for token in self._get_important_tokens(m.name_en) + self._name_to_tokens(m.roma_subname): self.name_tokens[token.lower()].add(m) for repl in self.replacement_tokens[token.lower()]: From 2ebd349273baae84603b2b8cb63cb7002339ba35 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 06:35:19 -0700 Subject: [PATCH 0469/1220] Completely remove Chrono Ma:Gia cog (#572) --- chronomagia/__init__.py | 9 -- chronomagia/chronomagia.py | 179 ------------------------------------- chronomagia/info.json | 26 ------ 3 files changed, 214 deletions(-) delete mode 100644 chronomagia/__init__.py delete mode 100644 chronomagia/chronomagia.py delete mode 100644 chronomagia/info.json diff --git a/chronomagia/__init__.py b/chronomagia/__init__.py deleted file mode 100644 index 67acde36..00000000 --- a/chronomagia/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .chronomagia import ChronoMagia - -__red_end_user_data_statement__ = "No personal data is stored." - - -def setup(bot): - n = ChronoMagia(bot) - bot.add_cog(n) - bot.loop.create_task(n.reload_cm_task()) diff --git a/chronomagia/chronomagia.py b/chronomagia/chronomagia.py deleted file mode 100644 index 5b08985f..00000000 --- a/chronomagia/chronomagia.py +++ /dev/null @@ -1,179 +0,0 @@ -import asyncio -import csv -import difflib -import discord -import logging -import os -import traceback -import tsutils -import urllib.parse -from io import BytesIO -from collections import OrderedDict -from redbot.core import checks, commands, data_manager -from redbot.core.utils.chat_formatting import inline -from tsutils import EmojiUpdater, Menu - -logger = logging.getLogger('red.padbot-cogs.chronomagia') - - -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) - - -SUMMARY_SHEET = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQsO9Xi9cKaUQWPvDjjIKpHotZ036LCTN66PuNoQwvb8qZi4LmEUEOYmHDyqUJUzghI28aPrQHfRSYd/pub?gid=1488138129&single=true&output=csv' -PIC_URL = 'https://storage.googleapis.com/mirubot-chronomagia/cards/{}.png' - - -class CmCard(object): - def __init__(self, csv_row): - row = [x.strip() for x in csv_row] - self.name = row[0] - self.name_clean = clean_name_for_query(self.name) - self.expansion = row[12] - self.rarity = row[1] - self.monspell = row[2] - self.cost = row[3] - self.type1 = row[4] - self.type2 = row[5] - self.atk = row[6] - self.defn = row[7] - self.atkeff = row[9] - self.cardeff = row[11] - - -class ChronoMagia(commands.Cog): - """ChronoMagia.""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.card_data = [] - self.menu = Menu(bot) - self.id_emoji = '\N{INFORMATION SOURCE}' - self.pic_emoji = '\N{FRAME WITH PICTURE}' - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - async def reload_cm_task(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('ChronoMagia'): - try: - await self.refresh_data() - logger.info('Done refreshing ChronoMagia') - except Exception as ex: - logger.exception("reload CM loop caught exception " + str(ex)) - await asyncio.sleep(60 * 60 * 1) - - async def refresh_data(self): - await self.bot.wait_until_ready() - - standard_expiry_secs = 2 * 60 * 60 - summary_text = await tsutils.async_cached_plain_request( - _data_file('summary.csv'), SUMMARY_SHEET, standard_expiry_secs) - file_reader = csv.reader(summary_text.splitlines(), delimiter=',') - next(file_reader, None) # skip header - self.card_data.clear() - for row in file_reader: - if not row or not row[0].strip(): - # Ignore empty rows - continue - if len(row) < 11: - logger.error('bad row: ', row) - self.card_data.append(CmCard(row)) - - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def cmid(self, ctx, *, query: str): - """ChronoMagia query.""" - query = clean_name_for_query(query) - if len(query) < 3: - await ctx.send(inline('query must be at least 3 characters')) - return - - names_to_card = {x.name_clean: x for x in self.card_data} - - # Check if the card name starts with the query - matches = list(filter(lambda x: x.startswith(query), names_to_card.keys())) - - # Find a card that closely matches the query - if not matches: - matches = difflib.get_close_matches(query, names_to_card.keys(), n=1, cutoff=.6) - - # Find a card that contains the query text - if not matches: - matches = list(filter(lambda x: query in x, names_to_card.keys())) - - if matches: - await self.do_menu(ctx, names_to_card[matches[0]]) - else: - await ctx.send(inline('no matches')) - - async def do_menu(self, ctx, c): - emoji_to_embed = OrderedDict() - emoji_to_embed[self.id_emoji] = make_embed(c) - emoji_to_embed[self.pic_emoji] = make_img_embed(c) - return await self._do_menu(ctx, self.id_emoji, emoji_to_embed) - - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): - remove_emoji = self.menu.emoji['no'] - emoji_to_embed[remove_emoji] = self.menu.reaction_delete_message - - try: - result_msg, result_embed = await self.menu.custom_menu(ctx, EmojiUpdater(emoji_to_embed), - starting_menu_emoji, timeout=20) - if result_msg and result_embed: - # Message is finished but not deleted, clear the footer - result_embed.set_footer(text=discord.Embed.Empty) - await result_msg.edit(embed=result_embed) - except Exception as ex: - logger.exception('Menu failure') - - -def make_base_embed(c: CmCard): - embed = discord.Embed() - embed.title = c.name - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed - - -def make_embed(c: CmCard): - embed = make_base_embed(c) - - embed.add_field( - name=c.monspell, value='{}\nCost {}'.format(c.rarity, c.cost), inline=True) - if c.monspell == 'Monster': - mtype = '\n{}/{} '.format(c.type1, c.type2) if c.type2 else '{} '.format(c.type1) - embed.add_field(name=mtype, value='Atk {}\nDef {}'.format(c.atk, c.defn), inline=True) - if c.expansion: - embed.add_field(name='Expansion', value=c.expansion, inline=False) - - if c.atkeff: - embed.add_field(name='Attack Effect', value=c.atkeff, inline=False) - - if c.cardeff: - embed.add_field(name='Card Effect', value=c.cardeff, inline=False) - else: - embed.add_field(name='Card Effect', value=c.cardeff, inline=True) - - return embed - - -def make_img_embed(c: CmCard): - embed = make_base_embed(c) - url = PIC_URL.format(urllib.parse.quote(c.name)) - embed.set_image(url=url) - return embed - - -def clean_name_for_query(name: str): - return name.strip().lower().replace(',', '') diff --git a/chronomagia/info.json b/chronomagia/info.json deleted file mode 100644 index 2160d7b0..00000000 --- a/chronomagia/info.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Displays data about the now defunct GungHo game, Chrono Ma:Gia.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]cmid to get started.", - "short": "Shows Chrono Ma:Gia data", - "min_bot_version": "3.4.0", - "max_bot_version": "0.0.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": true, - "disabled": false, - "required_cogs": {}, - "requirements": [ - "tsutils>=3.0.0", - "aiohttp" - ], - "tags": [ - "chronomagia" - ] -} From 63ba0373449bb091b819cb1d3c618bb218216242 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 20:08:32 -0700 Subject: [PATCH 0470/1220] Refactor & remove +0 stats from ^otherinfo (#573) * Remove +0 stats from ^stats * Use new embed syntax for otherinfo * Dedupe link text --- padinfo/id_menu.py | 60 ++-------------------------------- padinfo/view/links.py | 17 ++++++---- padinfo/view/otherinfo.py | 69 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 64 deletions(-) create mode 100644 padinfo/view/otherinfo.py diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 0bc266da..93942ffc 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -17,6 +17,7 @@ from padinfo.view.leader_skill import LeaderSkillView from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView +from padinfo.view.otherinfo import OtherInfoView from padinfo.view.pantheon import PantheonView from padinfo.view.pic import PicsView @@ -120,63 +121,8 @@ async def make_lookup_embed(self, m: "MonsterModel"): return LookupView.embed(m, color).to_embed() async def make_otherinfo_embed(self, m: "MonsterModel"): - embed = await self.make_base_embed(m) - # Clear the thumbnail, takes up too much space - embed.set_thumbnail(url='') - - body_text = '\n' - stat_cols = ['', 'HP', 'ATK', 'RCV'] - for plus in (0, 297): - body_text += '**Stats at +{}:**'.format(plus) - tbl = prettytable.PrettyTable(stat_cols) - tbl.hrules = prettytable.NONE - tbl.vrules = prettytable.NONE - tbl.align = "l" - levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) - for lv in levels: - for inh in (False, True): - hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) - row_name = 'Lv{}'.format(lv) - if inh: - row_name = '(Inh)' - tbl.add_row([row_name.format(plus), hp, atk, rcv]) - body_text += box(tbl.get_string()) - - body_text += "\n**JP Name**: {}".format(m.name_ja) - body_text += "\n[YouTube]({}) | [Skyozora]({}) | [PDX]({}) | [Ilimina]({})".format( - YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)), - SKYOZORA_TEMPLATE.format(m.monster_no_jp), - INFO_PDX_TEMPLATE.format(m.monster_no_jp), - ILMINA_TEMPLATE.format(m.monster_no_jp)) - - if m.history_us: - body_text += '\n**History:** {}'.format(m.history_us) - - body_text += '\n**Series:** {}'.format(self.db_context.graph.get_monster(m.monster_no).series.name) - body_text += '\n**Sell MP:** {:,}'.format(m.sell_mp) - if m.buy_mp is not None: - body_text += " **Buy MP:** {:,}".format(m.buy_mp) - - if m.exp < 1000000: - xp_text = '{:,}'.format(m.exp) - else: - xp_text = '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' - body_text += '\n**XP to Max:** {}'.format(xp_text) - body_text += ' **Max Level:**: {}'.format(m.level) - - # weighted stat calculation & display - hp, atk, rcv, weighted = m.stats() - body_text += '\n**Weighted stats:** {}'.format(weighted) - if m.limit_mult > 0: - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) - body_text += ' | LB {} (+{}%)'.format(lb_weighted, m.limit_mult) - - body_text += '\n**Fodder EXP:** {:,}'.format(m.fodder_exp) - body_text += '\n**Rarity:** {} **Cost:** {}'.format(m.rarity, m.cost) - - embed.description = body_text - - return embed + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return OtherInfoView.embed(m, color).to_embed() async def make_links_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 5a418006..65d47193 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -14,18 +14,21 @@ class LinksView: + @staticmethod + def linksbox(m): + return Box( + LinkedText('YouTube', youtube_search(m)), + LinkedText('Skyozora', skyozora(m)), + LinkedText('PDX', puzzledragonx(m)), + LinkedText('Ilmina', ilmina(m)), + delimiter=' | ') + @staticmethod def embed(m: "MonsterModel", color): return EmbedView( EmbedMain( color=color, title=MonsterHeader.long_v2(m).to_markdown(), - description=Box( - LinkedText('YouTube', youtube_search(m)), - LinkedText('Skyozora', skyozora(m)), - LinkedText('PDX', puzzledragonx(m)), - LinkedText('Ilmina', ilmina(m)), - delimiter=' | ' - ), + description=LinksView.linksbox(m), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m))) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py new file mode 100644 index 00000000..bbaefb7c --- /dev/null +++ b/padinfo/view/otherinfo.py @@ -0,0 +1,69 @@ +from typing import TYPE_CHECKING + +import prettytable +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import LabeledText, LinkedText, Text +from redbot.core.utils.chat_formatting import box + +from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina +from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.links import LinksView + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +def statsbox(m): + stat_cols = ['', 'HP', 'ATK', 'RCV'] + tbl = prettytable.PrettyTable(stat_cols) + tbl.hrules = prettytable.NONE + tbl.vrules = prettytable.NONE + tbl.align = "l" + levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) + for lv in levels: + for inh in (False, True): + hp, atk, rcv, _ = m.stats(lv, plus=297, inherit=inh) + row_name = '(Inh)' if inh else 'Lv{}'.format(lv) + tbl.add_row([row_name, hp, atk, rcv]) + return box(tbl.get_string()) + + +class OtherInfoView: + @staticmethod + def embed(m: "MonsterModel", color): + return EmbedView( + EmbedMain( + color=color, + title=MonsterHeader.long_v2(m).to_markdown(), + url=puzzledragonx(m)), + embed_footer=pad_info_footer(), + embed_fields=[ + EmbedField( + "Stats at +297:", + Box( + Text(statsbox(m)), + LabeledText("JP Name", m.name_ja), + LinksView.linksbox(m), + LabeledText("History", m.history_us) if m.history_us else None, + LabeledText("Series", m.series.name_en), + Box( + LabeledText("Sell MP", '{:,}'.format(m.sell_mp)), + LabeledText("Buy MP", '{:,}'.format(m.buy_mp)) if m.buy_mp else None, + delimiter=' '), + Box( + LabeledText("XP to Max", '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' + if m.exp >= 1000000 else '{:,}'.format(m.exp)), + LabeledText("Max Level", str(m.level)), + delimiter=' '), + Box( + LabeledText("Weighted Stats", str(m.stats()[3])), + Text('LB {} (+{}%)'.format(m.stats(lv=110)[3], m.limit_mult)) if m.limit_mult > 0 else None, + delimiter=' | '), + LabeledText("Fodder EXP", '{:,}'.format(m.fodder_exp)), + Box( + LabeledText("Rarity", str(m.rarity)), + LabeledText("Cost", str(m.cost)), + delimiter=' ')))]) From 8d5d1a5e3da7984d9680219098b08519862a88c8 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 20:26:00 -0700 Subject: [PATCH 0471/1220] Create idmeaning and add series to exportmodifiers (#578) * Create idmeaning and add series to exportmodifiers * Add alias --- dadguide/database_context.py | 5 +++++ padinfo/padinfo.py | 40 +++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 00558231..bb01897c 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -4,6 +4,7 @@ from .models.awoken_skill_model import AwokenSkillModel from .models.dungeon_model import DungeonModel from .models.scheduled_event_model import ScheduledEventModel +from .models.series_model import SeriesModel from .monster_graph import MonsterGraph SCHEDULED_EVENT_QUERY = """SELECT @@ -68,6 +69,10 @@ def get_all_awoken_skills(self) -> List[AwokenSkillModel]: result = self.database.query_many("SELECT * FROM awoken_skills", ()) return [AwokenSkillModel(**r) for r in result] + def get_all_series(self) -> List[SeriesModel]: + result = self.database.query_many("SELECT * FROM series", ()) + return [SeriesModel(**r) for r in result] + def has_database(self): return self.database.has_database() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8e7dd34c..e5de4aa6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1183,23 +1183,28 @@ async def idhelp(self, ctx, *, query=""): async def exportmodifiers(self, ctx): DGCOG = self.bot.get_cog("Dadguide") tms = DGCOG.token_maps + awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} + series = {s.series_id: s for s in DGCOG.database.get_all_series()} + o = ("Jump to:\n\n" "* [Types](#types)\n" "* [Evolutions](#evolutions)\n" "* [Misc](#misc)\n" "* [Awakenings](#awakenings)\n" + "* [Series](#series)\n" "* [Attributes](#attributes)\n\n\n\n") - anames = DGCOG.database.get_all_awoken_skills() - etable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.EVO_MAP.items()] o += "\n\n### Evolutions\n\n" + tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") ttable = [(k.name, ", ".join(map(inline, v))) for k, v in tms.TYPE_MAP.items()] o += "\n\n### Types\n\n" + tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") mtable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.MISC_MAP.items()] o += "\n\n### Misc\n\n" + tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") - atable = [(anames[k.value - 1].name_en, ", ".join(map(inline, v))) for k, v in tms.AWOKEN_MAP.items()] + atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in tms.AWOKEN_MAP.items()] o += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") + stable = [(series[k].name_en, ", ".join(map(inline, v))) + for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items()] + o += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in tms.COLOR_MAP.items()] ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in tms.SUB_COLOR_MAP.items()] @@ -1211,6 +1216,35 @@ async def exportmodifiers(self, ctx): await ctx.send(file=text_to_file(o, filename="table.md")) + @commands.command(aliases=["idcheckmod"]) + async def idmeaning(self, ctx, *, modifier): + modifier = modifier.replace(" ", "") + DGCOG = self.bot.get_cog("Dadguide") + tms = DGCOG.token_maps + awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} + series = {s.series_id: s for s in DGCOG.database.get_all_series()} + + meanings = [ + *[k.value for k, v in tms.EVO_MAP.items() if modifier in v], + *[k.name for k, v in tms.TYPE_MAP.items() if modifier in v], + *[k.value for k, v in tms.MISC_MAP.items() if modifier in v], + *[awakenings[k.value].name_en for k, v in tms.AWOKEN_MAP.items() if modifier in v], + *["Main Attr " + k.name.replace("Nil", "None") for k, v in tms.COLOR_MAP.items() if modifier in v], + *["Sub Attr " + k.name.replace("Nil", "None") for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], + *[series[k].name_en for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], + ] + for k, v in tms.DUAL_COLOR_MAP.items(): + if modifier not in v: + continue + k0name = k[0].name.replace("Nil", "None") + k1name = k[1].name.replace("Nil", "None") + meanings.append(k0name + "/" + k1name) + + if meanings: + await ctx.send(", ".join(meanings)) + else: + await ctx.send(f"There are no modifiers that match `{modifier}`.") + class PadInfoSettings(CogSettings): def make_default_settings(self): From e85b4eb44c03eba02c1e6ce1197813a5d996bcf9 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 20:58:31 -0700 Subject: [PATCH 0472/1220] Idmeaning - clarify token meanings (#580) * Create idmeaning and add series to exportmodifiers * Add alias * Clarify token meanings --- padinfo/padinfo.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e5de4aa6..8f7d2318 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1216,7 +1216,7 @@ async def exportmodifiers(self, ctx): await ctx.send(file=text_to_file(o, filename="table.md")) - @commands.command(aliases=["idcheckmod"]) + @commands.command(aliases=["idcheckmod", "lookupmod"]) async def idmeaning(self, ctx, *, modifier): modifier = modifier.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") @@ -1224,14 +1224,26 @@ async def idmeaning(self, ctx, *, modifier): awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} + def additmods(l, om): + if len(l) == 1: + return "" + return "; Additional modifiers: " + ', '.join(inline(m) for m in l if m != om) + meanings = [ - *[k.value for k, v in tms.EVO_MAP.items() if modifier in v], - *[k.name for k, v in tms.TYPE_MAP.items() if modifier in v], - *[k.value for k, v in tms.MISC_MAP.items() if modifier in v], - *[awakenings[k.value].name_en for k, v in tms.AWOKEN_MAP.items() if modifier in v], - *["Main Attr " + k.name.replace("Nil", "None") for k, v in tms.COLOR_MAP.items() if modifier in v], - *["Sub Attr " + k.name.replace("Nil", "None") for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], - *[series[k].name_en for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], + *["Evo: " + k.value + additmods(v, modifier) + for k, v in tms.EVO_MAP.items() if modifier in v], + *["Type: " + k.name + additmods(v, modifier) + for k, v in tms.TYPE_MAP.items() if modifier in v], + *["Misc: " + k.value + additmods(v, modifier) + for k, v in tms.MISC_MAP.items() if modifier in v], + *["Awakening: " + awakenings[k.value].name_en + additmods(v, modifier) + for k, v in tms.AWOKEN_MAP.items() if modifier in v], + *["Main Attr: " + k.name.replace("Nil", "None") + additmods(v, modifier) + for k, v in tms.COLOR_MAP.items() if modifier in v], + *["Sub Attr: " + k.name.replace("Nil", "None") + additmods(v, modifier) + for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], + *["Series: " + series[k].name_en + additmods(v, modifier) + for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], ] for k, v in tms.DUAL_COLOR_MAP.items(): if modifier not in v: From a0be1e69fdac5961e1fe45ae1d5ab12053d08dd0 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 21:10:30 -0700 Subject: [PATCH 0473/1220] Fix delimiter in idmeaning (#581) * Fix delimiter * Requested Changes --- padinfo/padinfo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8f7d2318..b3820a9c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1216,7 +1216,7 @@ async def exportmodifiers(self, ctx): await ctx.send(file=text_to_file(o, filename="table.md")) - @commands.command(aliases=["idcheckmod", "lookupmod"]) + @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod"]) async def idmeaning(self, ctx, *, modifier): modifier = modifier.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") @@ -1250,10 +1250,10 @@ def additmods(l, om): continue k0name = k[0].name.replace("Nil", "None") k1name = k[1].name.replace("Nil", "None") - meanings.append(k0name + "/" + k1name) + meanings.append("Dual Attr: " + k0name + "/" + k1name + additmods(v, modifier)) if meanings: - await ctx.send(", ".join(meanings)) + await ctx.send("\n".join(meanings)) else: await ctx.send(f"There are no modifiers that match `{modifier}`.") From bb907d190cd60524f9b82b3d881d33f6c0bbe882 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 23 Jan 2021 22:40:04 -0600 Subject: [PATCH 0474/1220] Add idmeaning to idhelp docs (#582) * Add idmeaning to idhelp docs * Update wording --- padinfo/padinfo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b3820a9c..2c29cb91 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1175,7 +1175,9 @@ async def debugiddist(self, ctx, s1, s2): @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query=""): """Get help with an id query""" - await ctx.send("See <{0}> for documentation on {1.prefix}id!".format(IDGUIDE, ctx)) + await ctx.send( + "See <{0}> for documentation on `{1.prefix}id`! Use `{1.prefix}idmeaning` to query the meaning of any modifier token.".format( + IDGUIDE, ctx)) if query: await self.debugid(ctx, query=query) From b150a9543a533f65014812df59fa6e503293e623 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Sat, 23 Jan 2021 21:03:45 -0800 Subject: [PATCH 0475/1220] Grab padinfo emojis after bot is ready rather than on bot construction --- padinfo/padinfo.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2c29cb91..f19fe6d1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -194,9 +194,6 @@ def __init__(self, bot, *args, **kwargs): self.config.register_user(survey_mode=0, color=None, beta_id3=False) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) - emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) - emoji_cache.refresh_from_discord_bot(self.bot) - def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.index_all = None @@ -223,6 +220,8 @@ async def reload_nicknames(self): while self == self.bot.get_cog('PadInfo'): wait_time = 60 * 60 * 1 try: + emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) + emoji_cache.refresh_from_discord_bot(self.bot) await self.refresh_index() except Exception as ex: wait_time = 5 @@ -1233,19 +1232,19 @@ def additmods(l, om): meanings = [ *["Evo: " + k.value + additmods(v, modifier) - for k, v in tms.EVO_MAP.items() if modifier in v], + for k, v in tms.EVO_MAP.items() if modifier in v], *["Type: " + k.name + additmods(v, modifier) - for k, v in tms.TYPE_MAP.items() if modifier in v], + for k, v in tms.TYPE_MAP.items() if modifier in v], *["Misc: " + k.value + additmods(v, modifier) - for k, v in tms.MISC_MAP.items() if modifier in v], + for k, v in tms.MISC_MAP.items() if modifier in v], *["Awakening: " + awakenings[k.value].name_en + additmods(v, modifier) - for k, v in tms.AWOKEN_MAP.items() if modifier in v], + for k, v in tms.AWOKEN_MAP.items() if modifier in v], *["Main Attr: " + k.name.replace("Nil", "None") + additmods(v, modifier) - for k, v in tms.COLOR_MAP.items() if modifier in v], + for k, v in tms.COLOR_MAP.items() if modifier in v], *["Sub Attr: " + k.name.replace("Nil", "None") + additmods(v, modifier) - for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], + for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], *["Series: " + series[k].name_en + additmods(v, modifier) - for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], + for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], ] for k, v in tms.DUAL_COLOR_MAP.items(): if modifier not in v: From ed770b93eaa04e8c9c2d92923f1a8abfb243d4cb Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 23:45:38 -0700 Subject: [PATCH 0476/1220] Remove modifiers related to a name tokens (#586) --- dadguide/monster_index.py | 23 ++++++++++------------- padinfo/padinfo.py | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 79ded4d1..8d7092e1 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -86,7 +86,8 @@ async def _build_monster_index(self, monsters): self.fluff_tokens = defaultdict(set) self.modifiers = defaultdict(set) - mod_maps = list(MODIFIER_MAPS.values()) + list(self.series_id_to_pantheon_nickname.values()) + mod_maps = [x for xs in list(MODIFIER_MAPS.values()) + list(self.series_id_to_pantheon_nickname.values()) + for x in xs] async for m in AsyncIter(monsters): self.modifiers[m] = await self.get_modifiers(m) @@ -120,33 +121,29 @@ async def _build_monster_index(self, monsters): for repl in self.replacement_tokens[token.lower()]: self.name_tokens[repl].add(me) if token not in HAZARDOUS_IN_NAME_PREFIXES: - for pas in mod_maps: - if token in pas: - self.modifiers[m].update(pas) + if token in mod_maps: + self.modifiers[m].update(token) for token in nametokens: if m in self.name_tokens[token.lower()]: continue self.fluff_tokens[token.lower()].add(m) for repl in self.replacement_tokens[token.lower()]: self.fluff_tokens[repl].add(m) - for pas in mod_maps: - if token in pas: - self.modifiers[m].update(pas) + if token in mod_maps: + self.modifiers[m].update(token) # Monster Nickname for nick in self.monster_id_to_nickname[m.monster_id]: self.manual_nick[nick].add(m) - for pas in mod_maps: - if nick in pas: - self.modifiers[m].update(pas) + if nick in mod_maps: + self.modifiers[m].update(nick) # Tree Nickname base_id = self.graph.get_base_id(m) for nick in self.monster_id_to_treename[base_id]: self.manual_tree[nick].add(m) - for pas in mod_maps: - if nick in pas: - self.modifiers[m].update(pas) + if nick in mod_maps: + self.modifiers[m].update(nick) @staticmethod def _name_to_tokens(oname): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f19fe6d1..c58195e8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1222,13 +1222,14 @@ async def idmeaning(self, ctx, *, modifier): modifier = modifier.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") tms = DGCOG.token_maps + awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_MAP.values() for aw in aws) + ")" awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} - def additmods(l, om): - if len(l) == 1: + def additmods(ms, om): + if len(ms) == 1: return "" - return "; Additional modifiers: " + ', '.join(inline(m) for m in l if m != om) + return "; Alternate Names: " + ', '.join(inline(m) for m in ms if m != om) meanings = [ *["Evo: " + k.value + additmods(v, modifier) @@ -1245,6 +1246,16 @@ def additmods(l, om): for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], *["Series: " + series[k].name_en + additmods(v, modifier) for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], +<<<<<<< Updated upstream +======= + + *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", modifier)], + *["Base Rarity: " + m for m in re.findall(r"^(\d+)\*b$", modifier)], + *[f"[UNSUPPORTED] Multiple Awakenings: {m}x {awakenings[a.value].name_en}" + f"{additmods([f'{m}*{d}' for d in v], modifier)}" + for m, ag in re.findall(r"^(\d+)\*{}$".format(awokengroup), modifier) + for a, v in tms.AWOKEN_MAP.items() if ag in v] +>>>>>>> Stashed changes ] for k, v in tms.DUAL_COLOR_MAP.items(): if modifier not in v: From b38de5d3bc6e03049d373d648c012ae76ae81def Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 23 Jan 2021 23:50:17 -0700 Subject: [PATCH 0477/1220] Fix Merge Conflicts (#587) --- padinfo/padinfo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c58195e8..30694a49 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1246,8 +1246,6 @@ def additmods(ms, om): for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], *["Series: " + series[k].name_en + additmods(v, modifier) for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], -<<<<<<< Updated upstream -======= *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", modifier)], *["Base Rarity: " + m for m in re.findall(r"^(\d+)\*b$", modifier)], @@ -1255,7 +1253,6 @@ def additmods(ms, om): f"{additmods([f'{m}*{d}' for d in v], modifier)}" for m, ag in re.findall(r"^(\d+)\*{}$".format(awokengroup), modifier) for a, v in tms.AWOKEN_MAP.items() if ag in v] ->>>>>>> Stashed changes ] for k, v in tms.DUAL_COLOR_MAP.items(): if modifier not in v: From 68d3c9f30a9f3fd008f7740a677f13d6a2172c42 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 24 Jan 2021 00:01:17 -0700 Subject: [PATCH 0478/1220] Add to set instead up updating with every char (#588) Fixes a bug where k, n, i, g, h, t were individually being added to tokens instead of the word knight --- dadguide/monster_index.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 8d7092e1..43b6b62e 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -86,7 +86,7 @@ async def _build_monster_index(self, monsters): self.fluff_tokens = defaultdict(set) self.modifiers = defaultdict(set) - mod_maps = [x for xs in list(MODIFIER_MAPS.values()) + list(self.series_id_to_pantheon_nickname.values()) + known_mods = [x for xs in list(MODIFIER_MAPS.values()) + list(self.series_id_to_pantheon_nickname.values()) for x in xs] async for m in AsyncIter(monsters): @@ -121,29 +121,29 @@ async def _build_monster_index(self, monsters): for repl in self.replacement_tokens[token.lower()]: self.name_tokens[repl].add(me) if token not in HAZARDOUS_IN_NAME_PREFIXES: - if token in mod_maps: - self.modifiers[m].update(token) + if token in known_mods: + self.modifiers[m].add(token) for token in nametokens: if m in self.name_tokens[token.lower()]: continue self.fluff_tokens[token.lower()].add(m) for repl in self.replacement_tokens[token.lower()]: self.fluff_tokens[repl].add(m) - if token in mod_maps: - self.modifiers[m].update(token) + if token in known_mods: + self.modifiers[m].add(token) # Monster Nickname for nick in self.monster_id_to_nickname[m.monster_id]: self.manual_nick[nick].add(m) - if nick in mod_maps: - self.modifiers[m].update(nick) + if nick in known_mods: + self.modifiers[m].add(nick) # Tree Nickname base_id = self.graph.get_base_id(m) for nick in self.monster_id_to_treename[base_id]: self.manual_tree[nick].add(m) - if nick in mod_maps: - self.modifiers[m].update(nick) + if nick in known_mods: + self.modifiers[m].add(nick) @staticmethod def _name_to_tokens(oname): From 11785f348af51df1fcf5d56b15317a19b224c447 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 24 Jan 2021 22:20:06 -0700 Subject: [PATCH 0479/1220] Remove idna and idjp (#591) Doing this early to improve index reload times --- padinfo/padinfo.py | 84 +++++++++------------------------------------- 1 file changed, 16 insertions(+), 68 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 30694a49..be6f5bb8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -7,7 +7,6 @@ import urllib.parse from collections import OrderedDict, defaultdict from datetime import datetime -from enum import Enum from io import BytesIO from typing import TYPE_CHECKING @@ -38,12 +37,6 @@ IDGUIDE = "https://github.com/TsubakiBotPad/pad-cogs/wiki/%5Eid-user-guide" -class ServerFilter(Enum): - any = 0 - na = 1 - jp = 2 - - def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) @@ -159,8 +152,6 @@ def __init__(self, bot, *args, **kwargs): self.settings = PadInfoSettings("padinfo") self.index_all = None - self.index_na = None - self.index_jp = None self.index_lock = asyncio.Lock() self.menu = Menu(bot) @@ -197,8 +188,6 @@ def __init__(self, bot, *args, **kwargs): def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.index_all = None - self.index_na = None - self.index_jp = None self.historic_lookups = {} self.historic_lookups_id2 = {} self.historic_lookups_id3 = {} @@ -241,13 +230,6 @@ async def refresh_index(self): async with self.index_lock: logger.debug('Loading ALL index') self.index_all = await dg_cog.create_index() - - logger.debug('Loading NA index') - self.index_na = await dg_cog.create_index(lambda m: m.on_na) - - logger.debug('Loading JP index') - self.index_jp = await dg_cog.create_index(lambda m: m.on_jp) - logger.info('Done refreshing indexes') def get_monster(self, monster_id: int): @@ -283,22 +265,16 @@ async def id1(self, ctx, *, query): @checks.bot_has_permissions(embed_links=True) async def idna(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" - if await self.config.user(ctx.author).beta_id3(): - await self._do_id3(ctx, "inna " + query) - else: - await self._do_id(ctx, query, server_filter=ServerFilter.na) + await self._do_id3(ctx, "inna " + query) @commands.command() @checks.bot_has_permissions(embed_links=True) async def idjp(self, ctx, *, query: str): """Monster info (limited to JP monsters ONLY)""" - if await self.config.user(ctx.author).beta_id3(): - await self._do_id3(ctx, "injp " + query) - else: - await self._do_id(ctx, query, server_filter=ServerFilter.jp) + await self._do_id3(ctx, "injp " + query) - async def _do_id(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = await self.findMonster1(query, server_filter=server_filter) + async def _do_id(self, ctx, query: str): + m, err, debug_info = await self.findMonster1(query) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) @@ -357,20 +333,8 @@ async def id2(self, ctx, *, query: str): """Monster info (main tab)""" await self._do_id2(ctx, query) - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def id2na(self, ctx, *, query: str): - """Monster info (limited to NA monsters ONLY)""" - await self._do_id2(ctx, query, server_filter=ServerFilter.na) - - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def id2jp(self, ctx, *, query: str): - """Monster info (limited to JP monsters ONLY)""" - await self._do_id2(ctx, query, server_filter=ServerFilter.jp) - - async def _do_id2(self, ctx, query: str, server_filter=ServerFilter.any): - m, err, debug_info = await self.findMonster2(query, server_filter=server_filter) + async def _do_id2(self, ctx, query: str): + m, err, debug_info = await self.findMonster2(query) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) else: @@ -1008,7 +972,7 @@ async def makeFailureMsg(self, ctx, query: str, err): await ctx.send(box(msg)) await ctx.send('Looking for the beta test? Type `{0.prefix}idset beta y`'.format(ctx)) - async def findMonsterCustom(self, ctx, query, server_filter=ServerFilter.any): + async def findMonsterCustom(self, ctx, query): if await self.config.user(ctx.author).beta_id3(): m = await self.findMonster3(query) if m: @@ -1016,11 +980,11 @@ async def findMonsterCustom(self, ctx, query, server_filter=ServerFilter.any): else: return None, "Monster not found", "" else: - return await self.findMonster1(query, server_filter) + return await self.findMonster1(query) - async def findMonster1(self, query, server_filter=ServerFilter.any): + async def findMonster1(self, query): query = rmdiacritics(query) - nm, err, debug_info = await self._findMonster(query, server_filter) + nm, err, debug_info = await self._findMonster(query) monster_no = nm.monster_id if nm else -1 self.historic_lookups[query] = monster_no @@ -1030,23 +994,15 @@ async def findMonster1(self, query, server_filter=ServerFilter.any): return m, err, debug_info - async def _findMonster(self, query, server_filter=ServerFilter.any) -> "NamedMonster": + async def _findMonster(self, query) -> "NamedMonster": while self.index_lock.locked(): await asyncio.sleep(1) - if server_filter.value == ServerFilter.any.value: - monster_index = self.index_all - elif server_filter.value == ServerFilter.na.value: - monster_index = self.index_na - elif server_filter.value == ServerFilter.jp.value: - monster_index = self.index_jp - else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) - return monster_index.find_monster(query) + return self.index_all.find_monster(query) - async def findMonster2(self, query, server_filter=ServerFilter.any): + async def findMonster2(self, query): query = rmdiacritics(query) - nm, err, debug_info = await self._findMonster2(query, server_filter) + nm, err, debug_info = await self._findMonster2(query) monster_no = nm.monster_id if nm else -1 self.historic_lookups_id2[query] = monster_no @@ -1056,19 +1012,11 @@ async def findMonster2(self, query, server_filter=ServerFilter.any): return m, err, debug_info - async def _findMonster2(self, query, server_filter=ServerFilter.any): + async def _findMonster2(self, query): while self.index_lock.locked(): await asyncio.sleep(1) - if server_filter == ServerFilter.any: - monster_index = self.index_all - elif server_filter == ServerFilter.na: - monster_index = self.index_na - elif server_filter == ServerFilter.jp: - monster_index = self.index_jp - else: - raise ValueError("server_filter must be type ServerFilter not " + str(type(server_filter))) - return monster_index.find_monster2(query) + return self.index_all.find_monster2(query) async def findMonster3(self, query): m = await self._findMonster3(query) From 6ccfb545b0d40150d5e21c53cf7f61d0345951e3 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 25 Jan 2021 00:22:07 -0700 Subject: [PATCH 0480/1220] Fix Batman and Shinrabansho Choco collab id displays (#596) Fixes an issue where jp & na id numbers of monsters in these two collabs disagree with each other. We are displaying the NA number and querying the NA number, but showing a warning message explaining what's going on. The JP number is available by querying the JP number along with the prefix `jp` attached to the number. If the user querying is a padglobal admin, an additional warning is displayed with some help text about the id config gdoc. --- dadguide/monster_index.py | 8 ++++++-- padinfo/padinfo.py | 6 +++++- padinfo/view/components/monster/header.py | 10 +++++----- padinfo/view/id.py | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 43b6b62e..346a010d 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -93,8 +93,12 @@ async def _build_monster_index(self, monsters): self.modifiers[m] = await self.get_modifiers(m) # ID - self.name_tokens[str(m.monster_id)].add(m) - self.name_tokens[str(m.monster_id % 10000)].add(m) + self.name_tokens[str(m.monster_no_na)].add(m) + if m.monster_id > 10000: + self.name_tokens[str(m.monster_id)].add(m) + if m.monster_no_na != m.monster_no_jp: + self.name_tokens['na'+str(m.monster_no_na)].add(m) + self.name_tokens['jp'+str(m.monster_no_jp)].add(m) # Name and Fluff Tokens manual = False diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index be6f5bb8..63ac73f1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -348,7 +348,11 @@ async def id3(self, ctx, *, query: str): async def _do_id3(self, ctx, query): m = await self.findMonster3(query) - + if m and m.monster_no_na != m.monster_no_jp: + await ctx.send("The NA ID and JP ID of this card differ! " + "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + \ + (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if + ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) if await self.config.do_survey(): asyncio.create_task(self.send_survey_after(ctx, query, m)) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 2ef9f43c..4dbbc3d9 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -13,7 +13,7 @@ class MonsterHeader: @staticmethod - def ja_suffix(m: "MonsterModel", subname_on_override=True): + def jp_suffix(m: "MonsterModel", subname_on_override=True): suffix = "" if m.roma_subname and (subname_on_override or m.name_en_override is None): suffix += ' [{}]'.format(m.roma_subname) @@ -29,7 +29,7 @@ def short(m: "MonsterModel", link=False): @staticmethod def long(m: "MonsterModel", link=False): - msg = MonsterHeader.short(m) + MonsterHeader.ja_suffix(m) + msg = MonsterHeader.short(m) + MonsterHeader.jp_suffix(m) return '[{}]({})'.format(msg, puzzledragonx(m)) if link else msg @staticmethod @@ -37,12 +37,12 @@ def name(m: "MonsterModel", link=False, show_jp=False): msg = '[{}] {}{}'.format( m.monster_no_na, m.name_en, - MonsterHeader.ja_suffix(m) if show_jp else '') + MonsterHeader.jp_suffix(m) if show_jp else '') return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) @staticmethod def long_v2(m: "MonsterModel", link=False): - msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, MonsterHeader.ja_suffix(m)) + msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, MonsterHeader.jp_suffix(m)) return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) @staticmethod @@ -51,6 +51,6 @@ def short_with_emoji(m: "MonsterModel", link=True): return Box( Text(format_emoji(MonsterEmoji.attribute(m))), LinkedText(msg, puzzledragonx(m)) if link else Text(msg), - Text(MonsterHeader.ja_suffix(m, False)), + Text(MonsterHeader.jp_suffix(m, False)), delimiter=' ' ) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 19c8c185..769adfea 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -160,7 +160,7 @@ def embed(m: "MonsterModel", color, is_transform_base, true_evo_type_raw, acquir EmbedField( "Alternate Evos", HighlightableLinks( - links=[LinkedText(str(m.monster_id), puzzledragonx(m)) for m in alt_monsters], + links=[LinkedText(str(m.monster_no_na), puzzledragonx(m)) for m in alt_monsters], highlighted=next(i for i, mon in enumerate(alt_monsters) if m.monster_id == mon.monster_id) ) ) From bd129a92d99b3502ee3058a32b243e432264c675 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 25 Jan 2021 05:57:58 -0700 Subject: [PATCH 0481/1220] Improve skillup/evo embed (#599) * Fix Skillup embed * Requested Changes * Requested Changes --- dadguide/models/monster_model.py | 5 ++++ padinfo/id_menu.py | 51 ++++++++++++++------------------ padinfo/padinfo.py | 14 +++++---- padinfo/view/materials.py | 37 +++++++++++++++-------- 4 files changed, 62 insertions(+), 45 deletions(-) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index ff6fbb90..0fc0ad20 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -135,6 +135,11 @@ def awakening_restricted_latents(self): def history_us(self): return '[{}] New Added'.format(self.reg_date) + @property + def stackable(self): + return any([t in [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] + for t in self.types]) and self.level == 1 + def awakening_count(self, awid): return len([x for x in self.awakenings if x.awoken_skill_id == awid]) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 93942ffc..98cc19c9 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -1,22 +1,19 @@ import random -import urllib.parse from typing import TYPE_CHECKING import discord -import prettytable from discord import Color -from redbot.core.utils.chat_formatting import box from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createSingleMultiplierText from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.materials import MaterialView from padinfo.view.evos import EvosView from padinfo.view.id import IdView from padinfo.view.leader_skill import LeaderSkillView from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView +from padinfo.view.materials import MaterialView from padinfo.view.otherinfo import OtherInfoView from padinfo.view.pantheon import PantheonView from padinfo.view.pic import PicsView @@ -25,12 +22,6 @@ from dadguide.database_context import DbContext from dadguide.models.monster_model import MonsterModel -INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' - -YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' -SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' -ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' - class IdMenu: def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = None): @@ -74,29 +65,33 @@ async def make_evo_embed_v2(self, m: "MonsterModel"): e = EvosView.embed(m, alt_versions, gem_versions, color) return e.to_embed() - def _add_mats_of_list(self, embed, monster_id_list, field_name): - if not len(monster_id_list): - return - field_data = '' - if len(monster_id_list) > 5: - field_data = '{} monsters'.format(len(monster_id_list)) - else: - item_count = min(len(monster_id_list), 5) - monster_list = [self.db_context.graph.get_monster(m) for m in monster_id_list] - for ae in sorted(monster_list, key=lambda x: x.monster_no_na, reverse=True)[:item_count]: - field_data += "{}\n".format(MonsterHeader.long(ae, link=True)) - embed.add_field(name=field_name, value=field_data) - async def make_evo_mats_embed(self, m: "MonsterModel"): mats = self.db_context.graph.evo_mats_by_monster(m) usedin = self.db_context.graph.material_of_monsters(m) evo_gem = self.db_context.graph.evo_gem_monster(m) gemusedin = self.db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] - skillups = [su for su in self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) - if self.db_context.graph.monster_is_farmable_evo(su) and su != m] if m.active_skill else [] + skillups = [] + skillup_evo_count = 0 + + if m.active_skill: + sums = [m for m in self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) + if self.db_context.graph.monster_is_farmable_evo(m)] + sugs = [self.db_context.graph.evo_gem_monster(su) for su in sums] + vsums = [] + for su in sums: + if not any(susu in vsums for susu in self.db_context.graph.get_alt_monsters(su)): + vsums.append(su) + skillups = [su for su in vsums + if self.db_context.graph.monster_is_farmable_evo(su) and + self.db_context.graph.get_base_id(su) != self.db_context.graph.get_base_id(m) and + su not in sugs] if m.active_skill else [] + skillup_evo_count = len(sums) - len(vsums) + + if not any([mats, usedin, gemusedin, skillups and not m.stackable]): + return None + link = "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - - return MaterialView.embed(m, mats, usedin, gemusedin, skillups, color).to_embed() + return MaterialView.embed(m, color, mats, usedin, gemusedin, skillups, skillup_evo_count, link).to_embed() async def make_pantheon_embed(self, m: "MonsterModel"): full_pantheon = self.db_context.get_monsters_by_series(m.series_id) @@ -104,7 +99,7 @@ async def make_pantheon_embed(self, m: "MonsterModel"): if len(pantheon_list) == 0 or len(pantheon_list) > 20: return None - series_name = self.db_context.graph.get_monster(m.monster_no).series.name + series_name = m.series.name_en color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return PantheonView.embed(m, color, pantheon_list, series_name).to_embed() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 63ac73f1..28c453fa 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -377,7 +377,9 @@ async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" m, err, debug_info = await self.findMonsterCustom(ctx, query) if m is not None: - await self._do_idmenu(ctx, m, self.mats_emoji) + menu = await self._do_idmenu(ctx, m, self.mats_emoji) + if menu == EMBED_NOT_GENERATED: + await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) else: await self.makeFailureMsg(ctx, query, err) @@ -433,6 +435,7 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): mats_embed = await menu.make_evo_mats_embed(m) pic_embed = await menu.make_picture_embed(m) other_info_embed = await menu.make_otherinfo_embed(m) + pantheon_embed = await menu.make_pantheon_embed(m) emoji_to_embed = OrderedDict() @@ -450,9 +453,9 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): emoji_to_embed[self.id_emoji] = id_embed emoji_to_embed[self.evo_emoji] = evo_embed - emoji_to_embed[self.mats_emoji] = mats_embed + if mats_embed: + emoji_to_embed[self.mats_emoji] = mats_embed emoji_to_embed[self.pic_emoji] = pic_embed - pantheon_embed = await menu.make_pantheon_embed(m) if pantheon_embed: emoji_to_embed[self.pantheon_emoji] = pantheon_embed @@ -1046,7 +1049,8 @@ async def _findMonster3(self, query): if t not in DGCOG.index2.all_modifiers: self.settings.add_typo_mod(t) - # print(mod_tokens, name_query_tokens) + print(mod_tokens, name_query_tokens) + print(mod_tokens, name_query_tokens) if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) @@ -1066,7 +1070,7 @@ async def _findMonster3(self, query): # no modifiers match any monster in the evo tree return - # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. mon = max(monster_gen, diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 2f505536..4a429418 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -3,6 +3,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.menu import EmbedView +from discordmenu.embed.text import LinkedText from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer @@ -15,28 +16,40 @@ MAX_MONS_TO_SHOW = 5 -def mat_use_field(mons, title): +def mat_use_field(mons, title, max_mons=MAX_MONS_TO_SHOW): + text = None + if len(mons) == 0: + text = "None" + elif len(mons) > max_mons: + text = f"({len(mons) - max_mons} more monster{'s' if len(mons) - max_mons > 1 else ''})" return EmbedField( title, - Box(*((MonsterHeader.short_with_emoji(em) for em in mons) - if len(mons) <= MAX_MONS_TO_SHOW - else [f"{len(mons)} monsters"]), - "None" if not mons else None)) + Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:max_mons]), text)) -def skillup_field(mons): + +def skillup_field(mons, sec, link): text = None + text2 = None if len(mons) == 0: text = "None" + + if sec: + text2 = Box( + f"({max(len(mons) - MAX_MONS_TO_SHOW, 0) + sec} ", + LinkedText(f"more monster{'s' if max(len(mons) - MAX_MONS_TO_SHOW, 0) + sec > 1 else ''}", link), + f", incl. {sec} alt evo{'s' if sec > 1 else ''})", + delimiter="") elif len(mons) > MAX_MONS_TO_SHOW: - text = f"({len(mons) - MAX_MONS_TO_SHOW} more monsters)" + text2 = f"({len(mons) - MAX_MONS_TO_SHOW} more monsters)" return EmbedField( "Skillups", - Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:MAX_MONS_TO_SHOW]), text)) + Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:MAX_MONS_TO_SHOW]), text, text2)) + class MaterialView: @staticmethod - def embed(m: "MonsterModel", mats, usedin, gemusedin, skillups, color): + def embed(m: "MonsterModel", color, mats, usedin, gemusedin, skillups, sec, link): return EmbedView( EmbedMain( color=color, @@ -46,9 +59,9 @@ def embed(m: "MonsterModel", mats, usedin, gemusedin, skillups, color): embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=pad_info_footer(), embed_fields=[f for f in [ - mat_use_field(mats, "Evo materials"), - mat_use_field(usedin, "Material for") if usedin else None, + mat_use_field(mats, "Evo materials") if mats or not m.stackable else None, + mat_use_field(usedin, "Material for", 10) if usedin else None, mat_use_field(gemusedin, "Evo gem is mat for") if gemusedin else None, - skillup_field(skillups) + skillup_field(skillups, sec, link) if not m.stackable else None ] if f is not None] ) From 4fc2663fcc26cddb0de3fc7b4fdd3b4c7ac192d3 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 25 Jan 2021 06:01:14 -0700 Subject: [PATCH 0482/1220] Write lssingle with new syntax (#600) * Write lssingle with new syntax * Remove imports --- padinfo/id_menu.py | 38 +++--------------------------------- padinfo/view/leader_skill.py | 18 ++++++++++++++--- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 98cc19c9..b5c3d68d 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -4,13 +4,9 @@ import discord from discord import Color -from padinfo.common.external_links import puzzledragonx -from padinfo.core.leader_skills import createSingleMultiplierText -from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage from padinfo.view.evos import EvosView from padinfo.view.id import IdView -from padinfo.view.leader_skill import LeaderSkillView +from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillSingleView from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView from padinfo.view.materials import MaterialView @@ -29,15 +25,6 @@ def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = N self.db_context = db_context self.allowed_emojis = allowed_emojis - async def make_base_embed(self, m: "MonsterModel"): - header = MonsterHeader.long(m) - embed = await self.make_custom_embed() - embed.set_thumbnail(url=MonsterImage.icon(m)) - embed.title = header - embed.url = puzzledragonx(m) - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed - async def get_user_embed_color(self, pdicog): color = await pdicog.config.user(self.ctx.author).color() if color is None: @@ -124,24 +111,5 @@ async def make_links_embed(self, m: "MonsterModel"): return LinksView.embed(m, color).to_embed() async def make_lssingle_embed(self, m: "MonsterModel"): - multiplier_text = createSingleMultiplierText(m.leader_skill) - - embed = await self.make_custom_embed() - embed.title = '{}\n\n'.format(multiplier_text) - description = '' - description += '\n**{}**\n{}'.format( - MonsterHeader.short(m, link=True), - m.leader_skill.desc if m.leader_skill else 'None') - embed.description = description - - return embed - - async def make_custom_embed(self): - pdicog = self.ctx.bot.get_cog("PadInfo") - color = await pdicog.config.user(self.ctx.author).color() - if color is None: - return discord.Embed() - elif color == "random": - return discord.Embed(color=random.randint(0x000000, 0xffffff)) - else: - return discord.Embed(color=discord.Color(color)) + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return LeaderSkillSingleView.embed(m, color).to_embed() diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 993d9195..4243618c 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -5,7 +5,7 @@ from discordmenu.embed.menu import EmbedView from discordmenu.embed.text import BoldText, Text -from padinfo.core.leader_skills import createMultiplierText +from padinfo.core.leader_skills import createMultiplierText, createSingleMultiplierText from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: @@ -25,5 +25,17 @@ def embed(left_m: "MonsterModel", right_m: "MonsterModel", color): Text(lls.desc if lls else 'None'), BoldText(MonsterHeader.name(right_m, link=True, show_jp=True)), Text(rls.desc if rls else 'None')), - color=color - )) + color=color)) + + +class LeaderSkillSingleView: + @staticmethod + def embed(m: "MonsterModel", color): + ls = m.leader_skill + return EmbedView( + EmbedMain( + title=createSingleMultiplierText(ls), + description=Box( + BoldText(MonsterHeader.name(m, link=True, show_jp=True)), + Text(ls.desc if ls else 'None')), + color=color)) From 7282a8b6ce67c4a4a867e2226c7f5fda21984fde Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 25 Jan 2021 21:48:21 -0700 Subject: [PATCH 0483/1220] Fix equip important token passdown delimitation (#607) --- dadguide/monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 346a010d..5dce4896 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -116,7 +116,7 @@ async def _build_monster_index(self, monsters): ts = re.findall(r"(\w+)'s", m.name_en.lower()) for me in self.graph.get_alt_monsters(m): for t2 in ts: - if t2 in me.name_en.lower(): + if t2 in self._name_to_tokens(me.name_en.lower()): self.name_tokens[t2].add(me) else: for me in self.graph.get_alt_monsters(m): From 9c4d9557c9958d971f9f71275c1a4d8fb156a82b Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 26 Jan 2021 03:43:24 -0700 Subject: [PATCH 0484/1220] Add emoji to idmeaning (#608) * Fix equip important token passdown delimitation * Add emoji to idmeaning * Requested Changes --- padinfo/common/emoji_map.py | 37 +++++++++++++++-------- padinfo/padinfo.py | 26 ++++++++-------- padinfo/view/components/monster/emoji.py | 15 --------- padinfo/view/components/monster/header.py | 5 ++- padinfo/view/id.py | 13 ++++---- 5 files changed, 45 insertions(+), 51 deletions(-) delete mode 100644 padinfo/view/components/monster/emoji.py diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 088217a8..1ed80c84 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -1,6 +1,6 @@ -from typing import Union +from enum import Enum +from typing import Union, Optional, Literal -from discord import Emoji from discordmenu.emoji_cache import emoji_cache AWAKENING_ID_TO_EMOJI_NAME_MAP = { @@ -91,11 +91,28 @@ def awakening_restricted_latent_emoji(latent: "AwakeningRestrictedLatent"): - return str(get_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value]))) + return get_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value])) -def emoji_markdown(name_or_number: Union[str, int]): - return format_emoji(get_emoji(name_or_number)) +def get_type_emoji(type: Enum): + return get_emoji('mons_type_'+type.name.lower()) + + +def get_attribute_emoji_by_monster(monster: "MonsterModel"): + attr1 = monster.attr1.name.lower() + attr2 = monster.attr2.name.lower() + emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) + return get_emoji(emoji) + +def get_attribute_emoji_by_enum(type1: Union[Enum, Literal[False]], type2: Optional[Union[Enum, Literal[bool]]] = None): + attr1 = 'nil' if type1 is False else type1.name.lower() + attr2 = 'nil' if type2 is False else type2.name.lower() if type2 else attr1 + emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) + return get_emoji(emoji) + + +def get_awakening_emoji(awid: Union[Enum, int], default: str = None): + return get_emoji(AWAKENING_ID_TO_EMOJI_NAME_MAP.get(awid if isinstance(awid, int) else awid.value, default)) def get_emoji(name): @@ -103,11 +120,5 @@ def get_emoji(name): name = AWAKENING_ID_TO_EMOJI_NAME_MAP[name] for e in emoji_cache.custom_emojis: if e.name == name: - return e - return name - - -def format_emoji(emoji: Union[Emoji, str]): - if isinstance(emoji, str): - return f":{emoji}:" - return f"<:{emoji.name}:{emoji.id}>" + return str(e) + return ":{}:".format(name) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 28c453fa..4abc9275 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -20,6 +20,7 @@ from tabulate import tabulate from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor +from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority from padinfo.id_menu import IdMenu @@ -1185,37 +1186,36 @@ async def idmeaning(self, ctx, *, modifier): def additmods(ms, om): if len(ms) == 1: return "" - return "; Alternate Names: " + ', '.join(inline(m) for m in ms if m != om) + return "\n\tAlternate names: " + ', '.join(inline(m) for m in ms if m != om) meanings = [ *["Evo: " + k.value + additmods(v, modifier) for k, v in tms.EVO_MAP.items() if modifier in v], - *["Type: " + k.name + additmods(v, modifier) + *["Type: " + get_type_emoji(k) + ' ' + k.name + additmods(v, modifier) for k, v in tms.TYPE_MAP.items() if modifier in v], *["Misc: " + k.value + additmods(v, modifier) for k, v in tms.MISC_MAP.items() if modifier in v], - *["Awakening: " + awakenings[k.value].name_en + additmods(v, modifier) + *["Awakening: " + get_awakening_emoji(k) + ' ' + awakenings[k.value].name_en + additmods(v, modifier) for k, v in tms.AWOKEN_MAP.items() if modifier in v], - *["Main Attr: " + k.name.replace("Nil", "None") + additmods(v, modifier) + *["Main attr: " + get_attribute_emoji_by_enum(k, None) + ' ' + k.name.replace("Nil", "None") + \ + additmods(v, modifier) for k, v in tms.COLOR_MAP.items() if modifier in v], - *["Sub Attr: " + k.name.replace("Nil", "None") + additmods(v, modifier) + *["Sub attr: " + get_attribute_emoji_by_enum(False, k) + ' ' + k.name.replace("Nil", "None") + \ + additmods(v, modifier) for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], + *["Dual attr: " + get_attribute_emoji_by_enum(k[0], k[1]) + ' ' + k[0].name.replace("Nil", "None") + \ + '/' + k[1].name.replace("Nil", "None") + additmods(v, modifier) + for k, v in tms.DUAL_COLOR_MAP.items() if modifier in v], *["Series: " + series[k].name_en + additmods(v, modifier) for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", modifier)], - *["Base Rarity: " + m for m in re.findall(r"^(\d+)\*b$", modifier)], - *[f"[UNSUPPORTED] Multiple Awakenings: {m}x {awakenings[a.value].name_en}" + *["Base rarity: " + m for m in re.findall(r"^(\d+)\*b$", modifier)], + *[f"[UNSUPPORTED] Multiple awakenings: {m}x {awakenings[a.value].name_en}" f"{additmods([f'{m}*{d}' for d in v], modifier)}" for m, ag in re.findall(r"^(\d+)\*{}$".format(awokengroup), modifier) for a, v in tms.AWOKEN_MAP.items() if ag in v] ] - for k, v in tms.DUAL_COLOR_MAP.items(): - if modifier not in v: - continue - k0name = k[0].name.replace("Nil", "None") - k1name = k[1].name.replace("Nil", "None") - meanings.append("Dual Attr: " + k0name + "/" + k1name + additmods(v, modifier)) if meanings: await ctx.send("\n".join(meanings)) diff --git a/padinfo/view/components/monster/emoji.py b/padinfo/view/components/monster/emoji.py deleted file mode 100644 index 2aef29a8..00000000 --- a/padinfo/view/components/monster/emoji.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import TYPE_CHECKING - -from padinfo.common.emoji_map import get_emoji - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - - -class MonsterEmoji: - @staticmethod - def attribute(monster: "MonsterModel"): - attr1 = monster.attr1.name.lower() - attr2 = monster.attr2.name.lower() - emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) - return get_emoji(emoji) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 4dbbc3d9..18f50860 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -3,9 +3,8 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import LinkedText, Text -from padinfo.common.emoji_map import format_emoji +from padinfo.common.emoji_map import get_attribute_emoji_by_monster from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.monster.emoji import MonsterEmoji if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -49,7 +48,7 @@ def long_v2(m: "MonsterModel", link=False): def short_with_emoji(m: "MonsterModel", link=True): msg = f"{m.monster_no_na} - {m.name_en}" return Box( - Text(format_emoji(MonsterEmoji.attribute(m))), + Text(get_attribute_emoji_by_monster(m)), LinkedText(msg, puzzledragonx(m)) if link else Text(msg), Text(MonsterHeader.jp_suffix(m, False)), delimiter=' ' diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 769adfea..be1ed256 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -5,7 +5,7 @@ from discordmenu.embed.menu import EmbedView from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText -from padinfo.common.emoji_map import get_emoji, format_emoji, AWAKENING_ID_TO_EMOJI_NAME_MAP, emoji_markdown +from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createMultiplierText from padinfo.view.components.base import pad_info_footer @@ -18,16 +18,15 @@ def _get_awakening_text(awakening: "AwakeningModel"): - return format_emoji(get_emoji( - AWAKENING_ID_TO_EMOJI_NAME_MAP.get(awakening.awoken_skill_id, awakening.name))) + return get_awakening_emoji(awakening.awoken_skill_id, awakening.name) def _killer_latent_emoji(latent_name: str): - return emoji_markdown('latent_killer_{}'.format(latent_name.lower())) + return get_emoji('latent_killer_{}'.format(latent_name.lower())) def _get_awakening_emoji_for_stats(m: "MonsterModel", i: int): - return emoji_markdown(i) if m.awakening_count(i) and not m.is_equip else '' + return get_awakening_emoji(i) if m.awakening_count(i) and not m.is_equip else '' def _get_stat_text(stat, lb_stat, icon): @@ -56,7 +55,7 @@ def awakenings_row(m: "MonsterModel"): return Box( Box(*[Text(e) for e in normal_awakenings_emojis], delimiter=' '), Box( - Text(emoji_markdown('sa_questionmark')), + Text(get_emoji('sa_questionmark')), *[Text(e) for e in super_awakenings_emojis], delimiter=' ') if len(super_awakenings_emojis) > 0 else None ) @@ -101,7 +100,7 @@ def stats(m: "MonsterModel"): @staticmethod def stats_header(m: "MonsterModel"): - voice = emoji_markdown(63) if m.awakening_count(63) and not m.is_equip else '' + voice = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' header = Box( Text(voice), Text('Stats'), From 31f6a7b94caef1ca4852b90976e45f518b852d8b Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 26 Jan 2021 04:14:08 -0700 Subject: [PATCH 0485/1220] Add icon to lookup (#612) --- padinfo/padinfo.py | 2 +- padinfo/view/lookup.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4abc9275..766f0763 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -384,7 +384,7 @@ async def evomats(self, ctx, *, query: str): else: await self.makeFailureMsg(ctx, query, err) - @commands.command() + @commands.command(aliases=["series"]) @checks.bot_has_permissions(embed_links=True) async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index 0449aeb9..85096bd0 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -1,10 +1,11 @@ from typing import TYPE_CHECKING -from discordmenu.embed.components import EmbedMain +from discordmenu.embed.components import EmbedMain, EmbedAuthor from discordmenu.embed.menu import EmbedView from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -14,7 +15,8 @@ class LookupView: @staticmethod def embed(m: "MonsterModel", color): return EmbedView( - EmbedMain( - color=color, - title=MonsterHeader.long_v2(m).to_markdown(), - url=puzzledragonx(m))) + EmbedMain(color=color), + embed_author=EmbedAuthor( + MonsterHeader.long_v2(m).to_markdown(), + puzzledragonx(m), + MonsterImage.icon(m))) From 737401b2914c4f44933e9bb435378133938f9acb Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 26 Jan 2021 17:16:35 -0600 Subject: [PATCH 0486/1220] Print evo gem ID number in evomats (#616) --- padinfo/id_menu.py | 3 ++- padinfo/view/materials.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index b5c3d68d..ffd8b02a 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -56,6 +56,7 @@ async def make_evo_mats_embed(self, m: "MonsterModel"): mats = self.db_context.graph.evo_mats_by_monster(m) usedin = self.db_context.graph.material_of_monsters(m) evo_gem = self.db_context.graph.evo_gem_monster(m) + gemid = str(evo_gem.monster_no_na) if evo_gem else None gemusedin = self.db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] skillups = [] skillup_evo_count = 0 @@ -78,7 +79,7 @@ async def make_evo_mats_embed(self, m: "MonsterModel"): return None link = "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return MaterialView.embed(m, color, mats, usedin, gemusedin, skillups, skillup_evo_count, link).to_embed() + return MaterialView.embed(m, color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link).to_embed() async def make_pantheon_embed(self, m: "MonsterModel"): full_pantheon = self.db_context.get_monsters_by_series(m.series_id) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 4a429418..811743c7 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -49,7 +49,7 @@ def skillup_field(mons, sec, link): class MaterialView: @staticmethod - def embed(m: "MonsterModel", color, mats, usedin, gemusedin, skillups, sec, link): + def embed(m: "MonsterModel", color, mats, usedin, gemid, gemusedin, skillups, sec, link): return EmbedView( EmbedMain( color=color, @@ -61,7 +61,7 @@ def embed(m: "MonsterModel", color, mats, usedin, gemusedin, skillups, sec, link embed_fields=[f for f in [ mat_use_field(mats, "Evo materials") if mats or not m.stackable else None, mat_use_field(usedin, "Material for", 10) if usedin else None, - mat_use_field(gemusedin, "Evo gem is mat for") if gemusedin else None, + mat_use_field(gemusedin, "Evo gem ({}) is mat for".format(gemid)) if gemusedin else None, skillup_field(skillups, sec, link) if not m.stackable else None ] if f is not None] ) From 47f6f6e9b2e2ad638c76ed30d1de788ba2928e80 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 26 Jan 2021 19:19:26 -0700 Subject: [PATCH 0487/1220] Do a second pass without mwtokens (#618) * Add icon to lookup * Do a second pass without mwtokens * Only do second pass if mwtokens --- padinfo/core/find_monster.py | 9 +++------ padinfo/padinfo.py | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index d1c02be8..2b2664e6 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import Set +from typing import Set, List, Tuple from Levenshtein import jaro_winkler @@ -30,7 +30,7 @@ class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 - def _merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): + def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result = [] s = 0 multi_word_tokens_sorted = sorted(valid_multi_word_tokens, @@ -66,10 +66,7 @@ def _monster_has_token(self, monster, token, monsterscore, monster_mods): return True return False - def interpret_query(self, raw_query: str, index2) -> (Set[str], Set[str]): - tokenized_query = raw_query.split() - tokenized_query = self._merge_multi_word_tokens(tokenized_query, index2.multi_word_tokens) - + def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], Set[str], Set[str]]: modifiers = [] negative_modifiers = set() name = set() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 766f0763..afd9ee08 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1042,7 +1042,17 @@ async def _findMonster3(self, query): await DGCOG.wait_until_ready() query = rmdiacritics(query).lower() - mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(query, DGCOG.index2) + tokenized_query = query.split() + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, DGCOG.index2.multi_word_tokens) + + return max( + await self.find_monster_search(tokenized_query, DGCOG), + await self.find_monster_search(mw_tokenized_query, DGCOG) + if tokenized_query != mw_tokenized_query else (None, None), + )[1] + + async def find_monster_search(self, tokenized_query, DGCOG): + mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(tokenized_query, DGCOG.index2) name_query_tokens.difference_update({'|'}) @@ -1051,13 +1061,12 @@ async def _findMonster3(self, query): self.settings.add_typo_mod(t) print(mod_tokens, name_query_tokens) - print(mod_tokens, name_query_tokens) if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) if monster_gen is None: # No monsters match the given name tokens - return + return None, None else: # There are no name tokens in the query monster_gen = {*DGCOG.database.get_all_monsters()} @@ -1069,7 +1078,7 @@ async def _findMonster3(self, query): DGCOG.index2.modifiers) if not monster_gen: # no modifiers match any monster in the evo tree - return + return None, None print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) @@ -1087,7 +1096,7 @@ async def _findMonster3(self, query): m.rarity, m.monster_no_na)) - return mon + return monster_score[mon], mon @commands.command(aliases=["iddebug"]) async def debugid(self, ctx, *, query): From b4d8e930bf4b6882e8fe01a1c2924da043ac92ac Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 26 Jan 2021 19:28:58 -0700 Subject: [PATCH 0488/1220] Fix comparison code (#619) --- padinfo/padinfo.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index afd9ee08..02b4b8bb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -353,7 +353,7 @@ async def _do_id3(self, ctx, query): await ctx.send("The NA ID and JP ID of this card differ! " "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + \ (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if - ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) + ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) if await self.config.do_survey(): asyncio.create_task(self.send_survey_after(ctx, query, m)) @@ -1048,7 +1048,7 @@ async def _findMonster3(self, query): return max( await self.find_monster_search(tokenized_query, DGCOG), await self.find_monster_search(mw_tokenized_query, DGCOG) - if tokenized_query != mw_tokenized_query else (None, None), + if tokenized_query != mw_tokenized_query else (0.0, None), )[1] async def find_monster_search(self, tokenized_query, DGCOG): @@ -1066,7 +1066,7 @@ async def find_monster_search(self, tokenized_query, DGCOG): monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) if monster_gen is None: # No monsters match the given name tokens - return None, None + return 0, None else: # There are no name tokens in the query monster_gen = {*DGCOG.database.get_all_monsters()} @@ -1078,7 +1078,7 @@ async def find_monster_search(self, tokenized_query, DGCOG): DGCOG.index2.modifiers) if not monster_gen: # no modifiers match any monster in the evo tree - return None, None + return 0, None print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) @@ -1086,7 +1086,8 @@ async def find_monster_search(self, tokenized_query, DGCOG): mon = max(monster_gen, key=lambda m: (monster_score[m], not m.is_equip, - bool(m.monster_id > 10000 and re.search(r"\d{4}", query)), # Match na on id overlap + # Match na on id overlap + bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), series_priority.get(m.series.series_type), m.on_na if m.series.series_type == "collab" else 0, DGCOG.database.graph.monster_is_rem_evo(m), From b48d488d55e8dfe15a7627273c81af3df3a803d9 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 26 Jan 2021 20:17:59 -0700 Subject: [PATCH 0489/1220] Add rest of tree's name tokens to equips (#621) --- dadguide/monster_index.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 5dce4896..ce423522 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -127,6 +127,13 @@ async def _build_monster_index(self, monsters): if token not in HAZARDOUS_IN_NAME_PREFIXES: if token in known_mods: self.modifiers[m].add(token) + # Add all name tokens as treenames for equips + if m.is_equip: + for me in self.graph.get_alt_monsters(m): + if not me.is_equip: + for nt in self._get_important_tokens(me.name_en): + self.name_tokens[nt].add(m) + for token in nametokens: if m in self.name_tokens[token.lower()]: continue From f829f249fe06b1f5e35c4fc78b1b9c90555b8769 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 26 Jan 2021 20:23:18 -0700 Subject: [PATCH 0490/1220] Fix overwritten values in known_mods (#622) * Fix overwritten values in known_mods * Fix Knight Kisaragi --- dadguide/monster_index.py | 14 +++++++------- dadguide/token_mappings.py | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index ce423522..6276f30f 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -86,8 +86,8 @@ async def _build_monster_index(self, monsters): self.fluff_tokens = defaultdict(set) self.modifiers = defaultdict(set) - known_mods = [x for xs in list(MODIFIER_MAPS.values()) + list(self.series_id_to_pantheon_nickname.values()) - for x in xs] + known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() + for x in xs}.union(KNOWN_MODIFIERS) async for m in AsyncIter(monsters): self.modifiers[m] = await self.get_modifiers(m) @@ -101,12 +101,13 @@ async def _build_monster_index(self, monsters): self.name_tokens['jp'+str(m.monster_no_jp)].add(m) # Name and Fluff Tokens - manual = False nametokens = self._name_to_tokens(m.name_en) + list(self.monster_id_to_nametokens[m.monster_id]) for me in self.graph.get_alt_ids_by_id(m.monster_id): for t in self.monster_id_to_nametokens[me]: if t in nametokens: self.name_tokens[t].add(m) + if token not in HAZARDOUS_IN_NAME_PREFIXES and token in known_mods: + self.modifiers[m].add(t) if not self.monster_id_to_nametokens[m.monster_id]: for token in self._get_important_tokens(m.name_en) + self._name_to_tokens(m.roma_subname): self.name_tokens[token.lower()].add(m) @@ -124,9 +125,8 @@ async def _build_monster_index(self, monsters): self.name_tokens[token].add(me) for repl in self.replacement_tokens[token.lower()]: self.name_tokens[repl].add(me) - if token not in HAZARDOUS_IN_NAME_PREFIXES: - if token in known_mods: - self.modifiers[m].add(token) + if token not in HAZARDOUS_IN_NAME_PREFIXES and token in known_mods: + self.modifiers[m].add(token) # Add all name tokens as treenames for equips if m.is_equip: for me in self.graph.get_alt_monsters(m): @@ -140,7 +140,7 @@ async def _build_monster_index(self, monsters): self.fluff_tokens[token.lower()].add(m) for repl in self.replacement_tokens[token.lower()]: self.fluff_tokens[repl].add(m) - if token in known_mods: + if token not in HAZARDOUS_IN_NAME_PREFIXES and token in known_mods: self.modifiers[m].add(token) # Monster Nickname diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index f66b0c77..8b7785c5 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -261,15 +261,15 @@ class MiscModifiers(Enum): 'mega awoken' }} -MODIFIER_MAPS = { - **COLOR_MAP, - **SUB_COLOR_MAP, - **DUAL_COLOR_MAP, - **TYPE_MAP, - **AWOKEN_MAP, - **EVO_MAP, - **MISC_MAP, -} +KNOWN_MODIFIERS = {v for vs in { + *COLOR_MAP.values(), + *SUB_COLOR_MAP.values(), + *DUAL_COLOR_MAP.values(), + *TYPE_MAP.values(), + *AWOKEN_MAP.values(), + *EVO_MAP.values(), + *MISC_MAP.values(), +} for v in vs} COLOR_TOKENS = { *sum(COLOR_MAP.values(), ()), From 1445a8bfc66a58d27da491700f3e11399e3c28b1 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 00:16:11 -0700 Subject: [PATCH 0491/1220] Refactor Index (#623) * Refactor Index * Add comments and rename variables * Add a massive comment right in the middle of the file * Add issue * hhhh --- dadguide/monster_index.py | 124 +++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 6276f30f..f5819f81 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -35,9 +35,8 @@ async def __ainit__(self, monsters, db): self.replacement_tokens = defaultdict(set) - nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET) - for m_id, name, *data in nickname_data: - lp, i, *_ = data + [None, None] + nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 4) + for m_id, name, lp, i in nickname_data: if m_id.isdigit() and not i: if lp: self.monster_id_to_nametokens[int(m_id)].update(self._name_to_tokens(name)) @@ -46,11 +45,10 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_nickname[int(m_id)].add(name.lower().replace(" ", "")) - treenames_data = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET) - for m_id, name, *data in treenames_data: - lp, i, *_ = data + [None, None] + treenames_data = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 4) + for m_id, name, mp, i in treenames_data: if m_id.isdigit() and not i: - if lp: + if mp: for em_id in self.graph.get_alt_ids_by_id(int(m_id)): self.monster_id_to_nametokens[em_id].update(self._name_to_tokens(name)) else: @@ -58,21 +56,25 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) - pantheon_data = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET) - for sid, name, *_ in pantheon_data: + pantheon_data = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, 2) + for sid, name in pantheon_data: if sid.isdigit(): if " " in name: self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) - nt_alias_data = await sheet_to_reader(NAME_TOKEN_ALIAS_SHEET) + nt_alias_data = await sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2) next(nt_alias_data) # Skip over heading - for token, alias, *_ in nt_alias_data: + for token, alias in nt_alias_data: self.replacement_tokens[token].add(alias) + self._known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() + for x in xs}.union(KNOWN_MODIFIERS) + self.manual = self.name_tokens = self.fluff_tokens = self.modifiers = defaultdict(set) await self._build_monster_index(monsters) - self.manual = combine_tokens(self.manual_nick, self.manual_tree) + + self.manual = combine_tokens_dicts(self.manual_nick, self.manual_tree) self.all_name_tokens = list(self.manual) + list(self.fluff_tokens) + list(self.name_tokens) self.all_modifiers = {p for ps in self.modifiers.values() for p in ps} self.suffixes = LEGAL_END_TOKENS @@ -86,9 +88,6 @@ async def _build_monster_index(self, monsters): self.fluff_tokens = defaultdict(set) self.modifiers = defaultdict(set) - known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() - for x in xs}.union(KNOWN_MODIFIERS) - async for m in AsyncIter(monsters): self.modifiers[m] = await self.get_modifiers(m) @@ -97,64 +96,57 @@ async def _build_monster_index(self, monsters): if m.monster_id > 10000: self.name_tokens[str(m.monster_id)].add(m) if m.monster_no_na != m.monster_no_jp: - self.name_tokens['na'+str(m.monster_no_na)].add(m) - self.name_tokens['jp'+str(m.monster_no_jp)].add(m) + self.name_tokens['na' + str(m.monster_no_na)].add(m) + self.name_tokens['jp' + str(m.monster_no_jp)].add(m) - # Name and Fluff Tokens + # Name Tokens nametokens = self._name_to_tokens(m.name_en) + list(self.monster_id_to_nametokens[m.monster_id]) + # Propagate name tokens throughout all evos for me in self.graph.get_alt_ids_by_id(m.monster_id): for t in self.monster_id_to_nametokens[me]: if t in nametokens: - self.name_tokens[t].add(m) - if token not in HAZARDOUS_IN_NAME_PREFIXES and token in known_mods: - self.modifiers[m].add(t) + self.add_name_token(self.name_tokens, t, m) if not self.monster_id_to_nametokens[m.monster_id]: + # Add important name tokens for token in self._get_important_tokens(m.name_en) + self._name_to_tokens(m.roma_subname): - self.name_tokens[token.lower()].add(m) - for repl in self.replacement_tokens[token.lower()]: - self.name_tokens[repl].add(m) + self.add_name_token(self.name_tokens, token, m) if m.is_equip: - ts = re.findall(r"(\w+)'s", m.name_en.lower()) - for me in self.graph.get_alt_monsters(m): - for t2 in ts: - if t2 in self._name_to_tokens(me.name_en.lower()): - self.name_tokens[t2].add(me) + possessives = re.findall(r"(\w+)'s", m.name_en.lower()) + for mevo in self.graph.get_alt_monsters(m): + for token2 in possessives: + if token2 in self._name_to_tokens(mevo.name_en.lower()): + self.add_name_token(self.name_tokens, token2, mevo) else: - for me in self.graph.get_alt_monsters(m): - if token in self._name_to_tokens(me.name_en): - self.name_tokens[token].add(me) - for repl in self.replacement_tokens[token.lower()]: - self.name_tokens[repl].add(me) - if token not in HAZARDOUS_IN_NAME_PREFIXES and token in known_mods: - self.modifiers[m].add(token) - # Add all name tokens as treenames for equips + for mevo in self.graph.get_alt_monsters(m): + if token in self._name_to_tokens(mevo.name_en): + self.add_name_token(self.name_tokens, token, mevo) + + # For equips only, add every name token from every other non-equip monster in the tree. + # This has the effect of making automated name tokens behave slightly more like treenames + # as opposed to nicknames, but only when dealing with equips, and is valuable so that we get + # the moving-through-tree effect with higher priority, but without having to add + # significantly more complicated logic in the lookup later on. + # Test case: Mizutsune is a nickname for Dark Aurora, ID 4148. Issue: #614 if m.is_equip: - for me in self.graph.get_alt_monsters(m): - if not me.is_equip: - for nt in self._get_important_tokens(me.name_en): - self.name_tokens[nt].add(m) + for mevo in self.graph.get_alt_monsters(m): + if not mevo.is_equip: + for token2 in self._get_important_tokens(mevo.name_en): + self.add_name_token(self.name_tokens, token2, m) + # Fluff tokens for token in nametokens: if m in self.name_tokens[token.lower()]: continue - self.fluff_tokens[token.lower()].add(m) - for repl in self.replacement_tokens[token.lower()]: - self.fluff_tokens[repl].add(m) - if token not in HAZARDOUS_IN_NAME_PREFIXES and token in known_mods: - self.modifiers[m].add(token) + self.add_name_token(self.fluff_tokens, token, m) # Monster Nickname for nick in self.monster_id_to_nickname[m.monster_id]: - self.manual_nick[nick].add(m) - if nick in known_mods: - self.modifiers[m].add(nick) + self.add_name_token(self.manual_nick, nick, m) # Tree Nickname base_id = self.graph.get_base_id(m) for nick in self.monster_id_to_treename[base_id]: - self.manual_tree[nick].add(m) - if nick in known_mods: - self.modifiers[m].add(nick) + self.add_name_token(self.manual_tree, nick, m) @staticmethod def _name_to_tokens(oname): @@ -316,20 +308,30 @@ async def get_modifiers(self, m): return modifiers + def add_name_token(self, token_dict, token, m): + for t in self.replacement_tokens[token.lower()].union({token.lower()}): + token_dict[t].add(m) + if t.lower() in self._known_mods and t.lower() not in HAZARDOUS_IN_NAME_PREFIXES: + self.modifiers[m].add(t.lower()) + # TODO: Move this to TSUtils -async def sheet_to_reader(url): +async def sheet_to_reader(url, length=None): async with aiohttp.ClientSession() as session: async with session.get(url) as response: file = io.StringIO(await response.text()) - return csv.reader(file, delimiter=',') - - -def combine_tokens(d1, d2): - do = defaultdict(set, d1) - for k, v in d2.items(): - do[k].update(v) - return do + if length is None: + return csv.reader(file, delimiter=',') + else: + return ((line + [None] * length)[:length] for line in csv.reader(file, delimiter=',')) + + +def combine_tokens_dicts(d1, *ds): + combined = defaultdict(set, d1) + for d2 in ds: + for k, v in d2.items(): + combined[k].update(v) + return combined def tcount(tstr): From e8c28fcd128b451f5e148f7b540830f7f0171b4b Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 00:32:26 -0700 Subject: [PATCH 0492/1220] Propagate name tokens when consistency after commas (#625) --- dadguide/monster_index.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index f5819f81..3c5825c3 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -101,13 +101,24 @@ async def _build_monster_index(self, monsters): # Name Tokens nametokens = self._name_to_tokens(m.name_en) + list(self.monster_id_to_nametokens[m.monster_id]) + last_token = m.name_en.split(',')[-1].strip() + autotoken = True + # Propagate name tokens throughout all evos - for me in self.graph.get_alt_ids_by_id(m.monster_id): - for t in self.monster_id_to_nametokens[me]: + for me in self.graph.get_alt_monsters(m): + if last_token != me.name_en.split(',')[-1].strip(): + autotoken = False + for t in self.monster_id_to_nametokens[me.monster_id]: if t in nametokens: self.add_name_token(self.name_tokens, t, m) - if not self.monster_id_to_nametokens[m.monster_id]: - # Add important name tokens + + # Add important tokens + if autotoken: + # Add a consistant last token as important token + for token in self._name_to_tokens(m.name_en.split(',')[-1].strip()): + self.add_name_token(self.name_tokens, token, m) + elif not self.monster_id_to_nametokens[m.monster_id]: + # Add name tokens by guessing which ones are important for token in self._get_important_tokens(m.name_en) + self._name_to_tokens(m.roma_subname): self.add_name_token(self.name_tokens, token, m) if m.is_equip: From 338aa338c4968d0a4dfb7e547b8cf6b65765e890 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 00:53:13 -0700 Subject: [PATCH 0493/1220] Stop making unnecessary index1 instances (#626) --- padinfo/padinfo.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 02b4b8bb..ec4dbeff 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -152,9 +152,6 @@ def __init__(self, bot, *args, **kwargs): self.settings = PadInfoSettings("padinfo") - self.index_all = None - self.index_lock = asyncio.Lock() - self.menu = Menu(bot) # These emojis are the keys into the idmenu submenus @@ -188,7 +185,6 @@ def __init__(self, bot, *args, **kwargs): def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty - self.index_all = None self.historic_lookups = {} self.historic_lookups_id2 = {} self.historic_lookups_id3 = {} @@ -228,11 +224,6 @@ async def refresh_index(self): logger.info('Waiting until DG is ready') await dg_cog.wait_until_ready() - async with self.index_lock: - logger.debug('Loading ALL index') - self.index_all = await dg_cog.create_index() - logger.info('Done refreshing indexes') - def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') return dg_cog.get_monster(monster_id) @@ -1003,10 +994,10 @@ async def findMonster1(self, query): return m, err, debug_info async def _findMonster(self, query) -> "NamedMonster": - while self.index_lock.locked(): - await asyncio.sleep(1) + DGCOG = self.bot.get_cog("Dadguide") + await DGCOG.wait_until_ready() - return self.index_all.find_monster(query) + return DGCOG.index.find_monster(query) async def findMonster2(self, query): query = rmdiacritics(query) @@ -1021,10 +1012,10 @@ async def findMonster2(self, query): return m, err, debug_info async def _findMonster2(self, query): - while self.index_lock.locked(): - await asyncio.sleep(1) + DGCOG = self.bot.get_cog("Dadguide") + await DGCOG.wait_until_ready() - return self.index_all.find_monster2(query) + return DGCOG.index.find_monster2(query) async def findMonster3(self, query): m = await self._findMonster3(query) From 24fb564148d782e9479c86e4494e424c736c0a70 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 03:38:10 -0700 Subject: [PATCH 0494/1220] Add forceindexreload & fir3 locks (#627) * Add fir locks * Fix typo --- padglobal/padglobal.py | 18 ++++++++++++++---- padinfo/padinfo.py | 28 ++++++++++------------------ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 3b7ec57e..d01b6dfa 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -114,6 +114,9 @@ def __init__(self, bot, *args, **kwargs): self.c_commands = safe_read_json(self.file_path) self.settings = PadGlobalSettings("padglobal") + self.fir_lock = asyncio.Lock() + self.fir3_lock = asyncio.Lock() + self._export_data() async def red_get_data_for_user(self, *, user_id): @@ -291,19 +294,26 @@ def list_or_none(li): @commands.command(aliases=['fir']) @is_padglobal_admin() async def forceindexreload(self, ctx): - async with ctx.typing(): + if self.fir_lock.locked(): + await ctx.send("Index is already being reloaded.") + return + + async with ctx.typing(), self.fir_lock: start = time.perf_counter() await ctx.send('Starting reload...') dadguide_cog = self.bot.get_cog('Dadguide') await dadguide_cog.reload_config_files() - padinfo_cog = self.bot.get_cog('PadInfo') - await padinfo_cog.refresh_index() + await dadguide_cog.wait_until_ready() await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) @commands.command(aliases=['fir3']) @is_padglobal_admin() async def forceindexreload3(self, ctx): - async with ctx.typing(): + if self.fir3_lock.locked(): + await ctx.send("Index2 is already being reloaded.") + return + + async with ctx.typing(), self.fir3_lock: start = time.perf_counter() dadguide_cog = self.bot.get_cog('Dadguide') dadguide_cog.index2 = await dadguide_cog.create_index2() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ec4dbeff..8a16b92f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -8,7 +8,7 @@ from collections import OrderedDict, defaultdict from datetime import datetime from io import BytesIO -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple, Optional import discord import tsutils @@ -181,7 +181,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite={}) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -208,22 +208,14 @@ async def reload_nicknames(self): try: emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) - await self.refresh_index() + dg_cog = self.bot.get_cog('Dadguide') + await dg_cog.wait_until_ready() except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) await asyncio.sleep(wait_time) - async def refresh_index(self): - """Refresh the monster indexes.""" - dg_cog = self.bot.get_cog('Dadguide') - if not dg_cog: - logger.warning("Cog 'Dadguide' not loaded") - return - logger.info('Waiting until DG is ready') - await dg_cog.wait_until_ready() - def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') return dg_cog.get_monster(monster_id) @@ -342,7 +334,7 @@ async def _do_id3(self, ctx, query): m = await self.findMonster3(query) if m and m.monster_no_na != m.monster_no_jp: await ctx.send("The NA ID and JP ID of this card differ! " - "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + \ + "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) if await self.config.do_survey(): @@ -1026,7 +1018,7 @@ async def findMonster3(self, query): return m - async def _findMonster3(self, query): + async def _findMonster3(self, query) -> Optional["MonsterModel"]: DGCOG = self.bot.get_cog("Dadguide") if DGCOG is None: raise ValueError("Dadguide cog is not loaded") @@ -1042,7 +1034,7 @@ async def _findMonster3(self, query): if tokenized_query != mw_tokenized_query else (0.0, None), )[1] - async def find_monster_search(self, tokenized_query, DGCOG): + async def find_monster_search(self, tokenized_query, DGCOG) -> Tuple[int, Optional["MonsterModel"]]: mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(tokenized_query, DGCOG.index2) name_query_tokens.difference_update({'|'}) @@ -1198,13 +1190,13 @@ def additmods(ms, om): for k, v in tms.MISC_MAP.items() if modifier in v], *["Awakening: " + get_awakening_emoji(k) + ' ' + awakenings[k.value].name_en + additmods(v, modifier) for k, v in tms.AWOKEN_MAP.items() if modifier in v], - *["Main attr: " + get_attribute_emoji_by_enum(k, None) + ' ' + k.name.replace("Nil", "None") + \ + *["Main attr: " + get_attribute_emoji_by_enum(k, None) + ' ' + k.name.replace("Nil", "None") + additmods(v, modifier) for k, v in tms.COLOR_MAP.items() if modifier in v], - *["Sub attr: " + get_attribute_emoji_by_enum(False, k) + ' ' + k.name.replace("Nil", "None") + \ + *["Sub attr: " + get_attribute_emoji_by_enum(False, k) + ' ' + k.name.replace("Nil", "None") + additmods(v, modifier) for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], - *["Dual attr: " + get_attribute_emoji_by_enum(k[0], k[1]) + ' ' + k[0].name.replace("Nil", "None") + \ + *["Dual attr: " + get_attribute_emoji_by_enum(k[0], k[1]) + ' ' + k[0].name.replace("Nil", "None") + '/' + k[1].name.replace("Nil", "None") + additmods(v, modifier) for k, v in tms.DUAL_COLOR_MAP.items() if modifier in v], *["Series: " + series[k].name_en + additmods(v, modifier) From dc774e923fa0f148a4f8e01720578aad65fea4a8 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 04:51:38 -0700 Subject: [PATCH 0495/1220] add idtest name/fluff (#628) --- padinfo/padinfo.py | 83 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8a16b92f..1f4f4d63 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -181,7 +181,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite={}) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -680,12 +680,29 @@ async def idtest(self, ctx): @idtest.command(name="add") async def idt_add(self, ctx, id: int, *, query): + """Add a test for the id3 test suite""" async with self.config.test_suite() as suite: suite[query] = {'result': id, 'ts': datetime.now().timestamp()} await ctx.tick() + @idtest.group(name="name", aliases=["fluff"]) + async def idt_name(self, ctx): + """Name/Fluff subcommands""" + + @idt_name.command(name="add") + async def idtn_add(self, ctx, id: int, token): + """Add a name token test to the id3 test suite""" + async with self.config.fluff_suite() as suite: + suite.append({ + 'id': id, + 'token': token, + 'fluff': 'fluff' in ctx.message.content + }) + await ctx.tick() + @idtest.command(name="import") async def idt_import(self, ctx, *, queries): + """Import id3 tests""" cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) async with self.config.test_suite() as suite: for query, result, reason in cases: @@ -694,6 +711,7 @@ async def idt_import(self, ctx, *, queries): @idtest.command(name="remove", aliases=["delete", "rm"]) async def idt_remove(self, ctx, *, item): + """Remove an id3 test""" async with self.config.test_suite() as suite: if item in suite: del suite[item] @@ -704,8 +722,18 @@ async def idt_remove(self, ctx, *, item): return await ctx.tick() + @idt_name.command(name="remove", aliases=["rm", "delete"]) + async def idtn_remove(self, ctx, *, item: int): + """Remove a name/fluff test""" + async with self.config.fluff_suite() as suite: + if item >= len(suite): + await ctx.send("There are not that many items.") + suite.remove(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item]) + await ctx.tick() + @idtest.command(name="setreason", aliases=["addreason"]) async def idt_setreason(self, ctx, number: int, *, reason): + """Set a reason for an id3 test case""" async with self.config.test_suite() as suite: if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") @@ -715,6 +743,7 @@ async def idt_setreason(self, ctx, number: int, *, reason): @idtest.command(name="list") async def idt_list(self, ctx): + """List id3 tests""" suite = await self.config.test_suite() o = "" ml = len(max(suite, key=len)) @@ -725,8 +754,22 @@ async def idt_list(self, ctx): for page in pagify(o): await ctx.send(box(page)) + @idt_name.command(name="list") + async def idtn_list(self, ctx): + """List name/fluff tests""" + suite = await self.config.fluff_suite() + o = "" + for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ + f"\t{'fluff' if v['fluff'] else 'name'}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + @idtest.command(name="listrecent") async def idt_listrecent(self, ctx, count: int = 0): + """List recent id3 tests""" suite = await self.config.test_suite() if count == 0: count = len(suite) @@ -741,10 +784,12 @@ async def idt_listrecent(self, ctx, count: int = 0): @idtest.command(name="run", aliases=["test"]) async def idt_run(self, ctx): + """Run all id3 tests""" suite = await self.config.test_suite() c = 0 o = "" ml = len(max(suite, key=len)) + 2 + rcircle = '\N{LARGE RED CIRCLE}' async with ctx.typing(): for q, r in suite.items(): m = await self.findMonster3(q) @@ -752,16 +797,48 @@ async def idt_run(self, ctx): if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{q.ljust(ml)} - Ex: {r['result']}, Ac: {mid}{reason}\n" + o += f"{q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" + else: + c += 1 + if c != len(suite): + o += f"\n\nTests complete. {c}/{len(suite)} succeeded." + else: + o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded" + random.choice('.....!!!!!?') + for page in pagify(o): + await ctx.send(box(page)) + + @idt_name.command(name="name") + async def idtn_run(self, ctx): + """Run all name/fluff tests""" + suite = await self.config.fluff_suite() + c = 0 + o = "" + rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' + async with ctx.typing(): + for v in suite: + fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] + + if (v['fluff'] and not fluff) or (not v['fluff'] and not name): + q = '"{}"'.format(v['token']) + o += f"{str(v['id']).ljust(4)} {q.ljust(10)} - " \ + f"{ycircle if name or fluff else rcircle} " \ + f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: c += 1 if c != len(suite): o += f"\n\nTests complete. {c}/{len(suite)} succeeded." else: - o += "\n\nAll tests succeeded." + o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded." for page in pagify(o): await ctx.send(box(page)) + @idtest.command(name="runall") + async def idt_runall(self, ctx): + """Run all tests""" + await self.idtn_run(ctx) + await self.idt_run(ctx) + @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" From bc9f006d61840da14901e7a4824f32795b67d79b Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 05:01:03 -0700 Subject: [PATCH 0496/1220] Fix command name for idtest name run (#629) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1f4f4d63..89d8dc3b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -807,7 +807,7 @@ async def idt_run(self, ctx): for page in pagify(o): await ctx.send(box(page)) - @idt_name.command(name="name") + @idt_name.command(name="run", aliases=["test"]) async def idtn_run(self, ctx): """Run all name/fluff tests""" suite = await self.config.fluff_suite() From 4ee8d2ae16ac17f1d381d671a0e9c2c0f555825a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 27 Jan 2021 06:11:18 -0600 Subject: [PATCH 0497/1220] Revert "Fix command name for idtest name run (#629)" (#630) This reverts commit bc9f006d61840da14901e7a4824f32795b67d79b. --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 89d8dc3b..1f4f4d63 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -807,7 +807,7 @@ async def idt_run(self, ctx): for page in pagify(o): await ctx.send(box(page)) - @idt_name.command(name="run", aliases=["test"]) + @idt_name.command(name="name") async def idtn_run(self, ctx): """Run all name/fluff tests""" suite = await self.config.fluff_suite() From 8ac74cb2f879093d5b7f628a59f3ae958a9b3e0c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 27 Jan 2021 06:11:42 -0600 Subject: [PATCH 0498/1220] Revert "add idtest name/fluff (#628)" (#631) This reverts commit dc774e923fa0f148a4f8e01720578aad65fea4a8. --- padinfo/padinfo.py | 83 ++-------------------------------------------- 1 file changed, 3 insertions(+), 80 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1f4f4d63..8a16b92f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -181,7 +181,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite={}) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -680,29 +680,12 @@ async def idtest(self, ctx): @idtest.command(name="add") async def idt_add(self, ctx, id: int, *, query): - """Add a test for the id3 test suite""" async with self.config.test_suite() as suite: suite[query] = {'result': id, 'ts': datetime.now().timestamp()} await ctx.tick() - @idtest.group(name="name", aliases=["fluff"]) - async def idt_name(self, ctx): - """Name/Fluff subcommands""" - - @idt_name.command(name="add") - async def idtn_add(self, ctx, id: int, token): - """Add a name token test to the id3 test suite""" - async with self.config.fluff_suite() as suite: - suite.append({ - 'id': id, - 'token': token, - 'fluff': 'fluff' in ctx.message.content - }) - await ctx.tick() - @idtest.command(name="import") async def idt_import(self, ctx, *, queries): - """Import id3 tests""" cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) async with self.config.test_suite() as suite: for query, result, reason in cases: @@ -711,7 +694,6 @@ async def idt_import(self, ctx, *, queries): @idtest.command(name="remove", aliases=["delete", "rm"]) async def idt_remove(self, ctx, *, item): - """Remove an id3 test""" async with self.config.test_suite() as suite: if item in suite: del suite[item] @@ -722,18 +704,8 @@ async def idt_remove(self, ctx, *, item): return await ctx.tick() - @idt_name.command(name="remove", aliases=["rm", "delete"]) - async def idtn_remove(self, ctx, *, item: int): - """Remove a name/fluff test""" - async with self.config.fluff_suite() as suite: - if item >= len(suite): - await ctx.send("There are not that many items.") - suite.remove(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item]) - await ctx.tick() - @idtest.command(name="setreason", aliases=["addreason"]) async def idt_setreason(self, ctx, number: int, *, reason): - """Set a reason for an id3 test case""" async with self.config.test_suite() as suite: if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") @@ -743,7 +715,6 @@ async def idt_setreason(self, ctx, number: int, *, reason): @idtest.command(name="list") async def idt_list(self, ctx): - """List id3 tests""" suite = await self.config.test_suite() o = "" ml = len(max(suite, key=len)) @@ -754,22 +725,8 @@ async def idt_list(self, ctx): for page in pagify(o): await ctx.send(box(page)) - @idt_name.command(name="list") - async def idtn_list(self, ctx): - """List name/fluff tests""" - suite = await self.config.fluff_suite() - o = "" - for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ - f"\t{'fluff' if v['fluff'] else 'name'}\n" - if not o: - await ctx.send("There are no test cases.") - for page in pagify(o): - await ctx.send(box(page)) - @idtest.command(name="listrecent") async def idt_listrecent(self, ctx, count: int = 0): - """List recent id3 tests""" suite = await self.config.test_suite() if count == 0: count = len(suite) @@ -784,12 +741,10 @@ async def idt_listrecent(self, ctx, count: int = 0): @idtest.command(name="run", aliases=["test"]) async def idt_run(self, ctx): - """Run all id3 tests""" suite = await self.config.test_suite() c = 0 o = "" ml = len(max(suite, key=len)) + 2 - rcircle = '\N{LARGE RED CIRCLE}' async with ctx.typing(): for q, r in suite.items(): m = await self.findMonster3(q) @@ -797,48 +752,16 @@ async def idt_run(self, ctx): if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" - else: - c += 1 - if c != len(suite): - o += f"\n\nTests complete. {c}/{len(suite)} succeeded." - else: - o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded" + random.choice('.....!!!!!?') - for page in pagify(o): - await ctx.send(box(page)) - - @idt_name.command(name="name") - async def idtn_run(self, ctx): - """Run all name/fluff tests""" - suite = await self.config.fluff_suite() - c = 0 - o = "" - rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' - async with ctx.typing(): - for v in suite: - fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] - - if (v['fluff'] and not fluff) or (not v['fluff'] and not name): - q = '"{}"'.format(v['token']) - o += f"{str(v['id']).ljust(4)} {q.ljust(10)} - " \ - f"{ycircle if name or fluff else rcircle} " \ - f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" + o += f"{q.ljust(ml)} - Ex: {r['result']}, Ac: {mid}{reason}\n" else: c += 1 if c != len(suite): o += f"\n\nTests complete. {c}/{len(suite)} succeeded." else: - o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded." + o += "\n\nAll tests succeeded." for page in pagify(o): await ctx.send(box(page)) - @idtest.command(name="runall") - async def idt_runall(self, ctx): - """Run all tests""" - await self.idtn_run(ctx) - await self.idt_run(ctx) - @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" From f61c365380952c567c82ba3f7b4254cf969a4b4a Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 17:17:01 -0700 Subject: [PATCH 0499/1220] Update padinfo.py (#632) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8a16b92f..302a68c6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -181,7 +181,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite={}) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty From 53600c912a4b5261f40cec77784634dbb2197159 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 27 Jan 2021 21:16:50 -0600 Subject: [PATCH 0500/1220] Restoring fluff_suite PR from last night 1/3 #633 This reverts commit f61c365380952c567c82ba3f7b4254cf969a4b4a. --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 302a68c6..8a16b92f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -181,7 +181,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite={}) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty From 9423275822808808c0d47ac8753da14fcb8879ac Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 27 Jan 2021 21:17:41 -0600 Subject: [PATCH 0501/1220] Restoring fluff_suite PR from last night 2/3 (#634) This reverts commit 8ac74cb2f879093d5b7f628a59f3ae958a9b3e0c. --- padinfo/padinfo.py | 83 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8a16b92f..1f4f4d63 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -181,7 +181,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite={}) + self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty @@ -680,12 +680,29 @@ async def idtest(self, ctx): @idtest.command(name="add") async def idt_add(self, ctx, id: int, *, query): + """Add a test for the id3 test suite""" async with self.config.test_suite() as suite: suite[query] = {'result': id, 'ts': datetime.now().timestamp()} await ctx.tick() + @idtest.group(name="name", aliases=["fluff"]) + async def idt_name(self, ctx): + """Name/Fluff subcommands""" + + @idt_name.command(name="add") + async def idtn_add(self, ctx, id: int, token): + """Add a name token test to the id3 test suite""" + async with self.config.fluff_suite() as suite: + suite.append({ + 'id': id, + 'token': token, + 'fluff': 'fluff' in ctx.message.content + }) + await ctx.tick() + @idtest.command(name="import") async def idt_import(self, ctx, *, queries): + """Import id3 tests""" cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) async with self.config.test_suite() as suite: for query, result, reason in cases: @@ -694,6 +711,7 @@ async def idt_import(self, ctx, *, queries): @idtest.command(name="remove", aliases=["delete", "rm"]) async def idt_remove(self, ctx, *, item): + """Remove an id3 test""" async with self.config.test_suite() as suite: if item in suite: del suite[item] @@ -704,8 +722,18 @@ async def idt_remove(self, ctx, *, item): return await ctx.tick() + @idt_name.command(name="remove", aliases=["rm", "delete"]) + async def idtn_remove(self, ctx, *, item: int): + """Remove a name/fluff test""" + async with self.config.fluff_suite() as suite: + if item >= len(suite): + await ctx.send("There are not that many items.") + suite.remove(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item]) + await ctx.tick() + @idtest.command(name="setreason", aliases=["addreason"]) async def idt_setreason(self, ctx, number: int, *, reason): + """Set a reason for an id3 test case""" async with self.config.test_suite() as suite: if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") @@ -715,6 +743,7 @@ async def idt_setreason(self, ctx, number: int, *, reason): @idtest.command(name="list") async def idt_list(self, ctx): + """List id3 tests""" suite = await self.config.test_suite() o = "" ml = len(max(suite, key=len)) @@ -725,8 +754,22 @@ async def idt_list(self, ctx): for page in pagify(o): await ctx.send(box(page)) + @idt_name.command(name="list") + async def idtn_list(self, ctx): + """List name/fluff tests""" + suite = await self.config.fluff_suite() + o = "" + for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ + f"\t{'fluff' if v['fluff'] else 'name'}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + @idtest.command(name="listrecent") async def idt_listrecent(self, ctx, count: int = 0): + """List recent id3 tests""" suite = await self.config.test_suite() if count == 0: count = len(suite) @@ -741,10 +784,12 @@ async def idt_listrecent(self, ctx, count: int = 0): @idtest.command(name="run", aliases=["test"]) async def idt_run(self, ctx): + """Run all id3 tests""" suite = await self.config.test_suite() c = 0 o = "" ml = len(max(suite, key=len)) + 2 + rcircle = '\N{LARGE RED CIRCLE}' async with ctx.typing(): for q, r in suite.items(): m = await self.findMonster3(q) @@ -752,16 +797,48 @@ async def idt_run(self, ctx): if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{q.ljust(ml)} - Ex: {r['result']}, Ac: {mid}{reason}\n" + o += f"{q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" + else: + c += 1 + if c != len(suite): + o += f"\n\nTests complete. {c}/{len(suite)} succeeded." + else: + o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded" + random.choice('.....!!!!!?') + for page in pagify(o): + await ctx.send(box(page)) + + @idt_name.command(name="name") + async def idtn_run(self, ctx): + """Run all name/fluff tests""" + suite = await self.config.fluff_suite() + c = 0 + o = "" + rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' + async with ctx.typing(): + for v in suite: + fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] + + if (v['fluff'] and not fluff) or (not v['fluff'] and not name): + q = '"{}"'.format(v['token']) + o += f"{str(v['id']).ljust(4)} {q.ljust(10)} - " \ + f"{ycircle if name or fluff else rcircle} " \ + f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: c += 1 if c != len(suite): o += f"\n\nTests complete. {c}/{len(suite)} succeeded." else: - o += "\n\nAll tests succeeded." + o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded." for page in pagify(o): await ctx.send(box(page)) + @idtest.command(name="runall") + async def idt_runall(self, ctx): + """Run all tests""" + await self.idtn_run(ctx) + await self.idt_run(ctx) + @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" From 8e6abdf7a285ed7f5ff0b6ac4cc4cb8505929bd9 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 27 Jan 2021 21:18:20 -0600 Subject: [PATCH 0502/1220] Restoring fluff_suite PR from last night 3/3 (#635) This reverts commit 4ee8d2ae16ac17f1d381d671a0e9c2c0f555825a. --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1f4f4d63..89d8dc3b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -807,7 +807,7 @@ async def idt_run(self, ctx): for page in pagify(o): await ctx.send(box(page)) - @idt_name.command(name="name") + @idt_name.command(name="run", aliases=["test"]) async def idtn_run(self, ctx): """Run all name/fluff tests""" suite = await self.config.fluff_suite() From 557af61563bd7068b3406c19705e09f3e22f8259 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 27 Jan 2021 22:25:26 -0700 Subject: [PATCH 0503/1220] Add import and add to idtest name/fluff (#639) --- padinfo/padinfo.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 89d8dc3b..5470edd0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -685,7 +685,7 @@ async def idt_add(self, ctx, id: int, *, query): suite[query] = {'result': id, 'ts': datetime.now().timestamp()} await ctx.tick() - @idtest.group(name="name", aliases=["fluff"]) + @idtest.group(name="name/fluff", aliases=["name", "fluff"]) async def idt_name(self, ctx): """Name/Fluff subcommands""" @@ -696,7 +696,9 @@ async def idtn_add(self, ctx, id: int, token): suite.append({ 'id': id, 'token': token, - 'fluff': 'fluff' in ctx.message.content + 'fluff': 'fluff' in ctx.message.content, + 'reason': '', + 'ts': datetime.now().timestamp() }) await ctx.tick() @@ -709,6 +711,22 @@ async def idt_import(self, ctx, *, queries): suite[query] = {'result': int(result), 'reason': reason, 'ts': datetime.now().timestamp()} await ctx.tick() + @idt_name.command(name="import") + async def idtn_import(self, ctx, *, queries): + """Import id3 tests""" + cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+)\s+(\w*) *(.*)', queries) + async with self.config.fluff_suite() as suite: + for query, result, fluff, reason in cases: + print(query, result, fluff, reason) + if not any(c['id'] == int(result) and c['token'] == query for c in suite): + suite.append({ + 'id': int(result), + 'token': query, + 'fluff': fluff == 'fluff', + 'reason': reason, + 'ts': datetime.now().timestamp()}) + await ctx.tick() + @idtest.command(name="remove", aliases=["delete", "rm"]) async def idt_remove(self, ctx, *, item): """Remove an id3 test""" @@ -741,6 +759,16 @@ async def idt_setreason(self, ctx, number: int, *, reason): suite[sorted(suite)[number]]['reason'] = reason await ctx.tick() + @idt_name.command(name="setreason", aliases=["addreason"]) + async def idtn_setreason(self, ctx, number: int, *, reason): + """Set a reason for an name/fluff test case""" + async with self.config.fluff_suite() as suite: + if number >= len(suite): + await ctx.react_quietly("\N{CROSS MARK}") + return + sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[number]['reason'] = reason + await ctx.tick() + @idtest.command(name="list") async def idt_list(self, ctx): """List id3 tests""" @@ -761,7 +789,7 @@ async def idtn_list(self, ctx): o = "" for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ - f"\t{'fluff' if v['fluff'] else 'name'}\n" + f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): From 1574964d1d149aa33cfd1579048a3caed1c67d24 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 00:22:19 -0700 Subject: [PATCH 0504/1220] Remove id2 (#642) --- dadguide/old_monster_index.py | 86 ----------------------------------- padinfo/id_menu.py | 4 +- padinfo/padinfo.py | 52 +++++---------------- 3 files changed, 14 insertions(+), 128 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 8c746ed6..018b347a 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -330,92 +330,6 @@ def find_monster(self, query): # couldn't find anything return None, "Could not find a match for: " + query, None - def find_monster2(self, query): - """Search with alternative method for resolving prefixes. - - Implements the lookup for id2, where you are allowed to specify multiple prefixes for a card. - All prefixes are required to be exactly matched by the card. - Follows a similar logic to the regular id but after each check, will remove any potential match that doesn't - contain every single specified prefix. - """ - query = tsutils.rmdiacritics(query).lower().strip() - # id search - if query.isdigit(): - m = self.monster_no_na_to_named_monster.get(int(query)) - if m is None: - return None, 'Looks like a monster ID but was not found', None - else: - return m, None, "ID lookup" - - # handle exact nickname match - if query in self.all_entries: - return self.all_entries[query], None, "Exact nickname" - - contains_ja = tsutils.contains_ja(query) - if len(query) < 2 and contains_ja: - return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_ja: - return None, 'Your query must be at least 4 letters', None - - # we want to look up only the main part of the query, and then verify that each result has the prefixes - # so break up the query into an array of prefixes, and a string (new_query) that will be the lookup - query_prefixes = [] - parts_of_query = query.split() - new_query = '' - for i, part in enumerate(parts_of_query): - if part in self.all_prefixes: - query_prefixes.append(part) - else: - new_query = ' '.join(parts_of_query[i:]) - break - - # if we don't have any prefixes, then default to using the regular id lookup - if len(query_prefixes) < 1: - return self.find_monster(query) - - matches = PotentialMatches() - - # prefix search for ids, take max id - for nickname, m in self.all_entries.items(): - if query.endswith("base {}".format(m.monster_id)): - matches.add( - find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) - matches.update_list(query_prefixes) - - # first try to get matches from nicknames - for nickname, m in self.all_entries.items(): - if new_query in nickname: - matches.add(m) - matches.update_list(query_prefixes) - - # if we don't have any candidates yet, pick a new method - if not matches.length(): - # try matching on exact names next - for nickname, m in self.all_en_name_to_monsters.items(): - if new_query in m.name_en.lower() or new_query in m.name_ja.lower(): - matches.add(m) - matches.update_list(query_prefixes) - - # check for exact match on pantheon name but only if needed - if not matches.length(): - for pantheon in self.all_pantheon_nicknames: - if new_query == pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, - self.pantheons) - matches.update_list(query_prefixes) - - # check for any match on pantheon name, again but only if needed - if not matches.length(): - for pantheon in self.all_pantheon_nicknames: - if new_query in pantheon.lower(): - matches.get_monsters_from_potential_pantheon_match(pantheon, self.pantheon_nick_to_name, - self.pantheons) - matches.update_list(query_prefixes) - - if matches.length(): - return matches.pick_best_monster(), None, None - return None, "Could not find a match for: " + query, None - @staticmethod def pick_best_monster(named_monster_list): return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index ffd8b02a..c50e203a 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -34,7 +34,7 @@ async def get_user_embed_color(self, pdicog): else: return discord.Color(color) - async def make_id_embed_v2(self, m: "MonsterModel"): + async def make_id_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) is_transform_base = self.db_context.graph.monster_is_transform_base(m) true_evo_type_raw = self.db_context.graph.true_evo_type_by_monster(m).value @@ -45,7 +45,7 @@ async def make_id_embed_v2(self, m: "MonsterModel"): e = IdView.embed(m, color, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters) return e.to_embed() - async def make_evo_embed_v2(self, m: "MonsterModel"): + async def make_evo_embed(self, m: "MonsterModel"): alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5470edd0..676a5c6c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -173,9 +173,6 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_file_path = _data_file('historic_lookups.json') self.historic_lookups = safe_read_json(self.historic_lookups_file_path) - self.historic_lookups_file_path_id2 = _data_file('historic_lookups_id2.json') - self.historic_lookups_id2 = safe_read_json(self.historic_lookups_file_path_id2) - self.historic_lookups_file_path_id3 = _data_file('historic_lookups_id3.json') self.historic_lookups_id3 = safe_read_json(self.historic_lookups_file_path_id3) @@ -186,7 +183,6 @@ def __init__(self, bot, *args, **kwargs): def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.historic_lookups = {} - self.historic_lookups_id2 = {} self.historic_lookups_id3 = {} async def red_get_data_for_user(self, *, user_id): @@ -312,17 +308,11 @@ async def checkbadness(self, ctx): await ctx.send(f"{bad}/{good + bad} ({int(round(bad / (good + bad) * 100)) if good or bad else 'NaN'}%)") @commands.command() - @checks.bot_has_permissions(embed_links=True) + @checks.bot_has_permissions() async def id2(self, ctx, *, query: str): """Monster info (main tab)""" - await self._do_id2(ctx, query) - - async def _do_id2(self, ctx, query: str): - m, err, debug_info = await self.findMonster2(query) - if m is not None: - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await self.makeFailureMsg(ctx, query, err) + await ctx.send("id2 has been discontinued! For an even better searching experience," + " opt into the id3 beta using `{0.prefix}idset beta y`".format(ctx.prefix)) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -414,8 +404,8 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) - id_embed = await menu.make_id_embed_v2(m) - evo_embed = await menu.make_evo_embed_v2(m) + id_embed = await menu.make_id_embed(m) + evo_embed = await menu.make_evo_embed(m) mats_embed = await menu.make_evo_mats_embed(m) pic_embed = await menu.make_picture_embed(m) other_info_embed = await menu.make_otherinfo_embed(m) @@ -467,7 +457,7 @@ async def _do_evolistmenu(self, ctx, sm): return else: emoji = char_to_emoji(chars[idx]) - emoji_to_embed[emoji] = await menu.make_id_embed_v2(m) + emoji_to_embed[emoji] = await menu.make_id_embed(m) if m.monster_id == sm.monster_id: starting_menu_emoji = emoji @@ -651,8 +641,8 @@ async def leaderskill(self, ctx, *, whole_query): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = await menu.make_ls_embed(left_m, right_m) - emoji_to_embed[self.left_emoji] = await menu.make_id_embed_v2(left_m) - emoji_to_embed[self.right_emoji] = await menu.make_id_embed_v2(right_m) + emoji_to_embed[self.left_emoji] = await menu.make_id_embed(left_m) + emoji_to_embed[self.right_emoji] = await menu.make_id_embed(right_m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -669,7 +659,7 @@ async def leaderskillsingle(self, ctx, *, query): menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = await menu.make_lssingle_embed(m) - emoji_to_embed[self.left_emoji] = await menu.make_id_embed_v2(m) + emoji_to_embed[self.left_emoji] = await menu.make_id_embed(m) await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) @@ -1026,14 +1016,14 @@ async def setvoicepath(self, ctx, *, path=''): @checks.is_owner() @padinfo.command() async def iddiff(self, ctx): - """Runs the diff checker for id and id2""" + """Runs the diff checker for id and id3""" await ctx.send("Running diff checker...") hist_aggreg = list(self.historic_lookups) s = 0 f = [] async for c, query in AsyncIter(enumerate(hist_aggreg)): - m1, err1, debug_info1 = await self.findMonsterCustom(ctx, query) - m2, err2, debug_info2 = await self.findMonster2(query) + m1, err1, debug_info1 = await self.findMonster1(query) + m2, err2, debug_info2 = await self.findMonster3(query) if c % 50 == 0: await ctx.send(inline("{}/{} complete.".format(c, len(hist_aggreg)))) if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): @@ -1096,24 +1086,6 @@ async def _findMonster(self, query) -> "NamedMonster": return DGCOG.index.find_monster(query) - async def findMonster2(self, query): - query = rmdiacritics(query) - nm, err, debug_info = await self._findMonster2(query) - - monster_no = nm.monster_id if nm else -1 - self.historic_lookups_id2[query] = monster_no - json.dump(self.historic_lookups_id2, open(self.historic_lookups_file_path_id2, "w+")) - - m = self.get_monster(nm.monster_id) if nm else None - - return m, err, debug_info - - async def _findMonster2(self, query): - DGCOG = self.bot.get_cog("Dadguide") - await DGCOG.wait_until_ready() - - return DGCOG.index.find_monster2(query) - async def findMonster3(self, query): m = await self._findMonster3(query) From 42ca8607b40902762f7d25aab629433276158e77 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 00:26:15 -0700 Subject: [PATCH 0505/1220] Fix id2 discontinuation message (#643) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 676a5c6c..3e56a4bf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -312,7 +312,7 @@ async def checkbadness(self, ctx): async def id2(self, ctx, *, query: str): """Monster info (main tab)""" await ctx.send("id2 has been discontinued! For an even better searching experience," - " opt into the id3 beta using `{0.prefix}idset beta y`".format(ctx.prefix)) + " opt into the id3 beta using `{}idset beta y`".format(ctx.prefix)) @commands.command() @checks.bot_has_permissions(embed_links=True) From 49682eaf363782038464b27eb8927f6b16df7835 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 00:50:01 -0700 Subject: [PATCH 0506/1220] Store idtest lastaction (#641) * Add lastaction * Add lastaction to setreason and list * Bold test suite names --- padinfo/emojiupdaters.py | 95 +++++++++++++++++++++++++++ padinfo/padinfo.py | 135 +++++++++++++-------------------------- 2 files changed, 141 insertions(+), 89 deletions(-) create mode 100644 padinfo/emojiupdaters.py diff --git a/padinfo/emojiupdaters.py b/padinfo/emojiupdaters.py new file mode 100644 index 00000000..6bebd759 --- /dev/null +++ b/padinfo/emojiupdaters.py @@ -0,0 +1,95 @@ +from typing import TYPE_CHECKING + +from tsutils import EmojiUpdater + +if TYPE_CHECKING: + from dadguide.database_context import DbContext + from dadguide.models.monster_model import MonsterModel + + +class IdEmojiUpdater(EmojiUpdater): + def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, selected_emoji=None, bot=None, + db_context: "DbContext" = None, **kwargs): + super().__init__(emoji_to_embed, **kwargs) + self.ctx = ctx + self.emoji_dict = emoji_to_embed + self.m = m + self.pad_info = pad_info + self.selected_emoji = selected_emoji + self.bot = bot + self.db_context = db_context + + self.pad_info.settings.log_emoji("start_" + selected_emoji) + + async def on_update(self, ctx, selected_emoji): + evo_id = self.pad_info.settings.checkEvoID(ctx.author.id) + self.pad_info.settings.log_emoji(selected_emoji) + if evo_id: + evos = sorted({*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) + index = evos.index(self.m.monster_id) + if selected_emoji == self.pad_info.previous_monster_emoji: + new_id = evos[index - 1] + elif selected_emoji == self.pad_info.next_monster_emoji: + if index == len(evos) - 1: + new_id = evos[0] + else: + new_id = evos[index + 1] + else: + self.selected_emoji = selected_emoji + return True + if new_id == self.m.monster_id: + return False + self.m = self.db_context.graph.get_monster(new_id) + else: + if selected_emoji == self.pad_info.previous_monster_emoji: + prev_monster = self.db_context.graph.numeric_prev_monster(self.m) + if prev_monster is None: + return False + self.m = prev_monster + elif selected_emoji == self.pad_info.next_monster_emoji: + next_monster = self.db_context.graph.numeric_next_monster(self.m) + if next_monster is None: + return False + self.m = next_monster + else: + self.selected_emoji = selected_emoji + return True + + self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, + m=self.m, scroll=sorted( + {*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) if evo_id else [], menu_type=1) + return True + + +class ScrollEmojiUpdater(EmojiUpdater): + def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, ms: "list[int]" = None, selected_emoji=None, + pad_info=None, bot=None, **kwargs): + super().__init__(emoji_to_embed, **kwargs) + self.ctx = ctx + self.emoji_dict = emoji_to_embed + self.m = m + self.ms = ms + self.pad_info = pad_info + self.selected_emoji = selected_emoji + self.bot = bot + + async def on_update(self, ctx, selected_emoji): + index = self.ms.index(self.m) + + if selected_emoji == self.pad_info.first_monster_emoji: + self.m = self.ms[0] + elif selected_emoji == self.pad_info.previous_monster_emoji: + self.m = self.ms[index - 1] + elif selected_emoji == self.pad_info.next_monster_emoji: + if index == len(self.ms) - 1: + self.m = self.ms[0] + else: + self.m = self.ms[index + 1] + elif selected_emoji == self.pad_info.last_monster_emoji: + self.m = self.ms[-1] + else: + self.selected_emoji = selected_emoji + return True + + self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, m=self.m, scroll=self.ms) + return True diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3e56a4bf..2cdb6d14 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -23,6 +23,7 @@ from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority +from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater from padinfo.id_menu import IdMenu from padinfo.view.components.monster.header import MonsterHeader @@ -55,94 +56,6 @@ def _data_file(file_name: str) -> str: } -class IdEmojiUpdater(EmojiUpdater): - def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, selected_emoji=None, bot=None, - db_context: "DbContext" = None, **kwargs): - super().__init__(emoji_to_embed, **kwargs) - self.ctx = ctx - self.emoji_dict = emoji_to_embed - self.m = m - self.pad_info = pad_info - self.selected_emoji = selected_emoji - self.bot = bot - self.db_context = db_context - - self.pad_info.settings.log_emoji("start_" + selected_emoji) - - async def on_update(self, ctx, selected_emoji): - evo_id = self.pad_info.settings.checkEvoID(ctx.author.id) - self.pad_info.settings.log_emoji(selected_emoji) - if evo_id: - evos = sorted({*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) - index = evos.index(self.m.monster_id) - if selected_emoji == self.pad_info.previous_monster_emoji: - new_id = evos[index - 1] - elif selected_emoji == self.pad_info.next_monster_emoji: - if index == len(evos) - 1: - new_id = evos[0] - else: - new_id = evos[index + 1] - else: - self.selected_emoji = selected_emoji - return True - if new_id == self.m.monster_id: - return False - self.m = self.db_context.graph.get_monster(new_id) - else: - if selected_emoji == self.pad_info.previous_monster_emoji: - prev_monster = self.db_context.graph.numeric_prev_monster(self.m) - if prev_monster is None: - return False - self.m = prev_monster - elif selected_emoji == self.pad_info.next_monster_emoji: - next_monster = self.db_context.graph.numeric_next_monster(self.m) - if next_monster is None: - return False - self.m = next_monster - else: - self.selected_emoji = selected_emoji - return True - - self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, - m=self.m, scroll=sorted( - {*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) if evo_id else [], menu_type=1) - return True - - -class ScrollEmojiUpdater(EmojiUpdater): - def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, ms: "list[int]" = None, selected_emoji=None, - pad_info=None, bot=None, **kwargs): - super().__init__(emoji_to_embed, **kwargs) - self.ctx = ctx - self.emoji_dict = emoji_to_embed - self.m = m - self.ms = ms - self.pad_info = pad_info - self.selected_emoji = selected_emoji - self.bot = bot - - async def on_update(self, ctx, selected_emoji): - index = self.ms.index(self.m) - - if selected_emoji == self.pad_info.first_monster_emoji: - self.m = self.ms[0] - elif selected_emoji == self.pad_info.previous_monster_emoji: - self.m = self.ms[index - 1] - elif selected_emoji == self.pad_info.next_monster_emoji: - if index == len(self.ms) - 1: - self.m = self.ms[0] - else: - self.m = self.ms[index + 1] - elif selected_emoji == self.pad_info.last_monster_emoji: - self.m = self.ms[-1] - else: - self.selected_emoji = selected_emoji - return True - - self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, m=self.m, scroll=self.ms) - return True - - class PadInfo(commands.Cog): """Info for PAD Cards""" @@ -177,7 +90,7 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups_id3 = safe_read_json(self.historic_lookups_file_path_id3) self.config = Config.get_conf(self, identifier=9401770) - self.config.register_user(survey_mode=0, color=None, beta_id3=False) + self.config.register_user(survey_mode=0, color=None, beta_id3=False, lastaction=None) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) def cog_unload(self): @@ -671,6 +584,11 @@ async def idtest(self, ctx): @idtest.command(name="add") async def idt_add(self, ctx, id: int, *, query): """Add a test for the id3 test suite""" + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + async with self.config.test_suite() as suite: suite[query] = {'result': id, 'ts': datetime.now().timestamp()} await ctx.tick() @@ -682,6 +600,11 @@ async def idt_name(self, ctx): @idt_name.command(name="add") async def idtn_add(self, ctx, id: int, token): """Add a name token test to the id3 test suite""" + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to add to the fluff/name test suite?"): + return + await self.config.user(ctx.author).lastaction.set('name') + async with self.config.fluff_suite() as suite: suite.append({ 'id': id, @@ -695,6 +618,11 @@ async def idtn_add(self, ctx, id: int, token): @idtest.command(name="import") async def idt_import(self, ctx, *, queries): """Import id3 tests""" + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) async with self.config.test_suite() as suite: for query, result, reason in cases: @@ -704,6 +632,11 @@ async def idt_import(self, ctx, *, queries): @idt_name.command(name="import") async def idtn_import(self, ctx, *, queries): """Import id3 tests""" + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + return + await self.config.user(ctx.author).lastaction.set('name') + cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+)\s+(\w*) *(.*)', queries) async with self.config.fluff_suite() as suite: for query, result, fluff, reason in cases: @@ -720,6 +653,11 @@ async def idtn_import(self, ctx, *, queries): @idtest.command(name="remove", aliases=["delete", "rm"]) async def idt_remove(self, ctx, *, item): """Remove an id3 test""" + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + async with self.config.test_suite() as suite: if item in suite: del suite[item] @@ -733,6 +671,11 @@ async def idt_remove(self, ctx, *, item): @idt_name.command(name="remove", aliases=["rm", "delete"]) async def idtn_remove(self, ctx, *, item: int): """Remove a name/fluff test""" + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + return + await self.config.user(ctx.author).lastaction.set('name') + async with self.config.fluff_suite() as suite: if item >= len(suite): await ctx.send("There are not that many items.") @@ -742,6 +685,11 @@ async def idtn_remove(self, ctx, *, item: int): @idtest.command(name="setreason", aliases=["addreason"]) async def idt_setreason(self, ctx, number: int, *, reason): """Set a reason for an id3 test case""" + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + async with self.config.test_suite() as suite: if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") @@ -752,6 +700,11 @@ async def idt_setreason(self, ctx, number: int, *, reason): @idt_name.command(name="setreason", aliases=["addreason"]) async def idtn_setreason(self, ctx, number: int, *, reason): """Set a reason for an name/fluff test case""" + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + return + await self.config.user(ctx.author).lastaction.set('name') + async with self.config.fluff_suite() as suite: if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") @@ -762,6 +715,8 @@ async def idtn_setreason(self, ctx, number: int, *, reason): @idtest.command(name="list") async def idt_list(self, ctx): """List id3 tests""" + await self.config.user(ctx.author).lastaction.set('id3') + suite = await self.config.test_suite() o = "" ml = len(max(suite, key=len)) @@ -775,6 +730,8 @@ async def idt_list(self, ctx): @idt_name.command(name="list") async def idtn_list(self, ctx): """List name/fluff tests""" + await self.config.user(ctx.author).lastaction.set('name') + suite = await self.config.fluff_suite() o = "" for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): From 26d69d3d9c15d6596222428325eda071d902382e Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 03:30:58 -0700 Subject: [PATCH 0507/1220] Move padglobal admin system to global admin (#645) --- crud/crud.py | 2 +- padglobal/padglobal.py | 43 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index c3e10ae9..ec3b1a67 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -49,7 +49,7 @@ def __init__(self, bot, *args, **kwargs): GADMIN_COG.register_perm("crud") else: raise errors.CogLoadError("Global Administration cog must be loaded. Make sure it's " - "installed from misc-cogs, and load it via `^load globaladmin`") + "installed from misc-cogs and load it via `^load globaladmin`") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index d01b6dfa..71b81840 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -5,20 +5,21 @@ import json import logging import os -import pytz import re -import time from collections import defaultdict from io import StringIO, BytesIO import aiohttp import discord import prettytable +import pytz +import time import tsutils from redbot.core import checks, data_manager -from redbot.core import commands +from redbot.core import commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta -from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json +from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json, \ + auth_check logger = logging.getLogger('red.padbot-cogs.padglobal') @@ -62,14 +63,6 @@ def mod_help(self, ctx, help_type): commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, "short_doc") -def is_padglobal_admin_check(ctx): - return checks.is_owner() or ctx.bot.get_cog("PadGlobal").settings.check_admin(ctx.author.id) - - -def is_padglobal_admin(): - return commands.check(is_padglobal_admin_check) - - async def lookup_named_monster(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') if padinfo_cog is None: @@ -110,6 +103,14 @@ def __init__(self, bot, *args, **kwargs): global PADGLOBAL_COG PADGLOBAL_COG = self self.bot = bot + + GADMIN_COG = self.bot.get_cog("GlobalAdmin") + if GADMIN_COG: + GADMIN_COG.register_perm("contentadmin") + else: + raise errors.CogLoadError("Global Administration cog must be loaded. Make sure it's " + "installed from misc-cogs and load it via `^load globaladmin`") + self.file_path = _data_file('commands.json') self.c_commands = safe_read_json(self.file_path) self.settings = PadGlobalSettings("padglobal") @@ -158,7 +159,7 @@ def _export_data(self): json.dump(results, f, indent=4) @commands.command() - @is_padglobal_admin() + @auth_check('contentadmin') async def breakglass(self, ctx, *, reason: str): """Shuts down the bot, for emergency use only. @@ -198,7 +199,7 @@ async def togglepadglobal(self, ctx): await ctx.send(inline('PAD Global commands {} on this server').format(status)) @commands.command() - @is_padglobal_admin() + @auth_check('contentadmin') async def debugid1dump(self, ctx): padinfo_cog = self.bot.get_cog('PadInfo') mi = padinfo_cog.index_all @@ -215,7 +216,7 @@ async def write_send(nn_map, file_name): await write_send(mi.two_word_entries, 'two_word_entries.csv') @commands.command(aliases=['iddebug1']) - @is_padglobal_admin() + @auth_check('contentadmin') async def debugid1(self, ctx, *, query): padinfo_cog = self.bot.get_cog('PadInfo') # m is a named monster @@ -292,7 +293,7 @@ def list_or_none(li): delete_msgs=sent_messages, timeout=30) @commands.command(aliases=['fir']) - @is_padglobal_admin() + @auth_check('contentadmin') async def forceindexreload(self, ctx): if self.fir_lock.locked(): await ctx.send("Index is already being reloaded.") @@ -307,7 +308,7 @@ async def forceindexreload(self, ctx): await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) @commands.command(aliases=['fir3']) - @is_padglobal_admin() + @auth_check('contentadmin') async def forceindexreload3(self, ctx): if self.fir3_lock.locked(): await ctx.send("Index2 is already being reloaded.") @@ -320,7 +321,7 @@ async def forceindexreload3(self, ctx): await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) @commands.group(aliases=['pdg']) - @is_padglobal_admin() + @auth_check('contentadmin') async def padglobal(self, ctx): """PAD global custom commands.""" @@ -740,9 +741,7 @@ async def which(self, ctx, *, term: str = None): await ctx.send(self.emojify(definition)) async def _resolve_which(self, ctx, term): - db_context = self.bot.get_cog('Dadguide').database - db_context: "DbContext" term = term.lower().replace('?', '') nm, _, _ = await lookup_named_monster(term) @@ -893,7 +892,7 @@ async def getwhich(self, ctx): await ctx.send(box(page)) @commands.command(aliases=['lookupdebug']) - @is_padglobal_admin() + @auth_check('contentadmin') async def debuglookup(self, ctx, *, term: str): """Shows why a query matches to a monster""" term = term.lower().replace('?', '') @@ -1275,7 +1274,7 @@ async def whichinvade(self, ctx): totime = curtime.replace(hour=6, minute=0, second=0, microsecond=0) if totime < curtime: totime += datetime.timedelta(1) - await ctx.send(inline("Invade switches in: " + humanize_timedelta(timedelta=totime-curtime))) + await ctx.send(inline("Invade switches in: " + humanize_timedelta(timedelta=totime - curtime))) def emojify(self, message): emojis = list() From d2f187f1db9364e60c01fbf71be36c8838ca7ae4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 28 Jan 2021 04:36:03 -0600 Subject: [PATCH 0508/1220] Add docstring to ^whichinvade (#646) --- padglobal/padglobal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 71b81840..6698f701 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1264,6 +1264,7 @@ async def rmleaderguide(self, ctx, monster_id: int): @commands.command(aliases=['currentinvade']) async def whichinvade(self, ctx): + """Display which yinyangdra is currently invading for Mystics & Spectres event""" pst = pytz.timezone("America/Los_Angeles") curtime = datetime.datetime.now(pst) if datetime.time(6) < curtime.time() < datetime.time(18): From e2d3d20a89ad87d1eef1e097f06df4303a9d2c93 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 04:03:50 -0700 Subject: [PATCH 0509/1220] Give ^idtest add a helpful confirmation (#644) * Add lastaction * Add lastaction to setreason and list * Bold test suite names * idtest add conformation * Update pypi version * Requested Changes * Fix idtest add * idtest fluff add old thing * add confirmation * Make confirmation really really long * 0 index * Requested Changes --- dadguide/info.json | 2 +- padinfo/padinfo.py | 55 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/dadguide/info.json b/dadguide/info.json index 41e2545d..48de9dd2 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils>=3.0.0", + "tsutils>=3.2.0", "pytz", "romkan", "networkx" diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2cdb6d14..b6f7dc33 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -590,8 +590,21 @@ async def idt_add(self, ctx, id: int, *, query): await self.config.user(ctx.author).lastaction.set('id3') async with self.config.test_suite() as suite: + oldd = suite.get(query, {}) + if oldd['result'] == id: + await ctx.send(f"This test case already exists with id `{sorted(suite).index(query)}`.") + return suite[query] = {'result': id, 'ts': datetime.now().timestamp()} - await ctx.tick() + + if await tsutils.get_reaction(ctx, f"Added with id `{sorted(suite).index(query)}`", + "\N{LEFTWARDS ARROW WITH HOOK}"): + if oldd: + suite[query] = oldd + else: + del suite[query] + await ctx.react_quietly("\N{CROSS MARK}") + else: + await ctx.tick() @idtest.group(name="name/fluff", aliases=["name", "fluff"]) async def idt_name(self, ctx): @@ -605,15 +618,43 @@ async def idtn_add(self, ctx, id: int, token): return await self.config.user(ctx.author).lastaction.set('name') + fluffy = 'fluff' in ctx.message.content async with self.config.fluff_suite() as suite: - suite.append({ + if any(t['id'] == id and t['token'] == token and t['fluff'] == fluffy for t in suite): + await ctx.send("This test already exists.") + return + + old = None + if any(t['id'] == id and t['token'] == token for t in suite): + old = [t for t in suite if t['id'] == id and t['token'] == token][0] + if not await tsutils.confirm_message(ctx, f"Are you sure you want to change" + f" the type of test case #{suite.index(old)}" + f" `{id} - {token}` from " + f" **{'fluff' if fluffy else 'name'}** to" + f" **{'name' if fluffy else 'fluff'}**?"): + await ctx.react_quietly("\N{CROSS MARK}") + return + suite.remove(old) + + case = { 'id': id, 'token': token, - 'fluff': 'fluff' in ctx.message.content, + 'fluff': fluffy, 'reason': '', 'ts': datetime.now().timestamp() - }) - await ctx.tick() + } + + suite.append(case) + suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) + + if await tsutils.get_reaction(ctx, f"Added with id `{suite.index(case)}`", "\N{LEFTWARDS ARROW WITH HOOK}"): + suite.pop() + if old: + suite.append(old) + await ctx.react_quietly("\N{CROSS MARK}") + else: + m = await ctx.send(f"Successfully added new case with id `{suite.index(case)}`") + await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") @idtest.command(name="import") async def idt_import(self, ctx, *, queries): @@ -685,6 +726,8 @@ async def idtn_remove(self, ctx, *, item: int): @idtest.command(name="setreason", aliases=["addreason"]) async def idt_setreason(self, ctx, number: int, *, reason): """Set a reason for an id3 test case""" + if reason == '""': + reason = "" if await self.config.user(ctx.author).lastaction() != 'id3' and \ not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): return @@ -700,6 +743,8 @@ async def idt_setreason(self, ctx, number: int, *, reason): @idt_name.command(name="setreason", aliases=["addreason"]) async def idtn_setreason(self, ctx, number: int, *, reason): """Set a reason for an name/fluff test case""" + if reason == '""': + reason = "" if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return From de9196458e5f06022edd2e68254aa0bc9195366a Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 04:57:07 -0700 Subject: [PATCH 0510/1220] idtest runall (#648) --- padinfo/padinfo.py | 50 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b6f7dc33..1e62cf94 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -720,6 +720,7 @@ async def idtn_remove(self, ctx, *, item: int): async with self.config.fluff_suite() as suite: if item >= len(suite): await ctx.send("There are not that many items.") + return suite.remove(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item]) await ctx.tick() @@ -856,8 +857,53 @@ async def idtn_run(self, ctx): @idtest.command(name="runall") async def idt_runall(self, ctx): """Run all tests""" - await self.idtn_run(ctx) - await self.idt_run(ctx) + rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' + + qsuite = await self.config.test_suite() + qo = "" + qc = 0 + ml = len(max(qsuite, key=len)) + 2 + async with ctx.typing(): + for c, q in enumerate(sorted(qsuite)): + m = await self.findMonster3(q) + mid = m and m.monster_id + if m is not None and m.monster_id != qsuite[q]['result'] or m is None and qsuite[q]['result'] >= 0: + reason = ' Reason: ' + r.get('reason') if qsuite[q].get('reason') else '' + q = '"' + q + '"' + qo += f"{str(c).rjust(4)}. {q.ljust(ml)} - {rcircle} Ex: {qsuite[q]['result']}, Ac: {mid}{reason}\n" + else: + qc += 1 + + fsuite = await self.config.fluff_suite() + fo = "" + fc = 0 + async with ctx.typing(): + for c, v in enumerate(fsuite): + fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] + + if (v['fluff'] and not fluff) or (not v['fluff'] and not name): + q = '"{}"'.format(v['token']) + fo += f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml-5)} - " \ + f"{ycircle if name or fluff else rcircle} " \ + f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" + else: + fc += 1 + + o = "" + if fo: + o += "[Failed Token Tests]\n" + fo + if qo: + o += "\n[Failed Query Tests]\n" + qo + + if qc + fc != len(fsuite) + len(qsuite): + o += f"\n\nTests complete. {qc + fc}/{len(fsuite) + len(qsuite)} succeeded." + else: + o += "\n\n\N{LARGE GREEN CIRCLE} \N{LARGE GREEN CIRCLE} All tests succeeded" \ + + random.choice(['.']*5 + ['!!']*5 + ['???']) + for page in pagify(o): + await ctx.send(box(page)) + @commands.command() async def padsay(self, ctx, server, *, query: str = None): From 0549d0dc219b7fdf112ec32b388bbf66ef18f6f2 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 05:34:50 -0700 Subject: [PATCH 0511/1220] Split name and fluff (#649) * Split name and fluff * exclusive -> inclusive --- padinfo/padinfo.py | 74 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1e62cf94..1dc2d75d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -606,19 +606,30 @@ async def idt_add(self, ctx, id: int, *, query): else: await ctx.tick() - @idtest.group(name="name/fluff", aliases=["name", "fluff"]) + @idtest.group(name="name") async def idt_name(self, ctx): - """Name/Fluff subcommands""" + """Name subcommands""" + + @idtest.group(name="fluff") + async def idt_fluff(self, ctx): + """Fluff subcommands""" @idt_name.command(name="add") async def idtn_add(self, ctx, id: int, token): """Add a name token test to the id3 test suite""" + await self.norf_add(ctx, id, token, False) + + @idt_fluff.command(name="add") + async def idtf_add(self, ctx, id: int, token): + """Add a fluff token test to the id3 test suite""" + await self.norf_add(ctx, id, token, True) + + async def norf_add(self, ctx, id: int, token, fluffy): if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.confirm_message(ctx, "Are you sure you want to add to the fluff/name test suite?"): return await self.config.user(ctx.author).lastaction.set('name') - fluffy = 'fluff' in ctx.message.content async with self.config.fluff_suite() as suite: if any(t['id'] == id and t['token'] == token and t['fluff'] == fluffy for t in suite): await ctx.send("This test already exists.") @@ -672,7 +683,15 @@ async def idt_import(self, ctx, *, queries): @idt_name.command(name="import") async def idtn_import(self, ctx, *, queries): - """Import id3 tests""" + """Import name/fluff tests""" + await self.norf_import(ctx, id, queries) + + @idt_fluff.command(name="import") + async def idtf_import(self, ctx, *, queries): + """Import name/fluff tests""" + await self.norf_import(ctx, id, queries) + + async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return @@ -709,9 +728,17 @@ async def idt_remove(self, ctx, *, item): return await ctx.tick() - @idt_name.command(name="remove", aliases=["rm", "delete"]) + @idt_name.command(name="remove") async def idtn_remove(self, ctx, *, item: int): """Remove a name/fluff test""" + await self.norf_remove(ctx, item) + + @idt_fluff.command(name="remove") + async def idtf_remove(self, ctx, *, item: int): + """Remove a name/fluff test""" + await self.norf_remove(ctx, item) + + async def norf_remove(self, ctx, item): if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return @@ -741,9 +768,17 @@ async def idt_setreason(self, ctx, number: int, *, reason): suite[sorted(suite)[number]]['reason'] = reason await ctx.tick() - @idt_name.command(name="setreason", aliases=["addreason"]) + @idt_name.command(name="setreason") async def idtn_setreason(self, ctx, number: int, *, reason): """Set a reason for an name/fluff test case""" + await self.norf_setreason(ctx, number, reason) + + @idt_fluff.command(name="setreason") + async def idtf_setreason(self, ctx, number: int, *, reason): + """Set a reason for an name/fluff test case""" + await self.norf_setreason(ctx, number, reason) + + async def norf_setreason(self, ctx, number, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'name' and \ @@ -774,15 +809,25 @@ async def idt_list(self, ctx): await ctx.send(box(page)) @idt_name.command(name="list") - async def idtn_list(self, ctx): + async def idtn_list(self, ctx, inclusive: bool = False): + """List name tests""" + await self.norf_list(ctx, False, inclusive) + + @idt_fluff.command(name="list") + async def idtf_list(self, ctx, inclusive: bool = False): + """List fluff tests""" + await self.norf_list(ctx, True, inclusive) + + async def norf_list(self, ctx, fluff, inclusive): """List name/fluff tests""" await self.config.user(ctx.author).lastaction.set('name') suite = await self.config.fluff_suite() o = "" for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ - f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" + if inclusive or v['fluff'] == fluff: + o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ + f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): @@ -828,8 +873,17 @@ async def idt_run(self, ctx): for page in pagify(o): await ctx.send(box(page)) - @idt_name.command(name="run", aliases=["test"]) + @idt_name.command(name="run") async def idtn_run(self, ctx): + """Run all name/fluff tests""" + await self.norf_run(ctx) + + @idt_fluff.command(name="run") + async def idtf_run(self, ctx): + """Run all name/fluff tests""" + await self.norf_run(ctx) + + async def norf_run(self, ctx): """Run all name/fluff tests""" suite = await self.config.fluff_suite() c = 0 From 774f9dfa46981e998d7a61020713c297b0f7520c Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 18:08:26 -0700 Subject: [PATCH 0512/1220] Add story modifier (#651) --- dadguide/monster_graph.py | 35 ++++++++++++++++++------- dadguide/monster_index.py | 16 +++++++++++ dadguide/token_mappings.py | 3 ++- tests/dadguide/database_context_test.py | 2 +- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 3230b2cc..81ea639d 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -293,6 +293,9 @@ def get_transform_tree(self, monster_id): ids.add(mid) return ids + def get_transform_monsters(self, monster): + return {self.get_monster(m) for m in self.get_transform_tree(monster.monster_id)} + def process_alt_versions(self, monster_id): ids = set() to_check = {monster_id} @@ -348,7 +351,7 @@ def get_transform_base_id_by_id(self, monster_id): curr = monster_id while curr not in seen: seen.add(curr) - next_ids = self.get_prev_transforms_by_monster_id(monster_id) + next_ids = self.get_prev_transform_ids_by_monster_id(monster_id) if next_ids: curr = next_ids.pop() else: @@ -413,35 +416,49 @@ def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: def true_evo_type_by_monster(self, monster: MonsterModel) -> InternalEvoType: return self.true_evo_type_by_monster_id(monster.monster_no) - def get_prev_evolution_by_monster_id(self, monster_id): + def get_prev_evolution_id_by_monster_id(self, monster_id): bes = self._get_edges(self.graph[monster_id], 'back_evolution') if bes: return bes.pop() return None + def get_prev_evolution_id_by_monster(self, monster: MonsterModel): + return self.get_prev_evolution_id_by_monster_id(monster.monster_no) + def get_prev_evolution_by_monster(self, monster: MonsterModel): - return self.get_prev_evolution_by_monster_id(monster.monster_no) + pe = self.get_prev_evolution_id_by_monster(monster) + return pe and self.get_monster(pe) - def get_next_evolutions_by_monster_id(self, monster_id): + def get_next_evolution_ids_by_monster_id(self, monster_id): return self._get_edges(self.graph[monster_id], 'evolution') + def get_next_evolution_ids_by_monster(self, monster: MonsterModel): + return self.get_next_evolution_ids_by_monster_id(monster.monster_no) + def get_next_evolutions_by_monster(self, monster: MonsterModel): - return self.get_next_evolutions_by_monster_id(monster.monster_no) + return {self.get_monster(mid) for mid in self.get_next_evolution_ids_by_monster(monster)} - def get_prev_transforms_by_monster_id(self, monster_id): + def get_prev_transform_ids_by_monster_id(self, monster_id): return self._get_edges(self.graph[monster_id], 'back_transformation') + def get_prev_transform_ids_by_monster(self, monster: MonsterModel): + return self.get_prev_transform_ids_by_monster_id(monster.monster_no) + def get_prev_transforms_by_monster(self, monster: MonsterModel): - return self.get_prev_evolution_by_monster_id(monster.monster_no) + return {self.get_monster(mid) for mid in self.get_prev_transform_ids_by_monster(monster)} - def get_next_transform_by_monster_id(self, monster_id): + def get_next_transform_id_by_monster_id(self, monster_id): bes = self._get_edges(self.graph[monster_id], 'transformation') if bes: return bes.pop() return None + def get_next_transform_id_by_monster(self, monster: MonsterModel): + return self.get_next_transform_id_by_monster_id(monster.monster_no) + def get_next_transform_by_monster(self, monster: MonsterModel): - return self.get_next_evolutions_by_monster_id(monster.monster_no) + nt = self.get_next_transform_id_by_monster(monster) + return nt and self.get_monster(nt) def evo_mats_by_monster_id(self, monster_id: int) -> List[MonsterModel]: evo = self.get_evo_by_monster_id(monster_id) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 3c5825c3..ae6e4bb5 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -286,6 +286,22 @@ async def get_modifiers(self, m): for t in MISC_MAP[MiscModifiers.CHIBI]: modifiers.add(t) + # Story + def is_story(m, do_transform=True): + if m.series_id == 196 or any(mat.series_id == 196 for mat in self.graph.evo_mats_by_monster(m)): + return True + if do_transform: + for pt in self.graph.get_transform_monsters(m): + if is_story(pt, False): + return True + pe = self.graph.get_prev_evolution_by_monster(m) + if pe and is_story(pe): + return True + return False + if is_story(m): + for t in MISC_MAP[MiscModifiers.STORY]: + modifiers.add(t) + # Method of Obtaining if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): for t in MISC_MAP[MiscModifiers.FARMABLE]: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 8b7785c5..60a169a4 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -235,7 +235,7 @@ class Awakenings(Enum): class MiscModifiers(Enum): CHIBI = 'Chibi' - NONCHIBI = 'Non-Chibi' + STORY = 'Story' FARMABLE = 'Farmable' REM = 'REM' MP = 'MP' @@ -247,6 +247,7 @@ class MiscModifiers(Enum): MISC_MAP = { MiscModifiers.CHIBI: ('chibi', 'mini'), + MiscModifiers.STORY: ('story',), MiscModifiers.FARMABLE: ('farmable',), MiscModifiers.REM: ('rem',), MiscModifiers.MP: ('mp',), diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index a444e513..6bf83f10 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -16,7 +16,7 @@ # evo types # 5392 is mega dkali -assert db_context.graph.get_prev_evolution_by_monster_id(5392) == 1588 +assert db_context.graph.get_prev_evolution_id_by_monster_id(5392) == 1588 assert db_context.graph.cur_evo_type_by_monster_id(5392) == EvoType.UvoAwoken assert db_context.graph.cur_evo_type_by_monster_id(1587) == EvoType.Base # base dkali assert db_context.graph.cur_evo_type_by_monster_id(6238) == EvoType.UuvoReincarnated # b sam3 From fcb851e541aef244c6dbb3518e6b5991a58aaea0 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 18:09:46 -0700 Subject: [PATCH 0513/1220] Fix idtest add (#654) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1dc2d75d..d249ddbb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -591,7 +591,7 @@ async def idt_add(self, ctx, id: int, *, query): async with self.config.test_suite() as suite: oldd = suite.get(query, {}) - if oldd['result'] == id: + if oldd.get('result') == id: await ctx.send(f"This test case already exists with id `{sorted(suite).index(query)}`.") return suite[query] = {'result': id, 'ts': datetime.now().timestamp()} From bf86875c0fadfbad8487ef88f64f500a460bd2f9 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 18:23:12 -0700 Subject: [PATCH 0514/1220] Fixes for idtest add (#655) * Fix idtest add * Add addreason to add * keep text * fluff --- padinfo/padinfo.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d249ddbb..e927b757 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -583,7 +583,9 @@ async def idtest(self, ctx): @idtest.command(name="add") async def idt_add(self, ctx, id: int, *, query): - """Add a test for the id3 test suite""" + """Add a test for the id3 test suite (Append `| reason` to add a reason)""" + query, *reason = query.split("|") + query = query.strip() if await self.config.user(ctx.author).lastaction() != 'id3' and \ not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): return @@ -594,7 +596,11 @@ async def idt_add(self, ctx, id: int, *, query): if oldd.get('result') == id: await ctx.send(f"This test case already exists with id `{sorted(suite).index(query)}`.") return - suite[query] = {'result': id, 'ts': datetime.now().timestamp()} + suite[query] = { + 'result': id, + 'ts': datetime.now().timestamp(), + 'reason': reason[0].strip() if reason else '' + } if await tsutils.get_reaction(ctx, f"Added with id `{sorted(suite).index(query)}`", "\N{LEFTWARDS ARROW WITH HOOK}"): @@ -604,6 +610,7 @@ async def idt_add(self, ctx, id: int, *, query): del suite[query] await ctx.react_quietly("\N{CROSS MARK}") else: + await ctx.send(f"Successfully added test case with id `{sorted(suite).index(query)}`") await ctx.tick() @idtest.group(name="name") @@ -615,16 +622,17 @@ async def idt_fluff(self, ctx): """Fluff subcommands""" @idt_name.command(name="add") - async def idtn_add(self, ctx, id: int, token): + async def idtn_add(self, ctx, id: int, token, *, reason=""): """Add a name token test to the id3 test suite""" - await self.norf_add(ctx, id, token, False) + await self.norf_add(ctx, id, token, reason, False) @idt_fluff.command(name="add") - async def idtf_add(self, ctx, id: int, token): + async def idtf_add(self, ctx, id: int, token, *, reason=""): """Add a fluff token test to the id3 test suite""" - await self.norf_add(ctx, id, token, True) + await self.norf_add(ctx, id, token, reason, True) - async def norf_add(self, ctx, id: int, token, fluffy): + async def norf_add(self, ctx, id: int, token, reason, fluffy): + reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.confirm_message(ctx, "Are you sure you want to add to the fluff/name test suite?"): return @@ -651,7 +659,7 @@ async def norf_add(self, ctx, id: int, token, fluffy): 'id': id, 'token': token, 'fluff': fluffy, - 'reason': '', + 'reason': reason, 'ts': datetime.now().timestamp() } From 59995fcaf7acb1910f1e8c8e5f371719926010f7 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Jan 2021 18:28:45 -0700 Subject: [PATCH 0515/1220] Make run set lastaction (#657) --- padinfo/padinfo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e927b757..e864cad3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -860,6 +860,7 @@ async def idt_listrecent(self, ctx, count: int = 0): async def idt_run(self, ctx): """Run all id3 tests""" suite = await self.config.test_suite() + await self.config.user(ctx.author).lastaction.set('id3') c = 0 o = "" ml = len(max(suite, key=len)) + 2 @@ -894,6 +895,7 @@ async def idtf_run(self, ctx): async def norf_run(self, ctx): """Run all name/fluff tests""" suite = await self.config.fluff_suite() + await self.config.user(ctx.author).lastaction.set('name') c = 0 o = "" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' From 1d3c84472365177dde0c5298d6a189f9346c2e7d Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 29 Jan 2021 19:21:57 -0700 Subject: [PATCH 0516/1220] make idmeaning show name token (#662) --- dadguide/monster_index.py | 4 ++++ padinfo/padinfo.py | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index ae6e4bb5..7091123b 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -28,6 +28,8 @@ async def __ainit__(self, monsters, db): for m in db.get_all_monsters()}) + self.mwtoken_creators = defaultdict(set) + self.multi_word_tokens = {tuple(m.series.name_en.lower().split()) for m in db.get_all_monsters() @@ -42,6 +44,7 @@ async def __ainit__(self, monsters, db): self.monster_id_to_nametokens[int(m_id)].update(self._name_to_tokens(name)) else: if " " in name: + self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(int(m_id))) self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_nickname[int(m_id)].add(name.lower().replace(" ", "")) @@ -53,6 +56,7 @@ async def __ainit__(self, monsters, db): self.monster_id_to_nametokens[em_id].update(self._name_to_tokens(name)) else: if " " in name: + self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(int(m_id))) self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e864cad3..4a250dfc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -16,7 +16,7 @@ from discordmenu.emoji_cache import emoji_cache from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter -from redbot.core.utils.chat_formatting import box, inline, pagify, text_to_file +from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor @@ -603,7 +603,7 @@ async def idt_add(self, ctx, id: int, *, query): } if await tsutils.get_reaction(ctx, f"Added with id `{sorted(suite).index(query)}`", - "\N{LEFTWARDS ARROW WITH HOOK}"): + "\N{LEFTWARDS ARROW WITH HOOK}"): if oldd: suite[query] = oldd else: @@ -948,7 +948,7 @@ async def idt_runall(self, ctx): if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) - fo += f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml-5)} - " \ + fo += f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)} - " \ f"{ycircle if name or fluff else rcircle} " \ f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: @@ -964,11 +964,10 @@ async def idt_runall(self, ctx): o += f"\n\nTests complete. {qc + fc}/{len(fsuite) + len(qsuite)} succeeded." else: o += "\n\n\N{LARGE GREEN CIRCLE} \N{LARGE GREEN CIRCLE} All tests succeeded" \ - + random.choice(['.']*5 + ['!!']*5 + ['???']) + + random.choice(['.'] * 5 + ['!!'] * 5 + ['???']) for page in pagify(o): await ctx.send(box(page)) - @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" @@ -1358,6 +1357,7 @@ async def exportmodifiers(self, ctx): @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod"]) async def idmeaning(self, ctx, *, modifier): + """Get all the meanings of a token (bold signifies base of a tree)""" modifier = modifier.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") tms = DGCOG.token_maps @@ -1365,6 +1365,29 @@ async def idmeaning(self, ctx, *, modifier): awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} + o = "" + + def write_name_token(dict, type, mwtoken=False): + def f(m, s): + return bold(s) if DGCOG.database.graph.monster_is_base(m) else s + o = "" + so = [] + for m in sorted(dict[modifier], key=lambda m: m.monster_id): + if (m in DGCOG.index2.mwtoken_creators[modifier]) == mwtoken: + so.append(m) + if len(so) > 5: + o += f"\n\n{type}\n" + ", ".join(f(m, str(m.monster_id)) for m in so[:10]) + o += f"... ({len(so)} total)" if len(so) > 10 else "" + elif so: + o += f"\n\n{type}\n" + "\n".join(f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) + return o + + o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", 1) + o += write_name_token(DGCOG.index2.manual, "[Manual Tokens]") + o += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") + o += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") + + def additmods(ms, om): if len(ms) == 1: return "" @@ -1399,8 +1422,9 @@ def additmods(ms, om): for a, v in tms.AWOKEN_MAP.items() if ag in v] ] - if meanings: - await ctx.send("\n".join(meanings)) + if meanings or o: + for page in pagify("\n".join(meanings) + "\n\n" + o.strip()): + await ctx.send(page) else: await ctx.send(f"There are no modifiers that match `{modifier}`.") From e1bfeaea8cf8b6744bb0a49c32c7bd714c4836d9 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 29 Jan 2021 19:24:55 -0700 Subject: [PATCH 0517/1220] IDMeaning wait until ready DG is ready (#663) * make idmeaning show name token * Wait until ready --- padinfo/padinfo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4a250dfc..b930a133 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1360,6 +1360,9 @@ async def idmeaning(self, ctx, *, modifier): """Get all the meanings of a token (bold signifies base of a tree)""" modifier = modifier.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") + + await DGCOG.wait_until_ready() + tms = DGCOG.token_maps awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_MAP.values() for aw in aws) + ")" awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} From 08cb9e3f4e2cfa1efa51cfbe4123a0647507990b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 30 Jan 2021 06:56:45 -0600 Subject: [PATCH 0518/1220] Remove PotentialMatches (was id2 only) (#666) --- dadguide/old_monster_index.py | 41 ----------------------------------- 1 file changed, 41 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 018b347a..ca61e75f 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -335,47 +335,6 @@ def pick_best_monster(named_monster_list): return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) -class PotentialMatches(object): - def __init__(self): - self.match_list = set() - - def add(self, m): - self.match_list.add(m) - - def update(self, monster_list): - self.match_list.update(monster_list) - - def length(self): - return len(self.match_list) - - def update_list(self, query_prefixes): - self._add_trees() - self._remove_any_without_all_prefixes(query_prefixes) - - def _add_trees(self): - to_add = set() - for m in self.match_list: - for evo in m.evolution_tree: - to_add.add(evo) - self.match_list.update(to_add) - - def _remove_any_without_all_prefixes(self, query_prefixes): - to_remove = set() - for m in self.match_list: - for prefix in query_prefixes: - if prefix not in m.prefixes: - to_remove.add(m) - break - self.match_list.difference_update(to_remove) - - def get_monsters_from_potential_pantheon_match(self, pantheon, pantheon_nick_to_name, pantheons): - full_name = pantheon_nick_to_name[pantheon] - self.update(pantheons[full_name]) - - def pick_best_monster(self): - return max(self.match_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) - - class NamedMonsterGroup(object): def __init__(self, evolution_tree: list, treename_overrides: list): base_monster = min(evolution_tree, key=lambda m: m.monster_id) From 7bc6a94492b1b838bbac39d9ab8999a9460ff2ce Mon Sep 17 00:00:00 2001 From: turtleworks <72906298+turtleworks@users.noreply.github.com> Date: Sat, 30 Jan 2021 17:43:58 -0500 Subject: [PATCH 0519/1220] Add daychange command to padglobal (#658) * Add daychange command to padglobal * Add daylight savings time status to daychange * Add time until DST changes to and trim output of daychange * Refactor daychange to reduce code duplication * Compact time display in daychange * Make daychange correctly state if DST will start or end * Make daychange less excited about DST --- padglobal/padglobal.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 6698f701..815d3fe5 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1277,6 +1277,47 @@ async def whichinvade(self, ctx): totime += datetime.timedelta(1) await ctx.send(inline("Invade switches in: " + humanize_timedelta(timedelta=totime - curtime))) + @commands.command(aliases=['resettime', 'newday', 'whenreset']) + async def daychange(self, ctx): + """Show DST information and how much time is left until the game day changes.""" + curtime = datetime.datetime.now(pytz.timezone("UTC")) + reset = curtime.replace(hour=8, minute=0, second=0, microsecond=0) + if reset < curtime: + reset += datetime.timedelta(1) + resetdelta = reset - curtime + # strip leftover seconds + resetdelta -= datetime.timedelta(seconds=resetdelta.total_seconds() % 60) + totalresetmins = int(resetdelta.total_seconds() // 60) + resethours = totalresetmins // 60 + newdayhours = resethours + 4 + mins = totalresetmins % 60 + + pst = datetime.datetime.now(pytz.timezone("America/Los_Angeles")) + if pst.dst(): + # earliest possible date of the first Sunday in November + dstthresh = pst.replace(month=11, day=1) + else: + # earliest possible date of the second Sunday in March + dstthresh = pst.replace(month=3, day=8) + + # calculate the day DST changes + if dstthresh < pst: + dstthresh = dstthresh.replace(year=(dstthresh.year + 1)) + # add days to make day of week equal 6 (Sunday, when Monday is 0) + dstthresh += datetime.timedelta(6 - dstthresh.weekday()) + + msg = "Reset (dungeons/events): **{}h {}m** ".format(resethours, mins) + msg += "(1:00 am PDT)" if pst.dst() else "(12:00 midnight PST)" + msg += ".\nNew day (mails): **{}h {}m** ".format(newdayhours, mins) + msg += "(5:00 am PDT)" if pst.dst() else "(4:00 am PST)" + msg += ".\nDST in North America is " + msg += "ACTIVE" if pst.dst() else "NOT ACTIVE" + msg += "! It will " + msg += "end" if pst.dst() else "start" + msg += " in " + humanize_timedelta(timedelta=dstthresh - pst) + "." + + await ctx.send(msg) + def emojify(self, message): emojis = list() emoteservers = self.settings.emojiServers() From dc307665ddbd8f4f04451c779a7edc7129c48e51 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Sun, 31 Jan 2021 05:27:36 -0600 Subject: [PATCH 0520/1220] Refactoring padinfo to use new menu (#670) --- padbuildimg/padbuildimg.py | 20 +- padglobal/padglobal.py | 3 +- padinfo/common/config.py | 17 ++ padinfo/common/discord.py | 12 + padinfo/core/find_monster.py | 125 ++++++++- padinfo/core/id.py | 20 ++ padinfo/core/leader_skills.py | 57 +++- padinfo/emojiupdaters.py | 8 +- padinfo/historic_lookups.py | 15 + padinfo/id_menu.py | 20 +- padinfo/info.json | 2 +- padinfo/ls_menu.py | 84 ++++++ padinfo/padinfo.py | 430 ++++++++++++----------------- padinfo/padinfo_settings.py | 60 ++++ padinfo/view/components/base.py | 23 +- padinfo/view/evos.py | 2 +- padinfo/view/id.py | 23 +- padinfo/view/leader_skill.py | 19 +- padinfo/view/links.py | 2 +- padinfo/view/lookup.py | 2 +- padinfo/view/materials.py | 2 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 2 +- padinfo/view/pic.py | 2 +- padinfo/view_state/__init__.py | 0 padinfo/view_state/base.py | 15 + padinfo/view_state/id.py | 48 ++++ padinfo/view_state/leader_skill.py | 31 +++ 28 files changed, 714 insertions(+), 332 deletions(-) create mode 100644 padinfo/common/config.py create mode 100644 padinfo/common/discord.py create mode 100644 padinfo/core/id.py create mode 100644 padinfo/historic_lookups.py create mode 100644 padinfo/ls_menu.py create mode 100644 padinfo/padinfo_settings.py create mode 100644 padinfo/view_state/__init__.py create mode 100644 padinfo/view_state/base.py create mode 100644 padinfo/view_state/id.py create mode 100644 padinfo/view_state/leader_skill.py diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 5134dd7f..e6d3258d 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -1,11 +1,13 @@ -import aiohttp import csv -import discord import math import os import re import urllib from io import BytesIO +from shutil import rmtree + +import aiohttp +import discord from PIL import Image from PIL import ImageChops from PIL import ImageDraw @@ -13,7 +15,6 @@ from ply import lex from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline -from shutil import rmtree from tsutils import CogSettings HELP_MSG = """ @@ -134,7 +135,7 @@ 607: 20, 608: 62, } -LATENTS_MAP.update({-k: 'n'+LATENTS_MAP[k] for k in AWO_RES_LATENT_TO_AWO_MAP}) +LATENTS_MAP.update({-k: 'n' + LATENTS_MAP[k] for k in AWO_RES_LATENT_TO_AWO_MAP}) REVERSE_LATENTS_MAP = {v: k for k, v in LATENTS_MAP.items()} TYPE_TO_KILLERS_MAP = { 'God': [207], # devil @@ -392,7 +393,7 @@ def validate_latents(card_dict, card, ass_card): card_types = [t.name for t in card.types] awos = {a.awoken_skill_id for a in card.awakenings[:-card.superawakening_count]} if card_dict['SUPER'] and card.superawakening_count: - awos.add(card.awakenings[-card.superawakening_count+card_dict['SUPER']-1].awoken_skill_id) + awos.add(card.awakenings[-card.superawakening_count + card_dict['SUPER'] - 1].awoken_skill_id) if ass_card and ass_card.is_equip: awos |= {a.awoken_skill_id for a in ass_card.awakenings} if latents is None: @@ -434,9 +435,10 @@ def idx_to_xy(idx): class PadBuildImageGenerator(object): - def __init__(self, params, padinfo_cog, build_name='pad_build'): + def __init__(self, params, padinfo_cog, dg_cog, build_name='pad_build'): self.params = params self.padinfo_cog = padinfo_cog + self.dg_cog = dg_cog self.lexer = PaDTeamLexer().build() self.build = { 'NAME': build_name, @@ -507,7 +509,7 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card, err, debug_info = await self.padinfo_cog.findMonster1(tok.value) + ass_card, err, debug_info = await self.padinfo_cog.fm1(self.dg_cog, tok.value) if ass_card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) elif tok.type == 'REPEAT': @@ -517,7 +519,7 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = await self.padinfo_cog.findMonster1(tok.value) + card, err, debug_info = await self.padinfo_cog.fm1(self.dg_cog, tok.value) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) if not card.is_inheritable: @@ -793,7 +795,7 @@ async def padbuildimg(self, ctx, *, build_str: str): async with ctx.typing(): params = self.settings.buildImgParams() try: - pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo')) + pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo'), self.bot.get_cog('Dadguide')) await pbg.process_build(build_str) pbg.generate_build_image() except commands.UserFeedbackCheckFailure as ex: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 815d3fe5..83b881c2 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -65,9 +65,10 @@ def mod_help(self, ctx, help_type): async def lookup_named_monster(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') + dg_cog = PADGLOBAL_COG.bot.get_cog('Dadguide') if padinfo_cog is None: raise Exception("Cog not Loaded") - nm, err, debug_info = await padinfo_cog._findMonster(str(query)) + nm, err, debug_info = await padinfo_cog.fm_(dg_cog, str(query)) return nm, err, debug_info diff --git a/padinfo/common/config.py b/padinfo/common/config.py new file mode 100644 index 00000000..211559c3 --- /dev/null +++ b/padinfo/common/config.py @@ -0,0 +1,17 @@ +from padinfo.common.discord import user_color_to_discord_color + + +class UserConfig: + def __init__(self, color, beta_id3): + self.color = color + self.beta_id3 = beta_id3 + + +class BotConfig: + @staticmethod + async def get_user(config, user_id): + user_config = config.user_from_id(user_id) + beta_id3 = await user_config.beta_id3() + user_color = await user_config.color() + color = user_color_to_discord_color(user_color) + return UserConfig(color, beta_id3) diff --git a/padinfo/common/discord.py b/padinfo/common/discord.py new file mode 100644 index 00000000..c26d5459 --- /dev/null +++ b/padinfo/common/discord.py @@ -0,0 +1,12 @@ +import random + +from discord import Color + + +def user_color_to_discord_color(color): + if color is None: + return Color.default() + elif color == "random": + return Color(random.randint(0x000000, 0xffffff)) + else: + return Color(color) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 2b2664e6..8418e326 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -1,7 +1,17 @@ +import json +import re from collections import defaultdict -from typing import Set, List, Tuple +from typing import Set, List, Tuple, Optional, TYPE_CHECKING from Levenshtein import jaro_winkler +from tsutils import rmdiacritics + +from padinfo.historic_lookups import historic_lookups, historic_lookups_file_path, historic_lookups_id3, \ + historic_lookups_file_path_id3 +from padinfo.padinfo_settings import settings + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel SERIES_TYPE_PRIORITY = { "regular": 4, @@ -167,4 +177,117 @@ def get_monster_evos(self, database, monster_gen, monster_score): return monster_evos +async def findMonsterCustom(dgcog, ctx, config, query): + if await config.user(ctx.author).beta_id3(): + m = await findMonster3(dgcog, query) + if m: + return m, "", "" + else: + return None, "Monster not found", "" + else: + return await findMonster1(dgcog, query) + + +async def findMonsterCustom2(dgcog, beta_id3, query): + if beta_id3: + m = await findMonster3(dgcog, query) + if m: + return m, "", "" + else: + return None, "Monster not found", "" + else: + return await findMonster1(dgcog, query) + + +async def findMonster1(dgcog, query): + query = rmdiacritics(query) + nm, err, debug_info = await _findMonster(dgcog, query) + + monster_no = nm.monster_id if nm else -1 + historic_lookups[query] = monster_no + json.dump(historic_lookups, open(historic_lookups_file_path, "w+")) + + m = dgcog.get_monster(nm.monster_id) if nm else None + + return m, err, debug_info + + +async def _findMonster(dgcog, query) -> "NamedMonster": + await dgcog.wait_until_ready() + return dgcog.index.find_monster(query) + + +async def findMonster3(dgcog, query): + m = await _findMonster3(dgcog, query) + + monster_no = m.monster_id if m else -1 + historic_lookups_id3[query] = monster_no + json.dump(historic_lookups_id3, open(historic_lookups_file_path_id3, "w+")) + + return m + + +async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: + await dgcog.wait_until_ready() + + query = rmdiacritics(query).lower() + tokenized_query = query.split() + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) + + return max( + await find_monster_search(tokenized_query, dgcog), + await find_monster_search(mw_tokenized_query, dgcog) + if tokenized_query != mw_tokenized_query else (0.0, None), + )[1] + + +async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["MonsterModel"]]: + mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(tokenized_query, dgcog.index2) + + name_query_tokens.difference_update({'|'}) + + for t in mod_tokens.union(neg_mod_tokens): + if t not in dgcog.index2.all_modifiers: + settings.add_typo_mod(t) + + print(mod_tokens, name_query_tokens) + + if name_query_tokens: + monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, dgcog.index2) + if monster_gen is None: + # No monsters match the given name tokens + return 0, None + else: + # There are no name tokens in the query + monster_gen = {*dgcog.database.get_all_monsters()} + monster_score = defaultdict(int) + + # Expand search to the evo tree + monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, monster_score) + monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_score, monster_gen, + dgcog.index2.modifiers) + if not monster_gen: + # no modifiers match any monster in the evo tree + return 0, None + + print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + + # Return most likely candidate based on query. + mon = max(monster_gen, + key=lambda m: (monster_score[m], + not m.is_equip, + # Match na on id overlap + bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), + SERIES_TYPE_PRIORITY.get(m.series.series_type), + m.on_na if m.series.series_type == "collab" else 0, + dgcog.database.graph.monster_is_rem_evo(m), + not all(t.value in [0, 12, 14, 15] for t in m.types), + not any(t.value in [0, 12, 14, 15] for t in m.types), + -dgcog.database.graph.get_base_id(m), + m.rarity, + m.monster_no_na)) + + return monster_score[mon], mon + + find_monster = FindMonster() diff --git a/padinfo/core/id.py b/padinfo/core/id.py new file mode 100644 index 00000000..0d1fdf19 --- /dev/null +++ b/padinfo/core/id.py @@ -0,0 +1,20 @@ +from padinfo.core.find_monster import findMonsterCustom2 + + +async def perform_id_query(dgcog, raw_query, beta_id3): + db_context = dgcog.database + monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) + acquire_raw, alt_monsters, base_rarity, is_transform_base, true_evo_type_raw = \ + await get_monster_misc_info(db_context, monster) + + return monster, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters + + +async def get_monster_misc_info(db_context, monster): + is_transform_base = db_context.graph.monster_is_transform_base(monster) + true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value + acquire_raw = db_context.graph.monster_acquisition(monster) + base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity + alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + key=lambda x: x.monster_id) + return acquire_raw, alt_monsters, base_rarity, is_transform_base, true_evo_type_raw diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 23cb66b7..215eee3f 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,11 +1,16 @@ +import re + +from padinfo.core.find_monster import findMonsterCustom2 + + def humanize_number(number, sigfigs=2): n = float("{0:.{1}g}".format(number, sigfigs)) if n >= 1e9: - return str(int(n//1e9))+"B" + return str(int(n // 1e9)) + "B" elif n >= 1e6: - return str(int(n//1e6))+"M" + return str(int(n // 1e6)) + "M" elif n >= 1e3: - return str(int(n//1e3))+"k" + return str(int(n // 1e3)) + "k" else: return str(int(n)) @@ -25,14 +30,14 @@ def createMultiplierText(ls1, ls2=None): hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 return format_ls_text( - hp1*hp2, - atk1*atk2, - rcv1*rcv2, + hp1 * hp2, + atk1 * atk2, + rcv1 * rcv2, 1 - (1 - resist1) * (1 - resist2), - combo1+combo2, - fua1+fua2, - mfua1+mfua2, - te1+te2 + combo1 + combo2, + fua1 + fua2, + mfua1 + mfua2, + te1 + te2 ) @@ -60,3 +65,35 @@ def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): extras = f"[{joined}]" if joined else '' return f"[{format_number(hp)}/{format_number(atk)}/{format_number(rcv)}{resist}] {extras}" + + +async def perform_leaderskill_query(dgcog, raw_query, beta_id3): + # Remove unicode quotation marks + query = re.sub("[\u201c\u201d]", '"', raw_query) + + # deliberate order in case of multiple different separators. + for sep in ('"', '/', ',', ' '): + if sep in query: + + left_query, *right_query = [x.strip() for x in query.split(sep) if x.strip()] or ( + '', '') # or in case of ^ls [sep] which is empty list + # split on first separator, with if x.strip() block to prevent null values from showing up, mainly for quotes support + # right query is the rest of query but in list form because of how .strip() works. bring it back to string form with ' '.join + right_query = ' '.join(q for q in right_query) + if sep == ' ': + # Handle a very specific failure case, user typing something like "uuvo ragdra" + nm, err, debug_info = dgcog.index.find_monster(query) + if not err and left_query in nm.prefixes: + left_query = query + right_query = None + + break + + else: # no separators + left_query, right_query = query, None + left_m, left_err, _ = await findMonsterCustom2(dgcog, beta_id3, left_query) + if right_query: + right_m, right_err, _ = await findMonsterCustom2(dgcog, beta_id3, right_query) + else: + right_m, right_err, = left_m, left_err + return left_err, left_m, left_query, right_err, right_m, right_query diff --git a/padinfo/emojiupdaters.py b/padinfo/emojiupdaters.py index 6bebd759..4499a114 100644 --- a/padinfo/emojiupdaters.py +++ b/padinfo/emojiupdaters.py @@ -2,6 +2,8 @@ from tsutils import EmojiUpdater +from padinfo.padinfo_settings import settings + if TYPE_CHECKING: from dadguide.database_context import DbContext from dadguide.models.monster_model import MonsterModel @@ -19,11 +21,11 @@ def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, self.bot = bot self.db_context = db_context - self.pad_info.settings.log_emoji("start_" + selected_emoji) + settings.log_emoji("start_" + selected_emoji) async def on_update(self, ctx, selected_emoji): - evo_id = self.pad_info.settings.checkEvoID(ctx.author.id) - self.pad_info.settings.log_emoji(selected_emoji) + evo_id = settings.checkEvoID(ctx.author.id) + settings.log_emoji(selected_emoji) if evo_id: evos = sorted({*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) index = evos.index(self.m.monster_id) diff --git a/padinfo/historic_lookups.py b/padinfo/historic_lookups.py new file mode 100644 index 00000000..8a6b880f --- /dev/null +++ b/padinfo/historic_lookups.py @@ -0,0 +1,15 @@ +import os + +from redbot.core import data_manager +from tsutils import safe_read_json + + +def _data_file(file_name: str) -> str: + return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) + + +historic_lookups_file_path = _data_file('historic_lookups.json') +historic_lookups = safe_read_json(historic_lookups_file_path) + +historic_lookups_file_path_id3 = _data_file('historic_lookups_id3.json') +historic_lookups_id3 = safe_read_json(historic_lookups_file_path_id3) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index c50e203a..44600fc2 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -4,6 +4,7 @@ import discord from discord import Color +from padinfo.core.id import get_monster_misc_info from padinfo.view.evos import EvosView from padinfo.view.id import IdView from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillSingleView @@ -13,6 +14,7 @@ from padinfo.view.otherinfo import OtherInfoView from padinfo.view.pantheon import PantheonView from padinfo.view.pic import PicsView +from padinfo.view_state.id import IdViewState if TYPE_CHECKING: from dadguide.database_context import DbContext @@ -36,13 +38,11 @@ async def get_user_embed_color(self, pdicog): async def make_id_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - is_transform_base = self.db_context.graph.monster_is_transform_base(m) - true_evo_type_raw = self.db_context.graph.true_evo_type_by_monster(m).value - acquire_raw = self.db_context.graph.monster_acquisition(m) - base_rarity = self.db_context.graph.get_base_monster_by_id(m.monster_no).rarity - alt_monsters = sorted({*self.db_context.graph.get_alt_monsters_by_id(m.monster_no)}, - key=lambda x: x.monster_id) - e = IdView.embed(m, color, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters) + acquire_raw, alt_monsters, base_rarity, is_transform_base, true_evo_type_raw = \ + await get_monster_misc_info(self.db_context, m) + state = IdViewState("", "TODO", "todo", "", m, color, is_transform_base, true_evo_type_raw, acquire_raw, + base_rarity, alt_monsters) + e = IdView.embed(state) return e.to_embed() async def make_evo_embed(self, m: "MonsterModel"): @@ -79,7 +79,8 @@ async def make_evo_mats_embed(self, m: "MonsterModel"): return None link = "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return MaterialView.embed(m, color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link).to_embed() + return MaterialView.embed(m, color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, + link).to_embed() async def make_pantheon_embed(self, m: "MonsterModel"): full_pantheon = self.db_context.get_monsters_by_series(m.series_id) @@ -97,7 +98,8 @@ async def make_picture_embed(self, m: "MonsterModel"): async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return LeaderSkillView.embed(left_m, right_m, color).to_embed() + embed = LeaderSkillView.embed(left_m, right_m, color) + return embed.to_embed() async def make_lookup_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) diff --git a/padinfo/info.json b/padinfo/info.json index 72a567ac..c23df3e9 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -23,7 +23,7 @@ "tsutils>=3.1.0", "prettytable", "python-levenshtein", - "discord-menu>=0.5.0" + "discord-menu>=0.6.0" ], "tags": [ "PAD" diff --git a/padinfo/ls_menu.py b/padinfo/ls_menu.py new file mode 100644 index 00000000..5ff09e36 --- /dev/null +++ b/padinfo/ls_menu.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING, Optional + +from discord import Message +from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.emoji_cache import emoji_cache +from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ + MessageOwnerReactionFilter +from tsutils import char_to_emoji + +from padinfo.view.id import IdView +from padinfo.view.leader_skill import LeaderSkillView +from padinfo.view_state.id import IdViewState +from padinfo.view_state.leader_skill import LeaderSkillViewState + +if TYPE_CHECKING: + pass + +emoji_button_names = ['\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r')] + + +class LeaderSkillMenu: + INITIAL_EMOJI = emoji_button_names[0] + MENU_TYPE = 'LeaderSkill' + + @staticmethod + def menu(original_author_id): + transitions = { + LeaderSkillMenu.INITIAL_EMOJI: LeaderSkillMenu.respond_to_house, + emoji_button_names[1]: LeaderSkillMenu.respond_to_l, + emoji_button_names[2]: LeaderSkillMenu.respond_to_r, + } + + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + MessageOwnerReactionFilter(original_author_id) + ] + + return EmbedMenu(reaction_filters, transitions, LeaderSkillMenu.ls_control) + + @staticmethod + async def respond_to_r(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + # Extract the query from the ls state + ims['query'] = ims['r_query'] + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = LeaderSkillMenu.id_control(id_view_state) + return id_control + + @staticmethod + async def respond_to_l(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + # Extract the query from the ls state + ims['query'] = ims['l_query'] + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = LeaderSkillMenu.id_control(id_view_state) + return id_control + + @staticmethod + async def respond_to_house(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + ls_view_state = await LeaderSkillViewState.deserialize(dgcog, user_config, ims) + ls_control = LeaderSkillMenu.ls_control(ls_view_state) + return ls_control + + @staticmethod + def ls_control(state: LeaderSkillViewState): + return EmbedControl( + [LeaderSkillView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + + @staticmethod + def id_control(state: IdViewState): + return EmbedControl( + [IdView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b930a133..7d125467 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -5,32 +5,39 @@ import random import re import urllib.parse -from collections import OrderedDict, defaultdict +from collections import OrderedDict from datetime import datetime from io import BytesIO -from typing import TYPE_CHECKING, Tuple, Optional +from typing import TYPE_CHECKING import discord import tsutils from Levenshtein import jaro_winkler +from discord import Color from discordmenu.emoji_cache import emoji_cache +from discordmenu.intra_message_state import IntraMessageState from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate -from tsutils import CogSettings, EmojiUpdater, Menu, char_to_emoji, rmdiacritics, safe_read_json, is_donor +from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor +from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji +from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info -from padinfo.core.find_monster import find_monster, SERIES_TYPE_PRIORITY as series_priority +from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ + findMonsterCustom2, findMonsterCustom from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater +from padinfo.historic_lookups import historic_lookups from padinfo.id_menu import IdMenu +from padinfo.ls_menu import LeaderSkillMenu +from padinfo.padinfo_settings import settings from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view_state.leader_skill import LeaderSkillViewState if TYPE_CHECKING: - from dadguide.database_context import DbContext - from dadguide.models.monster_model import MonsterModel - from dadguide.old_monster_index import NamedMonster + pass logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -62,9 +69,6 @@ class PadInfo(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - - self.settings = PadInfoSettings("padinfo") - self.menu = Menu(bot) # These emojis are the keys into the idmenu submenus @@ -83,20 +87,16 @@ def __init__(self, bot, *args, **kwargs): self.last_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' self.remove_emoji = self.menu.emoji['no'] - self.historic_lookups_file_path = _data_file('historic_lookups.json') - self.historic_lookups = safe_read_json(self.historic_lookups_file_path) - - self.historic_lookups_file_path_id3 = _data_file('historic_lookups_id3.json') - self.historic_lookups_id3 = safe_read_json(self.historic_lookups_file_path_id3) - self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False, lastaction=None) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) + self.fm1 = fm.findMonster1 + self.fm_ = fm._findMonster + def cog_unload(self): - # Manually nulling out database because the GC for cogs seems to be pretty shitty - self.historic_lookups = {} - self.historic_lookups_id3 = {} + """Manually nulling out database because the GC for cogs seems to be pretty shitty""" + # TODO.... manage historic_lookups??? probably not. async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -129,10 +129,44 @@ def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') return dg_cog.get_monster(monster_id) + @commands.Cog.listener('on_raw_reaction_add') + async def test_reaction_add(self, event): + channel = self.bot.get_channel(event.channel_id) + try: + message = await channel.fetch_message(event.message_id) + except discord.errors.NotFound: + return + + ims = IntraMessageState.extract_data(message.embeds[0]) + if not ims: + return + + emoji_obj = event.emoji + if isinstance(emoji_obj, str): + emoji_clicked = emoji_obj + else: + emoji_clicked = event.emoji.name + + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + if menu_type == LeaderSkillMenu.MENU_TYPE: + embed_menu = LeaderSkillMenu.menu(original_author_id) + if not (await embed_menu.should_respond(message, event)): + return + + dgcog = await self.get_dgcog() + user_config = await BotConfig.get_user(self.config, original_author_id) + data = { + 'dgcog': dgcog, + 'user_config': user_config + } + await embed_menu.transition(message, ims, emoji_clicked, **data) + @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: await ctx.send(MonsterHeader.short(m)) await ctx.send(box(m.name_ja)) @@ -166,8 +200,16 @@ async def idjp(self, ctx, *, query: str): """Monster info (limited to JP monsters ONLY)""" await self._do_id3(ctx, "injp " + query) + async def get_dgcog(self): + dgcog = self.bot.get_cog("Dadguide") + if dgcog is None: + raise ValueError("Dadguide cog is not loaded") + await dgcog.wait_until_ready() + return dgcog + async def _do_id(self, ctx, query: str): - m, err, debug_info = await self.findMonster1(query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonster1(dgcog, query) if m is not None: await self._do_idmenu(ctx, m, self.id_emoji) @@ -175,10 +217,11 @@ async def _do_id(self, ctx, query: str): await self.makeFailureMsg(ctx, query, err) async def send_survey_after(self, ctx, query, result_monster): + dgcog = await self.get_dgcog() sm = await self.config.user(ctx.author).survey_mode() sms = [1, await self.config.sometimes_perc() / 100, 0][sm] if random.random() < sms: - m1, _, _ = await self.findMonster1(query) + m1, _, _ = await findMonster1(dgcog, query) id1res = f"{m1.name_en} ({m1.monster_id})" if m1 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" params = urllib.parse.urlencode( @@ -234,7 +277,8 @@ async def id3(self, ctx, *, query: str): await self._do_id3(ctx, query) async def _do_id3(self, ctx, query): - m = await self.findMonster3(query) + dgcog = await self.get_dgcog() + m = await findMonster3(dgcog, query) if m and m.monster_no_na != m.monster_no_jp: await ctx.send("The NA ID and JP ID of this card differ! " "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + @@ -252,7 +296,8 @@ async def _do_id3(self, ctx, query): @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): """Monster info (evolutions tab)""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: await self._do_idmenu(ctx, m, self.evo_emoji) else: @@ -262,7 +307,8 @@ async def evos(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: menu = await self._do_idmenu(ctx, m, self.mats_emoji) if menu == EMBED_NOT_GENERATED: @@ -274,7 +320,8 @@ async def evomats(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) if menu == EMBED_NOT_GENERATED: @@ -289,7 +336,7 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_id) emoji_to_embed = await self.get_id_emoji_options(ctx, m=m, scroll=sorted({*alt_versions}, key=lambda - x: x.monster_id) if self.settings.checkEvoID( + x: x.monster_id) if settings.checkEvoID( ctx.author.id) else [], menu_type=1) return await self._do_menu( @@ -397,7 +444,8 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): @checks.bot_has_permissions(embed_links=True) async def pic(self, ctx, *, query: str): """Monster info (full image tab)""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: await self._do_idmenu(ctx, m, self.pic_emoji) else: @@ -407,7 +455,8 @@ async def pic(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def links(self, ctx, *, query: str): """Monster links""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: menu = IdMenu(ctx) embed = await menu.make_links_embed(m) @@ -420,7 +469,8 @@ async def links(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: await self._do_idmenu(ctx, m, self.other_info_emoji) else: @@ -430,7 +480,8 @@ async def otherinfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" - monster, err, _ = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + monster, err, _ = await findMonsterCustom(dgcog, ctx, self.config, query) if monster is None: await self.makeFailureMsg(ctx, query, err) return @@ -444,7 +495,8 @@ async def buttoninfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: menu = IdMenu(ctx, allowed_emojis=self.get_emojis()) embed = await menu.make_lookup_embed(m) @@ -456,7 +508,8 @@ async def lookup(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): """Monster info (for all monsters in the evo tree)""" - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: await self._do_evolistmenu(ctx, m) else: @@ -466,9 +519,9 @@ async def evolist(self, ctx, *, query): @checks.bot_has_permissions(embed_links=True) async def seriesscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" - DGCOG = self.bot.get_cog("Dadguide") - m, err, debug_info = await self.findMonsterCustom(ctx, query) - ms = DGCOG.database.get_monsters_by_series(m.series.series_id) + dgcog = self.bot.get_cog("Dadguide") + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) + ms = dgcog.database.get_monsters_by_series(m.series.series_id) ms.sort(key=lambda x: x.monster_id) ms = [m for m in ms if m.sell_mp >= 100] @@ -489,19 +542,19 @@ async def seriesscroll(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evoscroll(self, ctx, *, query: str): """Scroll through the monsters in a collab""" - DGCOG = self.bot.get_cog("Dadguide") - db_context: "DbContext" = DGCOG.database - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: - await self._do_scrollmenu(ctx, m, sorted(db_context.graph.get_alt_monsters(m), key=lambda x: x.monster_id), + await self._do_scrollmenu(ctx, m, + sorted(dgcog.database.graph.get_alt_monsters(m), key=lambda x: x.monster_id), self.id_emoji) else: await self.makeFailureMsg(ctx, query, err) @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) - async def leaderskill(self, ctx, *, whole_query): + async def leaderskill(self, ctx, *, raw_query): """Display the multiplier and leaderskills for two monsters Gets two monsters separated by a slash, wrapping quotes, a comma, @@ -510,66 +563,77 @@ async def leaderskill(self, ctx, *, whole_query): [p]ls r sonia "revo lu bu" [p]ls sonia lubu """ - DGCOG = self.bot.get_cog("Dadguide") - db_context = DGCOG.database + beta_id3 = await self.config.user(ctx.author).beta_id3() + dgcog = await self.get_dgcog() + l_err, l_mon, l_query, r_err, r_mon, r_query = await self.leaderskill_perform_query(dgcog, raw_query, beta_id3) + err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, try separating the queries with / or wrap with quotes.' + if l_err: + await ctx.send(inline(err_msg.format('Left', l_query))) + return + if r_err: + await ctx.send(inline(err_msg.format('Right', r_query))) + return + + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, color, l_mon, r_mon, + l_query, r_query) + menu = LeaderSkillMenu.menu(original_author_id) + await menu.create(ctx, state) + + async def leaderskill_perform_query(self, dgcog, raw_query, beta_id3): # Remove unicode quotation marks - whole_query = re.sub("[\u201c\u201d]", '"', whole_query) + query = re.sub("[\u201c\u201d]", '"', raw_query) # deliberate order in case of multiple different separators. for sep in ('"', '/', ',', ' '): - if sep in whole_query: + if sep in query: - left_query, *right_query = [x.strip() for x in whole_query.split(sep) if x.strip()] or ( + left_query, *right_query = [x.strip() for x in query.split(sep) if x.strip()] or ( '', '') # or in case of ^ls [sep] which is empty list # split on first separator, with if x.strip() block to prevent null values from showing up, mainly for quotes support # right query is the rest of query but in list form because of how .strip() works. bring it back to string form with ' '.join right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" - nm, err, debug_info = await self._findMonster(whole_query) + nm, err, debug_info = dgcog.index.find_monster(query) if not err and left_query in nm.prefixes: - left_query = whole_query + left_query = query right_query = None break else: # no separators - left_query, right_query = whole_query, None - - left_m, left_err, _ = await self.findMonsterCustom(ctx, left_query) + left_query, right_query = query, None + left_m, left_err, _ = await findMonsterCustom2(dgcog, beta_id3, left_query) if right_query: - right_m, right_err, _ = await self.findMonsterCustom(ctx, right_query) + right_m, right_err, _ = await findMonsterCustom2(dgcog, beta_id3, right_query) else: right_m, right_err, = left_m, left_err + return left_err, left_m, left_query, right_err, right_m, right_query - err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, try separating the queries with / or wrap with quotes.' - if left_err: - await ctx.send(inline(err_msg.format('Left', left_query))) - return - if right_err: - await ctx.send(inline(err_msg.format('Right', right_query))) - return + async def get_user_embed_color(self, ctx): + color = await self.config.user(ctx.author).color() + return self.user_color_to_discord_color(color) - menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) - emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = await menu.make_ls_embed(left_m, right_m) - emoji_to_embed[self.left_emoji] = await menu.make_id_embed(left_m) - emoji_to_embed[self.right_emoji] = await menu.make_id_embed(right_m) - - await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) + def user_color_to_discord_color(self, color): + if color is None: + return Color.default() + elif color == "random": + return Color(random.randint(0x000000, 0xffffff)) + else: + return discord.Color(color) @commands.command(aliases=['lssingle']) @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): - DGCOG = self.bot.get_cog("Dadguide") - db_context = DGCOG.database - - m, err, _ = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if err: await ctx.send(err) return - menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenu(ctx, db_context=dgcog.database, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = await menu.make_lssingle_embed(m) emoji_to_embed[self.left_emoji] = await menu.make_id_embed(m) @@ -860,6 +924,7 @@ async def idt_listrecent(self, ctx, count: int = 0): async def idt_run(self, ctx): """Run all id3 tests""" suite = await self.config.test_suite() + dgcog = await self.get_dgcog() await self.config.user(ctx.author).lastaction.set('id3') c = 0 o = "" @@ -867,7 +932,7 @@ async def idt_run(self, ctx): rcircle = '\N{LARGE RED CIRCLE}' async with ctx.typing(): for q, r in suite.items(): - m = await self.findMonster3(q) + m = await findMonster3(dgcog, q) mid = m and m.monster_id if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' @@ -922,17 +987,17 @@ async def norf_run(self, ctx): async def idt_runall(self, ctx): """Run all tests""" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' - + dgcog = await self.get_dgcog() qsuite = await self.config.test_suite() qo = "" qc = 0 ml = len(max(qsuite, key=len)) + 2 async with ctx.typing(): for c, q in enumerate(sorted(qsuite)): - m = await self.findMonster3(q) + m = await findMonster3(dgcog, q) mid = m and m.monster_id if m is not None and m.monster_id != qsuite[q]['result'] or m is None and qsuite[q]['result'] >= 0: - reason = ' Reason: ' + r.get('reason') if qsuite[q].get('reason') else '' + reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' q = '"' + q + '"' qo += f"{str(c).rjust(4)}. {q.ljust(ml)} - {rcircle} Ex: {qsuite[q]['result']}, Ac: {mid}{reason}\n" else: @@ -987,13 +1052,14 @@ async def padsay(self, ctx, server, *, query: str = None): server = 'na' query = query.strip().lower() - m, err, debug_info = await self.findMonsterCustom(ctx, query) + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: await ctx.send(inline("No voice file found for " + m.name_en)) return - base_dir = self.settings.voiceDir() + base_dir = settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) header = '{} ({})'.format(MonsterHeader.short(m), server) if not os.path.exists(voice_file): @@ -1015,12 +1081,12 @@ async def scroll(self, ctx, value): [p]idset scroll number [p]idset scroll evo""" if value in ['evo', 'default']: - if self.settings.setEvoID(ctx.author.id): + if settings.setEvoID(ctx.author.id): await ctx.tick() else: await ctx.send(inline("You're already using evo mode")) elif value in ['number']: - if self.settings.rmEvoID(ctx.author.id): + if settings.rmEvoID(ctx.author.id): await ctx.tick() else: await ctx.send(inline("You're already using number mode")) @@ -1092,36 +1158,36 @@ async def emojiservers(self, ctx): @checks.is_owner() async def es_add(self, ctx, server_id: int): """Add the emoji server by ID""" - ess = self.settings.emojiServers() + ess = settings.emojiServers() if server_id not in ess: ess.append(server_id) - self.settings.save_settings() + settings.save_settings() await ctx.tick() @emojiservers.command(name="remove", aliases=['rm', 'del']) @checks.is_owner() async def es_rm(self, ctx, server_id: int): """Remove the emoji server by ID""" - ess = self.settings.emojiServers() + ess = settings.emojiServers() if server_id not in ess: await ctx.send("That emoji server is not set.") return ess.remove(server_id) - self.settings.save_settings() + settings.save_settings() await ctx.tick() @emojiservers.command(name="list", aliases=['show']) @checks.is_owner() async def es_show(self, ctx): """List the emoji servers by ID""" - ess = self.settings.emojiServers() + ess = settings.emojiServers() await ctx.send(box("\n".join(str(s) for s in ess))) @padinfo.command() @checks.is_owner() async def setvoicepath(self, ctx, *, path=''): """Set path to the voice direcory""" - self.settings.setVoiceDir(path) + settings.setVoiceDir(path) await ctx.tick() @checks.is_owner() @@ -1129,12 +1195,13 @@ async def setvoicepath(self, ctx, *, path=''): async def iddiff(self, ctx): """Runs the diff checker for id and id3""" await ctx.send("Running diff checker...") - hist_aggreg = list(self.historic_lookups) + hist_aggreg = list(historic_lookups) s = 0 f = [] + dgcog = await self.get_dgcog() async for c, query in AsyncIter(enumerate(hist_aggreg)): - m1, err1, debug_info1 = await self.findMonster1(query) - m2, err2, debug_info2 = await self.findMonster3(query) + m1, err1, debug_info1 = await findMonster1(dgcog, query) + m2, err2, debug_info2 = await findMonster3(dgcog, query) if c % 50 == 0: await ctx.send(inline("{}/{} complete.".format(c, len(hist_aggreg)))) if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): @@ -1153,7 +1220,7 @@ async def iddiff(self, ctx): await ctx.send(file=file) def get_emojis(self): - server_ids = [int(sid) for sid in self.settings.emojiServers()] + server_ids = [int(sid) for sid in settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] async def makeFailureMsg(self, ctx, query: str, err): @@ -1169,131 +1236,30 @@ async def makeFailureMsg(self, ctx, query: str, err): await ctx.send(box(msg)) await ctx.send('Looking for the beta test? Type `{0.prefix}idset beta y`'.format(ctx)) - async def findMonsterCustom(self, ctx, query): - if await self.config.user(ctx.author).beta_id3(): - m = await self.findMonster3(query) - if m: - return m, "", "" - else: - return None, "Monster not found", "" - else: - return await self.findMonster1(query) - - async def findMonster1(self, query): - query = rmdiacritics(query) - nm, err, debug_info = await self._findMonster(query) - - monster_no = nm.monster_id if nm else -1 - self.historic_lookups[query] = monster_no - json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) - - m = self.get_monster(nm.monster_id) if nm else None - - return m, err, debug_info - - async def _findMonster(self, query) -> "NamedMonster": - DGCOG = self.bot.get_cog("Dadguide") - await DGCOG.wait_until_ready() - - return DGCOG.index.find_monster(query) - - async def findMonster3(self, query): - m = await self._findMonster3(query) - - monster_no = m.monster_id if m else -1 - self.historic_lookups_id3[query] = monster_no - json.dump(self.historic_lookups_id3, open(self.historic_lookups_file_path_id3, "w+")) - - return m - - async def _findMonster3(self, query) -> Optional["MonsterModel"]: - DGCOG = self.bot.get_cog("Dadguide") - if DGCOG is None: - raise ValueError("Dadguide cog is not loaded") - await DGCOG.wait_until_ready() - - query = rmdiacritics(query).lower() - tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, DGCOG.index2.multi_word_tokens) - - return max( - await self.find_monster_search(tokenized_query, DGCOG), - await self.find_monster_search(mw_tokenized_query, DGCOG) - if tokenized_query != mw_tokenized_query else (0.0, None), - )[1] - - async def find_monster_search(self, tokenized_query, DGCOG) -> Tuple[int, Optional["MonsterModel"]]: - mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(tokenized_query, DGCOG.index2) - - name_query_tokens.difference_update({'|'}) - - for t in mod_tokens.union(neg_mod_tokens): - if t not in DGCOG.index2.all_modifiers: - self.settings.add_typo_mod(t) - - print(mod_tokens, name_query_tokens) - - if name_query_tokens: - monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, DGCOG.index2) - if monster_gen is None: - # No monsters match the given name tokens - return 0, None - else: - # There are no name tokens in the query - monster_gen = {*DGCOG.database.get_all_monsters()} - monster_score = defaultdict(int) - - # Expand search to the evo tree - monster_gen = find_monster.get_monster_evos(DGCOG.database, monster_gen, monster_score) - monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_score, monster_gen, - DGCOG.index2.modifiers) - if not monster_gen: - # no modifiers match any monster in the evo tree - return 0, None - - print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) - - # Return most likely candidate based on query. - mon = max(monster_gen, - key=lambda m: (monster_score[m], - not m.is_equip, - # Match na on id overlap - bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), - series_priority.get(m.series.series_type), - m.on_na if m.series.series_type == "collab" else 0, - DGCOG.database.graph.monster_is_rem_evo(m), - not all(t.value in [0, 12, 14, 15] for t in m.types), - not any(t.value in [0, 12, 14, 15] for t in m.types), - -DGCOG.database.graph.get_base_id(m), - m.rarity, - m.monster_no_na)) - - return monster_score[mon], mon - @commands.command(aliases=["iddebug"]) async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" - DGCOG = self.bot.get_cog("Dadguide") - m = await self.findMonster3(query) + dgcog = self.bot.get_cog("Dadguide") + m = await findMonster3(dgcog, query) if m is None: await ctx.send(box("Your query didn't match any monsters.")) return - bm = DGCOG.database.graph.get_base_monster(m) - pfxs = DGCOG.index2.modifiers[m] - EVOANDTYPE = DGCOG.token_maps.EVO_TOKENS.union(DGCOG.token_maps.TYPE_TOKENS) + bm = dgcog.database.graph.get_base_monster(m) + pfxs = dgcog.index2.modifiers[m] + EVOANDTYPE = dgcog.token_maps.EVO_TOKENS.union(dgcog.token_maps.TYPE_TOKENS) o = (f"[{m.monster_id}] {m.name_en}\n" f"Base: [{bm.monster_id}] {bm.name_en}\n" f"Series: {m.series.name_en} ({m.series_id})\n\n" - f"[Name Tokens] {' '.join(sorted(t for t, ms in DGCOG.index2.name_tokens.items() if m in ms))}\n" - f"[Fluff Tokens] {' '.join(sorted(t for t, ms in DGCOG.index2.fluff_tokens.items() if m in ms))}\n\n" + f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.name_tokens.items() if m in ms))}\n" + f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.fluff_tokens.items() if m in ms))}\n\n" f"[Manual Tokens]\n" - f" Treenames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_tree.items() if m in ms))}\n" - f" Nicknames: {' '.join(sorted(t for t, ms in DGCOG.index2.manual_nick.items() if m in ms))}\n\n" + f" Treenames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_tree.items() if m in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_nick.items() if m in ms))}\n\n" f"[Modifier Tokens]\n" - f" Attribute: {' '.join(sorted(t for t in pfxs if t in DGCOG.token_maps.COLOR_TOKENS))}\n" - f" Awakening: {' '.join(sorted(t for t in pfxs if t in DGCOG.token_maps.AWAKENING_TOKENS))}\n" + f" Attribute: {' '.join(sorted(t for t in pfxs if t in dgcog.token_maps.COLOR_TOKENS))}\n" + f" Awakening: {' '.join(sorted(t for t in pfxs if t in dgcog.token_maps.AWAKENING_TOKENS))}\n" f" Evo & Type: {' '.join(sorted(t for t in pfxs if t in EVOANDTYPE))}\n" - f" Other: {' '.join(sorted(t for t in pfxs if t not in DGCOG.token_maps.OTHER_HIDDEN_TOKENS))}\n") + f" Other: {' '.join(sorted(t for t in pfxs if t not in dgcog.token_maps.OTHER_HIDDEN_TOKENS))}\n") for page in pagify(o): await ctx.send(box(page)) @@ -1362,7 +1328,7 @@ async def idmeaning(self, ctx, *, modifier): DGCOG = self.bot.get_cog("Dadguide") await DGCOG.wait_until_ready() - + tms = DGCOG.token_maps awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_MAP.values() for aw in aws) + ")" awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} @@ -1373,6 +1339,7 @@ async def idmeaning(self, ctx, *, modifier): def write_name_token(dict, type, mwtoken=False): def f(m, s): return bold(s) if DGCOG.database.graph.monster_is_base(m) else s + o = "" so = [] for m in sorted(dict[modifier], key=lambda m: m.monster_id): @@ -1390,7 +1357,6 @@ def f(m, s): o += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") o += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") - def additmods(ms, om): if len(ms) == 1: return "" @@ -1430,59 +1396,3 @@ def additmods(ms, om): await ctx.send(page) else: await ctx.send(f"There are no modifiers that match `{modifier}`.") - - -class PadInfoSettings(CogSettings): - def make_default_settings(self): - config = { - 'animation_dir': '', - 'alt_id_optout': [], - 'voice_dir_path': '', - 'emoji_use': {}, - 'typo_mods': [], - } - return config - - def emojiServers(self): - key = 'emoji_servers' - if key not in self.bot_settings: - self.bot_settings[key] = [] - return self.bot_settings[key] - - def setEmojiServers(self, emoji_servers): - es = self.emojiServers() - es.clear() - es.extend(emoji_servers) - self.save_settings() - - def setEvoID(self, user_id): - if self.checkEvoID(user_id): - return False - self.bot_settings['alt_id_optout'].remove(user_id) - self.save_settings() - return True - - def rmEvoID(self, user_id): - if not self.checkEvoID(user_id): - return False - self.bot_settings['alt_id_optout'].append(user_id) - self.save_settings() - return True - - def checkEvoID(self, user_id): - return user_id not in self.bot_settings['alt_id_optout'] - - def setVoiceDir(self, path): - self.bot_settings['voice_dir_path'] = path - self.save_settings() - - def voiceDir(self): - return self.bot_settings['voice_dir_path'] - - def log_emoji(self, emote): - self.bot_settings['emoji_use'][emote] = self.bot_settings['emoji_use'].get(emote, 0) + 1 - self.save_settings() - - def add_typo_mod(self, typo): - self.bot_settings['typo_mods'].append(typo) - self.save_settings() diff --git a/padinfo/padinfo_settings.py b/padinfo/padinfo_settings.py new file mode 100644 index 00000000..e66d843f --- /dev/null +++ b/padinfo/padinfo_settings.py @@ -0,0 +1,60 @@ +from tsutils import CogSettings + + +class PadInfoSettings(CogSettings): + def make_default_settings(self): + config = { + 'animation_dir': '', + 'alt_id_optout': [], + 'voice_dir_path': '', + 'emoji_use': {}, + 'typo_mods': [], + } + return config + + def emojiServers(self): + key = 'emoji_servers' + if key not in self.bot_settings: + self.bot_settings[key] = [] + return self.bot_settings[key] + + def setEmojiServers(self, emoji_servers): + es = self.emojiServers() + es.clear() + es.extend(emoji_servers) + self.save_settings() + + def setEvoID(self, user_id): + if self.checkEvoID(user_id): + return False + self.bot_settings['alt_id_optout'].remove(user_id) + self.save_settings() + return True + + def rmEvoID(self, user_id): + if not self.checkEvoID(user_id): + return False + self.bot_settings['alt_id_optout'].append(user_id) + self.save_settings() + return True + + def checkEvoID(self, user_id): + return user_id not in self.bot_settings['alt_id_optout'] + + def setVoiceDir(self, path): + self.bot_settings['voice_dir_path'] = path + self.save_settings() + + def voiceDir(self): + return self.bot_settings['voice_dir_path'] + + def log_emoji(self, emote): + self.bot_settings['emoji_use'][emote] = self.bot_settings['emoji_use'].get(emote, 0) + 1 + self.save_settings() + + def add_typo_mod(self, typo): + self.bot_settings['typo_mods'].append(typo) + self.save_settings() + + +settings = PadInfoSettings("padinfo") diff --git a/padinfo/view/components/base.py b/padinfo/view/components/base.py index acd3b484..67350b07 100644 --- a/padinfo/view/components/base.py +++ b/padinfo/view/components/base.py @@ -1,18 +1,17 @@ -from discordmenu.embed.components import EmbedFooter, EmbedMain +from discordmenu.embed.components import EmbedFooter +from discordmenu.intra_message_state import IntraMessageState -from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import monster_thumbnail +from padinfo.view_state.base import ViewState - -async def make_base_embed_v2(m: "MonsterModel", embed_color): - main = EmbedMain( - color=embed_color, - title=MonsterHeader.long_v2(m) - ) - footer = EmbedFooter('Requester may click the reactions below to switch tabs') - thumbnail = monster_thumbnail(m) - return main, footer, thumbnail +TSUBAKI_FLOWER_ICON_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/tsubaki/tsubakiflower.png' def pad_info_footer(): return EmbedFooter('Requester may click the reactions below to switch tabs') + + +def pad_info_footer_with_state(state: ViewState): + url = IntraMessageState.serialize(TSUBAKI_FLOWER_ICON_URL, state.serialize()) + return EmbedFooter( + 'Requester may click the reactions below to switch tabs', + icon_url=url) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index e4a80f70..a70e9187 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer diff --git a/padinfo/view/id.py b/padinfo/view/id.py index be1ed256..18257993 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,16 +1,17 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createMultiplierText -from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage +from padinfo.view_state.id import IdViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -128,19 +129,19 @@ def leader_skill_header(m: "MonsterModel"): ) @staticmethod - def embed(m: "MonsterModel", color, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, - alt_monsters: List["MonsterModel"]): + def embed(state: IdViewState): + m = state.monster fields = [ EmbedField( '/'.join(['{}'.format(t.name) for t in m.types]), Box( IdView.awakenings_row(m), - IdView.killers_row(m, is_transform_base) + IdView.killers_row(m, state.is_transform_base) ) ), EmbedField( 'Inheritable' if m.is_inheritable else 'Not inheritable', - IdView.misc_info(m, true_evo_type_raw, acquire_raw, base_rarity), + IdView.misc_info(m, state.true_evo_type_raw, state.acquire_raw, state.base_rarity), inline=True ), EmbedField( @@ -159,17 +160,17 @@ def embed(m: "MonsterModel", color, is_transform_base, true_evo_type_raw, acquir EmbedField( "Alternate Evos", HighlightableLinks( - links=[LinkedText(str(m.monster_no_na), puzzledragonx(m)) for m in alt_monsters], - highlighted=next(i for i, mon in enumerate(alt_monsters) if m.monster_id == mon.monster_id) + links=[LinkedText(str(m.monster_no_na), puzzledragonx(m)) for m in state.alt_monsters], + highlighted=next(i for i, mon in enumerate(state.alt_monsters) if m.monster_id == mon.monster_id) ) ) ] return EmbedView( EmbedMain( - color=color, + color=state.color, title=MonsterHeader.long_v2(m).to_markdown(), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), - embed_footer=pad_info_footer(), + embed_footer=pad_info_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 4243618c..e572bebc 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -2,11 +2,13 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from discordmenu.embed.text import BoldText, Text from padinfo.core.leader_skills import createMultiplierText, createSingleMultiplierText +from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view_state.leader_skill import LeaderSkillViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -14,18 +16,19 @@ class LeaderSkillView: @staticmethod - def embed(left_m: "MonsterModel", right_m: "MonsterModel", color): - lls = left_m.leader_skill - rls = right_m.leader_skill + def embed(state: LeaderSkillViewState): + lls = state.l_mon.leader_skill + rls = state.r_mon.leader_skill return EmbedView( - EmbedMain( + embed_main=EmbedMain( title=createMultiplierText(lls, rls), description=Box( - BoldText(MonsterHeader.name(left_m, link=True, show_jp=True)), + BoldText(MonsterHeader.name(state.l_mon, link=True, show_jp=True)), Text(lls.desc if lls else 'None'), - BoldText(MonsterHeader.name(right_m, link=True, show_jp=True)), + BoldText(MonsterHeader.name(state.r_mon, link=True, show_jp=True)), Text(rls.desc if rls else 'None')), - color=color)) + color=state.color), + embed_footer=pad_info_footer_with_state(state)) class LeaderSkillSingleView: diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 65d47193..8a82ae94 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LinkedText from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index 85096bd0..f7bb290a 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from discordmenu.embed.components import EmbedMain, EmbedAuthor -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 811743c7..561f7fcc 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LinkedText from padinfo.common.external_links import puzzledragonx diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index bbaefb7c..1d60e4b7 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -3,7 +3,7 @@ import prettytable from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LabeledText, LinkedText, Text from redbot.core.utils.chat_formatting import box diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 40fbeb05..8578e6ae 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index f3f8e2c5..752fbf70 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage -from discordmenu.embed.menu import EmbedView +from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LinkedText, Text from padinfo.common.external_links import puzzledragonx diff --git a/padinfo/view_state/__init__.py b/padinfo/view_state/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/view_state/base.py b/padinfo/view_state/base.py new file mode 100644 index 00000000..afc874c8 --- /dev/null +++ b/padinfo/view_state/base.py @@ -0,0 +1,15 @@ +class ViewState: + def __init__(self, original_author_id, menu_type, raw_query, extra_state=None): + self.extra_state = extra_state or {} + self.menu_type = menu_type + self.original_author_id = original_author_id + self.raw_query = raw_query + + def serialize(self): + ret = { + 'raw_query': self.raw_query, + 'menu_type': self.menu_type, + 'original_author_id': self.original_author_id, + } + ret.update(self.extra_state) + return ret diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py new file mode 100644 index 00000000..d71cb78b --- /dev/null +++ b/padinfo/view_state/id.py @@ -0,0 +1,48 @@ +from typing import List, TYPE_CHECKING + +from padinfo.common.config import UserConfig +from padinfo.core.id import perform_id_query +from padinfo.view_state.base import ViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class IdViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", color, + is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.acquire_raw = acquire_raw + self.alt_monsters = alt_monsters + self.color = color + self.base_rarity = base_rarity + self.is_transform_base = is_transform_base + self.monster = monster + self.query = query + self.true_evo_type_raw = true_evo_type_raw + + def serialize(self): + ret = super().serialize() + ret.update({ + 'query': self.query, + 'resolved_monster_id': self.monster.monster_id, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + monster, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + await perform_id_query(dgcog, query, user_config.beta_id3) + + return IdViewState(original_author_id, menu_type, raw_query, query, monster, user_config.color, + is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + extra_state=ims) diff --git a/padinfo/view_state/leader_skill.py b/padinfo/view_state/leader_skill.py new file mode 100644 index 00000000..374d290b --- /dev/null +++ b/padinfo/view_state/leader_skill.py @@ -0,0 +1,31 @@ +from padinfo.common.config import UserConfig +from padinfo.core.leader_skills import perform_leaderskill_query +from padinfo.view_state.base import ViewState + + +class LeaderSkillViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, color, l_mon, r_mon, l_query, r_query): + super().__init__(original_author_id, menu_type, raw_query, extra_state=None) + self.color = color + self.l_mon = l_mon + self.l_query = l_query + self.r_mon = r_mon + self.r_query = r_query + + def serialize(self): + ret = super().serialize() + ret.update({ + 'l_query': self.l_query, + 'r_query': self.r_query, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + _, l_mon, l_query, _, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, user_config.beta_id3) + return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, + r_query) From b25f227f0346b96544c6d9a2367e160e2f06b24e Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Sun, 31 Jan 2021 03:32:34 -0800 Subject: [PATCH 0521/1220] fix package hierarchy --- padinfo/core/find_monster.py | 4 ++-- padinfo/{ => core}/historic_lookups.py | 0 padinfo/{ => core}/padinfo_settings.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename padinfo/{ => core}/historic_lookups.py (100%) rename padinfo/{ => core}/padinfo_settings.py (100%) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 8418e326..4029560f 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -6,9 +6,9 @@ from Levenshtein import jaro_winkler from tsutils import rmdiacritics -from padinfo.historic_lookups import historic_lookups, historic_lookups_file_path, historic_lookups_id3, \ +from padinfo.core.historic_lookups import historic_lookups, historic_lookups_file_path, historic_lookups_id3, \ historic_lookups_file_path_id3 -from padinfo.padinfo_settings import settings +from padinfo.core.padinfo_settings import settings if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel diff --git a/padinfo/historic_lookups.py b/padinfo/core/historic_lookups.py similarity index 100% rename from padinfo/historic_lookups.py rename to padinfo/core/historic_lookups.py diff --git a/padinfo/padinfo_settings.py b/padinfo/core/padinfo_settings.py similarity index 100% rename from padinfo/padinfo_settings.py rename to padinfo/core/padinfo_settings.py From 74bfd10888b3a9a77f4234b56334f79bdc7b3ad1 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Sun, 31 Jan 2021 03:35:06 -0800 Subject: [PATCH 0522/1220] rename harder --- padinfo/emojiupdaters.py | 2 +- padinfo/padinfo.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/emojiupdaters.py b/padinfo/emojiupdaters.py index 4499a114..85b036a5 100644 --- a/padinfo/emojiupdaters.py +++ b/padinfo/emojiupdaters.py @@ -2,7 +2,7 @@ from tsutils import EmojiUpdater -from padinfo.padinfo_settings import settings +from padinfo.core.padinfo_settings import settings if TYPE_CHECKING: from dadguide.database_context import DbContext diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7d125467..0b861be3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -29,10 +29,10 @@ from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ findMonsterCustom2, findMonsterCustom from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater -from padinfo.historic_lookups import historic_lookups +from padinfo.core.historic_lookups import historic_lookups from padinfo.id_menu import IdMenu from padinfo.ls_menu import LeaderSkillMenu -from padinfo.padinfo_settings import settings +from padinfo.core.padinfo_settings import settings from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.leader_skill import LeaderSkillViewState From bd20d1bb48a9ec70091c3f76c758ace9bea6e6c2 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 31 Jan 2021 05:13:13 -0700 Subject: [PATCH 0523/1220] Gather async sheet loading (#673) --- dadguide/monster_index.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 7091123b..1307448f 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,3 +1,4 @@ +import asyncio import csv import io import re @@ -37,7 +38,13 @@ async def __ainit__(self, monsters, db): self.replacement_tokens = defaultdict(set) - nickname_data = await sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 4) + nickname_data, treenames_data, pantheon_data, nt_alias_data = await asyncio.gather( + sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 4), + sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 4), + sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, 2), + sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2) + ) + for m_id, name, lp, i in nickname_data: if m_id.isdigit() and not i: if lp: @@ -48,7 +55,6 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_nickname[int(m_id)].add(name.lower().replace(" ", "")) - treenames_data = await sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 4) for m_id, name, mp, i in treenames_data: if m_id.isdigit() and not i: if mp: @@ -60,14 +66,12 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) - pantheon_data = await sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, 2) for sid, name in pantheon_data: if sid.isdigit(): if " " in name: self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) - nt_alias_data = await sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2) next(nt_alias_data) # Skip over heading for token, alias in nt_alias_data: self.replacement_tokens[token].add(alias) @@ -302,6 +306,7 @@ def is_story(m, do_transform=True): if pe and is_story(pe): return True return False + if is_story(m): for t in MISC_MAP[MiscModifiers.STORY]: modifiers.add(t) From b9e97e3d81fbddb3c7af3c8238bd4208f3dda5b8 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 31 Jan 2021 18:32:31 -0600 Subject: [PATCH 0524/1220] Add cc modifier & readd cloud/tape (#677) * Add cc modifier & readd cloud/tape * Remove comment that's now a lie --- dadguide/token_mappings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 60a169a4..011e68bf 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -206,8 +206,8 @@ class Awakenings(Enum): Awakenings.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), Awakenings.UNBINDABLE: ('unbindable',), Awakenings.EXTMOVEPLUS: ('te+', 'finger+'), - Awakenings.CLOUDRESIST: ('cloudres',), # don't have cloud because that's a name token - Awakenings.TAPERESIST: ('taperes',), + Awakenings.CLOUDRESIST: ('cloudres', 'cloud'), + Awakenings.TAPERESIST: ('taperes', 'tape'), Awakenings.SKILLBOOSTPLUS: ('sb+',), Awakenings.HP80ORMORE: ('>80', 'highhp'), Awakenings.HP50ORLESS: ('<50', 'lowhp'), @@ -225,11 +225,11 @@ class Awakenings(Enum): Awakenings.UNPOISONABLE: ('resp+', 'p+',), Awakenings.JAMMERBLESSING: ('jblessing', 'sfj', 'jsurge'), Awakenings.POISONBLESSING: ('pblessing', 'sfp', 'psurge'), - Awakenings.REDCOMBOCOUNT: ('ccr',), - Awakenings.BLUECOMBOCOUNT: ('ccb',), - Awakenings.GREENCOMBOCOUNT: ('ccg',), - Awakenings.LIGHTCOMBOCOUNT: ('ccl',), - Awakenings.DARKCOMBOCOUNT: ('ccd',), + Awakenings.REDCOMBOCOUNT: ('ccr', 'cc'), + Awakenings.BLUECOMBOCOUNT: ('ccb', 'cc'), + Awakenings.GREENCOMBOCOUNT: ('ccg', 'cc'), + Awakenings.LIGHTCOMBOCOUNT: ('ccl', 'cc'), + Awakenings.DARKCOMBOCOUNT: ('ccd', 'cc'), } From 66ed4220ab7a2352132f08f3a5917c535a221966 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 05:23:29 -0700 Subject: [PATCH 0525/1220] Add Metaseries Modfiers (#685) --- dadguide/monster_index.py | 89 +++++++++++++++----------------------- dadguide/token_mappings.py | 3 ++ 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 1307448f..fd8cbff5 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -194,29 +194,23 @@ async def get_modifiers(self, m): basemon = self.graph.get_base_monster(m) # Main Color - for t in COLOR_MAP[m.attr1]: - modifiers.add(t) + modifiers.update(COLOR_MAP[m.attr1]) # Sub Color - for t in SUB_COLOR_MAP[m.attr2]: - modifiers.add(t) + modifiers.update(SUB_COLOR_MAP[m.attr2]) if m.attr1.value == 6: - for t in COLOR_MAP[m.attr2]: - modifiers.add(t) + modifiers.update(COLOR_MAP[m.attr2]) # Both Colors - for t in DUAL_COLOR_MAP[(m.attr1, m.attr2)]: - modifiers.add(t) + modifiers.update(DUAL_COLOR_MAP[(m.attr1, m.attr2)]) # Type for mt in m.types: - for t in TYPE_MAP[mt]: - modifiers.add(t) + modifiers.update(TYPE_MAP[mt]) # Series if m.series_id in self.series_id_to_pantheon_nickname: - for t in self.series_id_to_pantheon_nickname[m.series_id]: - modifiers.add(t) + modifiers.update(self.series_id_to_pantheon_nickname[m.series_id]) # Rarity modifiers.add(str(m.rarity) + "*") @@ -224,8 +218,7 @@ async def get_modifiers(self, m): # Base if self.graph.monster_is_base(m): - for t in EVO_MAP[EvoTypes.BASE]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.BASE]) special_evo = ('覚醒' in m.name_ja or 'awoken' in m.name_en or '転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Reincarnated" or @@ -235,64 +228,60 @@ async def get_modifiers(self, m): # Evo if self.graph.cur_evo_type_by_monster(m).value == 1 and not special_evo: - for t in EVO_MAP[EvoTypes.EVO]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.EVO]) # Uvo if self.graph.cur_evo_type_by_monster(m).value == 2 and not special_evo: - for t in EVO_MAP[EvoTypes.UVO]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.UVO]) # UUvo if self.graph.cur_evo_type_by_monster(m).value == 3 and not special_evo: - for t in EVO_MAP[EvoTypes.UUVO]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.UUVO]) # Transform if not self.graph.monster_is_transform_base(m): - for t in EVO_MAP[EvoTypes.TRANS]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.TRANS]) # Awoken if '覚醒' in m.name_ja or 'awoken' in m.name_en.lower(): - for t in EVO_MAP[EvoTypes.AWOKEN]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.AWOKEN]) # Mega Awoken if '極醒' in m.name_ja or 'mega awoken' in m.name_en.lower(): - for t in EVO_MAP[EvoTypes.MEGA]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.MEGA]) # Reincarnated if self.graph.true_evo_type_by_monster(m).value == "Reincarnated": - for t in EVO_MAP[EvoTypes.REVO]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.REVO]) # Super Reincarnated if '超転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated": - for t in EVO_MAP[EvoTypes.SREVO]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.SREVO]) # Pixel if (m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') or self.graph.true_evo_type_by_monster(m).value == "Pixel"): - for t in EVO_MAP[EvoTypes.PIXEL]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.PIXEL]) else: - for t in EVO_MAP[EvoTypes.NONPIXEL]: - modifiers.add(t) + modifiers.update(EVO_MAP[EvoTypes.NONPIXEL]) # Awakenings for aw in m.awakenings: - for t in AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]: - modifiers.add(t) + modifiers.update(AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]) # Chibi if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or \ 'ミニ' in m.name_ja or '(chibi)' in m.name_en: - for t in MISC_MAP[MiscModifiers.CHIBI]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.CHIBI]) + + # Series Type + if m.series.series_type == 'collab': + modifiers.update(MISC_MAP[MiscModifiers.COLLAB]) + if m.series.series_type == 'seasonal': + modifiers.update(MISC_MAP[MiscModifiers.SEASONAL]) + if m.series.series_type == 'event': + modifiers.update(MISC_MAP[MiscModifiers.EVENT]) # Story def is_story(m, do_transform=True): @@ -308,35 +297,27 @@ def is_story(m, do_transform=True): return False if is_story(m): - for t in MISC_MAP[MiscModifiers.STORY]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.STORY]) # Method of Obtaining if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): - for t in MISC_MAP[MiscModifiers.FARMABLE]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.FARMABLE]) if self.graph.monster_is_mp_evo(m): - for t in MISC_MAP[MiscModifiers.MP]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.MP]) if self.graph.monster_is_rem_evo(m): - for t in MISC_MAP[MiscModifiers.REM]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.REM]) # Server if m.on_jp: - for t in MISC_MAP[MiscModifiers.INJP]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.INJP]) if not m.on_na: - for t in MISC_MAP[MiscModifiers.ONLYJP]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.ONLYJP]) if m.on_na: - for t in MISC_MAP[MiscModifiers.INNA]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.INNA]) if not m.on_jp: - for t in MISC_MAP[MiscModifiers.ONLYNA]: - modifiers.add(t) + modifiers.update(MISC_MAP[MiscModifiers.ONLYNA]) if m.monster_id + 10000 in self.graph.nodes: modifiers.add("idjp") if m.monster_id > 10000: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 011e68bf..3686a9cc 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -243,6 +243,9 @@ class MiscModifiers(Enum): ONLYJP = 'Only in JP Server' INNA = 'In NA Server' ONLYNA = 'Only in NA Server' + EVENT = 'Metaseries: Event' + SEASONAL = 'Metaseries: Seasonal' + COLLAB = 'Metaseries: Collab' MISC_MAP = { From d21a8bf461e369df36b234f44584a93d4fe38046 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 05:34:36 -0700 Subject: [PATCH 0526/1220] Add regular modifier (#688) --- dadguide/monster_index.py | 10 ++++++---- dadguide/token_mappings.py | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index fd8cbff5..c5cf1ae6 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -276,12 +276,14 @@ async def get_modifiers(self, m): modifiers.update(MISC_MAP[MiscModifiers.CHIBI]) # Series Type - if m.series.series_type == 'collab': - modifiers.update(MISC_MAP[MiscModifiers.COLLAB]) - if m.series.series_type == 'seasonal': - modifiers.update(MISC_MAP[MiscModifiers.SEASONAL]) + if m.series.series_type == 'regular': + modifiers.update(MISC_MAP[MiscModifiers.REGULAR]) if m.series.series_type == 'event': modifiers.update(MISC_MAP[MiscModifiers.EVENT]) + if m.series.series_type == 'seasonal': + modifiers.update(MISC_MAP[MiscModifiers.SEASONAL]) + if m.series.series_type == 'collab': + modifiers.update(MISC_MAP[MiscModifiers.COLLAB]) # Story def is_story(m, do_transform=True): diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 3686a9cc..0a11224f 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -243,6 +243,7 @@ class MiscModifiers(Enum): ONLYJP = 'Only in JP Server' INNA = 'In NA Server' ONLYNA = 'Only in NA Server' + REGULAR = 'Metaseries: REGULAR' EVENT = 'Metaseries: Event' SEASONAL = 'Metaseries: Seasonal' COLLAB = 'Metaseries: Collab' @@ -257,7 +258,11 @@ class MiscModifiers(Enum): MiscModifiers.INJP: ('injp',), MiscModifiers.INNA: ('inna',), MiscModifiers.ONLYJP: ('jp',), - MiscModifiers.ONLYNA: ('na',) + MiscModifiers.ONLYNA: ('na',), + MiscModifiers.EVENT: ('regular',), + MiscModifiers.EVENT: ('event',), + MiscModifiers.SEASONAL: ('seasonal',), + MiscModifiers.COLLAB: ('collab',) } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { From 64a1557f56df0a96abe2f6439f1409b38bfac3f0 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 05:38:22 -0700 Subject: [PATCH 0527/1220] Fix Event Enum being added twice (#689) --- dadguide/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 0a11224f..ded4154f 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -259,7 +259,7 @@ class MiscModifiers(Enum): MiscModifiers.INNA: ('inna',), MiscModifiers.ONLYJP: ('jp',), MiscModifiers.ONLYNA: ('na',), - MiscModifiers.EVENT: ('regular',), + MiscModifiers.REGULAR: ('regular',), MiscModifiers.EVENT: ('event',), MiscModifiers.SEASONAL: ('seasonal',), MiscModifiers.COLLAB: ('collab',) From b836d9084bf299baaaa56231bba11ec24f5743e6 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 06:04:33 -0700 Subject: [PATCH 0528/1220] Make crud series add write to json correctly (#690) --- crud/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crud/crud.py b/crud/crud.py index ec3b1a67..071e9352 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -164,7 +164,7 @@ async def series_add(self, ctx, *elements): 'name_en': elements['name_en'], 'name_ko': elements['name_ko'], 'series_id': elements['series_id'], - 'series_type': elements['series_id'] + 'series_type': elements['series_type'] }) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) From 714bac5d54b6005118bbee0a154ce287d6baeae7 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 06:20:06 -0700 Subject: [PATCH 0529/1220] Make debug1 look at correct index (#691) --- padglobal/padglobal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 83b881c2..6108384c 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -202,8 +202,8 @@ async def togglepadglobal(self, ctx): @commands.command() @auth_check('contentadmin') async def debugid1dump(self, ctx): - padinfo_cog = self.bot.get_cog('PadInfo') - mi = padinfo_cog.index_all + dg_cog = self.bot.get_cog('Dadguide') + mi = dg_cog.index async def write_send(nn_map, file_name): data_holder = StringIO() From 834295eb5f6368c12138fa0d4c450ac0736187d2 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 15:05:58 -0700 Subject: [PATCH 0530/1220] Fix error on reaction add to embedless message (#693) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0b861be3..bd801fd4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -137,7 +137,7 @@ async def test_reaction_add(self, event): except discord.errors.NotFound: return - ims = IntraMessageState.extract_data(message.embeds[0]) + ims = message.embeds and IntraMessageState.extract_data(message.embeds[0]) if not ims: return From 881e82b365f02f03e1adfa16da68b814d93ca399 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 15:07:28 -0700 Subject: [PATCH 0531/1220] Fix pricecheck refactor (#694) --- dadguide/old_monster_index.py | 1 + padbuildimg/padbuildimg.py | 7 +++---- padglobal/padglobal.py | 3 +-- padinfo/padinfo.py | 4 ++-- pricecheck/pricecheck.py | 15 +++++++++------ 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index ca61e75f..8a0a7f18 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -452,6 +452,7 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref self.is_low_priority = monster_group.is_low_priority or monster.is_equip self.group_size = monster_group.group_size self.rarity = monster.rarity + self.sell_mp = monster.sell_mp # Used in fallback searches self.name_en = monster.name_en diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index e6d3258d..4ee1414a 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -435,10 +435,9 @@ def idx_to_xy(idx): class PadBuildImageGenerator(object): - def __init__(self, params, padinfo_cog, dg_cog, build_name='pad_build'): + def __init__(self, params, padinfo_cog, build_name='pad_build'): self.params = params self.padinfo_cog = padinfo_cog - self.dg_cog = dg_cog self.lexer = PaDTeamLexer().build() self.build = { 'NAME': build_name, @@ -509,7 +508,7 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card, err, debug_info = await self.padinfo_cog.fm1(self.dg_cog, tok.value) + ass_card, err, debug_info = await self.padinfo_cog.fm1(tok.value) if ass_card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) elif tok.type == 'REPEAT': @@ -519,7 +518,7 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = await self.padinfo_cog.fm1(self.dg_cog, tok.value) + card, err, debug_info = await self.padinfo_cog.fm1(tok.value) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) if not card.is_inheritable: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 6108384c..d820fbf0 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -65,10 +65,9 @@ def mod_help(self, ctx, help_type): async def lookup_named_monster(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') - dg_cog = PADGLOBAL_COG.bot.get_cog('Dadguide') if padinfo_cog is None: raise Exception("Cog not Loaded") - nm, err, debug_info = await padinfo_cog.fm_(dg_cog, str(query)) + nm, err, debug_info = await padinfo_cog.fm_(str(query)) return nm, err, debug_info diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bd801fd4..f51821c5 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -91,8 +91,8 @@ def __init__(self, bot, *args, **kwargs): self.config.register_user(survey_mode=0, color=None, beta_id3=False, lastaction=None) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) - self.fm1 = fm.findMonster1 - self.fm_ = fm._findMonster + self.fm1 = lambda q: fm.findMonster1(bot.get_cog("Dadguide"), q) + self.fm_ = lambda q: fm._findMonster(bot.get_cog("Dadguide"), q) def cog_unload(self): """Manually nulling out database because the GC for cogs seems to be pretty shitty""" diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 7a94fb17..15dd6223 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -50,13 +50,16 @@ async def pricecheck(self, ctx, *, query): return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog._findMonster(query) + nm, err, debug_info = await padinfo_cog.fm_(query) if not nm: await ctx.send(err) return async with self.config.pcs() as pcs: if str(nm.base_monster_no) not in pcs: - await ctx.send("{} does not have PC data.".format(nm.name_en)) + if nm.sell_mp < 100: + await ctx.send("{} does not have PC data.".format(nm.name_en)) + else: + await ctx.send("{} is not tradable.".format(nm.name_en)) return sc, foot = pcs[str(nm.base_monster_no)] pct = PC_TEXT.format(name=nm.name_en, @@ -87,7 +90,7 @@ async def set(self, ctx, stam_cost: float, *, query): return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog._findMonster(query) + nm, err, debug_info = await padinfo_cog.fm_(query) if not nm: await ctx.send(err) return @@ -107,7 +110,7 @@ async def setfooter(self, ctx, query, *, footer=""): return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog._findMonster(query) + nm, err, debug_info = await padinfo_cog.fm_(query) if not nm: await ctx.send(err) return @@ -127,7 +130,7 @@ async def remove(self, ctx, *, query): return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog._findMonster(query) + nm, err, debug_info = await padinfo_cog.fm_(query) if not nm: await ctx.send(err) return @@ -138,7 +141,7 @@ async def remove(self, ctx, *, query): del pcs[str(nm.base_monster_no)] await ctx.send(inline("Removed PC data from {}.".format(nm.name_en))) - @pcadmin.command(aliases=['set-demon-ly']) + @pcadmin.command() async def setdmonly(self, ctx, value: bool = True): """Tells a channel to send [p]pricecheck in dms.""" await self.config.channel(ctx.channel).dm.set(value) From 1dd3fc00f1bd08355944887dfcde4c309412863d Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 15:14:03 -0700 Subject: [PATCH 0532/1220] Fix debugid1 (#692) * Make debug1 look at correct index * Make debugid look at correct index in all cases --- padglobal/padglobal.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index d820fbf0..9d9eda3e 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -218,7 +218,7 @@ async def write_send(nn_map, file_name): @commands.command(aliases=['iddebug1']) @auth_check('contentadmin') async def debugid1(self, ctx, *, query): - padinfo_cog = self.bot.get_cog('PadInfo') + dg_cog = self.bot.get_cog('Dadguide') # m is a named monster m, err, debug_info = await lookup_named_monster(query) @@ -247,9 +247,9 @@ def list_or_none(li): msg += "\n prefixes: {}".format(list_or_none(m.prefixes)) msg += "\n\nAccepted nickname entries:" - accepted_nn = list(filter(lambda nn: m.monster_id == padinfo_cog.index_all.all_entries[nn].monster_id, + accepted_nn = list(filter(lambda nn: m.monster_id == dg_cog.index.all_entries[nn].monster_id, m.final_nicknames)) - accepted_twnn = list(filter(lambda nn: m.monster_id == padinfo_cog.index_all.two_word_entries[nn].monster_id, + accepted_twnn = list(filter(lambda nn: m.monster_id == dg_cog.index.two_word_entries[nn].monster_id, m.final_two_word_nicknames)) msg += "\n nicknames: {}".format(list_or_none(accepted_nn)) @@ -263,9 +263,9 @@ def list_or_none(li): m.final_two_word_nicknames)) replaced_nn_info = map(lambda nn: ( - nn, padinfo_cog.index_all.all_entries[nn]), replaced_nn) + nn, dg_cog.index.all_entries[nn]), replaced_nn) replaced_twnn_info = map( - lambda nn: (nn, padinfo_cog.index_all.two_word_entries[nn]), replaced_twnn) + lambda nn: (nn, dg_cog.index.two_word_entries[nn]), replaced_twnn) replaced_nn_text = list(map(lambda nn_info: '{} : {}. {}'.format( nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_en), From f008fd582c60a925260f60d179318a7f96502a53 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Feb 2021 20:01:21 -0700 Subject: [PATCH 0533/1220] Add transformbase modifier (#695) * Add basetransform modifier * Update token_mappings.py --- dadguide/monster_index.py | 2 ++ dadguide/token_mappings.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index c5cf1ae6..e3975347 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -241,6 +241,8 @@ async def get_modifiers(self, m): # Transform if not self.graph.monster_is_transform_base(m): modifiers.update(EVO_MAP[EvoTypes.TRANS]) + elif self.graph.get_next_transform_by_monster(m): + modifiers.update(EVO_MAP[EvoTypes.BASETRANS]) # Awoken if '覚醒' in m.name_ja or 'awoken' in m.name_en.lower(): diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index ded4154f..5cbf8aa0 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -48,6 +48,7 @@ class EvoTypes(Enum): EVO = 'Evolved' UVO = 'Ulimate' UUVO = 'Super Ultimate' + BASETRANS = 'Base Transform' TRANS = 'Transform' AWOKEN = 'Awoken' MEGA = 'Mega Awoken' @@ -62,6 +63,7 @@ class EvoTypes(Enum): EvoTypes.EVO: ('evo', 'evolved'), EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo', 'suvo'), + EvoTypes.BASETRANS: ('transformbase', 'transbase'), EvoTypes.TRANS: ('transform', 'trans', 'transformed'), EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'megaawoken'), From 484e2ed7317874b3895b6612d4392780446cf2f7 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Mon, 1 Feb 2021 23:20:36 -0600 Subject: [PATCH 0534/1220] changing to use on_reaction_add for speed and adding x2 as delete emoji (#698) --- padinfo/ls_menu.py | 6 +++-- padinfo/padinfo.py | 66 ++++++++++++---------------------------------- 2 files changed, 21 insertions(+), 51 deletions(-) diff --git a/padinfo/ls_menu.py b/padinfo/ls_menu.py index 5ff09e36..298890cb 100644 --- a/padinfo/ls_menu.py +++ b/padinfo/ls_menu.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, Optional from discord import Message +from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji_cache import emoji_cache from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ @@ -15,7 +16,8 @@ if TYPE_CHECKING: pass -emoji_button_names = ['\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r')] +emoji_button_names = ['\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r'), 'x2'] +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='x2') class LeaderSkillMenu: @@ -37,7 +39,7 @@ def menu(original_author_id): MessageOwnerReactionFilter(original_author_id) ] - return EmbedMenu(reaction_filters, transitions, LeaderSkillMenu.ls_control) + return EmbedMenu(reaction_filters, transitions, LeaderSkillMenu.ls_control, menu_emoji_config) @staticmethod async def respond_to_r(message: Optional[Message], ims, **data): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f51821c5..1cc8a499 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -27,12 +27,13 @@ from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom2, findMonsterCustom -from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater + findMonsterCustom from padinfo.core.historic_lookups import historic_lookups -from padinfo.id_menu import IdMenu -from padinfo.ls_menu import LeaderSkillMenu +from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings +from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater +from padinfo.id_menu import IdMenu +from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.leader_skill import LeaderSkillViewState @@ -129,29 +130,27 @@ def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') return dg_cog.get_monster(monster_id) - @commands.Cog.listener('on_raw_reaction_add') - async def test_reaction_add(self, event): - channel = self.bot.get_channel(event.channel_id) - try: - message = await channel.fetch_message(event.message_id) - except discord.errors.NotFound: + @commands.Cog.listener('on_reaction_add') + async def test_reaction_add(self, reaction, member): + emoji_obj = reaction.emoji + if isinstance(emoji_obj, str): + emoji_clicked = emoji_obj + else: + emoji_clicked = emoji_obj.name + + if emoji_clicked not in ls_menu_emoji_button_names: return + message = reaction.message ims = message.embeds and IntraMessageState.extract_data(message.embeds[0]) if not ims: return - emoji_obj = event.emoji - if isinstance(emoji_obj, str): - emoji_clicked = emoji_obj - else: - emoji_clicked = event.emoji.name - original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] if menu_type == LeaderSkillMenu.MENU_TYPE: embed_menu = LeaderSkillMenu.menu(original_author_id) - if not (await embed_menu.should_respond(message, event)): + if not (await embed_menu.should_respond(message, reaction, member)): return dgcog = await self.get_dgcog() @@ -565,7 +564,7 @@ async def leaderskill(self, ctx, *, raw_query): """ beta_id3 = await self.config.user(ctx.author).beta_id3() dgcog = await self.get_dgcog() - l_err, l_mon, l_query, r_err, r_mon, r_query = await self.leaderskill_perform_query(dgcog, raw_query, beta_id3) + l_err, l_mon, l_query, r_err, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, beta_id3) err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, try separating the queries with / or wrap with quotes.' if l_err: @@ -582,37 +581,6 @@ async def leaderskill(self, ctx, *, raw_query): menu = LeaderSkillMenu.menu(original_author_id) await menu.create(ctx, state) - async def leaderskill_perform_query(self, dgcog, raw_query, beta_id3): - # Remove unicode quotation marks - query = re.sub("[\u201c\u201d]", '"', raw_query) - - # deliberate order in case of multiple different separators. - for sep in ('"', '/', ',', ' '): - if sep in query: - - left_query, *right_query = [x.strip() for x in query.split(sep) if x.strip()] or ( - '', '') # or in case of ^ls [sep] which is empty list - # split on first separator, with if x.strip() block to prevent null values from showing up, mainly for quotes support - # right query is the rest of query but in list form because of how .strip() works. bring it back to string form with ' '.join - right_query = ' '.join(q for q in right_query) - if sep == ' ': - # Handle a very specific failure case, user typing something like "uuvo ragdra" - nm, err, debug_info = dgcog.index.find_monster(query) - if not err and left_query in nm.prefixes: - left_query = query - right_query = None - - break - - else: # no separators - left_query, right_query = query, None - left_m, left_err, _ = await findMonsterCustom2(dgcog, beta_id3, left_query) - if right_query: - right_m, right_err, _ = await findMonsterCustom2(dgcog, beta_id3, right_query) - else: - right_m, right_err, = left_m, left_err - return left_err, left_m, left_query, right_err, right_m, right_query - async def get_user_embed_color(self, ctx): color = await self.config.user(ctx.author).color() return self.user_color_to_discord_color(color) From 98b8c58518630e606d2ed3cf16823558f336b325 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 00:03:45 -0700 Subject: [PATCH 0535/1220] Fix multi word token priority (#702) --- dadguide/monster_index.py | 1 + padinfo/core/find_monster.py | 86 +++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index e3975347..a728a3e8 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -86,6 +86,7 @@ async def __ainit__(self, monsters, db): self.all_name_tokens = list(self.manual) + list(self.fluff_tokens) + list(self.name_tokens) self.all_modifiers = {p for ps in self.modifiers.values() for p in ps} self.suffixes = LEGAL_END_TOKENS + self.mwt_to_len = defaultdict(lambda: 1, {"".join(mw): len(mw) for mw in self.multi_word_tokens}) __init__ = __ainit__ diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 4029560f..1250355d 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -76,10 +76,11 @@ def _monster_has_token(self, monster, token, monsterscore, monster_mods): return True return False - def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], Set[str], Set[str]]: + def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: modifiers = [] negative_modifiers = set() name = set() + negative_name = set() longmods = [p for p in index2.all_modifiers if len(p) > 8] lastmodpos = False @@ -110,48 +111,71 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], lastmodpos = True modifiers.append(token) else: - name.update(tokenized_query[i:]) + tokenized_query = tokenized_query[i:] break + else: + tokenized_query = [] + + for token in tokenized_query: + negated = token.startswith("-") + token = token.lstrip('-') + if negated: + negative_name.add(token) + else: + name.add(token) - if not name and modifiers and lastmodpos: + if not (name or negative_name) and modifiers and lastmodpos: if index2.manual[modifiers[-1]]: name.add(modifiers[-1]) modifiers = modifiers[:-1] - return set(modifiers), negative_modifiers, name + return set(modifiers), negative_modifiers, name, negative_name - def process_name_tokens(self, name_query_tokens, index2): - monstergen = None + def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): + monstergen: set = None monsterscore = defaultdict(int) for t in name_query_tokens: - valid = set() - ms = sorted([nt for nt in index2.all_name_tokens if jaro_winkler(t, nt, .05) > self.TOKEN_JW_DISTANCE], - key=lambda nt: jaro_winkler(t, nt, .05), reverse=True) - ms += [token for token in index2.all_name_tokens if token.startswith(t)] - if not ms: - return None, None - for match in ms: - for m in index2.manual[match]: - if m not in valid: - monsterscore[m] += calc_ratio_prefix(t, match) + .001 - valid.add(m) - for m in index2.name_tokens[match]: - if m not in valid: - monsterscore[m] += calc_ratio_prefix(t, match) - valid.add(m) - for m in index2.fluff_tokens[match]: - if m not in valid: - monsterscore[m] += calc_ratio_prefix(t, match) / 2 - valid.add(m) - + valid = self.get_valid_monsters_from_name_token(t, index2, monsterscore) if monstergen is not None: monstergen.intersection_update(valid) else: monstergen = valid + for t in neg_name_tokens: + invalid = self.get_valid_monsters_from_name_token(t, index2, defaultdict(int)) + if monstergen is not None: + monstergen.difference_update(invalid) + else: + monstergen = set() + return monstergen, monsterscore + def get_valid_monsters_from_name_token(self, t, index2, monsterscore): + valid = set() + ms = sorted([nt for nt in index2.all_name_tokens if jaro_winkler(t, nt, .05) > self.TOKEN_JW_DISTANCE], + key=lambda nt: jaro_winkler(t, nt, .05), reverse=True) + ms += [token for token in index2.all_name_tokens if token.startswith(t)] + if not ms: + return None, None + for match in ms: + score = calc_ratio_prefix(t, match) + if index2.mwt_to_len[match] != 1: + score = score ** 10 * index2.mwt_to_len[match] + for m in index2.manual[match]: + if m not in valid: + monsterscore[m] += score + .001 + valid.add(m) + for m in index2.name_tokens[match]: + if m not in valid: + monsterscore[m] += score + valid.add(m) + for m in index2.fluff_tokens[match]: + if m not in valid: + monsterscore[m] += score / 2 + valid.add(m) + return valid + def process_modifiers(self, mod_tokens, neg_mod_tokens, monsterscore, potential_evos, monster_mods): for t in mod_tokens: potential_evos = {m for m in potential_evos if @@ -238,11 +262,13 @@ async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) if tokenized_query != mw_tokenized_query else (0.0, None), + key=lambda t: t[0] )[1] async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["MonsterModel"]]: - mod_tokens, neg_mod_tokens, name_query_tokens = find_monster.interpret_query(tokenized_query, dgcog.index2) + mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ + find_monster.interpret_query(tokenized_query, dgcog.index2) name_query_tokens.difference_update({'|'}) @@ -250,10 +276,10 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - print(mod_tokens, name_query_tokens) + # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) if name_query_tokens: - monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, dgcog.index2) + monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog.index2) if monster_gen is None: # No monsters match the given name tokens return 0, None @@ -270,7 +296,7 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo # no modifiers match any monster in the evo tree return 0, None - print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. mon = max(monster_gen, From d7c50f7be62f9327542f59ddb8c972bff46e8ce0 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 00:32:11 -0700 Subject: [PATCH 0536/1220] Add transform availiable killer latents (#704) --- dadguide/monster_graph.py | 3 +++ padinfo/core/id.py | 8 ++++---- padinfo/id_menu.py | 4 ++-- padinfo/view/id.py | 9 +++++---- padinfo/view_state/id.py | 8 ++++---- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 81ea639d..623700ae 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -363,6 +363,9 @@ def get_transform_base_id_by_id(self, monster_id): def get_transform_base_by_id(self, monster_id): return self.get_monster(self.get_transform_base_id_by_id(monster_id)) + def get_transform_base(self, monster): + return self.get_transform_base_by_id(monster.monster_id) + def monster_is_transform_base_by_id(self, monster_id: int) -> bool: return self.get_transform_base_id_by_id(monster_id) == monster_id diff --git a/padinfo/core/id.py b/padinfo/core/id.py index 0d1fdf19..16c9eb7f 100644 --- a/padinfo/core/id.py +++ b/padinfo/core/id.py @@ -4,17 +4,17 @@ async def perform_id_query(dgcog, raw_query, beta_id3): db_context = dgcog.database monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) - acquire_raw, alt_monsters, base_rarity, is_transform_base, true_evo_type_raw = \ + acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ await get_monster_misc_info(db_context, monster) - return monster, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters + return monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters async def get_monster_misc_info(db_context, monster): - is_transform_base = db_context.graph.monster_is_transform_base(monster) + transform_base = db_context.graph.get_transform_base(monster) true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value acquire_raw = db_context.graph.monster_acquisition(monster) base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, key=lambda x: x.monster_id) - return acquire_raw, alt_monsters, base_rarity, is_transform_base, true_evo_type_raw + return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 44600fc2..9ae78ee3 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -38,9 +38,9 @@ async def get_user_embed_color(self, pdicog): async def make_id_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - acquire_raw, alt_monsters, base_rarity, is_transform_base, true_evo_type_raw = \ + acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ await get_monster_misc_info(self.db_context, m) - state = IdViewState("", "TODO", "todo", "", m, color, is_transform_base, true_evo_type_raw, acquire_raw, + state = IdViewState("", "TODO", "todo", "", m, color, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters) e = IdView.embed(state) return e.to_embed() diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 18257993..a38b813a 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -62,9 +62,10 @@ def awakenings_row(m: "MonsterModel"): ) @staticmethod - def killers_row(m: "MonsterModel", is_transform_base): - killers_text = 'Any' if 'Any' in m.killers else ' '.join([_killer_latent_emoji(k) for k in m.killers]) - available_killer_text = 'Available killers:' if is_transform_base else 'Avail. killers (pre-xform):' + def killers_row(m: "MonsterModel", transform_base): + killers_text = 'Any' if 'Any' in m.killers else \ + ' '.join(_killer_latent_emoji(k) for k in transform_base.killers) + available_killer_text = 'Available killers:' if m==transform_base else 'Avail. killers (pre-xform):' return Box( BoldText(available_killer_text), Text('[{} slots]'.format(m.latent_slots)), @@ -136,7 +137,7 @@ def embed(state: IdViewState): '/'.join(['{}'.format(t.name) for t in m.types]), Box( IdView.awakenings_row(m), - IdView.killers_row(m, state.is_transform_base) + IdView.killers_row(m, state.transform_base) ) ), EmbedField( diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index d71cb78b..a25fcee1 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -10,14 +10,14 @@ class IdViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", color, - is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.acquire_raw = acquire_raw self.alt_monsters = alt_monsters self.color = color self.base_rarity = base_rarity - self.is_transform_base = is_transform_base + self.transform_base = transform_base self.monster = monster self.query = query self.true_evo_type_raw = true_evo_type_raw @@ -40,9 +40,9 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - monster, is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ await perform_id_query(dgcog, query, user_config.beta_id3) return IdViewState(original_author_id, menu_type, raw_query, query, monster, user_config.color, - is_transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, extra_state=ims) From 3d7973bd0536d657b882213777a765c6a38277d1 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 00:36:02 -0700 Subject: [PATCH 0537/1220] Adjust exponent for multiword token priority (#703) updates #702 --- padinfo/core/find_monster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 1250355d..070cd426 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -161,7 +161,7 @@ def get_valid_monsters_from_name_token(self, t, index2, monsterscore): for match in ms: score = calc_ratio_prefix(t, match) if index2.mwt_to_len[match] != 1: - score = score ** 10 * index2.mwt_to_len[match] + score = score ** 4 * index2.mwt_to_len[match] for m in index2.manual[match]: if m not in valid: monsterscore[m] += score + .001 From 756d1cb4e0e0aae0ca8e5567ef4ee1a6f8f29131 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 01:02:08 -0700 Subject: [PATCH 0538/1220] Fix evo matching on negative name token match (#705) * Fix evo matching on negative name token match * Fix series type --- padinfo/core/find_monster.py | 10 +++++----- padinfo/padinfo.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 070cd426..88aa329d 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -143,7 +143,7 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): monstergen = valid for t in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(t, index2, defaultdict(int)) + invalid = self.get_valid_monsters_from_name_token(t, index2, monsterscore, mult=-10) if monstergen is not None: monstergen.difference_update(invalid) else: @@ -151,7 +151,7 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): return monstergen, monsterscore - def get_valid_monsters_from_name_token(self, t, index2, monsterscore): + def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): valid = set() ms = sorted([nt for nt in index2.all_name_tokens if jaro_winkler(t, nt, .05) > self.TOKEN_JW_DISTANCE], key=lambda nt: jaro_winkler(t, nt, .05), reverse=True) @@ -164,15 +164,15 @@ def get_valid_monsters_from_name_token(self, t, index2, monsterscore): score = score ** 4 * index2.mwt_to_len[match] for m in index2.manual[match]: if m not in valid: - monsterscore[m] += score + .001 + monsterscore[m] += (score + .001) * mult valid.add(m) for m in index2.name_tokens[match]: if m not in valid: - monsterscore[m] += score + monsterscore[m] += score * mult valid.add(m) for m in index2.fluff_tokens[match]: if m not in valid: - monsterscore[m] += score / 2 + monsterscore[m] += score * mult / 2 valid.add(m) return valid diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1cc8a499..fd133b6c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1217,7 +1217,7 @@ async def debugid(self, ctx, *, query): EVOANDTYPE = dgcog.token_maps.EVO_TOKENS.union(dgcog.token_maps.TYPE_TOKENS) o = (f"[{m.monster_id}] {m.name_en}\n" f"Base: [{bm.monster_id}] {bm.name_en}\n" - f"Series: {m.series.name_en} ({m.series_id})\n\n" + f"Series: {m.series.name_en} ({m.series_id}, {m.series.series_type})\n\n" f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.name_tokens.items() if m in ms))}\n" f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.fluff_tokens.items() if m in ms))}\n\n" f"[Manual Tokens]\n" From 752bc31ec3d06c17362ca1784810344673f96ad9 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 01:11:39 -0700 Subject: [PATCH 0539/1220] Add idtest listnoreason (#708) --- padinfo/padinfo.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index fd133b6c..579599f2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -859,7 +859,6 @@ async def idtf_list(self, ctx, inclusive: bool = False): await self.norf_list(ctx, True, inclusive) async def norf_list(self, ctx, fluff, inclusive): - """List name/fluff tests""" await self.config.user(ctx.author).lastaction.set('name') suite = await self.config.fluff_suite() @@ -873,6 +872,46 @@ async def norf_list(self, ctx, fluff, inclusive): for page in pagify(o): await ctx.send(box(page)) + @idtest.command(name="listnoreason") + async def idt_listnoreason(self, ctx): + """List id3 tests with no reasons""" + await self.config.user(ctx.author).lastaction.set('id3') + + suite = await self.config.test_suite() + o = "" + ml = len(max(suite, key=len)) + for c, kv in enumerate(sorted(suite.items())): + if not kv[1].get('reason'): + o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + + @idt_name.command(name="listnoreason") + async def idtn_listnoreason(self, ctx, inclusive: bool = False): + """List name tests with no reasons""" + await self.norf_listnoreason(ctx, False, inclusive) + + @idt_fluff.command(name="listnoreason") + async def idtf_lisnoreasont(self, ctx, inclusive: bool = False): + """List fluff tests with no reasons""" + await self.norf_listnoreason(ctx, True, inclusive) + + async def norf_listnoreason(self, ctx, fluff, inclusive): + await self.config.user(ctx.author).lastaction.set('name') + + suite = await self.config.fluff_suite() + o = "" + for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + if inclusive or v['fluff'] == fluff and not v['reason']: + o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ + f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + @idtest.command(name="listrecent") async def idt_listrecent(self, ctx, count: int = 0): """List recent id3 tests""" From 8f5b98f8849ec22ea3fe924ebcfcd5dac667cd88 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Tue, 2 Feb 2021 02:16:23 -0600 Subject: [PATCH 0540/1220] Applying friend filter to messages (#709) --- padinfo/common/emoji_map.py | 2 +- padinfo/ls_menu.py | 8 ++++---- padinfo/padinfo.py | 10 +++++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 1ed80c84..0714f9ab 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Union, Optional, Literal -from discordmenu.emoji_cache import emoji_cache +from discordmenu.emoji.emoji_cache import emoji_cache AWAKENING_ID_TO_EMOJI_NAME_MAP = { 1: 'boost_hp', diff --git a/padinfo/ls_menu.py b/padinfo/ls_menu.py index 298890cb..2f92021b 100644 --- a/padinfo/ls_menu.py +++ b/padinfo/ls_menu.py @@ -3,9 +3,9 @@ from discord import Message from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl -from discordmenu.emoji_cache import emoji_cache +from discordmenu.emoji.emoji_cache import emoji_cache from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter + MessageOwnerReactionFilter, FriendReactionFilter from tsutils import char_to_emoji from padinfo.view.id import IdView @@ -25,7 +25,7 @@ class LeaderSkillMenu: MENU_TYPE = 'LeaderSkill' @staticmethod - def menu(original_author_id): + def menu(original_author_id, friend_ids): transitions = { LeaderSkillMenu.INITIAL_EMOJI: LeaderSkillMenu.respond_to_house, emoji_button_names[1]: LeaderSkillMenu.respond_to_l, @@ -36,7 +36,7 @@ def menu(original_author_id): reaction_filters = [ ValidEmojiReactionFilter(valid_emoji_names), NotPosterEmojiReactionFilter(), - MessageOwnerReactionFilter(original_author_id) + MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) ] return EmbedMenu(reaction_filters, transitions, LeaderSkillMenu.ls_control, menu_emoji_config) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 579599f2..c870905f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -14,7 +14,7 @@ import tsutils from Levenshtein import jaro_winkler from discord import Color -from discordmenu.emoji_cache import emoji_cache +from discordmenu.emoji.emoji_cache import emoji_cache from discordmenu.intra_message_state import IntraMessageState from redbot.core import checks, commands, data_manager, Config from redbot.core.utils import AsyncIter @@ -149,7 +149,9 @@ async def test_reaction_add(self, reaction, member): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] if menu_type == LeaderSkillMenu.MENU_TYPE: - embed_menu = LeaderSkillMenu.menu(original_author_id) + friend_cog = self.bot.get_cog("Friend") + friends = friend_cog and (await friend_cog.get_friends(original_author_id)) + embed_menu = LeaderSkillMenu.menu(original_author_id, friends) if not (await embed_menu.should_respond(message, reaction, member)): return @@ -576,9 +578,11 @@ async def leaderskill(self, ctx, *, raw_query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = friend_cog and (await friend_cog.get_friends(original_author_id)) state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, color, l_mon, r_mon, l_query, r_query) - menu = LeaderSkillMenu.menu(original_author_id) + menu = LeaderSkillMenu.menu(original_author_id, friends) await menu.create(ctx, state) async def get_user_embed_color(self, ctx): From 002ab7c89376201c93b7b2365b4f8cd5a048732e Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Tue, 2 Feb 2021 00:46:51 -0800 Subject: [PATCH 0541/1220] Dont let tsubaki react to test bots --- padinfo/ls_menu.py | 5 +++-- padinfo/padinfo.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/padinfo/ls_menu.py b/padinfo/ls_menu.py index 2f92021b..93fa4f50 100644 --- a/padinfo/ls_menu.py +++ b/padinfo/ls_menu.py @@ -5,7 +5,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter + MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji from padinfo.view.id import IdView @@ -25,7 +25,7 @@ class LeaderSkillMenu: MENU_TYPE = 'LeaderSkill' @staticmethod - def menu(original_author_id, friend_ids): + def menu(original_author_id, friend_ids, bot_id): transitions = { LeaderSkillMenu.INITIAL_EMOJI: LeaderSkillMenu.respond_to_house, emoji_button_names[1]: LeaderSkillMenu.respond_to_l, @@ -36,6 +36,7 @@ def menu(original_author_id, friend_ids): reaction_filters = [ ValidEmojiReactionFilter(valid_emoji_names), NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(bot_id), MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) ] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c870905f..439a55c7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -151,7 +151,7 @@ async def test_reaction_add(self, reaction, member): if menu_type == LeaderSkillMenu.MENU_TYPE: friend_cog = self.bot.get_cog("Friend") friends = friend_cog and (await friend_cog.get_friends(original_author_id)) - embed_menu = LeaderSkillMenu.menu(original_author_id, friends) + embed_menu = LeaderSkillMenu.menu(original_author_id, friends, self.bot.user.id) if not (await embed_menu.should_respond(message, reaction, member)): return @@ -161,7 +161,7 @@ async def test_reaction_add(self, reaction, member): 'dgcog': dgcog, 'user_config': user_config } - await embed_menu.transition(message, ims, emoji_clicked, **data) + await embed_menu.transition(message, ims, emoji_clicked, member, **data) @commands.command() async def jpname(self, ctx, *, query: str): @@ -582,7 +582,7 @@ async def leaderskill(self, ctx, *, raw_query): friends = friend_cog and (await friend_cog.get_friends(original_author_id)) state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, color, l_mon, r_mon, l_query, r_query) - menu = LeaderSkillMenu.menu(original_author_id, friends) + menu = LeaderSkillMenu.menu(original_author_id, friends, self.bot.user.id) await menu.create(ctx, state) async def get_user_embed_color(self, ctx): From a9acafb3c654b62632c57f0c54142022e0dd491b Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 02:11:00 -0700 Subject: [PATCH 0542/1220] Add better test case confirmation (#712) --- padinfo/padinfo.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 439a55c7..12d2030f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -638,15 +638,16 @@ async def idt_add(self, ctx, id: int, *, query): 'reason': reason[0].strip() if reason else '' } - if await tsutils.get_reaction(ctx, f"Added with id `{sorted(suite).index(query)}`", - "\N{LEFTWARDS ARROW WITH HOOK}"): + if await tsutils.get_reaction(ctx, f"Added test case `{id} - {query}`" + f" with ref `{sorted(suite).index(query)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): if oldd: suite[query] = oldd else: del suite[query] await ctx.react_quietly("\N{CROSS MARK}") else: - await ctx.send(f"Successfully added test case with id `{sorted(suite).index(query)}`") + await ctx.send(f"Successfully added test case `{id} - {query}` with ref `{sorted(suite).index(query)}`") await ctx.tick() @idtest.group(name="name") @@ -702,13 +703,16 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): suite.append(case) suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) - if await tsutils.get_reaction(ctx, f"Added with id `{suite.index(case)}`", "\N{LEFTWARDS ARROW WITH HOOK}"): + if await tsutils.get_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " + f"case `{id} - {token}` with ref `{suite.index(case)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): suite.pop() if old: suite.append(old) await ctx.react_quietly("\N{CROSS MARK}") else: - m = await ctx.send(f"Successfully added new case with id `{suite.index(case)}`") + m = await ctx.send(f"Successfully added {'fluff' if fluffy else 'name'} " + f"case `{id} - {token}` with ref `{suite.index(case)}`") await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") @idtest.command(name="import") From b3c6ef07758d1a77355c3b64d4ea0fbee0ba42af Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 02:14:37 -0700 Subject: [PATCH 0543/1220] Send message on bad id1 query (#696) * Send message on bad id1 query * Update old_monster_index.py --- dadguide/old_monster_index.py | 56 +++++++++++++++++++++-------------- padinfo/core/find_monster.py | 2 +- padinfo/core/leader_skills.py | 2 +- padinfo/padinfo.py | 6 ++++ 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 8a0a7f18..518ee6aa 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -1,24 +1,21 @@ import difflib +from collections import defaultdict -from redbot.core.utils import AsyncIter -from discord.utils import find as find_first import tsutils +from discord.utils import find as find_first +from redbot.core.utils import AsyncIter -from collections import defaultdict - -from .models.monster_model import MonsterModel -from .models.enum_types import Attribute -from .models.enum_types import EvoType -from .models.enum_types import InternalEvoType from dadguide.models.enum_types import Attribute, MonsterType from dadguide.models.monster_model import MonsterModel from dadguide.models.series_model import SeriesModel from .database_context import DbContext +from .models.enum_types import EvoType +from .models.enum_types import InternalEvoType class MonsterIndex(tsutils.aobject): async def __ainit__(self, monster_database: DbContext, nickname_overrides, treename_overrides, - panthname_overrides, accept_filter=None): + panthname_overrides, accept_filter=None): # Important not to hold onto anything except IDs here so we don't leak memory self.db_context = monster_database base_monster_ids = monster_database.get_monsters_where(monster_database.graph.monster_is_base) @@ -111,6 +108,7 @@ def named_monsters_sort(named_mon: NamedMonster): self.all_prefixes = set() self.pantheons = defaultdict(set) self.all_entries = {} + self.bad_entries = {} self.two_word_entries = {} for nm in named_monsters: self.all_prefixes.update(nm.prefixes) @@ -118,6 +116,8 @@ def named_monsters_sort(named_mon: NamedMonster): self.all_entries[nickname] = nm for nickname in nm.final_two_word_nicknames: self.two_word_entries[nickname] = nm + for nickname in nm.bad_nicknames: + self.bad_entries[nickname] = nm.bad_nicknames[nickname] if nm.series: for pantheon in self.all_pantheon_names: if pantheon.lower() == nm.series.lower(): @@ -246,9 +246,17 @@ def find_monster(self, query): # TODO: need to handle na_only? + err = None + if query in self.bad_entries: + err = ("It looks like this query won't be supported soon!" + f" Please start using `^id {self.bad_entries[query]}` instead, with a space." + " For more information, check out:" + " " + " or join the Tsubaki server (see `^tsubakiserver` for the link).") + # handle exact nickname match if query in self.all_entries: - return self.all_entries[query], None, "Exact nickname" + return self.all_entries[query], err, "Exact nickname" contains_ja = tsutils.contains_ja(query) if len(query) < 2 and contains_ja: @@ -272,7 +280,7 @@ def find_monster(self, query): if nickname.startswith(query + ' '): matches.add(m) if len(matches): - return self.pick_best_monster(matches), None, "Space nickname prefix, max of {}".format(len(matches)) + return self.pick_best_monster(matches), err, "Space nickname prefix, max of {}".format(len(matches)) # prefix search for nicknames, take max id for nickname, m in self.all_entries.items(): @@ -280,7 +288,7 @@ def find_monster(self, query): matches.add(m) if len(matches): all_names = ",".join(map(lambda x: x.name_en, matches)) - return self.pick_best_monster(matches), None, "Nickname prefix, max of {}, matches=({})".format( + return self.pick_best_monster(matches), err, "Nickname prefix, max of {}, matches=({})".format( len(matches), all_names) # prefix search for full name, take max id @@ -288,11 +296,11 @@ def find_monster(self, query): if m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query): matches.add(m) if len(matches): - return self.pick_best_monster(matches), None, "Full name, max of {}".format(len(matches)) + return self.pick_best_monster(matches), err, "Full name, max of {}".format(len(matches)) # for nicknames with 2 names, prefix search 2nd word, take max id if query in self.two_word_entries: - return self.two_word_entries[query], None, "Second-word nickname prefix, max of {}".format(len(matches)) + return self.two_word_entries[query], err, "Second-word nickname prefix, max of {}".format(len(matches)) # TODO: refactor 2nd search characteristcs for 2nd word @@ -301,30 +309,30 @@ def find_monster(self, query): if query in m.name_en.lower() or query in m.name_ja.lower(): matches.add(m) if len(matches): - return self.pick_best_monster(matches), None, 'Nickname contains nickname match ({})'.format( + return self.pick_best_monster(matches), err, 'Nickname contains nickname match ({})'.format( len(matches)) # No decent matches. Try near hits on nickname instead matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) if len(matches): match = matches[0] - return self.all_entries[match], None, 'Close nickname match ({})'.format(match) + return self.all_entries[match], err, 'Close nickname match ({})'.format(match) # Still no decent matches. Try near hits on full name instead matches = difflib.get_close_matches( query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) if len(matches): match = matches[0] - return self.all_en_name_to_monsters[match], None, 'Close name match ({})'.format(match) + return self.all_en_name_to_monsters[match], err, 'Close name match ({})'.format(match) # About to give up, try matching all words matches = set() for nickname, m in self.all_entries.items(): if (all(map(lambda x: x in m.name_en.lower(), query.split())) or - all(map(lambda x: x in m.name_ja.lower(), query.split()))): + all(map(lambda x: x in m.name_ja.lower(), query.split()))): matches.add(m) if len(matches): - return self.pick_best_monster(matches), None, 'All word match on full name, max of {}'.format( + return self.pick_best_monster(matches), err, 'All word match on full name, max of {}'.format( len(matches)) # couldn't find anything @@ -339,10 +347,9 @@ class NamedMonsterGroup(object): def __init__(self, evolution_tree: list, treename_overrides: list): base_monster = min(evolution_tree, key=lambda m: m.monster_id) - self.is_low_priority = ( - self._is_low_priority_monster(base_monster) - or self._is_low_priority_group(evolution_tree)) + self._is_low_priority_monster(base_monster) + or self._is_low_priority_group(evolution_tree)) self.group_size = len(evolution_tree) self.base_monster_no = base_monster.monster_id @@ -482,6 +489,8 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref if monster.roma_subname: self.final_nicknames.add(monster.roma_subname) + self.bad_nicknames = dict() + # For each treename, add nicknames for treename in self.group_treenames: # Add the treename directly @@ -490,8 +499,10 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref for prefix in self.prefixes: self.final_nicknames.add(prefix + treename) self.final_nicknames.add(prefix + ' ' + treename) + self.bad_nicknames[prefix + treename] = prefix + ' ' + treename self.final_two_word_nicknames = set() + # Slightly different process for two-word treenames. Does this make sense? Who knows. for treename in self.two_word_treenames: self.final_two_word_nicknames.add(treename) @@ -499,6 +510,7 @@ def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, pref for prefix in self.prefixes: self.final_two_word_nicknames.add(prefix + treename) self.final_two_word_nicknames.add(prefix + ' ' + treename) + self.bad_nicknames[prefix + treename] = prefix + ' ' + treename def set_evolution_tree(self, evolution_tree): """ diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 88aa329d..8d735efe 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -236,7 +236,7 @@ async def findMonster1(dgcog, query): return m, err, debug_info -async def _findMonster(dgcog, query) -> "NamedMonster": +async def _findMonster(dgcog, query) -> Tuple["NamedMonster", Optional[str], Optional[str]]: await dgcog.wait_until_ready() return dgcog.index.find_monster(query) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 215eee3f..078ccfac 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -83,7 +83,7 @@ async def perform_leaderskill_query(dgcog, raw_query, beta_id3): if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" nm, err, debug_info = dgcog.index.find_monster(query) - if not err and left_query in nm.prefixes: + if nm and left_query in nm.prefixes: left_query = query right_query = None diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 12d2030f..a45d01b1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -213,6 +213,12 @@ async def _do_id(self, ctx, query: str): m, err, debug_info = await findMonster1(dgcog, query) if m is not None: + async def send_error(err): + if err: + await asyncio.sleep(1) + await ctx.send(err) + asyncio.create_task(send_error(err)) + await self._do_idmenu(ctx, m, self.id_emoji) else: await self.makeFailureMsg(ctx, query, err) From 2adc97eac854941c00844de6478bf14849644639 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Feb 2021 02:22:21 -0700 Subject: [PATCH 0544/1220] Change x emoji back to standard (#713) --- padinfo/ls_menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/ls_menu.py b/padinfo/ls_menu.py index 93fa4f50..5c4633e4 100644 --- a/padinfo/ls_menu.py +++ b/padinfo/ls_menu.py @@ -16,8 +16,8 @@ if TYPE_CHECKING: pass -emoji_button_names = ['\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r'), 'x2'] -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='x2') +emoji_button_names = ['\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r'), '\N{CROSS MARK}'] +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') class LeaderSkillMenu: From f917e83b2f81a1861c4d1ede8f556f219b277508 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 2 Feb 2021 03:35:49 -0600 Subject: [PATCH 0545/1220] Update id1 warning message text (#714) --- dadguide/old_monster_index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 518ee6aa..261f097d 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -252,7 +252,8 @@ def find_monster(self, query): f" Please start using `^id {self.bad_entries[query]}` instead, with a space." " For more information, check out:" " " - " or join the Tsubaki server (see `^tsubakiserver` for the link).") + " or join the Tsubaki server ()." + " To start using the beta now, type `^idset beta y`!") # handle exact nickname match if query in self.all_entries: From 77b9da282cfed5169fe88181bc186d5192b52b6e Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 2 Feb 2021 23:53:29 -0600 Subject: [PATCH 0546/1220] Rename awoken type (#719) --- dadguide/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 5cbf8aa0..8eade221 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -37,7 +37,7 @@ MonsterType.Attacker: ('attacker', 'atk'), MonsterType.Devil: ('devil', 'dv'), MonsterType.Machine: ('machine', 'mech'), - MonsterType.Awoken: ('awoken',), + MonsterType.Awoken: ('awokentype', 'awotype'), MonsterType.Enhance: ('enhance', 'fodder', 'enh'), MonsterType.Vendor: ('vendor', 'redeemable'), } From 2284a31059da3f8db329b62369ec7e20251b6c94 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 3 Feb 2021 00:35:01 -0700 Subject: [PATCH 0547/1220] Fix less-strong token being added as best near-match (#722) Only one name token is allowed to be used as a near-match (as opposed to exact match) for each monster, and previously it was sometimes not the top-scoring token available; this change ensures that it's always the top-scoring token that will be used for the near-match. This bug was first discovered by the query "Hino" returning the base monster of the tree: The other evos were matching on just the name token "Hino" but the base evo was matching on the multi-word token "Hino Kagutsuchi." Due to #702, the mw token had higher priority than the exact match; this PR also addresses that issue (in fact, this part of the PR alone would have fixed this bug!). --- padinfo/core/find_monster.py | 35 +++++++++++++++++++++++------------ padinfo/padinfo.py | 13 +++++++++++-- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 8d735efe..d44b09bb 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel + from dadguide.old_monster_index import NamedMonster SERIES_TYPE_PRIORITY = { "regular": 4, @@ -28,12 +29,23 @@ def calc_ratio(s1, s2): return jaro_winkler(s1, s2, .05) -def calc_ratio_prefix(token, full_word): +def calc_ratio_prefix(token, full_word, index2, factor=.05): + mw = index2.mwt_to_len[full_word] != 1 + jw = jaro_winkler(token, full_word, factor) + if full_word == token: - return 1 + score = 1 elif len(token) >= 3 and full_word.startswith(token): - return .995 - return jaro_winkler(token, full_word, .05) + score = .995 + if mw and jw < score: + return score + else: + score = jw + + if mw: + score = score ** 4 * index2.mwt_to_len[full_word] + + return score class FindMonster: @@ -132,7 +144,7 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], return set(modifiers), negative_modifiers, name, negative_name def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): - monstergen: set = None + monstergen = None monsterscore = defaultdict(int) for t in name_query_tokens: @@ -153,15 +165,13 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): valid = set() - ms = sorted([nt for nt in index2.all_name_tokens if jaro_winkler(t, nt, .05) > self.TOKEN_JW_DISTANCE], - key=lambda nt: jaro_winkler(t, nt, .05), reverse=True) + ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_prefix(t, nt, index2) > self.TOKEN_JW_DISTANCE], + key=lambda nt: calc_ratio_prefix(t, nt, index2), reverse=True) ms += [token for token in index2.all_name_tokens if token.startswith(t)] if not ms: return None, None for match in ms: - score = calc_ratio_prefix(t, match) - if index2.mwt_to_len[match] != 1: - score = score ** 4 * index2.mwt_to_len[match] + score = calc_ratio_prefix(t, match, index2) for m in index2.manual[match]: if m not in valid: monsterscore[m] += (score + .001) * mult @@ -174,6 +184,7 @@ def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): if m not in valid: monsterscore[m] += score * mult / 2 valid.add(m) + return valid def process_modifiers(self, mod_tokens, neg_mod_tokens, monsterscore, potential_evos, monster_mods): @@ -276,7 +287,7 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog.index2) @@ -296,7 +307,7 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo # no modifiers match any monster in the evo tree return 0, None - # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. mon = max(monster_gen, diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a45d01b1..56b3dade 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -27,7 +27,7 @@ from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom + findMonsterCustom, calc_ratio_prefix from padinfo.core.historic_lookups import historic_lookups from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings @@ -1286,15 +1286,24 @@ async def debugid(self, ctx, *, query): @commands.command() async def debugiddist(self, ctx, s1, s2): + """Find the distance between two queries. + + For name tokens, the full word goes second as name token matching is not commutitive + """ + + dgcog = self.bot.get_cog("Dadguide") + dist = jaro_winkler(s1, s2) + dist2 = calc_ratio_prefix(s1, s2, dgcog.index2) yes = '\N{WHITE HEAVY CHECK MARK}' no = '\N{CROSS MARK}' await ctx.send(f"Printing info for {inline(s1)}, {inline(s2)}\n" + box(f"Jaro-Winkler Distance: {round(dist, 4)}\n" + f"Name Matching Distance: {round(dist2, 4)}\n" f"Modifier token threshold: {find_monster.MODIFIER_JW_DISTANCE} " f"{yes if dist >= find_monster.MODIFIER_JW_DISTANCE else no}\n" f"Name token threshold: {find_monster.TOKEN_JW_DISTANCE} " - f"{yes if dist >= find_monster.TOKEN_JW_DISTANCE else no}")) + f"{yes if dist2 >= find_monster.TOKEN_JW_DISTANCE else no}")) @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query=""): From be9a03dc37604c1853fd46003c3402c9a32eb19d Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 3 Feb 2021 01:08:43 -0700 Subject: [PATCH 0548/1220] Add evo to all evo monsters (#723) --- dadguide/monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a728a3e8..8d160a3d 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -228,7 +228,7 @@ async def get_modifiers(self, m): m.is_equip or '極醒' in m.name_ja) # Evo - if self.graph.cur_evo_type_by_monster(m).value == 1 and not special_evo: + if self.graph.cur_evo_type_by_monster(m).value == 1: modifiers.update(EVO_MAP[EvoTypes.EVO]) # Uvo From 0be36092a29246147fb5b47927761dc4090b2d6f Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 3 Feb 2021 04:15:49 -0700 Subject: [PATCH 0549/1220] Refactor evo type & stackable per pipeline update (#724) * Add new way to generate evo types * Base monsters are not evos * Fix spelling mistakes --- dadguide/models/evolution_model.py | 1 + dadguide/models/monster_model.py | 6 +----- dadguide/monster_graph.py | 29 +++++++++++++++++++++++++++++ dadguide/monster_index.py | 6 +++--- padinfo/id_menu.py | 2 +- padinfo/view/materials.py | 4 ++-- 6 files changed, 37 insertions(+), 11 deletions(-) diff --git a/dadguide/models/evolution_model.py b/dadguide/models/evolution_model.py index fbb2cca1..518dc543 100644 --- a/dadguide/models/evolution_model.py +++ b/dadguide/models/evolution_model.py @@ -4,6 +4,7 @@ class EvolutionModel(BaseModel): def __init__(self, **kwargs): self.evolution_type = kwargs['evolution_type'] + self.reversible = kwargs['reversible'] self.from_id = kwargs['from_id'] self.to_id = kwargs['to_id'] self.mat_1_id = kwargs['mat_1_id'] diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 0fc0ad20..abc5262b 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -64,6 +64,7 @@ def __init__(self, **m): self.attr2 = enum_or_none(Attribute, m['attribute_2_id'], Attribute.Nil) self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) self.is_inheritable = m['is_inheritable'] + self.is_stackable = m['is_stackable'] self.evo_gem_id = m['evo_gem_id'] self.orb_skin_id = m['orb_skin_id'] self.cost = m['cost'] @@ -135,11 +136,6 @@ def awakening_restricted_latents(self): def history_us(self): return '[{}] New Added'.format(self.reg_date) - @property - def stackable(self): - return any([t in [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] - for t in self.types]) and self.level == 1 - def awakening_count(self, awid): return len([x for x in self.awakenings if x.awoken_skill_id == awid]) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 623700ae..a1396221 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -192,6 +192,7 @@ def build_graph(self): type_2_id=m.type_2_id, type_3_id=m.type_3_id, is_inheritable=m.inheritable == 1, + is_stackable=m.stackable == 1, evo_gem_id=m.evo_gem_id, orb_skin_id=m.orb_skin_id, cost=m.cost, @@ -384,6 +385,9 @@ def get_numerical_sort_top_monster_by_id(self, monster_id): def get_evo_by_monster_id(self, monster_id) -> Optional[EvolutionModel]: return self._get_edge_model(self.graph[monster_id], 'back_evolution') + def get_evo_by_monster(self, monster) -> Optional[EvolutionModel]: + return self._get_edge_model(self.graph[monster.monster_id], 'back_evolution') + def cur_evo_type_by_monster_id(self, monster_id: int) -> EvoType: prev_evo = self.get_evo_by_monster_id(monster_id) return EvoType(prev_evo.evolution_type) if prev_evo else EvoType.Base @@ -391,6 +395,31 @@ def cur_evo_type_by_monster_id(self, monster_id: int) -> EvoType: def cur_evo_type_by_monster(self, monster: MonsterModel) -> EvoType: return self.cur_evo_type_by_monster_id(monster.monster_no) + def monster_is_reversible_evo(self, monster: MonsterModel) -> bool: + prev_evo = self.get_evo_by_monster(monster) + return prev_evo is not None and prev_evo.reversible + + def monster_is_reincarnated(self, monster: MonsterModel) -> bool: + if self.monster_is_reversible_evo(monster): + return False + prev = self.get_prev_evolution_by_monster(monster) + while prev: + if not self.monster_is_reversible_evo(prev): + return True + prev = self.get_prev_evolution_by_monster(prev) + return False + + def monster_is_normal_evo(self, monster: MonsterModel) -> bool: + return not (self.monster_is_reversible_evo(monster) + or self.monster_is_reincarnated(monster) + or self.monster_is_base(monster)) + + def monster_is_second_ultimate(self, monster: MonsterModel) -> bool: + if self.monster_is_reversible_evo(monster) == EvoType.UvoAwoken: + prev = self.get_prev_evolution_by_monster(monster) + if prev is not None: + return self.monster_is_reversible_evo(prev) == EvoType.UvoAwoken + def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: if self.get_base_id_by_id(monster_id) == monster_id: return InternalEvoType.Base diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 8d160a3d..10bb00af 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -228,15 +228,15 @@ async def get_modifiers(self, m): m.is_equip or '極醒' in m.name_ja) # Evo - if self.graph.cur_evo_type_by_monster(m).value == 1: + if self.graph.monster_is_normal_evo(m): modifiers.update(EVO_MAP[EvoTypes.EVO]) # Uvo - if self.graph.cur_evo_type_by_monster(m).value == 2 and not special_evo: + if self.graph.monster_is_reversible_evo(m) and not special_evo: modifiers.update(EVO_MAP[EvoTypes.UVO]) # UUvo - if self.graph.cur_evo_type_by_monster(m).value == 3 and not special_evo: + if self.graph.monster_is_second_ultimate(m): modifiers.update(EVO_MAP[EvoTypes.UUVO]) # Transform diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 9ae78ee3..52789426 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -75,7 +75,7 @@ async def make_evo_mats_embed(self, m: "MonsterModel"): su not in sugs] if m.active_skill else [] skillup_evo_count = len(sums) - len(vsums) - if not any([mats, usedin, gemusedin, skillups and not m.stackable]): + if not any([mats, usedin, gemusedin, skillups and not m.is_stackable]): return None link = "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 561f7fcc..79716566 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -59,9 +59,9 @@ def embed(m: "MonsterModel", color, mats, usedin, gemid, gemusedin, skillups, se embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=pad_info_footer(), embed_fields=[f for f in [ - mat_use_field(mats, "Evo materials") if mats or not m.stackable else None, + mat_use_field(mats, "Evo materials") if mats or not m.is_stackable else None, mat_use_field(usedin, "Material for", 10) if usedin else None, mat_use_field(gemusedin, "Evo gem ({}) is mat for".format(gemid)) if gemusedin else None, - skillup_field(skillups, sec, link) if not m.stackable else None + skillup_field(skillups, sec, link) if not m.is_stackable else None ] if f is not None] ) From fe4f6f2fbc51e42271ded17c51e3e0a8dfd3205f Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 3 Feb 2021 04:25:21 -0700 Subject: [PATCH 0550/1220] Bump discord-menu version (#725) --- padinfo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/info.json b/padinfo/info.json index c23df3e9..f570ab85 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -23,7 +23,7 @@ "tsutils>=3.1.0", "prettytable", "python-levenshtein", - "discord-menu>=0.6.0" + "discord-menu>=0.10.0" ], "tags": [ "PAD" From 7189d258dbbc05b0a6c45a2a1831150c9fe6de99 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 3 Feb 2021 04:32:46 -0700 Subject: [PATCH 0551/1220] Rename token_count (#726) --- dadguide/monster_index.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 10bb00af..e51ec828 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -184,10 +184,10 @@ def _get_important_tokens(cls, oname): return cls._name_to_tokens(oname) *n1, n2 = name n1 = ", ".join(n1) - if tcount(n1) == tcount(n2) or max(tcount(n1), tcount(n2)) < 3: + if token_count(n1) == token_count(n2) or max(token_count(n1), token_count(n2)) < 3: return cls._name_to_tokens(oname) else: - return cls._name_to_tokens(min(n1, n2, key=tcount)) + return cls._name_to_tokens(min(n1, n2, key=token_count)) async def get_modifiers(self, m): modifiers = set() @@ -356,7 +356,7 @@ def combine_tokens_dicts(d1, *ds): return combined -def tcount(tstr): +def token_count(tstr): tstr = re.sub(r"[^\w ]", "", tstr) tstr = re.sub(r"\(.+\)", "", tstr) return len([*filter(None, tstr.split())]) From 9843655e24df04e7915acceaf23f6a6d249165ce Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 3 Feb 2021 19:56:55 -0600 Subject: [PATCH 0552/1220] Adjust score for prefix-matching mw tokens (#728) This fixes a bug with Venus & Chris not matching their intended targets --- padinfo/core/find_monster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index d44b09bb..f28aec9e 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -43,7 +43,7 @@ def calc_ratio_prefix(token, full_word, index2, factor=.05): score = jw if mw: - score = score ** 4 * index2.mwt_to_len[full_word] + score = score ** 10 * index2.mwt_to_len[full_word] return score From 6ac5c3c9d73ebd5d7b52b617ecc0a1c544372654 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 4 Feb 2021 02:05:38 -0600 Subject: [PATCH 0553/1220] Bump version (#729) --- padinfo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/info.json b/padinfo/info.json index f570ab85..250e966b 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -23,7 +23,7 @@ "tsutils>=3.1.0", "prettytable", "python-levenshtein", - "discord-menu>=0.10.0" + "discord-menu>=0.12.0" ], "tags": [ "PAD" From c0c114d744265a00a5fd3ed4ef36ab816297b8df Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 4 Feb 2021 02:27:19 -0600 Subject: [PATCH 0554/1220] Empty list rather than None when no friendcog (#730) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 56b3dade..a042b6c3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -150,7 +150,7 @@ async def test_reaction_add(self, reaction, member): menu_type = ims['menu_type'] if menu_type == LeaderSkillMenu.MENU_TYPE: friend_cog = self.bot.get_cog("Friend") - friends = friend_cog and (await friend_cog.get_friends(original_author_id)) + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] embed_menu = LeaderSkillMenu.menu(original_author_id, friends, self.bot.user.id) if not (await embed_menu.should_respond(message, reaction, member)): return From 7c16c1e4392211893bceb0a9446e899bab6a4b5f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Feb 2021 03:11:51 -0700 Subject: [PATCH 0555/1220] Fix suvo not adding properly in id3 index (#731) --- dadguide/monster_graph.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index a1396221..12bdc563 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -415,10 +415,11 @@ def monster_is_normal_evo(self, monster: MonsterModel) -> bool: or self.monster_is_base(monster)) def monster_is_second_ultimate(self, monster: MonsterModel) -> bool: - if self.monster_is_reversible_evo(monster) == EvoType.UvoAwoken: + if self.monster_is_reversible_evo(monster): prev = self.get_prev_evolution_by_monster(monster) if prev is not None: - return self.monster_is_reversible_evo(prev) == EvoType.UvoAwoken + return self.monster_is_reversible_evo(prev) + return False def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: if self.get_base_id_by_id(monster_id) == monster_id: From 7c693bff8ef170b3fbf97e3979004a6109a36476 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Feb 2021 06:11:07 -0700 Subject: [PATCH 0556/1220] Calcluate treename earlier (#733) --- dadguide/monster_index.py | 25 ++++++++++++++++++------- padinfo/core/find_monster.py | 8 ++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index e51ec828..4b47cd51 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -121,6 +121,13 @@ async def _build_monster_index(self, monsters): if t in nametokens: self.add_name_token(self.name_tokens, t, m) + # Find likely treenames + treenames = set() + for me in self.graph.get_alt_monsters(m): + match = re.match("(?:Awoken|Reincarnated) (.*)", me.name_en) + if match: + treenames.add(match.group(1)) + # Add important tokens if autotoken: # Add a consistant last token as important token @@ -128,7 +135,7 @@ async def _build_monster_index(self, monsters): self.add_name_token(self.name_tokens, token, m) elif not self.monster_id_to_nametokens[m.monster_id]: # Add name tokens by guessing which ones are important - for token in self._get_important_tokens(m.name_en) + self._name_to_tokens(m.roma_subname): + for token in self._get_important_tokens(m.name_en, treenames) + self._name_to_tokens(m.roma_subname): self.add_name_token(self.name_tokens, token, m) if m.is_equip: possessives = re.findall(r"(\w+)'s", m.name_en.lower()) @@ -150,7 +157,7 @@ async def _build_monster_index(self, monsters): if m.is_equip: for mevo in self.graph.get_alt_monsters(m): if not mevo.is_equip: - for token2 in self._get_important_tokens(mevo.name_en): + for token2 in self._get_important_tokens(mevo.name_en, treenames): self.add_name_token(self.name_tokens, token2, m) # Fluff tokens @@ -172,19 +179,24 @@ async def _build_monster_index(self, monsters): def _name_to_tokens(oname): if not oname: return [] - oname = oname.lower() + oname = oname.lower().replace(',', '') name = re.sub(r'[\-+\']', ' ', oname) name = re.sub(r'[^a-z0-9 ]', '', name) return [t.strip() for t in set(name.split() + oname.split()) if t] @classmethod - def _get_important_tokens(cls, oname): + def _get_important_tokens(cls, oname, treenames=None): + if treenames is None: + treenames = set() + name = oname.split(", ") if len(name) == 1: return cls._name_to_tokens(oname) *n1, n2 = name n1 = ", ".join(n1) - if token_count(n1) == token_count(n2) or max(token_count(n1), token_count(n2)) < 3: + if treenames.intersection((n1, n2)): + return [t for n in treenames.intersection((n1, n2)) for t in cls._name_to_tokens(n)] + elif token_count(n1) == token_count(n2) or max(token_count(n1), token_count(n2)) < 3: return cls._name_to_tokens(oname) else: return cls._name_to_tokens(min(n1, n2, key=token_count)) @@ -357,6 +369,5 @@ def combine_tokens_dicts(d1, *ds): def token_count(tstr): - tstr = re.sub(r"[^\w ]", "", tstr) tstr = re.sub(r"\(.+\)", "", tstr) - return len([*filter(None, tstr.split())]) + return len(re.split(r'\W+', tstr)) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index f28aec9e..44e76dc3 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -169,7 +169,7 @@ def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): key=lambda nt: calc_ratio_prefix(t, nt, index2), reverse=True) ms += [token for token in index2.all_name_tokens if token.startswith(t)] if not ms: - return None, None + return None for match in ms: score = calc_ratio_prefix(t, match, index2) for m in index2.manual[match]: @@ -265,7 +265,7 @@ async def findMonster3(dgcog, query): async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: await dgcog.wait_until_ready() - query = rmdiacritics(query).lower() + query = rmdiacritics(query).lower().replace(",", "") tokenized_query = query.split() mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) @@ -291,16 +291,16 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog.index2) - if monster_gen is None: + if not monster_gen: # No monsters match the given name tokens return 0, None + monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, monster_score) else: # There are no name tokens in the query monster_gen = {*dgcog.database.get_all_monsters()} monster_score = defaultdict(int) # Expand search to the evo tree - monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, monster_score) monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_score, monster_gen, dgcog.index2.modifiers) if not monster_gen: From 4761db78e313e97613bc07304980d2bb5286ffbf Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Feb 2021 06:53:54 -0700 Subject: [PATCH 0557/1220] Update id1 error messaging (#735) * Add id1 error message * Better formatting --- padinfo/core/find_monster.py | 12 ++++++++++-- padinfo/padinfo.py | 10 +++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 44e76dc3..3c75a924 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -247,9 +247,17 @@ async def findMonster1(dgcog, query): return m, err, debug_info -async def _findMonster(dgcog, query) -> Tuple["NamedMonster", Optional[str], Optional[str]]: +async def _findMonster(dgcog, query) -> Tuple[Optional["NamedMonster"], Optional[str], Optional[str]]: await dgcog.wait_until_ready() - return dgcog.index.find_monster(query) + try: + return dgcog.index.find_monster(query) + except: + prefix = (await dgcog.bot.get_valid_prefixes())[0] + return (None, + f"Sorry, id1 doesn't support this query and we are no longer" + f" developing id1 features. Please use `{prefix}id3 {query}`! You can" + f" opt into using the beta all the time by running `{prefix}idset beta y`!", + None) async def findMonster3(dgcog, query): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a042b6c3..529eb1d1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1251,11 +1251,11 @@ async def makeFailureMsg(self, ctx, query: str, err): "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) return - msg = ('Lookup failed: {0}.\n' - 'Try one of , , [argbld]/[rgbld] . ' - 'Unexpected results? Use {1.prefix}helpid for more info.').format(err, ctx) - await ctx.send(box(msg)) - await ctx.send('Looking for the beta test? Type `{0.prefix}idset beta y`'.format(ctx)) + msg = ('Lookup failed: {0}\n\n').format(err) + await ctx.send(msg+ 'Try opting into the beta of our new id lookup for better results!' + ' Try `{0.prefix}id3 {1}` to use it one time, or `{0.prefix}idset beta y`' + ' to permanently opt into it (you can opt out with `{0.prefix}idset beta n`' + ' if you want to later).'.format(ctx, query)) @commands.command(aliases=["iddebug"]) async def debugid(self, ctx, *, query): From fe5757f3a52fcad93970c0e3a785ce63c21f81a1 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Feb 2021 07:50:28 -0700 Subject: [PATCH 0558/1220] Show supertokens in idmeaning (#736) --- dadguide/monster_index.py | 2 +- padinfo/core/find_monster.py | 33 ++++++++++------- padinfo/padinfo.py | 71 +++++++++++++++++++++--------------- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 4b47cd51..a61c7184 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -83,7 +83,7 @@ async def __ainit__(self, monsters, db): await self._build_monster_index(monsters) self.manual = combine_tokens_dicts(self.manual_nick, self.manual_tree) - self.all_name_tokens = list(self.manual) + list(self.fluff_tokens) + list(self.name_tokens) + self.all_name_tokens = combine_tokens_dicts(self.manual, self.fluff_tokens, self.name_tokens) self.all_modifiers = {p for ps in self.modifiers.values() for p in ps} self.suffixes = LEGAL_END_TOKENS self.mwt_to_len = defaultdict(lambda: 1, {"".join(mw): len(mw) for mw in self.multi_word_tokens}) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 3c75a924..e96c73d0 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -201,6 +201,25 @@ def process_modifiers(self, mod_tokens, neg_mod_tokens, monsterscore, potential_ return potential_evos + def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, monster_score=None): + if monster_score is None: + monster_score = defaultdict(int) + if tokenized_query is None: + tokenized_query = [] + return max(monsters, + key=lambda m: (monster_score[m], + not m.is_equip, + # Match na on id overlap + bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), + SERIES_TYPE_PRIORITY.get(m.series.series_type), + m.on_na if m.series.series_type == "collab" else 0, + dgcog.database.graph.monster_is_rem_evo(m), + not all(t.value in [0, 12, 14, 15] for t in m.types), + not any(t.value in [0, 12, 14, 15] for t in m.types), + -dgcog.database.graph.get_base_id(m), + m.rarity, + m.monster_no_na)) + def get_monster_evos(self, database, monster_gen, monster_score): monster_evos = set() for m in sorted(monster_gen, key=lambda m: monster_score[m], reverse=True): @@ -318,19 +337,7 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. - mon = max(monster_gen, - key=lambda m: (monster_score[m], - not m.is_equip, - # Match na on id overlap - bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), - SERIES_TYPE_PRIORITY.get(m.series.series_type), - m.on_na if m.series.series_type == "collab" else 0, - dgcog.database.graph.monster_is_rem_evo(m), - not all(t.value in [0, 12, 14, 15] for t in m.types), - not any(t.value in [0, 12, 14, 15] for t in m.types), - -dgcog.database.graph.get_base_id(m), - m.rarity, - m.monster_no_na)) + mon = find_monster.get_most_eligable_monster(dgcog, tokenized_query, monster_score) return monster_score[mon], mon diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 529eb1d1..b8f0ec9c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -217,6 +217,7 @@ async def send_error(err): if err: await asyncio.sleep(1) await ctx.send(err) + asyncio.create_task(send_error(err)) await self._do_idmenu(ctx, m, self.id_emoji) @@ -1252,10 +1253,10 @@ async def makeFailureMsg(self, ctx, query: str, err): "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) return msg = ('Lookup failed: {0}\n\n').format(err) - await ctx.send(msg+ 'Try opting into the beta of our new id lookup for better results!' - ' Try `{0.prefix}id3 {1}` to use it one time, or `{0.prefix}idset beta y`' - ' to permanently opt into it (you can opt out with `{0.prefix}idset beta n`' - ' if you want to later).'.format(ctx, query)) + await ctx.send(msg + 'Try opting into the beta of our new id lookup for better results!' + ' Try `{0.prefix}id3 {1}` to use it one time, or `{0.prefix}idset beta y`' + ' to permanently opt into it (you can opt out with `{0.prefix}idset beta n`' + ' if you want to later).'.format(ctx, query)) @commands.command(aliases=["iddebug"]) async def debugid(self, ctx, *, query): @@ -1352,9 +1353,9 @@ async def exportmodifiers(self, ctx): await ctx.send(file=text_to_file(o, filename="table.md")) @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod"]) - async def idmeaning(self, ctx, *, modifier): + async def idmeaning(self, ctx, *, token): """Get all the meanings of a token (bold signifies base of a tree)""" - modifier = modifier.replace(" ", "") + token = token.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") await DGCOG.wait_until_ready() @@ -1372,8 +1373,8 @@ def f(m, s): o = "" so = [] - for m in sorted(dict[modifier], key=lambda m: m.monster_id): - if (m in DGCOG.index2.mwtoken_creators[modifier]) == mwtoken: + for m in sorted(dict[token], key=lambda m: m.monster_id): + if (m in DGCOG.index2.mwtoken_creators[token]) == mwtoken: so.append(m) if len(so) > 5: o += f"\n\n{type}\n" + ", ".join(f(m, str(m.monster_id)) for m in so[:10]) @@ -1387,37 +1388,49 @@ def f(m, s): o += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") o += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") + submwtokens = [t for t in DGCOG.index2.multi_word_tokens if token in t] + if submwtokens: + o += "\n\n[Multi-word Super-tokens]\n" + for t in submwtokens: + creators = sorted(DGCOG.index2.mwtoken_creators["".join(t)], key=lambda m: m.monster_id) + o += f"{' '.join(t).title()}" + o += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' + o += (" ( \u2014> " + + str(find_monster.get_most_eligable_monster(DGCOG.index2.all_name_tokens[''.join(t)], + DGCOG).monster_id) + + ")\n") + def additmods(ms, om): if len(ms) == 1: return "" return "\n\tAlternate names: " + ', '.join(inline(m) for m in ms if m != om) meanings = [ - *["Evo: " + k.value + additmods(v, modifier) - for k, v in tms.EVO_MAP.items() if modifier in v], - *["Type: " + get_type_emoji(k) + ' ' + k.name + additmods(v, modifier) - for k, v in tms.TYPE_MAP.items() if modifier in v], - *["Misc: " + k.value + additmods(v, modifier) - for k, v in tms.MISC_MAP.items() if modifier in v], - *["Awakening: " + get_awakening_emoji(k) + ' ' + awakenings[k.value].name_en + additmods(v, modifier) - for k, v in tms.AWOKEN_MAP.items() if modifier in v], + *["Evo: " + k.value + additmods(v, token) + for k, v in tms.EVO_MAP.items() if token in v], + *["Type: " + get_type_emoji(k) + ' ' + k.name + additmods(v, token) + for k, v in tms.TYPE_MAP.items() if token in v], + *["Misc: " + k.value + additmods(v, token) + for k, v in tms.MISC_MAP.items() if token in v], + *["Awakening: " + get_awakening_emoji(k) + ' ' + awakenings[k.value].name_en + additmods(v, token) + for k, v in tms.AWOKEN_MAP.items() if token in v], *["Main attr: " + get_attribute_emoji_by_enum(k, None) + ' ' + k.name.replace("Nil", "None") + - additmods(v, modifier) - for k, v in tms.COLOR_MAP.items() if modifier in v], + additmods(v, token) + for k, v in tms.COLOR_MAP.items() if token in v], *["Sub attr: " + get_attribute_emoji_by_enum(False, k) + ' ' + k.name.replace("Nil", "None") + - additmods(v, modifier) - for k, v in tms.SUB_COLOR_MAP.items() if modifier in v], + additmods(v, token) + for k, v in tms.SUB_COLOR_MAP.items() if token in v], *["Dual attr: " + get_attribute_emoji_by_enum(k[0], k[1]) + ' ' + k[0].name.replace("Nil", "None") + - '/' + k[1].name.replace("Nil", "None") + additmods(v, modifier) - for k, v in tms.DUAL_COLOR_MAP.items() if modifier in v], - *["Series: " + series[k].name_en + additmods(v, modifier) - for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if modifier in v], + '/' + k[1].name.replace("Nil", "None") + additmods(v, token) + for k, v in tms.DUAL_COLOR_MAP.items() if token in v], + *["Series: " + series[k].name_en + additmods(v, token) + for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if token in v], - *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", modifier)], - *["Base rarity: " + m for m in re.findall(r"^(\d+)\*b$", modifier)], + *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", token)], + *["Base rarity: " + m for m in re.findall(r"^(\d+)\*b$", token)], *[f"[UNSUPPORTED] Multiple awakenings: {m}x {awakenings[a.value].name_en}" - f"{additmods([f'{m}*{d}' for d in v], modifier)}" - for m, ag in re.findall(r"^(\d+)\*{}$".format(awokengroup), modifier) + f"{additmods([f'{m}*{d}' for d in v], token)}" + for m, ag in re.findall(r"^(\d+)\*{}$".format(awokengroup), token) for a, v in tms.AWOKEN_MAP.items() if ag in v] ] @@ -1425,4 +1438,4 @@ def additmods(ms, om): for page in pagify("\n".join(meanings) + "\n\n" + o.strip()): await ctx.send(page) else: - await ctx.send(f"There are no modifiers that match `{modifier}`.") + await ctx.send(f"There are no modifiers that match `{token}`.") From fdf27e021975b49987004004776c828c7b3669c4 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Feb 2021 08:07:30 -0700 Subject: [PATCH 0559/1220] Fix id3 (was crashing on every query) (#737) --- padinfo/core/find_monster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index e96c73d0..b5192029 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -337,7 +337,7 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. - mon = find_monster.get_most_eligable_monster(dgcog, tokenized_query, monster_score) + mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, monster_score) return monster_score[mon], mon From e732b7fb65fd60f9ca70c31a0f4f50b6fec0273d Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Feb 2021 19:56:05 -0700 Subject: [PATCH 0560/1220] [id3] Don't edit input to token dict combiner (#740) --- dadguide/monster_index.py | 7 +++++-- padinfo/core/find_monster.py | 19 ++++++++++--------- padinfo/padinfo.py | 7 +++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a61c7184..a4941615 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -113,6 +113,9 @@ async def _build_monster_index(self, monsters): last_token = m.name_en.split(',')[-1].strip() autotoken = True + for jpt in m.name_ja.split(" "): + self.name_tokens[jpt].add(m) + # Propagate name tokens throughout all evos for me in self.graph.get_alt_monsters(m): if last_token != me.name_en.split(',')[-1].strip(): @@ -361,10 +364,10 @@ async def sheet_to_reader(url, length=None): def combine_tokens_dicts(d1, *ds): - combined = defaultdict(set, d1) + combined = defaultdict(set, d1.copy()) for d2 in ds: for k, v in d2.items(): - combined[k].update(v) + combined[k] = combined[k].union(v) return combined diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index b5192029..79da12a0 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -25,11 +25,11 @@ } -def calc_ratio(s1, s2): +def calc_ratio_modifier(s1, s2, dist=.05): return jaro_winkler(s1, s2, .05) -def calc_ratio_prefix(token, full_word, index2, factor=.05): +def calc_ratio_name(token, full_word, index2, factor=.05): mw = index2.mwt_to_len[full_word] != 1 jw = jaro_winkler(token, full_word, factor) @@ -66,7 +66,8 @@ def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): if len(mwt) > len(tokens) - c1: continue for c2, t in enumerate(mwt): - if (tokens[c1 + c2] != t and len(t) < 5) or calc_ratio(tokens[c1 + c2], t) < self.TOKEN_JW_DISTANCE: + if (tokens[c1 + c2] != t and len(t) < 5) \ + or calc_ratio_modifier(tokens[c1 + c2], t) < self.TOKEN_JW_DISTANCE: break else: s = len(mwt) - 1 @@ -82,7 +83,7 @@ def _monster_has_token(self, monster, token, monsterscore, monster_mods): monsterscore[monster] += 1 return True else: - closest = max(jaro_winkler(m, token, .05) for m in monster_mods) + closest = max(calc_ratio_modifier(m, token) for m in monster_mods) if closest > self.TOKEN_JW_DISTANCE: monsterscore[monster] += closest return True @@ -99,7 +100,7 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if any(jaro_winkler(m, token) > self.MODIFIER_JW_DISTANCE for m in index2.suffixes): + if any(calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in index2.suffixes): if negated: negative_modifiers.add(token) else: @@ -113,7 +114,7 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], negated = token.startswith("-") token = token.lstrip('-') if token in index2.all_modifiers or ( - any(jaro_winkler(m, token) > self.MODIFIER_JW_DISTANCE for m in longmods) + any(calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) and token not in index2.all_name_tokens and len(token) >= 8): if negated: @@ -165,13 +166,13 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): valid = set() - ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_prefix(t, nt, index2) > self.TOKEN_JW_DISTANCE], - key=lambda nt: calc_ratio_prefix(t, nt, index2), reverse=True) + ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_name(t, nt, index2) > self.TOKEN_JW_DISTANCE], + key=lambda nt: calc_ratio_name(t, nt, index2), reverse=True) ms += [token for token in index2.all_name_tokens if token.startswith(t)] if not ms: return None for match in ms: - score = calc_ratio_prefix(t, match, index2) + score = calc_ratio_name(t, match, index2) for m in index2.manual[match]: if m not in valid: monsterscore[m] += (score + .001) * mult diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b8f0ec9c..740ea1e8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -12,7 +12,6 @@ import discord import tsutils -from Levenshtein import jaro_winkler from discord import Color from discordmenu.emoji.emoji_cache import emoji_cache from discordmenu.intra_message_state import IntraMessageState @@ -27,7 +26,7 @@ from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom, calc_ratio_prefix + findMonsterCustom, calc_ratio_name, calc_ratio_modifier from padinfo.core.historic_lookups import historic_lookups from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings @@ -1294,8 +1293,8 @@ async def debugiddist(self, ctx, s1, s2): dgcog = self.bot.get_cog("Dadguide") - dist = jaro_winkler(s1, s2) - dist2 = calc_ratio_prefix(s1, s2, dgcog.index2) + dist = calc_ratio_modifier(s1, s2) + dist2 = calc_ratio_name(s1, s2, dgcog.index2) yes = '\N{WHITE HEAVY CHECK MARK}' no = '\N{CROSS MARK}' await ctx.send(f"Printing info for {inline(s1)}, {inline(s2)}\n" + From 42f2076775c7ca35b4368a7dfeaf834dad7ad201 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 5 Feb 2021 16:17:57 -0700 Subject: [PATCH 0561/1220] Allow non-geurilla dungeons to be used in AED (#742) --- padevents/padevents.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index aad67d83..ac2efcd9 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,6 +1,5 @@ import asyncio import datetime -import itertools import logging import re from collections import defaultdict @@ -10,6 +9,7 @@ from typing import TYPE_CHECKING import discord +import itertools import prettytable import pytz from redbot.core import checks @@ -20,6 +20,7 @@ if TYPE_CHECKING: from dadguide.models.scheduled_event_model import ScheduledEventModel + def user_is_donor(ctx, only_patron=False): if ctx.author.id in ctx.bot.owner_ids: return True @@ -28,6 +29,7 @@ def user_is_donor(ctx, only_patron=False): return False return donationcog.is_donor(ctx, only_patron) + logger = logging.getLogger('red.padbot-cogs.padevents') SUPPORTED_SERVERS = ["JP", "NA", "KR"] @@ -141,7 +143,7 @@ async def check_started(self): continue for aed in data.get('dmevents', []): if event.start_from_now_sec() > aed['offset'] * 60 \ - or event.group != aed['group'] \ + or (event.group not in (aed['group'], None)) \ or event.server != aed['server'] \ or (aed['key'], event.key) in self.rolepinged_events: continue From ee9f96ed8a7c0ee0259e3ff92282d36842fed3b4 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 5 Feb 2021 16:31:03 -0700 Subject: [PATCH 0562/1220] Add evo modifier to first evo/uvo of tree (#744) --- dadguide/monster_graph.py | 6 ++++++ dadguide/monster_index.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 12bdc563..55309cb1 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -414,6 +414,12 @@ def monster_is_normal_evo(self, monster: MonsterModel) -> bool: or self.monster_is_reincarnated(monster) or self.monster_is_base(monster)) + def monster_is_first_evo(self, monster: MonsterModel) -> bool: + prev = self.get_prev_evolution_by_monster(monster) + if prev: + return self.get_prev_evolution_by_monster(prev) is None + return False + def monster_is_second_ultimate(self, monster: MonsterModel) -> bool: if self.monster_is_reversible_evo(monster): prev = self.get_prev_evolution_by_monster(monster) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a4941615..0904a76d 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -243,7 +243,7 @@ async def get_modifiers(self, m): m.is_equip or '極醒' in m.name_ja) # Evo - if self.graph.monster_is_normal_evo(m): + if self.graph.monster_is_normal_evo(m) or self.graph.monster_is_first_evo(m): modifiers.update(EVO_MAP[EvoTypes.EVO]) # Uvo From 32f4c655e439caf091600d332dcbf8cf7efcc4cc Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 5 Feb 2021 18:36:19 -0600 Subject: [PATCH 0563/1220] Prep id for new menu (except arrow emojis) (#741) --- padinfo/common/external_links.py | 4 + padinfo/core/find_monster.py | 4 +- padinfo/core/id.py | 70 ++++++++ padinfo/id_menu.py | 290 +++++++++++++++++++------------ padinfo/id_menu_old.py | 130 ++++++++++++++ padinfo/padinfo.py | 68 +++++--- padinfo/view/evos.py | 30 ++-- padinfo/view/materials.py | 26 +-- padinfo/view/otherinfo.py | 10 +- padinfo/view/pantheon.py | 24 ++- padinfo/view/pic.py | 34 ++-- padinfo/view_state/evos.py | 43 +++++ padinfo/view_state/id.py | 4 +- padinfo/view_state/materials.py | 49 ++++++ padinfo/view_state/otherinfo.py | 40 +++++ padinfo/view_state/pantheon.py | 44 +++++ padinfo/view_state/pic.py | 40 +++++ 17 files changed, 712 insertions(+), 198 deletions(-) create mode 100644 padinfo/id_menu_old.py create mode 100644 padinfo/view_state/evos.py create mode 100644 padinfo/view_state/materials.py create mode 100644 padinfo/view_state/otherinfo.py create mode 100644 padinfo/view_state/pantheon.py create mode 100644 padinfo/view_state/pic.py diff --git a/padinfo/common/external_links.py b/padinfo/common/external_links.py index ad4254e6..79f2fc98 100644 --- a/padinfo/common/external_links.py +++ b/padinfo/common/external_links.py @@ -26,3 +26,7 @@ def skyozora(m: "MonsterModel"): def ilmina(m: "MonsterModel"): return ILMINA_TEMPLATE.format(m.monster_no_jp) + + +def ilmina_skill(m: "MonsterModel"): + return "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 79da12a0..3a1bf308 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -315,7 +315,7 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) if name_query_tokens: monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog.index2) @@ -335,7 +335,7 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo # no modifiers match any monster in the evo tree return 0, None - print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, monster_score) diff --git a/padinfo/core/id.py b/padinfo/core/id.py index 16c9eb7f..e2f36e12 100644 --- a/padinfo/core/id.py +++ b/padinfo/core/id.py @@ -1,3 +1,4 @@ +from padinfo.common.external_links import ilmina_skill from padinfo.core.find_monster import findMonsterCustom2 @@ -10,6 +11,68 @@ async def perform_id_query(dgcog, raw_query, beta_id3): return monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters +async def perform_evos_query(dgcog, raw_query, beta_id3): + db_context = dgcog.database + monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) + alt_versions, gem_versions = await get_monster_evos_info(db_context, monster) + return monster, alt_versions, gem_versions + + +async def perform_mats_query(dgcog, raw_query, beta_id3): + db_context = dgcog.database + monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) + mats = db_context.graph.evo_mats_by_monster(monster) + usedin = db_context.graph.material_of_monsters(monster) + evo_gem = db_context.graph.evo_gem_monster(monster) + gemid = str(evo_gem.monster_no_na) if evo_gem else None + gemusedin = db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] + skillups = [] + skillup_evo_count = 0 + link = ilmina_skill(monster) + + if monster.active_skill: + sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id) + if db_context.graph.monster_is_farmable_evo(m)] + sugs = [db_context.graph.evo_gem_monster(su) for su in sums] + vsums = [] + for su in sums: + if not any(susu in vsums for susu in db_context.graph.get_alt_monsters(su)): + vsums.append(su) + skillups = [su for su in vsums + if db_context.graph.monster_is_farmable_evo(su) and + db_context.graph.get_base_id(su) != db_context.graph.get_base_id(monster) and + su not in sugs] if monster.active_skill else [] + skillup_evo_count = len(sums) - len(vsums) + + if not any([mats, usedin, gemusedin, skillups and not monster.is_stackable]): + return None, None, None, None, None, None, None, None + + return monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link + + +async def perform_pic_query(dgcog, raw_query, beta_id3): + monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) + return monster + + +async def perform_pantheon_query(dgcog, raw_query, beta_id3): + db_context = dgcog.database + monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) + full_pantheon = db_context.get_monsters_by_series(monster.series_id) + pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) + if len(pantheon_list) == 0 or len(pantheon_list) > 20: + return None + + series_name = monster.series.name_en + + return monster, pantheon_list, series_name + + +async def perform_otherinfo_query(dgcog, raw_query, beta_id3): + monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) + return monster + + async def get_monster_misc_info(db_context, monster): transform_base = db_context.graph.get_transform_base(monster) true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value @@ -18,3 +81,10 @@ async def get_monster_misc_info(db_context, monster): alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, key=lambda x: x.monster_id) return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw + + +async def get_monster_evos_info(db_context, monster): + alt_versions = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + key=lambda x: x.monster_id) + gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) + return alt_versions, gem_versions diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 52789426..c7db24e4 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -1,118 +1,194 @@ -import random -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -import discord -from discord import Color +from discord import Message +from discordmenu.embed.emoji import EmbedMenuEmojiConfig +from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.emoji.emoji_cache import emoji_cache +from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ + MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter +from tsutils import char_to_emoji -from padinfo.core.id import get_monster_misc_info -from padinfo.view.evos import EvosView from padinfo.view.id import IdView -from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillSingleView -from padinfo.view.links import LinksView -from padinfo.view.lookup import LookupView -from padinfo.view.materials import MaterialView +from padinfo.view.materials import MaterialsView from padinfo.view.otherinfo import OtherInfoView from padinfo.view.pantheon import PantheonView -from padinfo.view.pic import PicsView +from padinfo.view.pic import PicView +from padinfo.view.evos import EvosView +from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState +from padinfo.view_state.materials import MaterialsViewState +from padinfo.view_state.otherinfo import OtherInfoViewState +from padinfo.view_state.pantheon import PantheonViewState +from padinfo.view_state.pic import PicViewState if TYPE_CHECKING: - from dadguide.database_context import DbContext - from dadguide.models.monster_model import MonsterModel + pass + +emoji_button_names = [ + # '\N{BLACK LEFT-POINTING TRIANGLE}', + # '\N{BLACK RIGHT-POINTING TRIANGLE}', + '\N{HOUSE BUILDING}', + char_to_emoji('e'), + '\N{MEAT ON BONE}', + '\N{FRAME WITH PICTURE}', + '\N{CLASSICAL BUILDING}', + '\N{SCROLL}', + '\N{CROSS MARK}', +] +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') class IdMenu: - def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = None): - self.ctx = ctx - self.db_context = db_context - self.allowed_emojis = allowed_emojis - - async def get_user_embed_color(self, pdicog): - color = await pdicog.config.user(self.ctx.author).color() - if color is None: - return Color.default() - elif color == "random": - return Color(random.randint(0x000000, 0xffffff)) - else: - return discord.Color(color) - - async def make_id_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ - await get_monster_misc_info(self.db_context, m) - state = IdViewState("", "TODO", "todo", "", m, color, transform_base, true_evo_type_raw, acquire_raw, - base_rarity, alt_monsters) - e = IdView.embed(state) - return e.to_embed() - - async def make_evo_embed(self, m: "MonsterModel"): - alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) - gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - e = EvosView.embed(m, alt_versions, gem_versions, color) - return e.to_embed() - - async def make_evo_mats_embed(self, m: "MonsterModel"): - mats = self.db_context.graph.evo_mats_by_monster(m) - usedin = self.db_context.graph.material_of_monsters(m) - evo_gem = self.db_context.graph.evo_gem_monster(m) - gemid = str(evo_gem.monster_no_na) if evo_gem else None - gemusedin = self.db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] - skillups = [] - skillup_evo_count = 0 - - if m.active_skill: - sums = [m for m in self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) - if self.db_context.graph.monster_is_farmable_evo(m)] - sugs = [self.db_context.graph.evo_gem_monster(su) for su in sums] - vsums = [] - for su in sums: - if not any(susu in vsums for susu in self.db_context.graph.get_alt_monsters(su)): - vsums.append(su) - skillups = [su for su in vsums - if self.db_context.graph.monster_is_farmable_evo(su) and - self.db_context.graph.get_base_id(su) != self.db_context.graph.get_base_id(m) and - su not in sugs] if m.active_skill else [] - skillup_evo_count = len(sums) - len(vsums) - - if not any([mats, usedin, gemusedin, skillups and not m.is_stackable]): - return None - link = "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return MaterialView.embed(m, color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, - link).to_embed() - - async def make_pantheon_embed(self, m: "MonsterModel"): - full_pantheon = self.db_context.get_monsters_by_series(m.series_id) - pantheon_list = list(filter(lambda x: self.db_context.graph.monster_is_base(x), full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 20: - return None - - series_name = m.series.name_en - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return PantheonView.embed(m, color, pantheon_list, series_name).to_embed() - - async def make_picture_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return PicsView.embed(m, color).to_embed() - - async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - embed = LeaderSkillView.embed(left_m, right_m, color) - return embed.to_embed() - - async def make_lookup_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return LookupView.embed(m, color).to_embed() - - async def make_otherinfo_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return OtherInfoView.embed(m, color).to_embed() - - async def make_links_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return LinksView.embed(m, color).to_embed() - - async def make_lssingle_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return LeaderSkillSingleView.embed(m, color).to_embed() + INITIAL_EMOJI = emoji_button_names[0] + MENU_TYPE = 'IdMenu' + + @staticmethod + def menu(original_author_id, friend_ids, bot_id): + transitions = { + # emoji_button_names[0]: respond_to_left, + # emoji_button_names[1]: respond_to_right, + IdMenu.INITIAL_EMOJI: IdMenu.respond_with_current_id, + emoji_button_names[1]: IdMenu.respond_with_evos, + emoji_button_names[2]: IdMenu.respond_with_mats, + emoji_button_names[3]: IdMenu.respond_with_picture, + emoji_button_names[4]: IdMenu.repond_with_pantheon, + emoji_button_names[5]: IdMenu.respond_with_otherinfo, + } + + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(bot_id), + MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) + ] + embed = EmbedMenu(reaction_filters, transitions, IdMenu.id_control, menu_emoji_config) + return embed + + @staticmethod + async def respond_to_left(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + # Extract the query from the id state + # TODO: let the user switch scroll modes in between then and now + ims['query'] = ims['left_arrow'] + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = IdMenu.id_control(id_view_state) + return id_control + + @staticmethod + async def respond_to_right(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + # Extract the query from the id state + # TODO: let the user switch scroll modes in between then and now + ims['query'] = ims['right_arrow'] + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = IdMenu.id_control(id_view_state) + return id_control + + @staticmethod + async def _deserialize_appropriately(ims, menu_type): + # need to deserialize the left-right arrows according to which type of tab we're in + # but I think this means maybe type needs to be a function of tab not of overall menu? + # unclear + pass + + @staticmethod + async def respond_with_current_id(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + view_state = await IdViewState.deserialize(dgcog, user_config, ims) + control = IdMenu.id_control(view_state) + return control + + @staticmethod + async def respond_with_evos(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + view_state = await EvosViewState.deserialize(dgcog, user_config, ims) + control = IdMenu.evos_control(view_state) + return control + + @staticmethod + async def respond_with_mats(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + view_state = await MaterialsViewState.deserialize(dgcog, user_config, ims) + control = IdMenu.mats_control(view_state) + return control + + @staticmethod + async def respond_with_picture(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + view_state = await PicViewState.deserialize(dgcog, user_config, ims) + control = IdMenu.pic_control(view_state) + return control + + @staticmethod + async def repond_with_pantheon(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + view_state = await PantheonViewState.deserialize(dgcog, user_config, ims) + control = IdMenu.pantheon_control(view_state) + return control + + @staticmethod + async def respond_with_otherinfo(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + view_state = await OtherInfoViewState.deserialize(dgcog, user_config, ims) + control = IdMenu.otherinfo_control(view_state) + return control + + @staticmethod + def id_control(state: IdViewState): + return EmbedControl( + [IdView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + + @staticmethod + def evos_control(state: EvosViewState): + return EmbedControl( + [EvosView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + + @staticmethod + def mats_control(state: MaterialsViewState): + return EmbedControl( + [MaterialsView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + + @staticmethod + def pic_control(state: PicViewState): + return EmbedControl( + [PicView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + + @staticmethod + def pantheon_control(state: PantheonViewState): + return EmbedControl( + [PantheonView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + + @staticmethod + def otherinfo_control(state: OtherInfoViewState): + return EmbedControl( + [OtherInfoView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + diff --git a/padinfo/id_menu_old.py b/padinfo/id_menu_old.py new file mode 100644 index 00000000..13dfd134 --- /dev/null +++ b/padinfo/id_menu_old.py @@ -0,0 +1,130 @@ +import random +from typing import TYPE_CHECKING + +import discord +from discord import Color + +from padinfo.core.id import get_monster_misc_info +from padinfo.view.evos import EvosView +from padinfo.view.id import IdView +from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillSingleView +from padinfo.view.links import LinksView +from padinfo.view.lookup import LookupView +from padinfo.view.materials import MaterialsView +from padinfo.view.otherinfo import OtherInfoView +from padinfo.view.pantheon import PantheonView +from padinfo.view.pic import PicView +from padinfo.view_state.evos import EvosViewState +from padinfo.view_state.id import IdViewState +from padinfo.view_state.materials import MaterialsViewState +from padinfo.view_state.otherinfo import OtherInfoViewState +from padinfo.view_state.pantheon import PantheonViewState +from padinfo.view_state.pic import PicViewState + +if TYPE_CHECKING: + from dadguide.database_context import DbContext + from dadguide.models.monster_model import MonsterModel + + +class IdMenu: + def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = None): + self.ctx = ctx + self.db_context = db_context + self.allowed_emojis = allowed_emojis + + async def get_user_embed_color(self, pdicog): + color = await pdicog.config.user(self.ctx.author).color() + if color is None: + return Color.default() + elif color == "random": + return Color(random.randint(0x000000, 0xffffff)) + else: + return discord.Color(color) + + async def make_id_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ + await get_monster_misc_info(self.db_context, m) + state = IdViewState("", "TODO", "todo", "", color, m, transform_base, true_evo_type_raw, acquire_raw, + base_rarity, alt_monsters) + e = IdView.embed(state) + return e.to_embed() + + async def make_evo_embed(self, m: "MonsterModel"): + alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) + gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + state = EvosViewState("", "TODO", "todo", "", color, m, alt_versions, gem_versions) + e = EvosView.embed(state) + return e.to_embed() + + async def make_evo_mats_embed(self, m: "MonsterModel"): + mats = self.db_context.graph.evo_mats_by_monster(m) + usedin = self.db_context.graph.material_of_monsters(m) + evo_gem = self.db_context.graph.evo_gem_monster(m) + gemid = str(evo_gem.monster_no_na) if evo_gem else None + gemusedin = self.db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] + skillups = [] + skillup_evo_count = 0 + + if m.active_skill: + sums = [m for m in self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) + if self.db_context.graph.monster_is_farmable_evo(m)] + sugs = [self.db_context.graph.evo_gem_monster(su) for su in sums] + vsums = [] + for su in sums: + if not any(susu in vsums for susu in self.db_context.graph.get_alt_monsters(su)): + vsums.append(su) + skillups = [su for su in vsums + if self.db_context.graph.monster_is_farmable_evo(su) and + self.db_context.graph.get_base_id(su) != self.db_context.graph.get_base_id(m) and + su not in sugs] if m.active_skill else [] + skillup_evo_count = len(sums) - len(vsums) + + if not any([mats, usedin, gemusedin, skillups and not m.is_stackable]): + return None + link = "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + + state = MaterialsViewState("", "TODO", "todo", "", color, m, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, + link) + + return MaterialsView.embed(state).to_embed() + + async def make_pantheon_embed(self, m: "MonsterModel"): + full_pantheon = self.db_context.get_monsters_by_series(m.series_id) + pantheon_list = list(filter(lambda x: self.db_context.graph.monster_is_base(x), full_pantheon)) + if len(pantheon_list) == 0 or len(pantheon_list) > 20: + return None + + series_name = m.series.name_en + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + state = PantheonViewState('', '', '', '', color, m, pantheon_list, series_name) + return PantheonView.embed(state).to_embed() + + async def make_picture_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + state = PicViewState('', '', '', '', color, m) + return PicView.embed(state).to_embed() + + async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + embed = LeaderSkillView.embed(left_m, right_m, color) + return embed.to_embed() + + async def make_lookup_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return LookupView.embed(m, color).to_embed() + + async def make_otherinfo_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + state = OtherInfoViewState('', '', '', '', color, m) + return OtherInfoView.embed(state).to_embed() + + async def make_links_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return LinksView.embed(m, color).to_embed() + + async def make_lssingle_embed(self, m: "MonsterModel"): + color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) + return LeaderSkillSingleView.embed(m, color).to_embed() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 740ea1e8..9c65db23 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -28,12 +28,15 @@ from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ findMonsterCustom, calc_ratio_name, calc_ratio_modifier from padinfo.core.historic_lookups import historic_lookups +from padinfo.core.id import perform_id_query from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater -from padinfo.id_menu import IdMenu +from padinfo.id_menu_old import IdMenu as IdMenuOld +from padinfo.id_menu import IdMenu, emoji_button_names as id_menu_emoji_button_names from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState if TYPE_CHECKING: @@ -137,7 +140,8 @@ async def test_reaction_add(self, reaction, member): else: emoji_clicked = emoji_obj.name - if emoji_clicked not in ls_menu_emoji_button_names: + if not (emoji_clicked in ls_menu_emoji_button_names or + emoji_clicked in id_menu_emoji_button_names): return message = reaction.message @@ -147,20 +151,29 @@ async def test_reaction_add(self, reaction, member): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - if menu_type == LeaderSkillMenu.MENU_TYPE: - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - embed_menu = LeaderSkillMenu.menu(original_author_id, friends, self.bot.user.id) - if not (await embed_menu.should_respond(message, reaction, member)): - return + menu_map = { + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu.menu, + IdMenu.MENU_TYPE: IdMenu.menu, + } - dgcog = await self.get_dgcog() - user_config = await BotConfig.get_user(self.config, original_author_id) - data = { - 'dgcog': dgcog, - 'user_config': user_config - } - await embed_menu.transition(message, ims, emoji_clicked, member, **data) + menu_func = menu_map.get(menu_type) + + if not menu_func: + return + + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + embed_menu = menu_func(original_author_id, friends, self.bot.user.id) + if not (await embed_menu.should_respond(message, reaction, member)): + return + + dgcog = await self.get_dgcog() + user_config = await BotConfig.get_user(self.config, original_author_id) + data = { + 'dgcog': dgcog, + 'user_config': user_config + } + await embed_menu.transition(message, ims, emoji_clicked, member, **data) @commands.command() async def jpname(self, ctx, *, query: str): @@ -182,6 +195,19 @@ async def _id(self, ctx, *, query: str): else: await self._do_id(ctx, query) + # dgcog = await self.get_dgcog() + # raw_query = query + # color = await self.get_user_embed_color(ctx) + # original_author_id = ctx.message.author.id + # friend_cog = self.bot.get_cog("Friend") + # friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + # monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + # await perform_id_query(dgcog, raw_query, await self.config.user(ctx.author).beta_id3()) + # state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, + # monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters) + # menu = IdMenu.menu(original_author_id, friends, self.bot.user.id) + # await menu.create(ctx, state) + @commands.command(aliases=["idold", "oldid"]) @checks.bot_has_permissions(embed_links=True) async def id1(self, ctx, *, query): @@ -369,7 +395,7 @@ async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): DGCOG = self.bot.get_cog("Dadguide") db_context = DGCOG.database - menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenuOld(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) id_embed = await menu.make_id_embed(m) evo_embed = await menu.make_evo_embed(m) @@ -414,7 +440,7 @@ async def _do_evolistmenu(self, ctx, sm): monsters.sort(key=lambda x: x.monster_id) emoji_to_embed = OrderedDict() - menu = IdMenu(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenuOld(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) starting_menu_emoji = None for idx, m in enumerate(monsters): chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" @@ -465,7 +491,7 @@ async def links(self, ctx, *, query: str): dgcog = await self.get_dgcog() m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: - menu = IdMenu(ctx) + menu = IdMenuOld(ctx) embed = await menu.make_links_embed(m) await ctx.send(embed=embed) @@ -505,7 +531,7 @@ async def lookup(self, ctx, *, query: str): dgcog = await self.get_dgcog() m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) if m is not None: - menu = IdMenu(ctx, allowed_emojis=self.get_emojis()) + menu = IdMenuOld(ctx, allowed_emojis=self.get_emojis()) embed = await menu.make_lookup_embed(m) await ctx.send(embed=embed) else: @@ -611,7 +637,7 @@ async def leaderskillsingle(self, ctx, *, query): if err: await ctx.send(err) return - menu = IdMenu(ctx, db_context=dgcog.database, allowed_emojis=self.get_emojis()) + menu = IdMenuOld(ctx, db_context=dgcog.database, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = await menu.make_lssingle_embed(m) emoji_to_embed[self.left_emoji] = await menu.make_id_embed(m) @@ -754,7 +780,7 @@ async def norf_import(self, ctx, queries): cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+)\s+(\w*) *(.*)', queries) async with self.config.fluff_suite() as suite: for query, result, fluff, reason in cases: - print(query, result, fluff, reason) + # print(query, result, fluff, reason) if not any(c['id'] == int(result) and c['token'] == query for c in suite): suite.append({ 'id': int(result), diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index a70e9187..13cfd01c 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -1,16 +1,12 @@ -from typing import TYPE_CHECKING - from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel +from padinfo.view_state.evos import EvosViewState def _evo_lines(monsters, current_monster): @@ -24,23 +20,23 @@ def _evo_lines(monsters, current_monster): class EvosView: @staticmethod - def embed(m: "MonsterModel", alt_versions, gem_versions, color): + def embed(state: EvosViewState): fields = [ EmbedField( - ("{} evolution" if len(alt_versions) == 1 else "{} evolutions").format(len(alt_versions)), - Box(*_evo_lines(alt_versions, m)))] + ("{} evolution" if len(state.alt_versions) == 1 else "{} evolutions").format(len(state.alt_versions)), + Box(*_evo_lines(state.alt_versions, state.monster)))] - if gem_versions: + if state.gem_versions: fields.append( EmbedField( - ("{} evolve gem" if len(gem_versions) == 1 else "{} evolve gems").format(len(gem_versions)), - Box(*_evo_lines(gem_versions, m)))) + ("{} evolve gem" if len(state.gem_versions) == 1 else "{} evolve gems").format(len(state.gem_versions)), + Box(*_evo_lines(state.gem_versions, state.monster)))) return EmbedView( EmbedMain( - color=color, - title=MonsterHeader.long_v2(m).to_markdown(), - url=puzzledragonx(m)), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), - embed_footer=pad_info_footer(), + color=state.color, + title=MonsterHeader.long_v2(state.monster).to_markdown(), + url=puzzledragonx(state.monster)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), + embed_footer=pad_info_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 79716566..f3d45f3e 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -6,9 +6,10 @@ from discordmenu.embed.text import LinkedText from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.base import pad_info_footer, pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage +from padinfo.view_state.materials import MaterialsViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -47,21 +48,22 @@ def skillup_field(mons, sec, link): Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:MAX_MONS_TO_SHOW]), text, text2)) -class MaterialView: +class MaterialsView: @staticmethod - def embed(m: "MonsterModel", color, mats, usedin, gemid, gemusedin, skillups, sec, link): + def embed(state: MaterialsViewState): + # m: "MonsterModel", color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link return EmbedView( EmbedMain( - color=color, - title=MonsterHeader.long_v2(m).to_markdown(), - url=puzzledragonx(m) + color=state.color, + title=MonsterHeader.long_v2(state.monster).to_markdown(), + url=puzzledragonx(state.monster) ), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), - embed_footer=pad_info_footer(), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), + embed_footer=pad_info_footer_with_state(state), embed_fields=[f for f in [ - mat_use_field(mats, "Evo materials") if mats or not m.is_stackable else None, - mat_use_field(usedin, "Material for", 10) if usedin else None, - mat_use_field(gemusedin, "Evo gem ({}) is mat for".format(gemid)) if gemusedin else None, - skillup_field(skillups, sec, link) if not m.is_stackable else None + mat_use_field(state.mats, "Evo materials") if state.mats or not state.monster.is_stackable else None, + mat_use_field(state.usedin, "Material for", 10) if state.usedin else None, + mat_use_field(state.gemusedin, "Evo gem ({}) is mat for".format(state.gemid)) if state.gemusedin else None, + skillup_field(state.skillups, state.skillup_evo_count, state.link) if not state.monster.is_stackable else None ] if f is not None] ) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 1d60e4b7..9cf64b48 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -8,9 +8,10 @@ from redbot.core.utils.chat_formatting import box from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina -from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.links import LinksView +from padinfo.view_state.otherinfo import OtherInfoViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -33,13 +34,14 @@ def statsbox(m): class OtherInfoView: @staticmethod - def embed(m: "MonsterModel", color): + def embed(state: OtherInfoViewState): + m = state.monster return EmbedView( EmbedMain( - color=color, + color=state.color, title=MonsterHeader.long_v2(m).to_markdown(), url=puzzledragonx(m)), - embed_footer=pad_info_footer(), + embed_footer=pad_info_footer_with_state(state), embed_fields=[ EmbedField( "Stats at +297:", diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 8578e6ae..24c8606e 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -1,35 +1,31 @@ -from typing import TYPE_CHECKING - from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel +from padinfo.view_state.pantheon import PantheonViewState class PantheonView: @staticmethod - def embed(m: "MonsterModel", color, pantheon_list, series_name): + def embed(state: PantheonViewState): fields = [EmbedField( - 'Pantheon: {}'.format(series_name), + 'Pantheon: {}'.format(state.series_name), Box( *[MonsterHeader.short_with_emoji(m) - for m in sorted(pantheon_list, key=lambda x: x.monster_no_na)] + for m in sorted(state.pantheon_list, key=lambda x: x.monster_no_na)] ) )] return EmbedView( EmbedMain( - color=color, - title=MonsterHeader.long_v2(m).to_markdown(), - url=puzzledragonx(m)), - embed_footer=pad_info_footer(), + color=state.color, + title=MonsterHeader.long_v2(state.monster).to_markdown(), + url=puzzledragonx(state.monster)), + embed_footer=pad_info_footer_with_state(state), embed_fields=fields, - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), ) diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 752fbf70..43b8c925 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -1,50 +1,46 @@ -from typing import TYPE_CHECKING - from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LinkedText, Text from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.base import pad_info_footer +from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel +from padinfo.view_state.pic import PicViewState -class PicsView: +class PicView: @staticmethod - def embed(m: "MonsterModel", color): - url = MonsterImage.picture(m) - animated = m.has_animation + def embed(state: PicViewState): + url = MonsterImage.picture(state.monster) + animated = state.monster.has_animation fields = [EmbedField( 'Extra Links', Box( Box( Text('Animation:'), - LinkedText('(MP4)', MonsterImage.video(m)), + LinkedText('(MP4)', MonsterImage.video(state.monster)), Text('|'), - LinkedText('(GIF)', MonsterImage.gif(m)), + LinkedText('(GIF)', MonsterImage.gif(state.monster)), delimiter=' ' ) if animated else None, Box( Text('Orb Skin:'), - LinkedText('Regular', MonsterImage.orb_skin(m)), + LinkedText('Regular', MonsterImage.orb_skin(state.monster)), Text('|'), - LinkedText('Color Blind', MonsterImage.orb_skin_colorblind(m)), + LinkedText('Color Blind', MonsterImage.orb_skin_colorblind(state.monster)), delimiter=' ' - ) if m.orb_skin_id else None, + ) if state.monster.orb_skin_id else None, ) )] return EmbedView( EmbedMain( - color=color, - title=MonsterHeader.long_v2(m).to_markdown(), - url=puzzledragonx(m)), - embed_footer=pad_info_footer(), + color=state.color, + title=MonsterHeader.long_v2(state.monster).to_markdown(), + url=puzzledragonx(state.monster)), + embed_footer=pad_info_footer_with_state(state), embed_fields=fields, embed_body_image=EmbedBodyImage(url), ) diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py new file mode 100644 index 00000000..ca881407 --- /dev/null +++ b/padinfo/view_state/evos.py @@ -0,0 +1,43 @@ +from typing import List, TYPE_CHECKING + +from padinfo.common.config import UserConfig +from padinfo.core.id import perform_evos_query +from padinfo.view_state.base import ViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class EvosViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.alt_versions = alt_versions + self.gem_versions = gem_versions + self.query = query + self.monster = monster + self.color = color + + def serialize(self): + ret = super().serialize() + ret.update({ + 'query': self.query, + 'resolved_monster_id': self.monster.monster_id + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + + original_author_id = ims['original_author_id'] + + menu_type = ims['menu_type'] + + query = ims.get('query') or raw_query + + monster, alt_versions, gem_versions = await perform_evos_query(dgcog, query, user_config.beta_id3) + + return EvosViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + alt_versions, gem_versions, extra_state=ims) diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index a25fcee1..2ab2c04c 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -9,7 +9,7 @@ class IdViewState(ViewState): - def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", color, + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) @@ -43,6 +43,6 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ await perform_id_query(dgcog, query, user_config.beta_id3) - return IdViewState(original_author_id, menu_type, raw_query, query, monster, user_config.color, + return IdViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, extra_state=ims) diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py new file mode 100644 index 00000000..fc75b2ff --- /dev/null +++ b/padinfo/view_state/materials.py @@ -0,0 +1,49 @@ +from typing import List, TYPE_CHECKING, Optional + +from padinfo.common.config import UserConfig +from padinfo.core.id import perform_mats_query +from padinfo.view_state.base import ViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class MaterialsViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], + gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.link = link + self.skillup_evo_count = skillup_evo_count + self.skillups = skillups + self.gemusedin = gemusedin + self.mats = mats + self.usedin = usedin + self.gemid = gemid + self.query = query + self.monster = monster + self.color = color + + def serialize(self): + ret = super().serialize() + ret.update({ + 'query': self.query, + 'resolved_monster_id': self.monster.monster_id + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + + original_author_id = ims['original_author_id'] + + menu_type = ims['menu_type'] + + query = ims.get('query') or raw_query + + monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = await perform_mats_query(dgcog, query, user_config.beta_id3) + + return MaterialsViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, extra_state=ims) diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py new file mode 100644 index 00000000..bb8bb064 --- /dev/null +++ b/padinfo/view_state/otherinfo.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from padinfo.common.config import UserConfig +from padinfo.core.id import perform_otherinfo_query +from padinfo.view_state.base import ViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class OtherInfoViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.color = color + self.monster = monster + self.query = query + + def serialize(self): + ret = super().serialize() + ret.update({ + 'query': self.query, + 'resolved_monster_id': self.monster.monster_id, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + monster = await perform_otherinfo_query(dgcog, query, user_config.beta_id3) + + return OtherInfoViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + extra_state=ims) diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py new file mode 100644 index 00000000..310b691c --- /dev/null +++ b/padinfo/view_state/pantheon.py @@ -0,0 +1,44 @@ +from typing import TYPE_CHECKING, List + +from padinfo.common.config import UserConfig +from padinfo.core.id import perform_pantheon_query +from padinfo.view_state.base import ViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class PantheonViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + pantheon_list: List["MonsterModel"], series_name: str, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.series_name = series_name + self.pantheon_list = pantheon_list + self.color = color + self.monster = monster + self.query = query + + def serialize(self): + ret = super().serialize() + ret.update({ + 'query': self.query, + 'resolved_monster_id': self.monster.monster_id, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + monster, pantheon_list, series_name = await perform_pantheon_query(dgcog, query, user_config.beta_id3) + + return PantheonViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + pantheon_list, series_name, + extra_state=ims) diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py new file mode 100644 index 00000000..bf3063ec --- /dev/null +++ b/padinfo/view_state/pic.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from padinfo.common.config import UserConfig +from padinfo.core.id import perform_pic_query +from padinfo.view_state.base import ViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class PicViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.color = color + self.monster = monster + self.query = query + + def serialize(self): + ret = super().serialize() + ret.update({ + 'query': self.query, + 'resolved_monster_id': self.monster.monster_id, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + monster = await perform_pic_query(dgcog, query, user_config.beta_id3) + + return PicViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + extra_state=ims) From 11e995ed7994f071e3ce176ad7115a23133010c1 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 5 Feb 2021 21:12:08 -0600 Subject: [PATCH 0564/1220] Refactor new menu view states to not re-query (#748) --- padinfo/core/id.py | 77 ++++----------------------------- padinfo/id_menu.py | 10 ++--- padinfo/padinfo.py | 9 ++-- padinfo/view_state/evos.py | 17 +++++++- padinfo/view_state/id.py | 11 +++-- padinfo/view_state/materials.py | 41 +++++++++++++++++- padinfo/view_state/otherinfo.py | 7 ++- padinfo/view_state/pantheon.py | 21 ++++++++- padinfo/view_state/pic.py | 7 ++- 9 files changed, 108 insertions(+), 92 deletions(-) diff --git a/padinfo/core/id.py b/padinfo/core/id.py index e2f36e12..63ff240e 100644 --- a/padinfo/core/id.py +++ b/padinfo/core/id.py @@ -1,76 +1,22 @@ -from padinfo.common.external_links import ilmina_skill from padinfo.core.find_monster import findMonsterCustom2 -async def perform_id_query(dgcog, raw_query, beta_id3): - db_context = dgcog.database - monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) - acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ - await get_monster_misc_info(db_context, monster) - - return monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters - - -async def perform_evos_query(dgcog, raw_query, beta_id3): - db_context = dgcog.database +async def get_monster_by_query(dgcog, raw_query, beta_id3): monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) - alt_versions, gem_versions = await get_monster_evos_info(db_context, monster) - return monster, alt_versions, gem_versions - - -async def perform_mats_query(dgcog, raw_query, beta_id3): - db_context = dgcog.database - monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) - mats = db_context.graph.evo_mats_by_monster(monster) - usedin = db_context.graph.material_of_monsters(monster) - evo_gem = db_context.graph.evo_gem_monster(monster) - gemid = str(evo_gem.monster_no_na) if evo_gem else None - gemusedin = db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] - skillups = [] - skillup_evo_count = 0 - link = ilmina_skill(monster) - - if monster.active_skill: - sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id) - if db_context.graph.monster_is_farmable_evo(m)] - sugs = [db_context.graph.evo_gem_monster(su) for su in sums] - vsums = [] - for su in sums: - if not any(susu in vsums for susu in db_context.graph.get_alt_monsters(su)): - vsums.append(su) - skillups = [su for su in vsums - if db_context.graph.monster_is_farmable_evo(su) and - db_context.graph.get_base_id(su) != db_context.graph.get_base_id(monster) and - su not in sugs] if monster.active_skill else [] - skillup_evo_count = len(sums) - len(vsums) - - if not any([mats, usedin, gemusedin, skillups and not monster.is_stackable]): - return None, None, None, None, None, None, None, None - - return monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link + return monster -async def perform_pic_query(dgcog, raw_query, beta_id3): - monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) +async def get_monster_by_id(dgcog, id): + monster = dgcog.database.graph.get_monster(id) return monster -async def perform_pantheon_query(dgcog, raw_query, beta_id3): +async def get_id_view_state_data(dgcog, monster): db_context = dgcog.database - monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) - full_pantheon = db_context.get_monsters_by_series(monster.series_id) - pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 20: - return None - - series_name = monster.series.name_en - - return monster, pantheon_list, series_name - + acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ + await get_monster_misc_info(db_context, monster) -async def perform_otherinfo_query(dgcog, raw_query, beta_id3): - monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) - return monster + return transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters async def get_monster_misc_info(db_context, monster): @@ -81,10 +27,3 @@ async def get_monster_misc_info(db_context, monster): alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, key=lambda x: x.monster_id) return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw - - -async def get_monster_evos_info(db_context, monster): - alt_versions = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, - key=lambda x: x.monster_id) - gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) - return alt_versions, gem_versions diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index c7db24e4..63f3e6b8 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -45,8 +45,8 @@ class IdMenu: @staticmethod def menu(original_author_id, friend_ids, bot_id): transitions = { - # emoji_button_names[0]: respond_to_left, - # emoji_button_names[1]: respond_to_right, + # emoji_button_names[0]: IdMenu.respond_with_left, + # emoji_button_names[1]: IdMenu.respond_with_right, IdMenu.INITIAL_EMOJI: IdMenu.respond_with_current_id, emoji_button_names[1]: IdMenu.respond_with_evos, emoji_button_names[2]: IdMenu.respond_with_mats, @@ -66,24 +66,22 @@ def menu(original_author_id, friend_ids, bot_id): return embed @staticmethod - async def respond_to_left(message: Optional[Message], ims, **data): + async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] # Extract the query from the id state - # TODO: let the user switch scroll modes in between then and now ims['query'] = ims['left_arrow'] id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) id_control = IdMenu.id_control(id_view_state) return id_control @staticmethod - async def respond_to_right(message: Optional[Message], ims, **data): + async def respond_with_right(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] # Extract the query from the id state - # TODO: let the user switch scroll modes in between then and now ims['query'] = ims['right_arrow'] id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) id_control = IdMenu.id_control(id_view_state) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9c65db23..914e6c0b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -28,12 +28,12 @@ from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ findMonsterCustom, calc_ratio_name, calc_ratio_modifier from padinfo.core.historic_lookups import historic_lookups -from padinfo.core.id import perform_id_query +from padinfo.core.id import get_monster_by_query, get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater -from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.id_menu import IdMenu, emoji_button_names as id_menu_emoji_button_names +from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.id import IdViewState @@ -201,8 +201,9 @@ async def _id(self, ctx, *, query: str): # original_author_id = ctx.message.author.id # friend_cog = self.bot.get_cog("Friend") # friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - # monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ - # await perform_id_query(dgcog, raw_query, await self.config.user(ctx.author).beta_id3()) + # monster = await get_monster_by_query(dgcog, raw_query, await self.config.user(ctx.author).beta_id3()) + # transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + # await get_id_view_state_data(dgcog, monster) # state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, # monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters) # menu = IdMenu.menu(original_author_id, friends, self.bot.user.id) diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index ca881407..ba551308 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -1,7 +1,7 @@ from typing import List, TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import perform_evos_query +from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -37,7 +37,20 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): query = ims.get('query') or raw_query - monster, alt_versions, gem_versions = await perform_evos_query(dgcog, query, user_config.beta_id3) + resolved_monster_id = ims.get('resolved_monster_id') + + monster = await (get_monster_by_id(dgcog, resolved_monster_id) + if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + + alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) return EvosViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_versions, gem_versions, extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + alt_versions = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + key=lambda x: x.monster_id) + gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) + return alt_versions, gem_versions diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 2ab2c04c..553debd5 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -1,7 +1,7 @@ from typing import List, TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import perform_id_query +from padinfo.core.id import get_monster_by_query, get_id_view_state_data, get_monster_by_id from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -40,8 +40,13 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ - await perform_id_query(dgcog, query, user_config.beta_id3) + resolved_monster_id = ims.get('resolved_monster_id') + + monster = await (get_monster_by_id(dgcog, resolved_monster_id) + if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + await get_id_view_state_data(dgcog, monster) return IdViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index fc75b2ff..25badbaa 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -1,7 +1,8 @@ from typing import List, TYPE_CHECKING, Optional from padinfo.common.config import UserConfig -from padinfo.core.id import perform_mats_query +from padinfo.common.external_links import ilmina_skill +from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -43,7 +44,43 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): query = ims.get('query') or raw_query - monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = await perform_mats_query(dgcog, query, user_config.beta_id3) + resolved_monster_id = ims.get('resolved_monster_id') + + monster = await (get_monster_by_id(dgcog, resolved_monster_id) + if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = await MaterialsViewState.query(dgcog, monster) return MaterialsViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + mats = db_context.graph.evo_mats_by_monster(monster) + usedin = db_context.graph.material_of_monsters(monster) + evo_gem = db_context.graph.evo_gem_monster(monster) + gemid = str(evo_gem.monster_no_na) if evo_gem else None + gemusedin = db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] + skillups = [] + skillup_evo_count = 0 + link = ilmina_skill(monster) + + if monster.active_skill: + sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id) + if db_context.graph.monster_is_farmable_evo(m)] + sugs = [db_context.graph.evo_gem_monster(su) for su in sums] + vsums = [] + for su in sums: + if not any(susu in vsums for susu in db_context.graph.get_alt_monsters(su)): + vsums.append(su) + skillups = [su for su in vsums + if db_context.graph.monster_is_farmable_evo(su) and + db_context.graph.get_base_id(su) != db_context.graph.get_base_id(monster) and + su not in sugs] if monster.active_skill else [] + skillup_evo_count = len(sums) - len(vsums) + + if not any([mats, usedin, gemusedin, skillups and not monster.is_stackable]): + return None, None, None, None, None, None, None, None + + return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py index bb8bb064..423f5980 100644 --- a/padinfo/view_state/otherinfo.py +++ b/padinfo/view_state/otherinfo.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import perform_otherinfo_query +from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -34,7 +34,10 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - monster = await perform_otherinfo_query(dgcog, query, user_config.beta_id3) + resolved_monster_id = ims.get('resolved_monster_id') + + monster = await (get_monster_by_id(dgcog, resolved_monster_id) + if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) return OtherInfoViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, extra_state=ims) diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index 310b691c..a1f901f6 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig -from padinfo.core.id import perform_pantheon_query +from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -37,8 +37,25 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - monster, pantheon_list, series_name = await perform_pantheon_query(dgcog, query, user_config.beta_id3) + resolved_monster_id = ims.get('resolved_monster_id') + + monster = await (get_monster_by_id(dgcog, resolved_monster_id) + if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + + pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) return PantheonViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, pantheon_list, series_name, extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + full_pantheon = db_context.get_monsters_by_series(monster.series_id) + pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) + if len(pantheon_list) == 0 or len(pantheon_list) > 20: + return None + + series_name = monster.series.name_en + + return pantheon_list, series_name \ No newline at end of file diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py index bf3063ec..2fad69fe 100644 --- a/padinfo/view_state/pic.py +++ b/padinfo/view_state/pic.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import perform_pic_query +from padinfo.core.id import get_monster_by_query, get_monster_by_id from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -34,7 +34,10 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - monster = await perform_pic_query(dgcog, query, user_config.beta_id3) + resolved_monster_id = ims.get('resolved_monster_id') + + monster = await (get_monster_by_id(dgcog, resolved_monster_id) + if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) return PicViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, extra_state=ims) From 599ad26b7a1cf4f2a78295d3ba2435cf381dc3fa Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 6 Feb 2021 15:38:07 -0700 Subject: [PATCH 0565/1220] Support override tree/nicknames fields in gdoc (#747) --- dadguide/dadguide.py | 8 ++++---- dadguide/monster_index.py | 40 +++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b4f46392..5438300a 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -176,17 +176,17 @@ async def download_and_refresh_nicknames(self): await self._download_override_files() logger.info('Loading dg name overrides') - nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN, 5) - treename_overrides = self._csv_to_tuples(TREENAME_FILE_PATTERN, 5) + nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN, 6) + treename_overrides = self._csv_to_tuples(TREENAME_FILE_PATTERN, 6) panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN, 3) self.nickname_overrides = defaultdict(set) - for id, nick, _, _, i in nickname_overrides: + for id, nick, _, _, _, i in nickname_overrides: if id.isdigit() and not i: self.nickname_overrides[int(id)].add(nick.lower()) self.treename_overrides = defaultdict(set) - for id, treename, _, _, i in treename_overrides: + for id, treename, _, _, _, i in treename_overrides: if id.isdigit() and not i: self.treename_overrides[int(id)].add(treename.lower()) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 0904a76d..53c7d91d 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -37,34 +37,42 @@ async def __ainit__(self, monsters, db): if " " in m.series.name_en}.union(MULTI_WORD_TOKENS) self.replacement_tokens = defaultdict(set) + self.treename_overrides = set() nickname_data, treenames_data, pantheon_data, nt_alias_data = await asyncio.gather( - sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 4), - sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 4), + sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 5), + sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 5), sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, 2), sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2) ) - for m_id, name, lp, i in nickname_data: + for m_id, name, lp, ov, i in nickname_data: if m_id.isdigit() and not i: + mid = int(m_id) + if ov: + self.treename_overrides.add(mid) if lp: - self.monster_id_to_nametokens[int(m_id)].update(self._name_to_tokens(name)) + self.monster_id_to_nametokens[mid].update(self._name_to_tokens(name)) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(int(m_id))) + self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(mid)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.monster_id_to_nickname[int(m_id)].add(name.lower().replace(" ", "")) + self.monster_id_to_nickname[mid].add(name.lower().replace(" ", "")) - for m_id, name, mp, i in treenames_data: + for m_id, name, mp, ov, i in treenames_data: if m_id.isdigit() and not i: + mid = int(m_id) + if ov: + for emid in self.graph.get_alt_ids_by_id(mid): + self.treename_overrides.add(emid) if mp: - for em_id in self.graph.get_alt_ids_by_id(int(m_id)): - self.monster_id_to_nametokens[em_id].update(self._name_to_tokens(name)) + for emid in self.graph.get_alt_ids_by_id(mid): + self.monster_id_to_nametokens[emid].update(self._name_to_tokens(name)) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(int(m_id))) + self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(mid)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.monster_id_to_treename[int(m_id)].add(name.lower().replace(" ", "")) + self.monster_id_to_treename[mid].add(name.lower().replace(" ", "")) for sid, name in pantheon_data: if sid.isdigit(): @@ -109,7 +117,7 @@ async def _build_monster_index(self, monsters): self.name_tokens['jp' + str(m.monster_no_jp)].add(m) # Name Tokens - nametokens = self._name_to_tokens(m.name_en) + list(self.monster_id_to_nametokens[m.monster_id]) + nametokens = self._name_to_tokens(m.name_en) last_token = m.name_en.split(',')[-1].strip() autotoken = True @@ -132,11 +140,15 @@ async def _build_monster_index(self, monsters): treenames.add(match.group(1)) # Add important tokens - if autotoken: + for t in self.monster_id_to_nametokens[m.monster_id]: + self.add_name_token(self.name_tokens, t, m) + if m.monster_id in self.treename_overrides: + pass + elif autotoken: # Add a consistant last token as important token for token in self._name_to_tokens(m.name_en.split(',')[-1].strip()): self.add_name_token(self.name_tokens, token, m) - elif not self.monster_id_to_nametokens[m.monster_id]: + else: # Add name tokens by guessing which ones are important for token in self._get_important_tokens(m.name_en, treenames) + self._name_to_tokens(m.roma_subname): self.add_name_token(self.name_tokens, token, m) From 78d4134010dee0d85836552bd7c1cf33c3c88757 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:19:24 -0600 Subject: [PATCH 0566/1220] Delete scrollmenu commands in prep for new menu (#752) --- padinfo/padinfo.py | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 914e6c0b..58c1f5ae 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -381,15 +381,6 @@ async def _do_idmenu(self, ctx, m, starting_menu_emoji): db_context=db_context) ) - async def _do_scrollmenu(self, ctx, m, ms, starting_menu_emoji): - emoji_to_embed = await self.get_id_emoji_options(ctx, m=m, scroll=ms) - return await self._do_menu( - ctx, - starting_menu_emoji, - ScrollEmojiUpdater(ctx, emoji_to_embed, pad_info=self, bot=self.bot, - m=m, ms=ms, selected_emoji=starting_menu_emoji) - ) - async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): if scroll is None: scroll = [] @@ -549,43 +540,6 @@ async def evolist(self, ctx, *, query): else: await self.makeFailureMsg(ctx, query, err) - @commands.command(aliases=['collabscroll']) - @checks.bot_has_permissions(embed_links=True) - async def seriesscroll(self, ctx, *, query: str): - """Scroll through the monsters in a collab""" - dgcog = self.bot.get_cog("Dadguide") - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) - ms = dgcog.database.get_monsters_by_series(m.series.series_id) - - ms.sort(key=lambda x: x.monster_id) - ms = [m for m in ms if m.sell_mp >= 100] - - if not ms: - await ctx.send("There are no monsters in that series worth more than 99 monster points.") - return - - if m not in ms: - m = m if m in ms else ms[0] - - if m is not None: - await self._do_scrollmenu(ctx, m, ms, self.id_emoji) - else: - await self.makeFailureMsg(ctx, query, err) - - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def evoscroll(self, ctx, *, query: str): - """Scroll through the monsters in a collab""" - dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) - - if m is not None: - await self._do_scrollmenu(ctx, m, - sorted(dgcog.database.graph.get_alt_monsters(m), key=lambda x: x.monster_id), - self.id_emoji) - else: - await self.makeFailureMsg(ctx, query, err) - @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) async def leaderskill(self, ctx, *, raw_query): From f07bb52e844c3ace10c6281c9b7ecf5be4b04e7d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:53:48 -0600 Subject: [PATCH 0567/1220] Convert Id views to using new menu code (#750) --- padinfo/emojiupdaters.py | 97 ---------- padinfo/id_menu.py | 117 ++++++++---- padinfo/id_menu_old.py | 81 +------- padinfo/padinfo.py | 319 +++++++++++++++++--------------- padinfo/view_state/evos.py | 29 +-- padinfo/view_state/id.py | 21 ++- padinfo/view_state/materials.py | 27 ++- padinfo/view_state/otherinfo.py | 19 +- padinfo/view_state/pantheon.py | 30 +-- padinfo/view_state/pic.py | 16 +- 10 files changed, 340 insertions(+), 416 deletions(-) delete mode 100644 padinfo/emojiupdaters.py diff --git a/padinfo/emojiupdaters.py b/padinfo/emojiupdaters.py deleted file mode 100644 index 85b036a5..00000000 --- a/padinfo/emojiupdaters.py +++ /dev/null @@ -1,97 +0,0 @@ -from typing import TYPE_CHECKING - -from tsutils import EmojiUpdater - -from padinfo.core.padinfo_settings import settings - -if TYPE_CHECKING: - from dadguide.database_context import DbContext - from dadguide.models.monster_model import MonsterModel - - -class IdEmojiUpdater(EmojiUpdater): - def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, pad_info=None, selected_emoji=None, bot=None, - db_context: "DbContext" = None, **kwargs): - super().__init__(emoji_to_embed, **kwargs) - self.ctx = ctx - self.emoji_dict = emoji_to_embed - self.m = m - self.pad_info = pad_info - self.selected_emoji = selected_emoji - self.bot = bot - self.db_context = db_context - - settings.log_emoji("start_" + selected_emoji) - - async def on_update(self, ctx, selected_emoji): - evo_id = settings.checkEvoID(ctx.author.id) - settings.log_emoji(selected_emoji) - if evo_id: - evos = sorted({*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) - index = evos.index(self.m.monster_id) - if selected_emoji == self.pad_info.previous_monster_emoji: - new_id = evos[index - 1] - elif selected_emoji == self.pad_info.next_monster_emoji: - if index == len(evos) - 1: - new_id = evos[0] - else: - new_id = evos[index + 1] - else: - self.selected_emoji = selected_emoji - return True - if new_id == self.m.monster_id: - return False - self.m = self.db_context.graph.get_monster(new_id) - else: - if selected_emoji == self.pad_info.previous_monster_emoji: - prev_monster = self.db_context.graph.numeric_prev_monster(self.m) - if prev_monster is None: - return False - self.m = prev_monster - elif selected_emoji == self.pad_info.next_monster_emoji: - next_monster = self.db_context.graph.numeric_next_monster(self.m) - if next_monster is None: - return False - self.m = next_monster - else: - self.selected_emoji = selected_emoji - return True - - self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, - m=self.m, scroll=sorted( - {*self.db_context.graph.get_alt_ids_by_id(self.m.monster_id)}) if evo_id else [], menu_type=1) - return True - - -class ScrollEmojiUpdater(EmojiUpdater): - def __init__(self, ctx, emoji_to_embed, m: "MonsterModel" = None, ms: "list[int]" = None, selected_emoji=None, - pad_info=None, bot=None, **kwargs): - super().__init__(emoji_to_embed, **kwargs) - self.ctx = ctx - self.emoji_dict = emoji_to_embed - self.m = m - self.ms = ms - self.pad_info = pad_info - self.selected_emoji = selected_emoji - self.bot = bot - - async def on_update(self, ctx, selected_emoji): - index = self.ms.index(self.m) - - if selected_emoji == self.pad_info.first_monster_emoji: - self.m = self.ms[0] - elif selected_emoji == self.pad_info.previous_monster_emoji: - self.m = self.ms[index - 1] - elif selected_emoji == self.pad_info.next_monster_emoji: - if index == len(self.ms) - 1: - self.m = self.ms[0] - else: - self.m = self.ms[index + 1] - elif selected_emoji == self.pad_info.last_monster_emoji: - self.m = self.ms[-1] - else: - self.selected_emoji = selected_emoji - return True - - self.emoji_dict = await self.pad_info.get_id_emoji_options(self.ctx, m=self.m, scroll=self.ms) - return True diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 63f3e6b8..dcf652a3 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -8,6 +8,9 @@ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji +from padinfo.core.padinfo_settings import settings + +from dadguide.database_context import DbContext from padinfo.view.id import IdView from padinfo.view.materials import MaterialsView from padinfo.view.otherinfo import OtherInfoView @@ -22,11 +25,11 @@ from padinfo.view_state.pic import PicViewState if TYPE_CHECKING: - pass + from dadguide.models.monster_model import MonsterModel emoji_button_names = [ - # '\N{BLACK LEFT-POINTING TRIANGLE}', - # '\N{BLACK RIGHT-POINTING TRIANGLE}', + '\N{BLACK LEFT-POINTING TRIANGLE}', + '\N{BLACK RIGHT-POINTING TRIANGLE}', '\N{HOUSE BUILDING}', char_to_emoji('e'), '\N{MEAT ON BONE}', @@ -39,20 +42,22 @@ class IdMenu: - INITIAL_EMOJI = emoji_button_names[0] + INITIAL_EMOJI = emoji_button_names[2] MENU_TYPE = 'IdMenu' @staticmethod - def menu(original_author_id, friend_ids, bot_id): + def menu(original_author_id, friend_ids, bot_id, initial_control=None): + if initial_control is None: + initial_control = IdMenu.id_control transitions = { - # emoji_button_names[0]: IdMenu.respond_with_left, - # emoji_button_names[1]: IdMenu.respond_with_right, - IdMenu.INITIAL_EMOJI: IdMenu.respond_with_current_id, - emoji_button_names[1]: IdMenu.respond_with_evos, - emoji_button_names[2]: IdMenu.respond_with_mats, - emoji_button_names[3]: IdMenu.respond_with_picture, - emoji_button_names[4]: IdMenu.repond_with_pantheon, - emoji_button_names[5]: IdMenu.respond_with_otherinfo, + emoji_button_names[0]: IdMenu.respond_with_left, + emoji_button_names[1]: IdMenu.respond_with_right, + emoji_button_names[2]: IdMenu.respond_with_current_id, + emoji_button_names[3]: IdMenu.respond_with_evos, + emoji_button_names[4]: IdMenu.respond_with_mats, + emoji_button_names[5]: IdMenu.respond_with_picture, + emoji_button_names[6]: IdMenu.respond_with_pantheon, + emoji_button_names[7]: IdMenu.respond_with_otherinfo, } valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) @@ -62,37 +67,79 @@ def menu(original_author_id, friend_ids, bot_id): BotAuthoredMessageReactionFilter(bot_id), MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) ] - embed = EmbedMenu(reaction_filters, transitions, IdMenu.id_control, menu_emoji_config) + embed = EmbedMenu(reaction_filters, transitions, initial_control, menu_emoji_config) return embed @staticmethod - async def respond_with_left(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] - user_config = data['user_config'] + def get_pane_types(): + # This lets us write the functionality for the arrows, we need to know what current pane + # corresponds to what response type. - # Extract the query from the id state - ims['query'] = ims['left_arrow'] - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) - id_control = IdMenu.id_control(id_view_state) - return id_control + # The strings are pane_types as defined in ViewState serialize functions, the functions are + # IdMenu responses. + + return { + 'id': IdMenu.respond_with_current_id, + 'evos': IdMenu.respond_with_evos, + 'materials': IdMenu.respond_with_mats, + 'otherinfo': IdMenu.respond_with_otherinfo, + 'pantheon': IdMenu.respond_with_pantheon, + 'pic': IdMenu.respond_with_picture, + } @staticmethod - async def respond_with_right(message: Optional[Message], ims, **data): + async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] - user_config = data['user_config'] + db_context: "DbContext" = dgcog.database + m = db_context.graph.get_monster(ims['resolved_monster_id']) + + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + new_monster_id = str(IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll)) + ims['resolved_monster_id'] = new_monster_id + pane_type = ims['pane_type'] + pane_type_to_func_map = IdMenu.get_pane_types() + response_func = pane_type_to_func_map[pane_type] + return await response_func(message, ims, **data) - # Extract the query from the id state - ims['query'] = ims['right_arrow'] - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) - id_control = IdMenu.id_control(id_view_state) - return id_control + @staticmethod + def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): + if use_evo_scroll: + evos = sorted({*db_context.graph.get_alt_ids_by_id(monster.monster_id)}) + index = evos.index(monster.monster_id) + new_id = evos[index - 1] + return new_id + else: + prev_monster = db_context.graph.numeric_prev_monster(monster) + return prev_monster.monster_id if prev_monster else monster.monster_id @staticmethod - async def _deserialize_appropriately(ims, menu_type): - # need to deserialize the left-right arrows according to which type of tab we're in - # but I think this means maybe type needs to be a function of tab not of overall menu? - # unclear - pass + async def respond_with_right(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + db_context: "DbContext" = dgcog.database + m = db_context.graph.get_monster(ims['resolved_monster_id']) + + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll)) + ims['resolved_monster_id'] = new_monster_id + pane_type = ims.get('pane_type') + pane_type_to_func_map = IdMenu.get_pane_types() + response_func = pane_type_to_func_map[pane_type] + return await response_func(message, ims, **data) + + @staticmethod + def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): + if use_evo_scroll: + evos = sorted({*db_context.graph.get_alt_ids_by_id(monster.monster_id)}) + index = evos.index(monster.monster_id) + if index == len(evos) - 1: + # cycle back to the beginning of the evos list + new_id = evos[0] + else: + new_id = evos[index + 1] + return new_id + else: + next_monster = db_context.graph.numeric_next_monster(monster) + return next_monster.monster_id if next_monster else monster.monster_id @staticmethod async def respond_with_current_id(message: Optional[Message], ims, **data): @@ -131,7 +178,7 @@ async def respond_with_picture(message: Optional[Message], ims, **data): return control @staticmethod - async def repond_with_pantheon(message: Optional[Message], ims, **data): + async def respond_with_pantheon(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] diff --git a/padinfo/id_menu_old.py b/padinfo/id_menu_old.py index 13dfd134..1c831c86 100644 --- a/padinfo/id_menu_old.py +++ b/padinfo/id_menu_old.py @@ -5,21 +5,12 @@ from discord import Color from padinfo.core.id import get_monster_misc_info -from padinfo.view.evos import EvosView +from padinfo.core.padinfo_settings import settings from padinfo.view.id import IdView from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillSingleView from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView -from padinfo.view.materials import MaterialsView -from padinfo.view.otherinfo import OtherInfoView -from padinfo.view.pantheon import PantheonView -from padinfo.view.pic import PicView -from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState -from padinfo.view_state.materials import MaterialsViewState -from padinfo.view_state.otherinfo import OtherInfoViewState -from padinfo.view_state.pantheon import PantheonViewState -from padinfo.view_state.pic import PicViewState if TYPE_CHECKING: from dadguide.database_context import DbContext @@ -46,81 +37,15 @@ async def make_id_embed(self, m: "MonsterModel"): acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ await get_monster_misc_info(self.db_context, m) state = IdViewState("", "TODO", "todo", "", color, m, transform_base, true_evo_type_raw, acquire_raw, - base_rarity, alt_monsters) + base_rarity, alt_monsters, + use_evo_scroll=settings.checkEvoID(self.ctx.author.id)) e = IdView.embed(state) return e.to_embed() - async def make_evo_embed(self, m: "MonsterModel"): - alt_versions = self.db_context.graph.get_alt_monsters_by_id(m.monster_no) - gem_versions = list(filter(None, map(self.db_context.graph.evo_gem_monster, alt_versions))) - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - state = EvosViewState("", "TODO", "todo", "", color, m, alt_versions, gem_versions) - e = EvosView.embed(state) - return e.to_embed() - - async def make_evo_mats_embed(self, m: "MonsterModel"): - mats = self.db_context.graph.evo_mats_by_monster(m) - usedin = self.db_context.graph.material_of_monsters(m) - evo_gem = self.db_context.graph.evo_gem_monster(m) - gemid = str(evo_gem.monster_no_na) if evo_gem else None - gemusedin = self.db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] - skillups = [] - skillup_evo_count = 0 - - if m.active_skill: - sums = [m for m in self.db_context.get_monsters_by_active(m.active_skill.active_skill_id) - if self.db_context.graph.monster_is_farmable_evo(m)] - sugs = [self.db_context.graph.evo_gem_monster(su) for su in sums] - vsums = [] - for su in sums: - if not any(susu in vsums for susu in self.db_context.graph.get_alt_monsters(su)): - vsums.append(su) - skillups = [su for su in vsums - if self.db_context.graph.monster_is_farmable_evo(su) and - self.db_context.graph.get_base_id(su) != self.db_context.graph.get_base_id(m) and - su not in sugs] if m.active_skill else [] - skillup_evo_count = len(sums) - len(vsums) - - if not any([mats, usedin, gemusedin, skillups and not m.is_stackable]): - return None - link = "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - - state = MaterialsViewState("", "TODO", "todo", "", color, m, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, - link) - - return MaterialsView.embed(state).to_embed() - - async def make_pantheon_embed(self, m: "MonsterModel"): - full_pantheon = self.db_context.get_monsters_by_series(m.series_id) - pantheon_list = list(filter(lambda x: self.db_context.graph.monster_is_base(x), full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 20: - return None - - series_name = m.series.name_en - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - state = PantheonViewState('', '', '', '', color, m, pantheon_list, series_name) - return PantheonView.embed(state).to_embed() - - async def make_picture_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - state = PicViewState('', '', '', '', color, m) - return PicView.embed(state).to_embed() - - async def make_ls_embed(self, left_m: "MonsterModel", right_m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - embed = LeaderSkillView.embed(left_m, right_m, color) - return embed.to_embed() - async def make_lookup_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return LookupView.embed(m, color).to_embed() - async def make_otherinfo_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - state = OtherInfoViewState('', '', '', '', color, m) - return OtherInfoView.embed(state).to_embed() - async def make_links_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return LinksView.embed(m, color).to_embed() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 58c1f5ae..afa623bc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -26,18 +26,22 @@ from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom, calc_ratio_name, calc_ratio_modifier + findMonsterCustom, calc_ratio_name, calc_ratio_modifier, findMonsterCustom2 from padinfo.core.historic_lookups import historic_lookups -from padinfo.core.id import get_monster_by_query, get_id_view_state_data +from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings -from padinfo.emojiupdaters import IdEmojiUpdater, ScrollEmojiUpdater from padinfo.id_menu import IdMenu, emoji_button_names as id_menu_emoji_button_names from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState +from padinfo.view_state.materials import MaterialsViewState +from padinfo.view_state.otherinfo import OtherInfoViewState +from padinfo.view_state.pantheon import PantheonViewState +from padinfo.view_state.pic import PicViewState if TYPE_CHECKING: pass @@ -128,6 +132,13 @@ async def reload_nicknames(self): await asyncio.sleep(wait_time) + async def get_dgcog(self): + dgcog = self.bot.get_cog("Dadguide") + if dgcog is None: + raise ValueError("Dadguide cog is not loaded") + await dgcog.wait_until_ready() + return dgcog + def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') return dg_cog.get_monster(monster_id) @@ -190,65 +201,75 @@ async def jpname(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def _id(self, ctx, *, query: str): """Monster info (main tab)""" - if await self.config.user(ctx.author).beta_id3(): - await self._do_id3(ctx, query) - else: - await self._do_id(ctx, query) - - # dgcog = await self.get_dgcog() - # raw_query = query - # color = await self.get_user_embed_color(ctx) - # original_author_id = ctx.message.author.id - # friend_cog = self.bot.get_cog("Friend") - # friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - # monster = await get_monster_by_query(dgcog, raw_query, await self.config.user(ctx.author).beta_id3()) - # transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ - # await get_id_view_state_data(dgcog, monster) - # state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - # monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters) - # menu = IdMenu.menu(original_author_id, friends, self.bot.user.id) - # await menu.create(ctx, state) + await self._do_id(ctx, query) @commands.command(aliases=["idold", "oldid"]) @checks.bot_has_permissions(embed_links=True) async def id1(self, ctx, *, query): """Do a search via id1""" - await self._do_id(ctx, query) + await self._do_id(ctx, query, force_id3_pref=False) @commands.command() @checks.bot_has_permissions(embed_links=True) async def idna(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" - await self._do_id3(ctx, "inna " + query) + await self._do_id(ctx, "inna " + query, force_id3_pref=True) @commands.command() @checks.bot_has_permissions(embed_links=True) async def idjp(self, ctx, *, query: str): """Monster info (limited to JP monsters ONLY)""" - await self._do_id3(ctx, "injp " + query) + await self._do_id(ctx, "injp " + query, force_id3_pref=True) - async def get_dgcog(self): - dgcog = self.bot.get_cog("Dadguide") - if dgcog is None: - raise ValueError("Dadguide cog is not loaded") - await dgcog.wait_until_ready() - return dgcog + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def id3(self, ctx, *, query: str): + """Monster info (main tab)""" + await self._do_id(ctx, query, force_id3_pref=True) + + async def _do_id(self, ctx, query: str, force_id3_pref=None): + # dgcog = await self.get_dgcog() + beta_id3 = await self.config.user(ctx.author).beta_id3() + if force_id3_pref is not None: + beta_id3 = force_id3_pref - async def _do_id(self, ctx, query: str): dgcog = await self.get_dgcog() - m, err, debug_info = await findMonster1(dgcog, query) + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + monster, err, debug_info = await findMonsterCustom2(dgcog, beta_id3, + raw_query) - if m is not None: - async def send_error(err): - if err: - await asyncio.sleep(1) - await ctx.send(err) + if not monster: + await self.makeFailureMsg(ctx, query, err) + return - asyncio.create_task(send_error(err)) + async def send_error(error): + if error: + await asyncio.sleep(1) + await ctx.send(error) - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await self.makeFailureMsg(ctx, query, err) + asyncio.create_task(send_error(err)) + + # id3 messaging stuff + if beta_id3 and monster and monster.monster_no_na != monster.monster_no_jp: + await ctx.send("The NA ID and JP ID of this card differ! " + "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + + (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if + ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) + + if beta_id3 and await self.config.do_survey(): + asyncio.create_task(self.send_survey_after(ctx, query, monster)) + + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + await get_id_view_state_data(dgcog, monster) + state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, + monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + use_evo_scroll=settings.checkEvoID(ctx.author.id)) + menu = IdMenu.menu(original_author_id, friends, self.bot.user.id) + await menu.create(ctx, state) async def send_survey_after(self, ctx, query, result_monster): dgcog = await self.get_dgcog() @@ -304,125 +325,90 @@ async def id2(self, ctx, *, query: str): await ctx.send("id2 has been discontinued! For an even better searching experience," " opt into the id3 beta using `{}idset beta y`".format(ctx.prefix)) - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def id3(self, ctx, *, query: str): - """Monster info (main tab)""" - await self._do_id3(ctx, query) - - async def _do_id3(self, ctx, query): - dgcog = await self.get_dgcog() - m = await findMonster3(dgcog, query) - if m and m.monster_no_na != m.monster_no_jp: - await ctx.send("The NA ID and JP ID of this card differ! " - "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + - (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if - ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) - if await self.config.do_survey(): - asyncio.create_task(self.send_survey_after(ctx, query, m)) - - if m is not None: - await self._do_idmenu(ctx, m, self.id_emoji) - else: - await self.makeFailureMsg(ctx, query, "No monster matched") - @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): """Monster info (evolutions tab)""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) - if m is not None: - await self._do_idmenu(ctx, m, self.evo_emoji) - else: + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + + monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), + raw_query) + + if monster is None: await self.makeFailureMsg(ctx, query, err) + return + + alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + + state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, + monster, alt_versions, gem_versions, + use_evo_scroll=settings.checkEvoID(ctx.author.id)) + menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.evos_control) + await menu.create(ctx, state) @commands.command(name="mats", aliases=['evomats', 'evomat', 'skillups']) @checks.bot_has_permissions(embed_links=True) async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) - if m is not None: - menu = await self._do_idmenu(ctx, m, self.mats_emoji) - if menu == EMBED_NOT_GENERATED: - await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) - else: + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), + raw_query) + + if not monster: await self.makeFailureMsg(ctx, query, err) + return + + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = \ + await MaterialsViewState.query(dgcog, monster) + + if mats is None: + await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) + return + + state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, + use_evo_scroll=settings.checkEvoID(ctx.author.id)) + menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.mats_control) + await menu.create(ctx, state) @commands.command(aliases=["series"]) @checks.bot_has_permissions(embed_links=True) async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) - if m is not None: - menu = await self._do_idmenu(ctx, m, self.pantheon_emoji) - if menu == EMBED_NOT_GENERATED: - await ctx.send(inline('Not a pantheon monster')) - else: - await self.makeFailureMsg(ctx, query, err) - - async def _do_idmenu(self, ctx, m, starting_menu_emoji): - DGCOG = self.bot.get_cog("Dadguide") - db_context = DGCOG.database - - alt_versions = db_context.graph.get_alt_monsters_by_id(m.monster_id) - emoji_to_embed = await self.get_id_emoji_options(ctx, - m=m, scroll=sorted({*alt_versions}, key=lambda - x: x.monster_id) if settings.checkEvoID( - ctx.author.id) else [], menu_type=1) - - return await self._do_menu( - ctx, - starting_menu_emoji, - IdEmojiUpdater(ctx, emoji_to_embed, pad_info=self, - m=m, selected_emoji=starting_menu_emoji, bot=self.bot, - db_context=db_context) - ) - - async def get_id_emoji_options(self, ctx, m=None, scroll=None, menu_type=0): - if scroll is None: - scroll = [] - DGCOG = self.bot.get_cog("Dadguide") - db_context = DGCOG.database - - menu = IdMenuOld(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) - - id_embed = await menu.make_id_embed(m) - evo_embed = await menu.make_evo_embed(m) - mats_embed = await menu.make_evo_mats_embed(m) - pic_embed = await menu.make_picture_embed(m) - other_info_embed = await menu.make_otherinfo_embed(m) - pantheon_embed = await menu.make_pantheon_embed(m) - - emoji_to_embed = OrderedDict() - - # it's impossible for the previous/next ones to be accessed because - # IdEmojiUpdater won't allow it, however they have to be defined - # so that the buttons display in the first place + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - if len(scroll) > 1 and menu_type != 1: - emoji_to_embed[self.first_monster_emoji] = None - if len(scroll) != 1: - emoji_to_embed[self.previous_monster_emoji] = None - emoji_to_embed[self.next_monster_emoji] = None - if len(scroll) > 1 and menu_type != 1: - emoji_to_embed[self.last_monster_emoji] = None + monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), + raw_query) - emoji_to_embed[self.id_emoji] = id_embed - emoji_to_embed[self.evo_emoji] = evo_embed - if mats_embed: - emoji_to_embed[self.mats_emoji] = mats_embed - emoji_to_embed[self.pic_emoji] = pic_embed - if pantheon_embed: - emoji_to_embed[self.pantheon_emoji] = pantheon_embed + if monster is None: + await self.makeFailureMsg(ctx, query, err) + return - emoji_to_embed[self.other_info_emoji] = other_info_embed + pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + if pantheon_list is None: + await ctx.send(inline('Too many monsters in this series to display')) + return - # remove emoji needs to be last - emoji_to_embed[self.remove_emoji] = self.menu.reaction_delete_message - return emoji_to_embed + state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, + monster, pantheon_list, series_name, + use_evo_scroll=settings.checkEvoID(ctx.author.id)) + menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pantheon_control) + await menu.create(ctx, state) async def _do_evolistmenu(self, ctx, sm): DGCOG = self.bot.get_cog("Dadguide") @@ -470,11 +456,24 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): async def pic(self, ctx, *, query: str): """Monster info (full image tab)""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) - if m is not None: - await self._do_idmenu(ctx, m, self.pic_emoji) - else: + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + + monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), + raw_query) + + if monster is None: await self.makeFailureMsg(ctx, query, err) + return + + state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, + monster, + use_evo_scroll=settings.checkEvoID(ctx.author.id)) + menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pic_control) + await menu.create(ctx, state) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -495,11 +494,24 @@ async def links(self, ctx, *, query: str): async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) - if m is not None: - await self._do_idmenu(ctx, m, self.other_info_emoji) - else: + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + + monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), + raw_query) + + if monster is None: await self.makeFailureMsg(ctx, query, err) + return + + state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, + monster, + use_evo_scroll=settings.checkEvoID(ctx.author.id)) + menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.otherinfo_control) + await menu.create(ctx, state) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -610,7 +622,7 @@ async def idt_add(self, ctx, id: int, *, query): query, *reason = query.split("|") query = query.strip() if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -658,7 +670,8 @@ async def idtf_add(self, ctx, id: int, token, *, reason=""): async def norf_add(self, ctx, id: int, token, reason, fluffy): reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to add to the fluff/name test suite?"): + not await tsutils.confirm_message(ctx, + "Are you sure you want to add to the fluff/name test suite?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -706,7 +719,7 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): async def idt_import(self, ctx, *, queries): """Import id3 tests""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -728,7 +741,7 @@ async def idtf_import(self, ctx, *, queries): async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -749,7 +762,7 @@ async def norf_import(self, ctx, queries): async def idt_remove(self, ctx, *, item): """Remove an id3 test""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -775,7 +788,7 @@ async def idtf_remove(self, ctx, *, item: int): async def norf_remove(self, ctx, item): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -792,7 +805,7 @@ async def idt_setreason(self, ctx, number: int, *, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -817,7 +830,7 @@ async def norf_setreason(self, ctx, number, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index ba551308..6889c542 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -11,6 +11,7 @@ class EvosViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], + use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.alt_versions = alt_versions @@ -18,34 +19,38 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.query = query self.monster = monster self.color = color + self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ + 'pane_type': 'evos', 'query': self.query, - 'resolved_monster_id': self.monster.monster_id + 'resolved_monster_id': self.monster.monster_id, + 'use_evo_scroll': str(self.use_evo_scroll), }) return ret @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - - original_author_id = ims['original_author_id'] - - menu_type = ims['menu_type'] - - query = ims.get('query') or raw_query - - resolved_monster_id = ims.get('resolved_monster_id') + raw_query = ims['raw_query'] + resolved_monster_id = int(ims.get('resolved_monster_id')) monster = await (get_monster_by_id(dgcog, resolved_monster_id) if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) - alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + menu_type = ims['menu_type'] + return EvosViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - alt_versions, gem_versions, extra_state=ims) + alt_versions, gem_versions, + use_evo_scroll=use_evo_scroll, + extra_state=ims) @staticmethod async def query(dgcog, monster): diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 553debd5..ef109b0e 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -11,6 +11,7 @@ class IdViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], + use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.acquire_raw = acquire_raw @@ -21,12 +22,15 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.monster = monster self.query = query self.true_evo_type_raw = true_evo_type_raw + self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ + 'pane_type': 'id', 'query': self.query, 'resolved_monster_id': self.monster.monster_id, + 'use_evo_scroll': str(self.use_evo_scroll), }) return ret @@ -34,20 +38,21 @@ def serialize(self): async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] - # This is to support the 2 vs 1 monster query difference between ^ls and ^id - query = ims.get('query') or raw_query - - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - - resolved_monster_id = ims.get('resolved_monster_id') - + resolved_monster_id = int(ims.get('resolved_monster_id')) monster = await (get_monster_by_id(dgcog, resolved_monster_id) if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ await get_id_view_state_data(dgcog, monster) + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + menu_type = ims['menu_type'] + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + return IdViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + use_evo_scroll=use_evo_scroll, extra_state=ims) diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 25badbaa..2d8f9dfb 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -13,6 +13,7 @@ class MaterialsViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, + use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.link = link @@ -25,12 +26,15 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.query = query self.monster = monster self.color = color + self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ + 'pane_type': 'materials', 'query': self.query, - 'resolved_monster_id': self.monster.monster_id + 'resolved_monster_id': self.monster.monster_id, + 'use_evo_scroll': str(self.use_evo_scroll), }) return ret @@ -38,21 +42,24 @@ def serialize(self): async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] - original_author_id = ims['original_author_id'] - - menu_type = ims['menu_type'] - - query = ims.get('query') or raw_query - - resolved_monster_id = ims.get('resolved_monster_id') + resolved_monster_id = int(ims.get('resolved_monster_id')) monster = await (get_monster_by_id(dgcog, resolved_monster_id) if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = await MaterialsViewState.query(dgcog, monster) + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + menu_type = ims['menu_type'] + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + return MaterialsViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, extra_state=ims) + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, + use_evo_scroll=use_evo_scroll, + extra_state=ims) @staticmethod async def query(dgcog, monster): @@ -81,6 +88,6 @@ async def query(dgcog, monster): skillup_evo_count = len(sums) - len(vsums) if not any([mats, usedin, gemusedin, skillups and not monster.is_stackable]): - return None, None, None, None, None, None, None, None + return None, None, None, None, None, None, None return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py index 423f5980..1200ca66 100644 --- a/padinfo/view_state/otherinfo.py +++ b/padinfo/view_state/otherinfo.py @@ -10,17 +10,21 @@ class OtherInfoViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.color = color self.monster = monster self.query = query + self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ + 'pane_type': 'otherinfo', 'query': self.query, 'resolved_monster_id': self.monster.monster_id, + 'use_evo_scroll': str(self.use_evo_scroll), }) return ret @@ -28,16 +32,17 @@ def serialize(self): async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] - # This is to support the 2 vs 1 monster query difference between ^ls and ^id - query = ims.get('query') or raw_query - - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - - resolved_monster_id = ims.get('resolved_monster_id') + resolved_monster_id = int(ims.get('resolved_monster_id')) monster = await (get_monster_by_id(dgcog, resolved_monster_id) if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + query = ims.get('query') or raw_query + + menu_type = ims['menu_type'] + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + return OtherInfoViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + use_evo_scroll=use_evo_scroll, extra_state=ims) diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index a1f901f6..d56938a4 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -11,6 +11,7 @@ class PantheonViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", pantheon_list: List["MonsterModel"], series_name: str, + use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.series_name = series_name @@ -18,12 +19,15 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.color = color self.monster = monster self.query = query + self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ + 'pane_type': 'pantheon', 'query': self.query, 'resolved_monster_id': self.monster.monster_id, + 'use_evo_scroll': str(self.use_evo_scroll), }) return ret @@ -31,31 +35,35 @@ def serialize(self): async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] - # This is to support the 2 vs 1 monster query difference between ^ls and ^id - query = ims.get('query') or raw_query - - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - - resolved_monster_id = ims.get('resolved_monster_id') + resolved_monster_id = int(ims.get('resolved_monster_id')) monster = await (get_monster_by_id(dgcog, resolved_monster_id) if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + menu_type = ims['menu_type'] + return PantheonViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - pantheon_list, series_name, - extra_state=ims) + pantheon_list, series_name, + use_evo_scroll=use_evo_scroll, + extra_state=ims) @staticmethod async def query(dgcog, monster): db_context = dgcog.database full_pantheon = db_context.get_monsters_by_series(monster.series_id) + if not full_pantheon: + return None, None pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) if len(pantheon_list) == 0 or len(pantheon_list) > 20: - return None + return None, None series_name = monster.series.name_en - return pantheon_list, series_name \ No newline at end of file + return pantheon_list, series_name diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py index 2fad69fe..2e4292a2 100644 --- a/padinfo/view_state/pic.py +++ b/padinfo/view_state/pic.py @@ -10,17 +10,21 @@ class PicViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.color = color self.monster = monster self.query = query + self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ + 'pane_type': 'pic', 'query': self.query, 'resolved_monster_id': self.monster.monster_id, + 'use_evo_scroll': str(self.use_evo_scroll), }) return ret @@ -28,16 +32,18 @@ def serialize(self): async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] + resolved_monster_id = int(ims.get('resolved_monster_id')) + + monster = await (get_monster_by_id(dgcog, resolved_monster_id) + if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] - resolved_monster_id = ims.get('resolved_monster_id') - - monster = await (get_monster_by_id(dgcog, resolved_monster_id) - if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) - return PicViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + use_evo_scroll=use_evo_scroll, extra_state=ims) From 64ab89f8545cb2ef2e771b1346234b5554c81219 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 6 Feb 2021 17:03:07 -0700 Subject: [PATCH 0568/1220] Fix return type for name token processor (#749) --- padinfo/core/find_monster.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 3a1bf308..45f79e76 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -169,8 +169,6 @@ def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_name(t, nt, index2) > self.TOKEN_JW_DISTANCE], key=lambda nt: calc_ratio_name(t, nt, index2), reverse=True) ms += [token for token in index2.all_name_tokens if token.startswith(t)] - if not ms: - return None for match in ms: score = calc_ratio_name(t, match, index2) for m in index2.manual[match]: From a05e4b2da2485ec9fdaa6e8e1361e3c23a20dac1 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Sat, 6 Feb 2021 16:21:12 -0800 Subject: [PATCH 0569/1220] Refactoring Id menu emoji data --- padinfo/id_menu.py | 91 ++++++++++++++------------------- padinfo/padinfo.py | 22 ++++---- padinfo/pane_names.py | 7 +++ padinfo/view_state/evos.py | 5 +- padinfo/view_state/id.py | 4 +- padinfo/view_state/materials.py | 8 +-- padinfo/view_state/otherinfo.py | 4 +- padinfo/view_state/pantheon.py | 5 +- padinfo/view_state/pic.py | 5 +- 9 files changed, 70 insertions(+), 81 deletions(-) create mode 100644 padinfo/pane_names.py diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index dcf652a3..719fc345 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -8,15 +8,14 @@ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji -from padinfo.core.padinfo_settings import settings - from dadguide.database_context import DbContext +from padinfo.pane_names import IdMenuPaneNames +from padinfo.view.evos import EvosView from padinfo.view.id import IdView from padinfo.view.materials import MaterialsView from padinfo.view.otherinfo import OtherInfoView from padinfo.view.pantheon import PantheonView from padinfo.view.pic import PicView -from padinfo.view.evos import EvosView from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.materials import MaterialsViewState @@ -27,66 +26,27 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel -emoji_button_names = [ - '\N{BLACK LEFT-POINTING TRIANGLE}', - '\N{BLACK RIGHT-POINTING TRIANGLE}', - '\N{HOUSE BUILDING}', - char_to_emoji('e'), - '\N{MEAT ON BONE}', - '\N{FRAME WITH PICTURE}', - '\N{CLASSICAL BUILDING}', - '\N{SCROLL}', - '\N{CROSS MARK}', -] menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') class IdMenu: - INITIAL_EMOJI = emoji_button_names[2] MENU_TYPE = 'IdMenu' @staticmethod def menu(original_author_id, friend_ids, bot_id, initial_control=None): if initial_control is None: initial_control = IdMenu.id_control - transitions = { - emoji_button_names[0]: IdMenu.respond_with_left, - emoji_button_names[1]: IdMenu.respond_with_right, - emoji_button_names[2]: IdMenu.respond_with_current_id, - emoji_button_names[3]: IdMenu.respond_with_evos, - emoji_button_names[4]: IdMenu.respond_with_mats, - emoji_button_names[5]: IdMenu.respond_with_picture, - emoji_button_names[6]: IdMenu.respond_with_pantheon, - emoji_button_names[7]: IdMenu.respond_with_otherinfo, - } - - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) + + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(IdMenuPanes.emoji_names()) reaction_filters = [ ValidEmojiReactionFilter(valid_emoji_names), NotPosterEmojiReactionFilter(), BotAuthoredMessageReactionFilter(bot_id), MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) ] - embed = EmbedMenu(reaction_filters, transitions, initial_control, menu_emoji_config) + embed = EmbedMenu(reaction_filters, IdMenuPanes.transitions(), initial_control, menu_emoji_config) return embed - @staticmethod - def get_pane_types(): - # This lets us write the functionality for the arrows, we need to know what current pane - # corresponds to what response type. - - # The strings are pane_types as defined in ViewState serialize functions, the functions are - # IdMenu responses. - - return { - 'id': IdMenu.respond_with_current_id, - 'evos': IdMenu.respond_with_evos, - 'materials': IdMenu.respond_with_mats, - 'otherinfo': IdMenu.respond_with_otherinfo, - 'pantheon': IdMenu.respond_with_pantheon, - 'pic': IdMenu.respond_with_picture, - } - @staticmethod async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] @@ -97,7 +57,7 @@ async def respond_with_left(message: Optional[Message], ims, **data): new_monster_id = str(IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll)) ims['resolved_monster_id'] = new_monster_id pane_type = ims['pane_type'] - pane_type_to_func_map = IdMenu.get_pane_types() + pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] return await response_func(message, ims, **data) @@ -122,7 +82,7 @@ async def respond_with_right(message: Optional[Message], ims, **data): new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll)) ims['resolved_monster_id'] = new_monster_id pane_type = ims.get('pane_type') - pane_type_to_func_map = IdMenu.get_pane_types() + pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] return await response_func(message, ims, **data) @@ -199,41 +159,66 @@ async def respond_with_otherinfo(message: Optional[Message], ims, **data): def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def evos_control(state: EvosViewState): return EmbedControl( [EvosView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def mats_control(state: MaterialsViewState): return EmbedControl( [MaterialsView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def pic_control(state: PicViewState): return EmbedControl( [PicView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def pantheon_control(state: PantheonViewState): return EmbedControl( [PantheonView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def otherinfo_control(state: OtherInfoViewState): return EmbedControl( [OtherInfoView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) + +class IdMenuPanes: + INITIAL_EMOJI = '\N{HOUSE BUILDING}' + DATA = { + IdMenu.respond_with_left: ('\N{BLACK LEFT-POINTING TRIANGLE}', None), + IdMenu.respond_with_right: ('\N{BLACK RIGHT-POINTING TRIANGLE}', None), + IdMenu.respond_with_current_id: ('\N{HOUSE BUILDING}', IdMenuPaneNames.id), + IdMenu.respond_with_evos: (char_to_emoji('e'), IdMenuPaneNames.evos), + IdMenu.respond_with_mats: ('\N{MEAT ON BONE}', IdMenuPaneNames.materials), + IdMenu.respond_with_picture: ('\N{FRAME WITH PICTURE}', IdMenuPaneNames.pic), + IdMenu.respond_with_pantheon: ('\N{CLASSICAL BUILDING}', IdMenuPaneNames.pantheon), + IdMenu.respond_with_otherinfo: ('\N{SCROLL}', IdMenuPaneNames.otherinfo), + } + + @classmethod + def emoji_names(cls): + return [v[0] for k, v in cls.DATA.items()] + + @classmethod + def transitions(cls): + return {v[0]: k for k, v in cls.DATA.items()} + + @classmethod + def pane_types(cls): + return {v[1]: k for k, v in cls.DATA.items() if v[1]} diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index afa623bc..cf2739cf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -31,7 +31,7 @@ from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings -from padinfo.id_menu import IdMenu, emoji_button_names as id_menu_emoji_button_names +from padinfo.id_menu import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader @@ -152,7 +152,7 @@ async def test_reaction_add(self, reaction, member): emoji_clicked = emoji_obj.name if not (emoji_clicked in ls_menu_emoji_button_names or - emoji_clicked in id_menu_emoji_button_names): + emoji_clicked in IdMenuPanes.emoji_names()): return message = reaction.message @@ -622,7 +622,7 @@ async def idt_add(self, ctx, id: int, *, query): query, *reason = query.split("|") query = query.strip() if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -670,8 +670,8 @@ async def idtf_add(self, ctx, id: int, token, *, reason=""): async def norf_add(self, ctx, id: int, token, reason, fluffy): reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, - "Are you sure you want to add to the fluff/name test suite?"): + not await tsutils.confirm_message(ctx, + "Are you sure you want to add to the fluff/name test suite?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -719,7 +719,7 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): async def idt_import(self, ctx, *, queries): """Import id3 tests""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -741,7 +741,7 @@ async def idtf_import(self, ctx, *, queries): async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -762,7 +762,7 @@ async def norf_import(self, ctx, queries): async def idt_remove(self, ctx, *, item): """Remove an id3 test""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -788,7 +788,7 @@ async def idtf_remove(self, ctx, *, item: int): async def norf_remove(self, ctx, item): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -805,7 +805,7 @@ async def idt_setreason(self, ctx, number: int, *, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -830,7 +830,7 @@ async def norf_setreason(self, ctx, number, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') diff --git a/padinfo/pane_names.py b/padinfo/pane_names.py new file mode 100644 index 00000000..7a165018 --- /dev/null +++ b/padinfo/pane_names.py @@ -0,0 +1,7 @@ +class IdMenuPaneNames: + id = 'id' + evos = 'evos' + materials = 'materials' + otherinfo = 'otherinfo' + pantheon = 'pantheon' + pic = 'pic' diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index 6889c542..fcc55d58 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -2,6 +2,7 @@ from padinfo.common.config import UserConfig from padinfo.core.id import get_monster_by_id, get_monster_by_query +from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -24,7 +25,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst def serialize(self): ret = super().serialize() ret.update({ - 'pane_type': 'evos', + 'pane_type': IdMenuPaneNames.evos, 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), @@ -40,9 +41,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) - # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query - original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index ef109b0e..0b0aacbf 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -2,6 +2,7 @@ from padinfo.common.config import UserConfig from padinfo.core.id import get_monster_by_query, get_id_view_state_data, get_monster_by_id +from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -27,7 +28,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst def serialize(self): ret = super().serialize() ret.update({ - 'pane_type': 'id', + 'pane_type': IdMenuPaneNames.id, 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), @@ -47,7 +48,6 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query - menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 2d8f9dfb..06289193 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -3,6 +3,7 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill from padinfo.core.id import get_monster_by_id, get_monster_by_query +from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -31,7 +32,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst def serialize(self): ret = super().serialize() ret.update({ - 'pane_type': 'materials', + 'pane_type': IdMenuPaneNames.materials, 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), @@ -47,11 +48,10 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): monster = await (get_monster_by_id(dgcog, resolved_monster_id) if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = await MaterialsViewState.query(dgcog, monster) + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = \ + await MaterialsViewState.query(dgcog, monster) - # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query - menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py index 1200ca66..4b82b52a 100644 --- a/padinfo/view_state/otherinfo.py +++ b/padinfo/view_state/otherinfo.py @@ -2,6 +2,7 @@ from padinfo.common.config import UserConfig from padinfo.core.id import get_monster_by_id, get_monster_by_query +from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -21,7 +22,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst def serialize(self): ret = super().serialize() ret.update({ - 'pane_type': 'otherinfo', + 'pane_type': IdMenuPaneNames.otherinfo, 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), @@ -38,7 +39,6 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) query = ims.get('query') or raw_query - menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index d56938a4..497a34ba 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -2,6 +2,7 @@ from padinfo.common.config import UserConfig from padinfo.core.id import get_monster_by_id, get_monster_by_query +from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -24,7 +25,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst def serialize(self): ret = super().serialize() ret.update({ - 'pane_type': 'pantheon', + 'pane_type': IdMenuPaneNames.pantheon, 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), @@ -42,9 +43,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) - # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query - original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py index 2e4292a2..073efac7 100644 --- a/padinfo/view_state/pic.py +++ b/padinfo/view_state/pic.py @@ -2,6 +2,7 @@ from padinfo.common.config import UserConfig from padinfo.core.id import get_monster_by_query, get_monster_by_id +from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState if TYPE_CHECKING: @@ -21,7 +22,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst def serialize(self): ret = super().serialize() ret.update({ - 'pane_type': 'pic', + 'pane_type': IdMenuPaneNames.pic, 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), @@ -37,9 +38,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): monster = await (get_monster_by_id(dgcog, resolved_monster_id) if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) - # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query - original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] From 585e298df17b3be59c63e334a64eee5bbc4f41bb Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 6 Feb 2021 19:17:26 -0600 Subject: [PATCH 0570/1220] Fix ls menu & refactor get monster from ims (#753) * View states get monster from common function * Fix ls menu not working * Fix placement of nulling monster id --- padinfo/core/id.py | 13 ------------- padinfo/ls_menu.py | 2 ++ padinfo/view_state/common.py | 13 +++++++++++++ padinfo/view_state/evos.py | 9 +++------ padinfo/view_state/id.py | 11 ++++------- padinfo/view_state/materials.py | 10 +++------- padinfo/view_state/otherinfo.py | 10 +++------- padinfo/view_state/pantheon.py | 11 +++-------- padinfo/view_state/pic.py | 10 +++------- 9 files changed, 34 insertions(+), 55 deletions(-) create mode 100644 padinfo/view_state/common.py diff --git a/padinfo/core/id.py b/padinfo/core/id.py index 63ff240e..afeaa983 100644 --- a/padinfo/core/id.py +++ b/padinfo/core/id.py @@ -1,16 +1,3 @@ -from padinfo.core.find_monster import findMonsterCustom2 - - -async def get_monster_by_query(dgcog, raw_query, beta_id3): - monster, _, _ = await findMonsterCustom2(dgcog, beta_id3, raw_query) - return monster - - -async def get_monster_by_id(dgcog, id): - monster = dgcog.database.graph.get_monster(id) - return monster - - async def get_id_view_state_data(dgcog, monster): db_context = dgcog.database acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ diff --git a/padinfo/ls_menu.py b/padinfo/ls_menu.py index 5c4633e4..4a5d53b2 100644 --- a/padinfo/ls_menu.py +++ b/padinfo/ls_menu.py @@ -49,6 +49,7 @@ async def respond_to_r(message: Optional[Message], ims, **data): # Extract the query from the ls state ims['query'] = ims['r_query'] + ims['resolved_monster_id'] = None id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) id_control = LeaderSkillMenu.id_control(id_view_state) return id_control @@ -60,6 +61,7 @@ async def respond_to_l(message: Optional[Message], ims, **data): # Extract the query from the ls state ims['query'] = ims['l_query'] + ims['resolved_monster_id'] = None id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) id_control = LeaderSkillMenu.id_control(id_view_state) return id_control diff --git a/padinfo/view_state/common.py b/padinfo/view_state/common.py new file mode 100644 index 00000000..d1ad9f70 --- /dev/null +++ b/padinfo/view_state/common.py @@ -0,0 +1,13 @@ +from padinfo.common.config import UserConfig +from padinfo.core.find_monster import findMonsterCustom2 + + +async def get_monster_from_ims(dgcog, user_config: UserConfig, ims: dict): + query = ims.get('query') or ims['raw_query'] + + resolved_monster_id_str = ims.get('resolved_monster_id') + resolved_monster_id = int(resolved_monster_id_str) if resolved_monster_id_str else None + if resolved_monster_id: + return dgcog.database.graph.get_monster(resolved_monster_id) + monster, _, _ = await findMonsterCustom2(dgcog, user_config.beta_id3, query) + return monster diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index fcc55d58..eff0fd8c 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -1,9 +1,9 @@ from typing import List, TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -34,13 +34,10 @@ def serialize(self): @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - - raw_query = ims['raw_query'] - resolved_monster_id = int(ims.get('resolved_monster_id')) - monster = await (get_monster_by_id(dgcog, resolved_monster_id) - if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + monster = await get_monster_from_ims(dgcog, user_config, ims) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 0b0aacbf..1b4902b3 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -1,9 +1,10 @@ from typing import List, TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import get_monster_by_query, get_id_view_state_data, get_monster_by_id +from padinfo.core.id import get_id_view_state_data from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -37,15 +38,11 @@ def serialize(self): @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - - resolved_monster_id = int(ims.get('resolved_monster_id')) - monster = await (get_monster_by_id(dgcog, resolved_monster_id) - if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) - + monster = await get_monster_from_ims(dgcog, user_config, ims) transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ await get_id_view_state_data(dgcog, monster) + raw_query = ims['raw_query'] # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query menu_type = ims['menu_type'] diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 06289193..91e37596 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -2,9 +2,9 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill -from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -41,16 +41,12 @@ def serialize(self): @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - - resolved_monster_id = int(ims.get('resolved_monster_id')) - - monster = await (get_monster_by_id(dgcog, resolved_monster_id) - if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + monster = await get_monster_from_ims(dgcog, user_config, ims) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = \ await MaterialsViewState.query(dgcog, monster) + raw_query = ims['raw_query'] query = ims.get('query') or raw_query menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py index 4b82b52a..8ef18d92 100644 --- a/padinfo/view_state/otherinfo.py +++ b/padinfo/view_state/otherinfo.py @@ -1,9 +1,9 @@ from typing import TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -31,13 +31,9 @@ def serialize(self): @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - - resolved_monster_id = int(ims.get('resolved_monster_id')) - - monster = await (get_monster_by_id(dgcog, resolved_monster_id) - if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + monster = await get_monster_from_ims(dgcog, user_config, ims) + raw_query = ims['raw_query'] query = ims.get('query') or raw_query menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index 497a34ba..9c3a636f 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -1,9 +1,9 @@ from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig -from padinfo.core.id import get_monster_by_id, get_monster_by_query from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -34,15 +34,10 @@ def serialize(self): @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - - resolved_monster_id = int(ims.get('resolved_monster_id')) - - monster = await (get_monster_by_id(dgcog, resolved_monster_id) - if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) - + monster = await get_monster_from_ims(dgcog, user_config, ims) pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py index 073efac7..b5041239 100644 --- a/padinfo/view_state/pic.py +++ b/padinfo/view_state/pic.py @@ -1,9 +1,9 @@ from typing import TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import get_monster_by_query, get_monster_by_id from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -31,13 +31,9 @@ def serialize(self): @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - - resolved_monster_id = int(ims.get('resolved_monster_id')) - - monster = await (get_monster_by_id(dgcog, resolved_monster_id) - if resolved_monster_id else get_monster_by_query(dgcog, raw_query, user_config.beta_id3)) + monster = await get_monster_from_ims(dgcog, user_config, ims) + raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' From 1a7c5cbdd1f72e9e20e4271f0f1db79d38635b07 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 6 Feb 2021 20:22:19 -0700 Subject: [PATCH 0571/1220] Create ^idtraceback (#751) * Add idtraceback * Requested Changes --- padinfo/core/find_monster.py | 101 +++++++++++++++++++++-------------- padinfo/padinfo.py | 71 +++++++++++++++++++++--- 2 files changed, 126 insertions(+), 46 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 45f79e76..c4ee9d0b 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -1,7 +1,7 @@ import json import re from collections import defaultdict -from typing import Set, List, Tuple, Optional, TYPE_CHECKING +from typing import Set, List, Tuple, Optional, TYPE_CHECKING, Mapping from Levenshtein import jaro_winkler from tsutils import rmdiacritics @@ -77,15 +77,18 @@ def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result.append(token) return result - def _monster_has_token(self, monster, token, monsterscore, monster_mods): + def _monster_has_token(self, monster, token, matches, monster_mods): if len(token) < 6: if token in monster_mods: - monsterscore[monster] += 1 + matches[monster].mod.add(f"{token} - {token}") + matches[monster].score += 1 return True else: - closest = max(calc_ratio_modifier(m, token) for m in monster_mods) - if closest > self.TOKEN_JW_DISTANCE: - monsterscore[monster] += closest + closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) + rat = calc_ratio_modifier(closest, token) + if rat > self.TOKEN_JW_DISTANCE: + matches[monster].mod.add(f"{token} - {closest}") + matches[monster].score += rat return True return False @@ -144,27 +147,26 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], return set(modifiers), negative_modifiers, name, negative_name - def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): + def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2, matches): monstergen = None - monsterscore = defaultdict(int) for t in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(t, index2, monsterscore) + valid = self.get_valid_monsters_from_name_token(t, index2, matches) if monstergen is not None: monstergen.intersection_update(valid) else: monstergen = valid for t in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(t, index2, monsterscore, mult=-10) + invalid = self.get_valid_monsters_from_name_token(t, index2, matches, mult=-10) if monstergen is not None: monstergen.difference_update(invalid) else: monstergen = set() - return monstergen, monsterscore + return monstergen - def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): + def get_valid_monsters_from_name_token(self, t, index2, matches, mult=1): valid = set() ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_name(t, nt, index2) > self.TOKEN_JW_DISTANCE], key=lambda nt: calc_ratio_name(t, nt, index2), reverse=True) @@ -173,40 +175,43 @@ def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): score = calc_ratio_name(t, match, index2) for m in index2.manual[match]: if m not in valid: - monsterscore[m] += (score + .001) * mult + matches[m].name.add(f"{t} - {match}") + matches[m].score += (score + .001) * mult valid.add(m) for m in index2.name_tokens[match]: if m not in valid: - monsterscore[m] += score * mult + matches[m].name.add(f"{t} - {match}") + matches[m].score += score * mult valid.add(m) for m in index2.fluff_tokens[match]: if m not in valid: - monsterscore[m] += score * mult / 2 + matches[m].name.add(f"{t} - {match}") + matches[m].score += score * mult / 2 valid.add(m) return valid - def process_modifiers(self, mod_tokens, neg_mod_tokens, monsterscore, potential_evos, monster_mods): + def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches, monster_mods): for t in mod_tokens: potential_evos = {m for m in potential_evos if - self._monster_has_token(m, t, monsterscore, monster_mods[m])} + self._monster_has_token(m, t, matches, monster_mods[m])} if not potential_evos: return None for t in neg_mod_tokens: potential_evos = {m for m in potential_evos if - not self._monster_has_token(m, t, monsterscore, monster_mods[m])} + not self._monster_has_token(m, t, matches, monster_mods[m])} if not potential_evos: return None return potential_evos - def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, monster_score=None): - if monster_score is None: - monster_score = defaultdict(int) + def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, matches=None): + if matches is None: + monster_score = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] return max(monsters, - key=lambda m: (monster_score[m], + key=lambda m: (matches[m].score, not m.is_equip, # Match na on id overlap bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), @@ -219,13 +224,14 @@ def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, monst m.rarity, m.monster_no_na)) - def get_monster_evos(self, database, monster_gen, monster_score): + def get_monster_evos(self, database, monster_gen, matches): monster_evos = set() - for m in sorted(monster_gen, key=lambda m: monster_score[m], reverse=True): + for m in sorted(monster_gen, key=lambda m: matches[m].score, reverse=True): for evo in database.graph.get_alt_monsters(m): monster_evos.add(evo) - if monster_score[evo] < monster_score[m]: - monster_score[evo] = monster_score[m] - .003 + if matches[evo].score < matches[m].score: + matches[evo].name = {t + f" (from evo {m.monster_id})" for t in matches[m].name} + matches[evo].score = matches[m].score - .003 return monster_evos @@ -269,7 +275,7 @@ async def _findMonster(dgcog, query) -> Tuple[Optional["NamedMonster"], Optional await dgcog.wait_until_ready() try: return dgcog.index.find_monster(query) - except: + except Exception: prefix = (await dgcog.bot.get_valid_prefixes())[0] return (None, f"Sorry, id1 doesn't support this query and we are no longer" @@ -298,12 +304,22 @@ async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: return max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (0.0, None), - key=lambda t: t[0] - )[1] + if tokenized_query != mw_tokenized_query else (None, MonsterMatch()), + key=lambda t: t[1].get(t[0], MonsterMatch()).score + )[0] -async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["MonsterModel"]]: +class MonsterMatch: + def __init__(self, score=0, name=None, mod=None): + self.score = score + if name is None: + self.name = set() + if mod is None: + self.mod = set() + + +async def find_monster_search(tokenized_query, dgcog) -> \ + Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch]]: mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ find_monster.interpret_query(tokenized_query, dgcog.index2) @@ -313,32 +329,37 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + matches = defaultdict(MonsterMatch) if name_query_tokens: - monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog.index2) + monster_gen = find_monster.process_name_tokens(name_query_tokens, + neg_name_tokens, + dgcog.index2, + matches) if not monster_gen: # No monsters match the given name tokens - return 0, None - monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, monster_score) + return None, {} + monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, matches) else: # There are no name tokens in the query monster_gen = {*dgcog.database.get_all_monsters()} monster_score = defaultdict(int) # Expand search to the evo tree - monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_score, monster_gen, + monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_gen, matches, dgcog.index2.modifiers) if not monster_gen: # no modifiers match any monster in the evo tree - return 0, None + return None, {} - # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) + if k in monster_gen}) # Return most likely candidate based on query. - mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, monster_score) + mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, matches) - return monster_score[mon], mon + return mon, matches find_monster = FindMonster() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index cf2739cf..ed8878ec 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -19,14 +19,15 @@ from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate -from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor +from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor, rmdiacritics from padinfo.common.config import BotConfig -from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji +from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ + get_attribute_emoji_by_monster from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom, calc_ratio_name, calc_ratio_modifier, findMonsterCustom2 + findMonsterCustom, calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom2 from padinfo.core.historic_lookups import historic_lookups from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query @@ -732,12 +733,12 @@ async def idt_import(self, ctx, *, queries): @idt_name.command(name="import") async def idtn_import(self, ctx, *, queries): """Import name/fluff tests""" - await self.norf_import(ctx, id, queries) + await self.norf_import(ctx, queries) @idt_fluff.command(name="import") async def idtf_import(self, ctx, *, queries): """Import name/fluff tests""" - await self.norf_import(ctx, id, queries) + await self.norf_import(ctx, queries) async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ @@ -838,6 +839,7 @@ async def norf_setreason(self, ctx, number, reason): if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") return + # noinspection PyTypeChecker sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[number]['reason'] = reason await ctx.tick() @@ -1376,7 +1378,7 @@ def f(m, s): o += f"\n\n{type}\n" + "\n".join(f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) return o - o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", 1) + o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) o += write_name_token(DGCOG.index2.manual, "[Manual Tokens]") o += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") o += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") @@ -1432,3 +1434,60 @@ def additmods(ms, om): await ctx.send(page) else: await ctx.send(f"There are no modifiers that match `{token}`.") + + @commands.command(aliases=["tracebackid", "tbid", "idtb"]) + async def idtraceback(self, ctx, *, query): + """Get the traceback of an id query""" + mid = None + if "/" in query: + query, mid = query.split("/", 1) + if not mid.strip().isdigit(): + await ctx.send("Monster id must be an int.") + return + mid = int(mid.strip()) + + dgcog = self.bot.get_cog("Dadguide") + await dgcog.wait_until_ready() + + query = rmdiacritics(query).strip().lower().replace(",", "") + tokenized_query = query.split() + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) + + bestmatch, matches = max( + await find_monster_search(tokenized_query, dgcog), + await find_monster_search(mw_tokenized_query, dgcog) + if tokenized_query != mw_tokenized_query else (None, {None: 0.0}, {None: set()}), + key=lambda t: t[1][t[0]].score if t[0] else 0 + ) + + if bestmatch is None: + await ctx.send("No monster matched.") + return + + if mid is not None: + selected = {m for m in matches if m.monster_id == mid} + if not selected: + await ctx.send("The requested monster was not found as a result of the query.") + return + monster = selected.pop() + else: + monster = bestmatch + + score = matches[monster].score + ntokens = matches[monster].name + mtokens = matches[monster].mod + lower_prio = {m for m in matches if matches[m].score == matches[monster].score}.difference({monster}) + if len(lower_prio) > 10: + lpstr = f"{len(lower_prio)} other monsters." + else: + lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) + + mtokenstr = '\n'.join(sorted(mtokens)) + ntokenstr = '\n'.join(sorted(ntokens)) + + await ctx.send(f"**Monster matched**: " + f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})\n" + f"**Total Score**: {score}\n\n" + f"**Matched Name Tokens**:\n{ntokenstr}\n\n" + f"**Matched Mod Tokens**:\n{mtokenstr}\n\n" + + (f"**Equally Scoring Matches**:\n{lpstr}" if lower_prio else "")) From fbebfa842241909c4ec8afc2c149d7e2f7d6a0fe Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 6 Feb 2021 21:39:10 -0600 Subject: [PATCH 0572/1220] Revert "Create ^idtraceback (#751)" (#755) This reverts commit 1a7c5cbdd1f72e9e20e4271f0f1db79d38635b07. --- padinfo/core/find_monster.py | 101 ++++++++++++++--------------------- padinfo/padinfo.py | 71 +++--------------------- 2 files changed, 46 insertions(+), 126 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index c4ee9d0b..45f79e76 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -1,7 +1,7 @@ import json import re from collections import defaultdict -from typing import Set, List, Tuple, Optional, TYPE_CHECKING, Mapping +from typing import Set, List, Tuple, Optional, TYPE_CHECKING from Levenshtein import jaro_winkler from tsutils import rmdiacritics @@ -77,18 +77,15 @@ def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result.append(token) return result - def _monster_has_token(self, monster, token, matches, monster_mods): + def _monster_has_token(self, monster, token, monsterscore, monster_mods): if len(token) < 6: if token in monster_mods: - matches[monster].mod.add(f"{token} - {token}") - matches[monster].score += 1 + monsterscore[monster] += 1 return True else: - closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) - rat = calc_ratio_modifier(closest, token) - if rat > self.TOKEN_JW_DISTANCE: - matches[monster].mod.add(f"{token} - {closest}") - matches[monster].score += rat + closest = max(calc_ratio_modifier(m, token) for m in monster_mods) + if closest > self.TOKEN_JW_DISTANCE: + monsterscore[monster] += closest return True return False @@ -147,26 +144,27 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], return set(modifiers), negative_modifiers, name, negative_name - def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2, matches): + def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): monstergen = None + monsterscore = defaultdict(int) for t in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(t, index2, matches) + valid = self.get_valid_monsters_from_name_token(t, index2, monsterscore) if monstergen is not None: monstergen.intersection_update(valid) else: monstergen = valid for t in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(t, index2, matches, mult=-10) + invalid = self.get_valid_monsters_from_name_token(t, index2, monsterscore, mult=-10) if monstergen is not None: monstergen.difference_update(invalid) else: monstergen = set() - return monstergen + return monstergen, monsterscore - def get_valid_monsters_from_name_token(self, t, index2, matches, mult=1): + def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): valid = set() ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_name(t, nt, index2) > self.TOKEN_JW_DISTANCE], key=lambda nt: calc_ratio_name(t, nt, index2), reverse=True) @@ -175,43 +173,40 @@ def get_valid_monsters_from_name_token(self, t, index2, matches, mult=1): score = calc_ratio_name(t, match, index2) for m in index2.manual[match]: if m not in valid: - matches[m].name.add(f"{t} - {match}") - matches[m].score += (score + .001) * mult + monsterscore[m] += (score + .001) * mult valid.add(m) for m in index2.name_tokens[match]: if m not in valid: - matches[m].name.add(f"{t} - {match}") - matches[m].score += score * mult + monsterscore[m] += score * mult valid.add(m) for m in index2.fluff_tokens[match]: if m not in valid: - matches[m].name.add(f"{t} - {match}") - matches[m].score += score * mult / 2 + monsterscore[m] += score * mult / 2 valid.add(m) return valid - def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches, monster_mods): + def process_modifiers(self, mod_tokens, neg_mod_tokens, monsterscore, potential_evos, monster_mods): for t in mod_tokens: potential_evos = {m for m in potential_evos if - self._monster_has_token(m, t, matches, monster_mods[m])} + self._monster_has_token(m, t, monsterscore, monster_mods[m])} if not potential_evos: return None for t in neg_mod_tokens: potential_evos = {m for m in potential_evos if - not self._monster_has_token(m, t, matches, monster_mods[m])} + not self._monster_has_token(m, t, monsterscore, monster_mods[m])} if not potential_evos: return None return potential_evos - def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, matches=None): - if matches is None: - monster_score = defaultdict(MonsterMatch) + def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, monster_score=None): + if monster_score is None: + monster_score = defaultdict(int) if tokenized_query is None: tokenized_query = [] return max(monsters, - key=lambda m: (matches[m].score, + key=lambda m: (monster_score[m], not m.is_equip, # Match na on id overlap bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), @@ -224,14 +219,13 @@ def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, match m.rarity, m.monster_no_na)) - def get_monster_evos(self, database, monster_gen, matches): + def get_monster_evos(self, database, monster_gen, monster_score): monster_evos = set() - for m in sorted(monster_gen, key=lambda m: matches[m].score, reverse=True): + for m in sorted(monster_gen, key=lambda m: monster_score[m], reverse=True): for evo in database.graph.get_alt_monsters(m): monster_evos.add(evo) - if matches[evo].score < matches[m].score: - matches[evo].name = {t + f" (from evo {m.monster_id})" for t in matches[m].name} - matches[evo].score = matches[m].score - .003 + if monster_score[evo] < monster_score[m]: + monster_score[evo] = monster_score[m] - .003 return monster_evos @@ -275,7 +269,7 @@ async def _findMonster(dgcog, query) -> Tuple[Optional["NamedMonster"], Optional await dgcog.wait_until_ready() try: return dgcog.index.find_monster(query) - except Exception: + except: prefix = (await dgcog.bot.get_valid_prefixes())[0] return (None, f"Sorry, id1 doesn't support this query and we are no longer" @@ -304,22 +298,12 @@ async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: return max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, MonsterMatch()), - key=lambda t: t[1].get(t[0], MonsterMatch()).score - )[0] + if tokenized_query != mw_tokenized_query else (0.0, None), + key=lambda t: t[0] + )[1] -class MonsterMatch: - def __init__(self, score=0, name=None, mod=None): - self.score = score - if name is None: - self.name = set() - if mod is None: - self.mod = set() - - -async def find_monster_search(tokenized_query, dgcog) -> \ - Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch]]: +async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["MonsterModel"]]: mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ find_monster.interpret_query(tokenized_query, dgcog.index2) @@ -329,37 +313,32 @@ async def find_monster_search(tokenized_query, dgcog) -> \ if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) - matches = defaultdict(MonsterMatch) + # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) if name_query_tokens: - monster_gen = find_monster.process_name_tokens(name_query_tokens, - neg_name_tokens, - dgcog.index2, - matches) + monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog.index2) if not monster_gen: # No monsters match the given name tokens - return None, {} - monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, matches) + return 0, None + monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, monster_score) else: # There are no name tokens in the query monster_gen = {*dgcog.database.get_all_monsters()} monster_score = defaultdict(int) # Expand search to the evo tree - monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_gen, matches, + monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_score, monster_gen, dgcog.index2.modifiers) if not monster_gen: # no modifiers match any monster in the evo tree - return None, {} + return 0, None - print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) - if k in monster_gen}) + # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) # Return most likely candidate based on query. - mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, matches) + mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, monster_score) - return mon, matches + return monster_score[mon], mon find_monster = FindMonster() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ed8878ec..cf2739cf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -19,15 +19,14 @@ from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate -from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor, rmdiacritics +from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor from padinfo.common.config import BotConfig -from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ - get_attribute_emoji_by_monster +from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom, calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom2 + findMonsterCustom, calc_ratio_name, calc_ratio_modifier, findMonsterCustom2 from padinfo.core.historic_lookups import historic_lookups from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query @@ -733,12 +732,12 @@ async def idt_import(self, ctx, *, queries): @idt_name.command(name="import") async def idtn_import(self, ctx, *, queries): """Import name/fluff tests""" - await self.norf_import(ctx, queries) + await self.norf_import(ctx, id, queries) @idt_fluff.command(name="import") async def idtf_import(self, ctx, *, queries): """Import name/fluff tests""" - await self.norf_import(ctx, queries) + await self.norf_import(ctx, id, queries) async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ @@ -839,7 +838,6 @@ async def norf_setreason(self, ctx, number, reason): if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") return - # noinspection PyTypeChecker sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[number]['reason'] = reason await ctx.tick() @@ -1378,7 +1376,7 @@ def f(m, s): o += f"\n\n{type}\n" + "\n".join(f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) return o - o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) + o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", 1) o += write_name_token(DGCOG.index2.manual, "[Manual Tokens]") o += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") o += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") @@ -1434,60 +1432,3 @@ def additmods(ms, om): await ctx.send(page) else: await ctx.send(f"There are no modifiers that match `{token}`.") - - @commands.command(aliases=["tracebackid", "tbid", "idtb"]) - async def idtraceback(self, ctx, *, query): - """Get the traceback of an id query""" - mid = None - if "/" in query: - query, mid = query.split("/", 1) - if not mid.strip().isdigit(): - await ctx.send("Monster id must be an int.") - return - mid = int(mid.strip()) - - dgcog = self.bot.get_cog("Dadguide") - await dgcog.wait_until_ready() - - query = rmdiacritics(query).strip().lower().replace(",", "") - tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) - - bestmatch, matches = max( - await find_monster_search(tokenized_query, dgcog), - await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, {None: 0.0}, {None: set()}), - key=lambda t: t[1][t[0]].score if t[0] else 0 - ) - - if bestmatch is None: - await ctx.send("No monster matched.") - return - - if mid is not None: - selected = {m for m in matches if m.monster_id == mid} - if not selected: - await ctx.send("The requested monster was not found as a result of the query.") - return - monster = selected.pop() - else: - monster = bestmatch - - score = matches[monster].score - ntokens = matches[monster].name - mtokens = matches[monster].mod - lower_prio = {m for m in matches if matches[m].score == matches[monster].score}.difference({monster}) - if len(lower_prio) > 10: - lpstr = f"{len(lower_prio)} other monsters." - else: - lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) - - mtokenstr = '\n'.join(sorted(mtokens)) - ntokenstr = '\n'.join(sorted(ntokens)) - - await ctx.send(f"**Monster matched**: " - f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})\n" - f"**Total Score**: {score}\n\n" - f"**Matched Name Tokens**:\n{ntokenstr}\n\n" - f"**Matched Mod Tokens**:\n{mtokenstr}\n\n" + - (f"**Equally Scoring Matches**:\n{lpstr}" if lower_prio else "")) From 0c6d14ba544810b4f29634dbb7ac1760616aa6a3 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 6 Feb 2021 21:42:30 -0600 Subject: [PATCH 0573/1220] Undoing last revert becuase fix is ready (#757) This reverts commit fbebfa842241909c4ec8afc2c149d7e2f7d6a0fe. --- padinfo/core/find_monster.py | 101 +++++++++++++++++++++-------------- padinfo/padinfo.py | 71 +++++++++++++++++++++--- 2 files changed, 126 insertions(+), 46 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 45f79e76..c4ee9d0b 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -1,7 +1,7 @@ import json import re from collections import defaultdict -from typing import Set, List, Tuple, Optional, TYPE_CHECKING +from typing import Set, List, Tuple, Optional, TYPE_CHECKING, Mapping from Levenshtein import jaro_winkler from tsutils import rmdiacritics @@ -77,15 +77,18 @@ def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result.append(token) return result - def _monster_has_token(self, monster, token, monsterscore, monster_mods): + def _monster_has_token(self, monster, token, matches, monster_mods): if len(token) < 6: if token in monster_mods: - monsterscore[monster] += 1 + matches[monster].mod.add(f"{token} - {token}") + matches[monster].score += 1 return True else: - closest = max(calc_ratio_modifier(m, token) for m in monster_mods) - if closest > self.TOKEN_JW_DISTANCE: - monsterscore[monster] += closest + closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) + rat = calc_ratio_modifier(closest, token) + if rat > self.TOKEN_JW_DISTANCE: + matches[monster].mod.add(f"{token} - {closest}") + matches[monster].score += rat return True return False @@ -144,27 +147,26 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], return set(modifiers), negative_modifiers, name, negative_name - def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2): + def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2, matches): monstergen = None - monsterscore = defaultdict(int) for t in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(t, index2, monsterscore) + valid = self.get_valid_monsters_from_name_token(t, index2, matches) if monstergen is not None: monstergen.intersection_update(valid) else: monstergen = valid for t in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(t, index2, monsterscore, mult=-10) + invalid = self.get_valid_monsters_from_name_token(t, index2, matches, mult=-10) if monstergen is not None: monstergen.difference_update(invalid) else: monstergen = set() - return monstergen, monsterscore + return monstergen - def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): + def get_valid_monsters_from_name_token(self, t, index2, matches, mult=1): valid = set() ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_name(t, nt, index2) > self.TOKEN_JW_DISTANCE], key=lambda nt: calc_ratio_name(t, nt, index2), reverse=True) @@ -173,40 +175,43 @@ def get_valid_monsters_from_name_token(self, t, index2, monsterscore, mult=1): score = calc_ratio_name(t, match, index2) for m in index2.manual[match]: if m not in valid: - monsterscore[m] += (score + .001) * mult + matches[m].name.add(f"{t} - {match}") + matches[m].score += (score + .001) * mult valid.add(m) for m in index2.name_tokens[match]: if m not in valid: - monsterscore[m] += score * mult + matches[m].name.add(f"{t} - {match}") + matches[m].score += score * mult valid.add(m) for m in index2.fluff_tokens[match]: if m not in valid: - monsterscore[m] += score * mult / 2 + matches[m].name.add(f"{t} - {match}") + matches[m].score += score * mult / 2 valid.add(m) return valid - def process_modifiers(self, mod_tokens, neg_mod_tokens, monsterscore, potential_evos, monster_mods): + def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches, monster_mods): for t in mod_tokens: potential_evos = {m for m in potential_evos if - self._monster_has_token(m, t, monsterscore, monster_mods[m])} + self._monster_has_token(m, t, matches, monster_mods[m])} if not potential_evos: return None for t in neg_mod_tokens: potential_evos = {m for m in potential_evos if - not self._monster_has_token(m, t, monsterscore, monster_mods[m])} + not self._monster_has_token(m, t, matches, monster_mods[m])} if not potential_evos: return None return potential_evos - def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, monster_score=None): - if monster_score is None: - monster_score = defaultdict(int) + def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, matches=None): + if matches is None: + monster_score = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] return max(monsters, - key=lambda m: (monster_score[m], + key=lambda m: (matches[m].score, not m.is_equip, # Match na on id overlap bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), @@ -219,13 +224,14 @@ def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, monst m.rarity, m.monster_no_na)) - def get_monster_evos(self, database, monster_gen, monster_score): + def get_monster_evos(self, database, monster_gen, matches): monster_evos = set() - for m in sorted(monster_gen, key=lambda m: monster_score[m], reverse=True): + for m in sorted(monster_gen, key=lambda m: matches[m].score, reverse=True): for evo in database.graph.get_alt_monsters(m): monster_evos.add(evo) - if monster_score[evo] < monster_score[m]: - monster_score[evo] = monster_score[m] - .003 + if matches[evo].score < matches[m].score: + matches[evo].name = {t + f" (from evo {m.monster_id})" for t in matches[m].name} + matches[evo].score = matches[m].score - .003 return monster_evos @@ -269,7 +275,7 @@ async def _findMonster(dgcog, query) -> Tuple[Optional["NamedMonster"], Optional await dgcog.wait_until_ready() try: return dgcog.index.find_monster(query) - except: + except Exception: prefix = (await dgcog.bot.get_valid_prefixes())[0] return (None, f"Sorry, id1 doesn't support this query and we are no longer" @@ -298,12 +304,22 @@ async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: return max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (0.0, None), - key=lambda t: t[0] - )[1] + if tokenized_query != mw_tokenized_query else (None, MonsterMatch()), + key=lambda t: t[1].get(t[0], MonsterMatch()).score + )[0] -async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["MonsterModel"]]: +class MonsterMatch: + def __init__(self, score=0, name=None, mod=None): + self.score = score + if name is None: + self.name = set() + if mod is None: + self.mod = set() + + +async def find_monster_search(tokenized_query, dgcog) -> \ + Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch]]: mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ find_monster.interpret_query(tokenized_query, dgcog.index2) @@ -313,32 +329,37 @@ async def find_monster_search(tokenized_query, dgcog) -> Tuple[int, Optional["Mo if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + matches = defaultdict(MonsterMatch) if name_query_tokens: - monster_gen, monster_score = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog.index2) + monster_gen = find_monster.process_name_tokens(name_query_tokens, + neg_name_tokens, + dgcog.index2, + matches) if not monster_gen: # No monsters match the given name tokens - return 0, None - monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, monster_score) + return None, {} + monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, matches) else: # There are no name tokens in the query monster_gen = {*dgcog.database.get_all_monsters()} monster_score = defaultdict(int) # Expand search to the evo tree - monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_score, monster_gen, + monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_gen, matches, dgcog.index2.modifiers) if not monster_gen: # no modifiers match any monster in the evo tree - return 0, None + return None, {} - # print({k: v for k, v in sorted(monster_score.items(), key=lambda kv: kv[1], reverse=True) if k in monster_gen}) + print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) + if k in monster_gen}) # Return most likely candidate based on query. - mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, monster_score) + mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, matches) - return monster_score[mon], mon + return mon, matches find_monster = FindMonster() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index cf2739cf..ed8878ec 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -19,14 +19,15 @@ from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate -from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor +from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor, rmdiacritics from padinfo.common.config import BotConfig -from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji +from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ + get_attribute_emoji_by_monster from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom, calc_ratio_name, calc_ratio_modifier, findMonsterCustom2 + findMonsterCustom, calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom2 from padinfo.core.historic_lookups import historic_lookups from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query @@ -732,12 +733,12 @@ async def idt_import(self, ctx, *, queries): @idt_name.command(name="import") async def idtn_import(self, ctx, *, queries): """Import name/fluff tests""" - await self.norf_import(ctx, id, queries) + await self.norf_import(ctx, queries) @idt_fluff.command(name="import") async def idtf_import(self, ctx, *, queries): """Import name/fluff tests""" - await self.norf_import(ctx, id, queries) + await self.norf_import(ctx, queries) async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ @@ -838,6 +839,7 @@ async def norf_setreason(self, ctx, number, reason): if number >= len(suite): await ctx.react_quietly("\N{CROSS MARK}") return + # noinspection PyTypeChecker sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[number]['reason'] = reason await ctx.tick() @@ -1376,7 +1378,7 @@ def f(m, s): o += f"\n\n{type}\n" + "\n".join(f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) return o - o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", 1) + o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) o += write_name_token(DGCOG.index2.manual, "[Manual Tokens]") o += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") o += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") @@ -1432,3 +1434,60 @@ def additmods(ms, om): await ctx.send(page) else: await ctx.send(f"There are no modifiers that match `{token}`.") + + @commands.command(aliases=["tracebackid", "tbid", "idtb"]) + async def idtraceback(self, ctx, *, query): + """Get the traceback of an id query""" + mid = None + if "/" in query: + query, mid = query.split("/", 1) + if not mid.strip().isdigit(): + await ctx.send("Monster id must be an int.") + return + mid = int(mid.strip()) + + dgcog = self.bot.get_cog("Dadguide") + await dgcog.wait_until_ready() + + query = rmdiacritics(query).strip().lower().replace(",", "") + tokenized_query = query.split() + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) + + bestmatch, matches = max( + await find_monster_search(tokenized_query, dgcog), + await find_monster_search(mw_tokenized_query, dgcog) + if tokenized_query != mw_tokenized_query else (None, {None: 0.0}, {None: set()}), + key=lambda t: t[1][t[0]].score if t[0] else 0 + ) + + if bestmatch is None: + await ctx.send("No monster matched.") + return + + if mid is not None: + selected = {m for m in matches if m.monster_id == mid} + if not selected: + await ctx.send("The requested monster was not found as a result of the query.") + return + monster = selected.pop() + else: + monster = bestmatch + + score = matches[monster].score + ntokens = matches[monster].name + mtokens = matches[monster].mod + lower_prio = {m for m in matches if matches[m].score == matches[monster].score}.difference({monster}) + if len(lower_prio) > 10: + lpstr = f"{len(lower_prio)} other monsters." + else: + lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) + + mtokenstr = '\n'.join(sorted(mtokens)) + ntokenstr = '\n'.join(sorted(ntokens)) + + await ctx.send(f"**Monster matched**: " + f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})\n" + f"**Total Score**: {score}\n\n" + f"**Matched Name Tokens**:\n{ntokenstr}\n\n" + f"**Matched Mod Tokens**:\n{mtokenstr}\n\n" + + (f"**Equally Scoring Matches**:\n{lpstr}" if lower_prio else "")) From 4b50c4462aa71a4368bc4c1e7a641c26017b2510 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 6 Feb 2021 20:48:50 -0700 Subject: [PATCH 0574/1220] Fix bug in id3 introduced in #751 (#758) See also #755, #757 --- padinfo/core/find_monster.py | 2 +- padinfo/padinfo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index c4ee9d0b..cc532cdf 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -304,7 +304,7 @@ async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: return max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, MonsterMatch()), + if tokenized_query != mw_tokenized_query else (None, {}), key=lambda t: t[1].get(t[0], MonsterMatch()).score )[0] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ed8878ec..ee888fec 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1456,7 +1456,7 @@ async def idtraceback(self, ctx, *, query): bestmatch, matches = max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, {None: 0.0}, {None: set()}), + if tokenized_query != mw_tokenized_query else (None, {}), key=lambda t: t[1][t[0]].score if t[0] else 0 ) From ebc3706c685270f21e3ad923e8d2c0e348fc476f Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 6 Feb 2021 21:06:59 -0700 Subject: [PATCH 0575/1220] Move idtest and its subcommands out of padinfo.py (#756) --- padinfo/idtest_mixin.py | 455 ++++++++++++++++++++++++++++++++++++++++ padinfo/padinfo.py | 442 +------------------------------------- 2 files changed, 457 insertions(+), 440 deletions(-) create mode 100644 padinfo/idtest_mixin.py diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py new file mode 100644 index 00000000..7cae29da --- /dev/null +++ b/padinfo/idtest_mixin.py @@ -0,0 +1,455 @@ +import random +import re +from datetime import datetime + +import tsutils +from redbot.core import commands, Config +from redbot.core.utils.chat_formatting import box, pagify + +from padinfo.core.find_monster import findMonster3 + + +class IdTest: + bot = None + get_dgcog = None + + def __init__(self): + self.config = Config.get_conf(self, identifier=-1) + + @commands.group() + # @checks.is_owner() + async def idtest(self, ctx): + """ID Test suite subcommands""" + + @idtest.command(name="add") + async def idt_add(self, ctx, id: int, *, query): + """Add a test for the id3 test suite (Append `| reason` to add a reason)""" + query, *reason = query.split("|") + query = query.strip() + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + + async with self.config.test_suite() as suite: + oldd = suite.get(query, {}) + if oldd.get('result') == id: + await ctx.send(f"This test case already exists with id `{sorted(suite).index(query)}`.") + return + suite[query] = { + 'result': id, + 'ts': datetime.now().timestamp(), + 'reason': reason[0].strip() if reason else '' + } + + if await tsutils.get_reaction(ctx, f"Added test case `{id} - {query}`" + f" with ref `{sorted(suite).index(query)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): + if oldd: + suite[query] = oldd + else: + del suite[query] + await ctx.react_quietly("\N{CROSS MARK}") + else: + await ctx.send(f"Successfully added test case `{id} - {query}` with ref `{sorted(suite).index(query)}`") + await ctx.tick() + + @idtest.group(name="name") + async def idt_name(self, ctx): + """Name subcommands""" + + @idtest.group(name="fluff") + async def idt_fluff(self, ctx): + """Fluff subcommands""" + + @idt_name.command(name="add") + async def idtn_add(self, ctx, id: int, token, *, reason=""): + """Add a name token test to the id3 test suite""" + await self.norf_add(ctx, id, token, reason, False) + + @idt_fluff.command(name="add") + async def idtf_add(self, ctx, id: int, token, *, reason=""): + """Add a fluff token test to the id3 test suite""" + await self.norf_add(ctx, id, token, reason, True) + + async def norf_add(self, ctx, id: int, token, reason, fluffy): + reason = reason.lstrip("| ") + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, + "Are you sure you want to add to the fluff/name test suite?"): + return + await self.config.user(ctx.author).lastaction.set('name') + + async with self.config.fluff_suite() as suite: + if any(t['id'] == id and t['token'] == token and t['fluff'] == fluffy for t in suite): + await ctx.send("This test already exists.") + return + + old = None + if any(t['id'] == id and t['token'] == token for t in suite): + old = [t for t in suite if t['id'] == id and t['token'] == token][0] + if not await tsutils.confirm_message(ctx, f"Are you sure you want to change" + f" the type of test case #{suite.index(old)}" + f" `{id} - {token}` from " + f" **{'fluff' if fluffy else 'name'}** to" + f" **{'name' if fluffy else 'fluff'}**?"): + await ctx.react_quietly("\N{CROSS MARK}") + return + suite.remove(old) + + case = { + 'id': id, + 'token': token, + 'fluff': fluffy, + 'reason': reason, + 'ts': datetime.now().timestamp() + } + + suite.append(case) + suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) + + if await tsutils.get_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " + f"case `{id} - {token}` with ref `{suite.index(case)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): + suite.pop() + if old: + suite.append(old) + await ctx.react_quietly("\N{CROSS MARK}") + else: + m = await ctx.send(f"Successfully added {'fluff' if fluffy else 'name'} " + f"case `{id} - {token}` with ref `{suite.index(case)}`") + await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") + + @idtest.command(name="import") + async def idt_import(self, ctx, *, queries): + """Import id3 tests""" + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + + cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) + async with self.config.test_suite() as suite: + for query, result, reason in cases: + suite[query] = {'result': int(result), 'reason': reason, 'ts': datetime.now().timestamp()} + await ctx.tick() + + @idt_name.command(name="import") + async def idtn_import(self, ctx, *, queries): + """Import name/fluff tests""" + await self.norf_import(ctx, queries) + + @idt_fluff.command(name="import") + async def idtf_import(self, ctx, *, queries): + """Import name/fluff tests""" + await self.norf_import(ctx, queries) + + async def norf_import(self, ctx, queries): + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + return + await self.config.user(ctx.author).lastaction.set('name') + + cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+)\s+(\w*) *(.*)', queries) + async with self.config.fluff_suite() as suite: + for query, result, fluff, reason in cases: + # print(query, result, fluff, reason) + if not any(c['id'] == int(result) and c['token'] == query for c in suite): + suite.append({ + 'id': int(result), + 'token': query, + 'fluff': fluff == 'fluff', + 'reason': reason, + 'ts': datetime.now().timestamp()}) + await ctx.tick() + + @idtest.command(name="remove", aliases=["delete", "rm"]) + async def idt_remove(self, ctx, *, item): + """Remove an id3 test""" + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + + async with self.config.test_suite() as suite: + if item in suite: + del suite[item] + elif item.isdigit() and int(item) < len(suite): + del suite[sorted(suite)[int(item)]] + else: + await ctx.react_quietly("\N{CROSS MARK}") + return + await ctx.tick() + + @idt_name.command(name="remove") + async def idtn_remove(self, ctx, *, item: int): + """Remove a name/fluff test""" + await self.norf_remove(ctx, item) + + @idt_fluff.command(name="remove") + async def idtf_remove(self, ctx, *, item: int): + """Remove a name/fluff test""" + await self.norf_remove(ctx, item) + + async def norf_remove(self, ctx, item): + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + return + await self.config.user(ctx.author).lastaction.set('name') + + async with self.config.fluff_suite() as suite: + if item >= len(suite): + await ctx.send("There are not that many items.") + return + suite.remove(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item]) + await ctx.tick() + + @idtest.command(name="setreason", aliases=["addreason"]) + async def idt_setreason(self, ctx, number: int, *, reason): + """Set a reason for an id3 test case""" + if reason == '""': + reason = "" + if await self.config.user(ctx.author).lastaction() != 'id3' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + return + await self.config.user(ctx.author).lastaction.set('id3') + + async with self.config.test_suite() as suite: + if number >= len(suite): + await ctx.react_quietly("\N{CROSS MARK}") + return + suite[sorted(suite)[number]]['reason'] = reason + await ctx.tick() + + @idt_name.command(name="setreason") + async def idtn_setreason(self, ctx, number: int, *, reason): + """Set a reason for an name/fluff test case""" + await self.norf_setreason(ctx, number, reason) + + @idt_fluff.command(name="setreason") + async def idtf_setreason(self, ctx, number: int, *, reason): + """Set a reason for an name/fluff test case""" + await self.norf_setreason(ctx, number, reason) + + async def norf_setreason(self, ctx, number, reason): + if reason == '""': + reason = "" + if await self.config.user(ctx.author).lastaction() != 'name' and \ + not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + return + await self.config.user(ctx.author).lastaction.set('name') + + async with self.config.fluff_suite() as suite: + if number >= len(suite): + await ctx.react_quietly("\N{CROSS MARK}") + return + # noinspection PyTypeChecker + sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[number]['reason'] = reason + await ctx.tick() + + @idtest.command(name="list") + async def idt_list(self, ctx): + """List id3 tests""" + await self.config.user(ctx.author).lastaction.set('id3') + + suite = await self.config.test_suite() + o = "" + ml = len(max(suite, key=len)) + for c, kv in enumerate(sorted(suite.items())): + o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + + @idt_name.command(name="list") + async def idtn_list(self, ctx, inclusive: bool = False): + """List name tests""" + await self.norf_list(ctx, False, inclusive) + + @idt_fluff.command(name="list") + async def idtf_list(self, ctx, inclusive: bool = False): + """List fluff tests""" + await self.norf_list(ctx, True, inclusive) + + async def norf_list(self, ctx, fluff, inclusive): + await self.config.user(ctx.author).lastaction.set('name') + + suite = await self.config.fluff_suite() + o = "" + for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + if inclusive or v['fluff'] == fluff: + o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ + f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + + @idtest.command(name="listnoreason") + async def idt_listnoreason(self, ctx): + """List id3 tests with no reasons""" + await self.config.user(ctx.author).lastaction.set('id3') + + suite = await self.config.test_suite() + o = "" + ml = len(max(suite, key=len)) + for c, kv in enumerate(sorted(suite.items())): + if not kv[1].get('reason'): + o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + + @idt_name.command(name="listnoreason") + async def idtn_listnoreason(self, ctx, inclusive: bool = False): + """List name tests with no reasons""" + await self.norf_listnoreason(ctx, False, inclusive) + + @idt_fluff.command(name="listnoreason") + async def idtf_lisnoreasont(self, ctx, inclusive: bool = False): + """List fluff tests with no reasons""" + await self.norf_listnoreason(ctx, True, inclusive) + + async def norf_listnoreason(self, ctx, fluff, inclusive): + await self.config.user(ctx.author).lastaction.set('name') + + suite = await self.config.fluff_suite() + o = "" + for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + if inclusive or v['fluff'] == fluff and not v['reason']: + o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ + f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + + @idtest.command(name="listrecent") + async def idt_listrecent(self, ctx, count: int = 0): + """List recent id3 tests""" + suite = await self.config.test_suite() + if count == 0: + count = len(suite) + o = "" + ml = len(max(suite, key=len)) + for c, kv in enumerate(sorted(suite.items(), key=lambda kv: kv[1].get('ts', 0), reverse=True)[:count]): + o += f"{kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + if not o: + await ctx.send("There are no test cases.") + for page in pagify(o): + await ctx.send(box(page)) + + @idtest.command(name="run", aliases=["test"]) + async def idt_run(self, ctx): + """Run all id3 tests""" + suite = await self.config.test_suite() + dgcog = await self.get_dgcog() + await self.config.user(ctx.author).lastaction.set('id3') + c = 0 + o = "" + ml = len(max(suite, key=len)) + 2 + rcircle = '\N{LARGE RED CIRCLE}' + async with ctx.typing(): + for q, r in suite.items(): + m = await findMonster3(dgcog, q) + mid = m and m.monster_id + if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: + reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' + q = '"' + q + '"' + o += f"{q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" + else: + c += 1 + if c != len(suite): + o += f"\n\nTests complete. {c}/{len(suite)} succeeded." + else: + o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded" + random.choice('.....!!!!!?') + for page in pagify(o): + await ctx.send(box(page)) + + @idt_name.command(name="run") + async def idtn_run(self, ctx): + """Run all name/fluff tests""" + await self.norf_run(ctx) + + @idt_fluff.command(name="run") + async def idtf_run(self, ctx): + """Run all name/fluff tests""" + await self.norf_run(ctx) + + async def norf_run(self, ctx): + """Run all name/fluff tests""" + suite = await self.config.fluff_suite() + await self.config.user(ctx.author).lastaction.set('name') + c = 0 + o = "" + rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' + async with ctx.typing(): + for v in suite: + fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] + + if (v['fluff'] and not fluff) or (not v['fluff'] and not name): + q = '"{}"'.format(v['token']) + o += f"{str(v['id']).ljust(4)} {q.ljust(10)} - " \ + f"{ycircle if name or fluff else rcircle} " \ + f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" + else: + c += 1 + if c != len(suite): + o += f"\n\nTests complete. {c}/{len(suite)} succeeded." + else: + o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded." + for page in pagify(o): + await ctx.send(box(page)) + + @idtest.command(name="runall") + async def idt_runall(self, ctx): + """Run all tests""" + rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' + dgcog = await self.get_dgcog() + qsuite = await self.config.test_suite() + qo = "" + qc = 0 + ml = len(max(qsuite, key=len)) + 2 + async with ctx.typing(): + for c, q in enumerate(sorted(qsuite)): + m = await findMonster3(dgcog, q) + mid = m and m.monster_id + if m is not None and m.monster_id != qsuite[q]['result'] or m is None and qsuite[q]['result'] >= 0: + reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' + q = '"' + q + '"' + qo += f"{str(c).rjust(4)}. {q.ljust(ml)} - {rcircle} Ex: {qsuite[q]['result']}, Ac: {mid}{reason}\n" + else: + qc += 1 + + fsuite = await self.config.fluff_suite() + fo = "" + fc = 0 + async with ctx.typing(): + for c, v in enumerate(fsuite): + fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] + + if (v['fluff'] and not fluff) or (not v['fluff'] and not name): + q = '"{}"'.format(v['token']) + fo += f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)} - " \ + f"{ycircle if name or fluff else rcircle} " \ + f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" + else: + fc += 1 + + o = "" + if fo: + o += "[Failed Token Tests]\n" + fo + if qo: + o += "\n[Failed Query Tests]\n" + qo + + if qc + fc != len(fsuite) + len(qsuite): + o += f"\n\nTests complete. {qc + fc}/{len(fsuite) + len(qsuite)} succeeded." + else: + o += "\n\n\N{LARGE GREEN CIRCLE} \N{LARGE GREEN CIRCLE} All tests succeeded" \ + + random.choice(['.'] * 5 + ['!!'] * 5 + ['???']) + for page in pagify(o): + await ctx.send(box(page)) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ee888fec..86c1b694 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -6,7 +6,6 @@ import re import urllib.parse from collections import OrderedDict -from datetime import datetime from io import BytesIO from typing import TYPE_CHECKING @@ -34,6 +33,7 @@ from padinfo.core.padinfo_settings import settings from padinfo.id_menu import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld +from padinfo.idtest_mixin import IdTest from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.evos import EvosViewState @@ -71,7 +71,7 @@ def _data_file(file_name: str) -> str: } -class PadInfo(commands.Cog): +class PadInfo(commands.Cog, IdTest): """Info for PAD Cards""" def __init__(self, bot, *args, **kwargs): @@ -612,444 +612,6 @@ async def leaderskillsingle(self, ctx, *, query): await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) - @commands.group() - # @checks.is_owner() - async def idtest(self, ctx): - """ID Test suite subcommands""" - - @idtest.command(name="add") - async def idt_add(self, ctx, id: int, *, query): - """Add a test for the id3 test suite (Append `| reason` to add a reason)""" - query, *reason = query.split("|") - query = query.strip() - if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): - return - await self.config.user(ctx.author).lastaction.set('id3') - - async with self.config.test_suite() as suite: - oldd = suite.get(query, {}) - if oldd.get('result') == id: - await ctx.send(f"This test case already exists with id `{sorted(suite).index(query)}`.") - return - suite[query] = { - 'result': id, - 'ts': datetime.now().timestamp(), - 'reason': reason[0].strip() if reason else '' - } - - if await tsutils.get_reaction(ctx, f"Added test case `{id} - {query}`" - f" with ref `{sorted(suite).index(query)}`", - "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): - if oldd: - suite[query] = oldd - else: - del suite[query] - await ctx.react_quietly("\N{CROSS MARK}") - else: - await ctx.send(f"Successfully added test case `{id} - {query}` with ref `{sorted(suite).index(query)}`") - await ctx.tick() - - @idtest.group(name="name") - async def idt_name(self, ctx): - """Name subcommands""" - - @idtest.group(name="fluff") - async def idt_fluff(self, ctx): - """Fluff subcommands""" - - @idt_name.command(name="add") - async def idtn_add(self, ctx, id: int, token, *, reason=""): - """Add a name token test to the id3 test suite""" - await self.norf_add(ctx, id, token, reason, False) - - @idt_fluff.command(name="add") - async def idtf_add(self, ctx, id: int, token, *, reason=""): - """Add a fluff token test to the id3 test suite""" - await self.norf_add(ctx, id, token, reason, True) - - async def norf_add(self, ctx, id: int, token, reason, fluffy): - reason = reason.lstrip("| ") - if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, - "Are you sure you want to add to the fluff/name test suite?"): - return - await self.config.user(ctx.author).lastaction.set('name') - - async with self.config.fluff_suite() as suite: - if any(t['id'] == id and t['token'] == token and t['fluff'] == fluffy for t in suite): - await ctx.send("This test already exists.") - return - - old = None - if any(t['id'] == id and t['token'] == token for t in suite): - old = [t for t in suite if t['id'] == id and t['token'] == token][0] - if not await tsutils.confirm_message(ctx, f"Are you sure you want to change" - f" the type of test case #{suite.index(old)}" - f" `{id} - {token}` from " - f" **{'fluff' if fluffy else 'name'}** to" - f" **{'name' if fluffy else 'fluff'}**?"): - await ctx.react_quietly("\N{CROSS MARK}") - return - suite.remove(old) - - case = { - 'id': id, - 'token': token, - 'fluff': fluffy, - 'reason': reason, - 'ts': datetime.now().timestamp() - } - - suite.append(case) - suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) - - if await tsutils.get_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " - f"case `{id} - {token}` with ref `{suite.index(case)}`", - "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): - suite.pop() - if old: - suite.append(old) - await ctx.react_quietly("\N{CROSS MARK}") - else: - m = await ctx.send(f"Successfully added {'fluff' if fluffy else 'name'} " - f"case `{id} - {token}` with ref `{suite.index(case)}`") - await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") - - @idtest.command(name="import") - async def idt_import(self, ctx, *, queries): - """Import id3 tests""" - if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): - return - await self.config.user(ctx.author).lastaction.set('id3') - - cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) - async with self.config.test_suite() as suite: - for query, result, reason in cases: - suite[query] = {'result': int(result), 'reason': reason, 'ts': datetime.now().timestamp()} - await ctx.tick() - - @idt_name.command(name="import") - async def idtn_import(self, ctx, *, queries): - """Import name/fluff tests""" - await self.norf_import(ctx, queries) - - @idt_fluff.command(name="import") - async def idtf_import(self, ctx, *, queries): - """Import name/fluff tests""" - await self.norf_import(ctx, queries) - - async def norf_import(self, ctx, queries): - if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): - return - await self.config.user(ctx.author).lastaction.set('name') - - cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+)\s+(\w*) *(.*)', queries) - async with self.config.fluff_suite() as suite: - for query, result, fluff, reason in cases: - # print(query, result, fluff, reason) - if not any(c['id'] == int(result) and c['token'] == query for c in suite): - suite.append({ - 'id': int(result), - 'token': query, - 'fluff': fluff == 'fluff', - 'reason': reason, - 'ts': datetime.now().timestamp()}) - await ctx.tick() - - @idtest.command(name="remove", aliases=["delete", "rm"]) - async def idt_remove(self, ctx, *, item): - """Remove an id3 test""" - if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): - return - await self.config.user(ctx.author).lastaction.set('id3') - - async with self.config.test_suite() as suite: - if item in suite: - del suite[item] - elif item.isdigit() and int(item) < len(suite): - del suite[sorted(suite)[int(item)]] - else: - await ctx.react_quietly("\N{CROSS MARK}") - return - await ctx.tick() - - @idt_name.command(name="remove") - async def idtn_remove(self, ctx, *, item: int): - """Remove a name/fluff test""" - await self.norf_remove(ctx, item) - - @idt_fluff.command(name="remove") - async def idtf_remove(self, ctx, *, item: int): - """Remove a name/fluff test""" - await self.norf_remove(ctx, item) - - async def norf_remove(self, ctx, item): - if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): - return - await self.config.user(ctx.author).lastaction.set('name') - - async with self.config.fluff_suite() as suite: - if item >= len(suite): - await ctx.send("There are not that many items.") - return - suite.remove(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item]) - await ctx.tick() - - @idtest.command(name="setreason", aliases=["addreason"]) - async def idt_setreason(self, ctx, number: int, *, reason): - """Set a reason for an id3 test case""" - if reason == '""': - reason = "" - if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): - return - await self.config.user(ctx.author).lastaction.set('id3') - - async with self.config.test_suite() as suite: - if number >= len(suite): - await ctx.react_quietly("\N{CROSS MARK}") - return - suite[sorted(suite)[number]]['reason'] = reason - await ctx.tick() - - @idt_name.command(name="setreason") - async def idtn_setreason(self, ctx, number: int, *, reason): - """Set a reason for an name/fluff test case""" - await self.norf_setreason(ctx, number, reason) - - @idt_fluff.command(name="setreason") - async def idtf_setreason(self, ctx, number: int, *, reason): - """Set a reason for an name/fluff test case""" - await self.norf_setreason(ctx, number, reason) - - async def norf_setreason(self, ctx, number, reason): - if reason == '""': - reason = "" - if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): - return - await self.config.user(ctx.author).lastaction.set('name') - - async with self.config.fluff_suite() as suite: - if number >= len(suite): - await ctx.react_quietly("\N{CROSS MARK}") - return - # noinspection PyTypeChecker - sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[number]['reason'] = reason - await ctx.tick() - - @idtest.command(name="list") - async def idt_list(self, ctx): - """List id3 tests""" - await self.config.user(ctx.author).lastaction.set('id3') - - suite = await self.config.test_suite() - o = "" - ml = len(max(suite, key=len)) - for c, kv in enumerate(sorted(suite.items())): - o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" - if not o: - await ctx.send("There are no test cases.") - for page in pagify(o): - await ctx.send(box(page)) - - @idt_name.command(name="list") - async def idtn_list(self, ctx, inclusive: bool = False): - """List name tests""" - await self.norf_list(ctx, False, inclusive) - - @idt_fluff.command(name="list") - async def idtf_list(self, ctx, inclusive: bool = False): - """List fluff tests""" - await self.norf_list(ctx, True, inclusive) - - async def norf_list(self, ctx, fluff, inclusive): - await self.config.user(ctx.author).lastaction.set('name') - - suite = await self.config.fluff_suite() - o = "" - for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - if inclusive or v['fluff'] == fluff: - o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ - f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" - if not o: - await ctx.send("There are no test cases.") - for page in pagify(o): - await ctx.send(box(page)) - - @idtest.command(name="listnoreason") - async def idt_listnoreason(self, ctx): - """List id3 tests with no reasons""" - await self.config.user(ctx.author).lastaction.set('id3') - - suite = await self.config.test_suite() - o = "" - ml = len(max(suite, key=len)) - for c, kv in enumerate(sorted(suite.items())): - if not kv[1].get('reason'): - o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" - if not o: - await ctx.send("There are no test cases.") - for page in pagify(o): - await ctx.send(box(page)) - - @idt_name.command(name="listnoreason") - async def idtn_listnoreason(self, ctx, inclusive: bool = False): - """List name tests with no reasons""" - await self.norf_listnoreason(ctx, False, inclusive) - - @idt_fluff.command(name="listnoreason") - async def idtf_lisnoreasont(self, ctx, inclusive: bool = False): - """List fluff tests with no reasons""" - await self.norf_listnoreason(ctx, True, inclusive) - - async def norf_listnoreason(self, ctx, fluff, inclusive): - await self.config.user(ctx.author).lastaction.set('name') - - suite = await self.config.fluff_suite() - o = "" - for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - if inclusive or v['fluff'] == fluff and not v['reason']: - o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ - f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" - if not o: - await ctx.send("There are no test cases.") - for page in pagify(o): - await ctx.send(box(page)) - - @idtest.command(name="listrecent") - async def idt_listrecent(self, ctx, count: int = 0): - """List recent id3 tests""" - suite = await self.config.test_suite() - if count == 0: - count = len(suite) - o = "" - ml = len(max(suite, key=len)) - for c, kv in enumerate(sorted(suite.items(), key=lambda kv: kv[1].get('ts', 0), reverse=True)[:count]): - o += f"{kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" - if not o: - await ctx.send("There are no test cases.") - for page in pagify(o): - await ctx.send(box(page)) - - @idtest.command(name="run", aliases=["test"]) - async def idt_run(self, ctx): - """Run all id3 tests""" - suite = await self.config.test_suite() - dgcog = await self.get_dgcog() - await self.config.user(ctx.author).lastaction.set('id3') - c = 0 - o = "" - ml = len(max(suite, key=len)) + 2 - rcircle = '\N{LARGE RED CIRCLE}' - async with ctx.typing(): - for q, r in suite.items(): - m = await findMonster3(dgcog, q) - mid = m and m.monster_id - if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: - reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' - q = '"' + q + '"' - o += f"{q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" - else: - c += 1 - if c != len(suite): - o += f"\n\nTests complete. {c}/{len(suite)} succeeded." - else: - o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded" + random.choice('.....!!!!!?') - for page in pagify(o): - await ctx.send(box(page)) - - @idt_name.command(name="run") - async def idtn_run(self, ctx): - """Run all name/fluff tests""" - await self.norf_run(ctx) - - @idt_fluff.command(name="run") - async def idtf_run(self, ctx): - """Run all name/fluff tests""" - await self.norf_run(ctx) - - async def norf_run(self, ctx): - """Run all name/fluff tests""" - suite = await self.config.fluff_suite() - await self.config.user(ctx.author).lastaction.set('name') - c = 0 - o = "" - rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' - async with ctx.typing(): - for v in suite: - fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] - - if (v['fluff'] and not fluff) or (not v['fluff'] and not name): - q = '"{}"'.format(v['token']) - o += f"{str(v['id']).ljust(4)} {q.ljust(10)} - " \ - f"{ycircle if name or fluff else rcircle} " \ - f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" - else: - c += 1 - if c != len(suite): - o += f"\n\nTests complete. {c}/{len(suite)} succeeded." - else: - o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded." - for page in pagify(o): - await ctx.send(box(page)) - - @idtest.command(name="runall") - async def idt_runall(self, ctx): - """Run all tests""" - rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' - dgcog = await self.get_dgcog() - qsuite = await self.config.test_suite() - qo = "" - qc = 0 - ml = len(max(qsuite, key=len)) + 2 - async with ctx.typing(): - for c, q in enumerate(sorted(qsuite)): - m = await findMonster3(dgcog, q) - mid = m and m.monster_id - if m is not None and m.monster_id != qsuite[q]['result'] or m is None and qsuite[q]['result'] >= 0: - reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' - q = '"' + q + '"' - qo += f"{str(c).rjust(4)}. {q.ljust(ml)} - {rcircle} Ex: {qsuite[q]['result']}, Ac: {mid}{reason}\n" - else: - qc += 1 - - fsuite = await self.config.fluff_suite() - fo = "" - fc = 0 - async with ctx.typing(): - for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] - - if (v['fluff'] and not fluff) or (not v['fluff'] and not name): - q = '"{}"'.format(v['token']) - fo += f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)} - " \ - f"{ycircle if name or fluff else rcircle} " \ - f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" - else: - fc += 1 - - o = "" - if fo: - o += "[Failed Token Tests]\n" + fo - if qo: - o += "\n[Failed Query Tests]\n" + qo - - if qc + fc != len(fsuite) + len(qsuite): - o += f"\n\nTests complete. {qc + fc}/{len(fsuite) + len(qsuite)} succeeded." - else: - o += "\n\n\N{LARGE GREEN CIRCLE} \N{LARGE GREEN CIRCLE} All tests succeeded" \ - + random.choice(['.'] * 5 + ['!!'] * 5 + ['???']) - for page in pagify(o): - await ctx.send(box(page)) - @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" From a6909b92e94f60f601c528b864103d2bbf361c5c Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 6 Feb 2021 21:12:33 -0700 Subject: [PATCH 0576/1220] Perm gate idtest & print ref for failing tests (#760) --- padinfo/idtest_mixin.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index 7cae29da..9b7435a2 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -3,7 +3,7 @@ from datetime import datetime import tsutils -from redbot.core import commands, Config +from redbot.core import commands, Config, checks from redbot.core.utils.chat_formatting import box, pagify from padinfo.core.find_monster import findMonster3 @@ -17,11 +17,11 @@ def __init__(self): self.config = Config.get_conf(self, identifier=-1) @commands.group() - # @checks.is_owner() async def idtest(self, ctx): """ID Test suite subcommands""" @idtest.command(name="add") + @checks.is_owner() async def idt_add(self, ctx, id: int, *, query): """Add a test for the id3 test suite (Append `| reason` to add a reason)""" query, *reason = query.split("|") @@ -63,11 +63,13 @@ async def idt_fluff(self, ctx): """Fluff subcommands""" @idt_name.command(name="add") + @checks.is_owner() async def idtn_add(self, ctx, id: int, token, *, reason=""): """Add a name token test to the id3 test suite""" await self.norf_add(ctx, id, token, reason, False) @idt_fluff.command(name="add") + @checks.is_owner() async def idtf_add(self, ctx, id: int, token, *, reason=""): """Add a fluff token test to the id3 test suite""" await self.norf_add(ctx, id, token, reason, True) @@ -121,6 +123,7 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") @idtest.command(name="import") + @checks.is_owner() async def idt_import(self, ctx, *, queries): """Import id3 tests""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ @@ -135,11 +138,13 @@ async def idt_import(self, ctx, *, queries): await ctx.tick() @idt_name.command(name="import") + @checks.is_owner() async def idtn_import(self, ctx, *, queries): """Import name/fluff tests""" await self.norf_import(ctx, queries) @idt_fluff.command(name="import") + @checks.is_owner() async def idtf_import(self, ctx, *, queries): """Import name/fluff tests""" await self.norf_import(ctx, queries) @@ -164,6 +169,7 @@ async def norf_import(self, ctx, queries): await ctx.tick() @idtest.command(name="remove", aliases=["delete", "rm"]) + @checks.is_owner() async def idt_remove(self, ctx, *, item): """Remove an id3 test""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ @@ -182,11 +188,13 @@ async def idt_remove(self, ctx, *, item): await ctx.tick() @idt_name.command(name="remove") + @checks.is_owner() async def idtn_remove(self, ctx, *, item: int): """Remove a name/fluff test""" await self.norf_remove(ctx, item) @idt_fluff.command(name="remove") + @checks.is_owner() async def idtf_remove(self, ctx, *, item: int): """Remove a name/fluff test""" await self.norf_remove(ctx, item) @@ -205,6 +213,7 @@ async def norf_remove(self, ctx, item): await ctx.tick() @idtest.command(name="setreason", aliases=["addreason"]) + @checks.is_owner() async def idt_setreason(self, ctx, number: int, *, reason): """Set a reason for an id3 test case""" if reason == '""': @@ -222,11 +231,13 @@ async def idt_setreason(self, ctx, number: int, *, reason): await ctx.tick() @idt_name.command(name="setreason") + @checks.is_owner() async def idtn_setreason(self, ctx, number: int, *, reason): """Set a reason for an name/fluff test case""" await self.norf_setreason(ctx, number, reason) @idt_fluff.command(name="setreason") + @checks.is_owner() async def idtf_setreason(self, ctx, number: int, *, reason): """Set a reason for an name/fluff test case""" await self.norf_setreason(ctx, number, reason) @@ -352,13 +363,14 @@ async def idt_run(self, ctx): ml = len(max(suite, key=len)) + 2 rcircle = '\N{LARGE RED CIRCLE}' async with ctx.typing(): - for q, r in suite.items(): + for c, qr in enumerate(sorted(suite.items())): + q, r = qr m = await findMonster3(dgcog, q) mid = m and m.monster_id if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" + o += f"{c}. {q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" else: c += 1 if c != len(suite): @@ -386,13 +398,13 @@ async def norf_run(self, ctx): o = "" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' async with ctx.typing(): - for v in suite: + for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) - o += f"{str(v['id']).ljust(4)} {q.ljust(10)} - " \ + o += f"{c}. {str(v['id']).ljust(4)} {q.ljust(10)} - " \ f"{ycircle if name or fluff else rcircle} " \ f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: From acb2e7d3513d5def39fd7d57e75ff2e8b833edf8 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 6 Feb 2021 21:26:23 -0700 Subject: [PATCH 0577/1220] Fix idtest handling of failing tests (#762) --- padinfo/idtest_mixin.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index 9b7435a2..6bdd6e0b 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -363,14 +363,14 @@ async def idt_run(self, ctx): ml = len(max(suite, key=len)) + 2 rcircle = '\N{LARGE RED CIRCLE}' async with ctx.typing(): - for c, qr in enumerate(sorted(suite.items())): + for i, qr in enumerate(sorted(suite.items())): q, r = qr m = await findMonster3(dgcog, q) mid = m and m.monster_id if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{c}. {q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" + o += f"{i}. {q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" else: c += 1 if c != len(suite): @@ -398,13 +398,13 @@ async def norf_run(self, ctx): o = "" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' async with ctx.typing(): - for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + for i, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) - o += f"{c}. {str(v['id']).ljust(4)} {q.ljust(10)} - " \ + o += f"{i}. {str(v['id']).ljust(4)} {q.ljust(10)} - " \ f"{ycircle if name or fluff else rcircle} " \ f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: @@ -431,8 +431,9 @@ async def idt_runall(self, ctx): mid = m and m.monster_id if m is not None and m.monster_id != qsuite[q]['result'] or m is None and qsuite[q]['result'] >= 0: reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' - q = '"' + q + '"' - qo += f"{str(c).rjust(4)}. {q.ljust(ml)} - {rcircle} Ex: {qsuite[q]['result']}, Ac: {mid}{reason}\n" + qq = '"' + q + '"' + qo += (f"{str(c).rjust(4)}. {qq.ljust(ml)} - {rcircle} " + f"Ex: {qsuite[q]['result']}, Ac: {mid}{reason}\n") else: qc += 1 From cb6cb97ae3c598da181730e69c3f7f22364ea403 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 7 Feb 2021 19:33:24 -0500 Subject: [PATCH 0578/1220] Create transforminfo command (#754) * Add rudimentary transforminfo command (no features) * Update transforminfo stuff to keep up with padinfo refactor * Add emojis to transforminfo to show overview of base and transformed cards * Add all but misc. info to transforminfo * Have transforminfo use the transformbase modifier * Refactor transforminfo a lot to keep up with changes and go stateless * Make a bunch of minor changes so transforminfo actually loads * Fix bug where transforminfo required id3 beta * Stop using the transformbase prefix in transforminfo * Update transforminfo query * Separate IdView's awakenings_row method out and remove duplication in TransformInfoView * Get transforminfo reactions working with view state * Remember that this information is neither fresh nor cool * Clear monster id in tf menu and remove an unnecessary import * Make a bunch of naming changes in transforminfo code * Have transforminfo print different errors for no match and no transformation * Tweak transforminfo error handling a little more * Check if friend cog is loaded in transforminfo * Clean up code related to transforminfo Do some pep8 formatting and remove some unused variables * Move misc. info retrieval out of core transforminfo query * Make some more name changes in transforminfo code --- padinfo/core/transforminfo.py | 23 ++++++++ padinfo/padinfo.py | 41 +++++++++++++- padinfo/tf_menu.py | 88 +++++++++++++++++++++++++++++ padinfo/view/id.py | 28 +++++---- padinfo/view/transforminfo.py | 85 ++++++++++++++++++++++++++++ padinfo/view_state/transforminfo.py | 50 ++++++++++++++++ 6 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 padinfo/core/transforminfo.py create mode 100644 padinfo/tf_menu.py create mode 100644 padinfo/view/transforminfo.py create mode 100644 padinfo/view_state/transforminfo.py diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py new file mode 100644 index 00000000..96003b07 --- /dev/null +++ b/padinfo/core/transforminfo.py @@ -0,0 +1,23 @@ +from padinfo.core.find_monster import findMonsterCustom2 + + +async def perform_transforminfo_query(dgcog, raw_query, beta_id3): + db_context = dgcog.database + mgraph = dgcog.database.graph + found_monster, err, debug_info = await findMonsterCustom2(dgcog, beta_id3, raw_query) + + if not found_monster: + return found_monster, err, debug_info, None + + altversions = mgraph.process_alt_versions(found_monster.monster_id) + for mon_id in sorted(altversions): + if mgraph.monster_is_transform_base_by_id(mon_id): + transformed_mon = dgcog.get_monster(mgraph.get_next_transform_id_by_monster_id(mon_id)) + if transformed_mon: + base_mon = dgcog.get_monster(mon_id) + break + + if not transformed_mon: + return found_monster, err, debug_info, transformed_mon + + return base_mon, err, debug_info, transformed_mon diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 86c1b694..dbebe3a7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -30,11 +30,13 @@ from padinfo.core.historic_lookups import historic_lookups from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query +from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.core.padinfo_settings import settings from padinfo.id_menu import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.idtest_mixin import IdTest from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names +from padinfo.tf_menu import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState @@ -43,6 +45,7 @@ from padinfo.view_state.otherinfo import OtherInfoViewState from padinfo.view_state.pantheon import PantheonViewState from padinfo.view_state.pic import PicViewState +from padinfo.view_state.transforminfo import TransformInfoViewState if TYPE_CHECKING: pass @@ -153,7 +156,8 @@ async def test_reaction_add(self, reaction, member): emoji_clicked = emoji_obj.name if not (emoji_clicked in ls_menu_emoji_button_names or - emoji_clicked in IdMenuPanes.emoji_names()): + emoji_clicked in IdMenuPanes.emoji_names() or + emoji_clicked in tf_menu_emoji_button_names): return message = reaction.message @@ -166,6 +170,7 @@ async def test_reaction_add(self, reaction, member): menu_map = { LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu.menu, IdMenu.MENU_TYPE: IdMenu.menu, + TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu } menu_func = menu_map.get(menu_type) @@ -612,6 +617,40 @@ async def leaderskillsingle(self, ctx, *, query): await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) + @commands.command(aliases=['tfinfo', 'xforminfo']) + @checks.bot_has_permissions(embed_links=True) + async def transforminfo(self, ctx, *, query): + """Show info about a transform card, including some helpful details about the base card.""" + beta_id3 = await self.config.user(ctx.author).beta_id3() + dgcog = await self.get_dgcog() + base_mon, err, debug_info, transformed_mon = \ + await perform_transforminfo_query(dgcog, query, beta_id3) + + if not base_mon: + await self.makeFailureMsg(ctx, query, err) + return + + if not transformed_mon: + await ctx.send('Your query `{}` found [{}] {}, '.format(query, base_mon.monster_id, + base_mon.name_en) + 'which has no evos that transform.') + return + + if err: + await ctx.send(err) + return + + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + acquire_raw, base_rarity, true_evo_type_raw = \ + await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, + color, base_mon, transformed_mon, base_rarity, acquire_raw, + true_evo_type_raw) + menu = TransformInfoMenu.menu(original_author_id, friends, self.bot.user.id) + await menu.create(ctx, state) + @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" diff --git a/padinfo/tf_menu.py b/padinfo/tf_menu.py new file mode 100644 index 00000000..19b85cbc --- /dev/null +++ b/padinfo/tf_menu.py @@ -0,0 +1,88 @@ +from typing import TYPE_CHECKING, Optional + +from discord import Message +from discordmenu.embed.emoji import EmbedMenuEmojiConfig +from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.emoji.emoji_cache import emoji_cache +from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ + MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter + +from padinfo.view.id import IdView +from padinfo.view.transforminfo import TransformInfoView +from padinfo.view_state.id import IdViewState +from padinfo.view_state.transforminfo import TransformInfoViewState + +if TYPE_CHECKING: + pass + +emoji_button_names = ['\N{HOUSE BUILDING}', '\N{DOWNWARDS BLACK ARROW}', '\N{UPWARDS BLACK ARROW}', + '\N{CROSS MARK}'] +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') + +class TransformInfoMenu: + INITIAL_EMOJI = emoji_button_names[0] + MENU_TYPE = 'TransformInfo' + + @staticmethod + def menu(original_author_id, friend_ids, bot_id): + transitions = { + TransformInfoMenu.INITIAL_EMOJI: TransformInfoMenu.respond_with_overview, + emoji_button_names[1]: TransformInfoMenu.respond_with_base, + emoji_button_names[2]: TransformInfoMenu.respond_with_transform, + } + + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(bot_id), + MessageOwnerReactionFilter(original_author_id, + FriendReactionFilter(original_author_id, friend_ids)) + ] + + return EmbedMenu(reaction_filters, transitions, TransformInfoMenu.tf_control, + menu_emoji_config) + + @staticmethod + async def respond_with_base(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + ims['query'] = str(ims['b_resolved_monster_id']) + ims['resolved_monster_id'] = None + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = TransformInfoMenu.id_control(id_view_state) + return id_control + + @staticmethod + async def respond_with_transform(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + ims['query'] = str(ims['t_resolved_monster_id']) + ims['resolved_monster_id'] = None + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = TransformInfoMenu.id_control(id_view_state) + return id_control + + @staticmethod + async def respond_with_overview(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + tf_view_state = await TransformInfoViewState.deserialize(dgcog, user_config, ims) + tf_control = TransformInfoMenu.tf_control(tf_view_state) + return tf_control + + @staticmethod + def tf_control(state: TransformInfoViewState): + return EmbedControl( + [TransformInfoView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) + + @staticmethod + def id_control(state: IdViewState): + return EmbedControl( + [IdView.embed(state)], + [emoji_cache.get_by_name(e) for e in emoji_button_names] + ) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index a38b813a..a43a1af8 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -45,20 +45,28 @@ def _monster_is_enhance(m: "MonsterModel"): class IdView: @staticmethod - def awakenings_row(m: "MonsterModel"): - if len(m.awakenings) == 0: - return Box(Text('No Awakenings')) - + def normal_awakenings_row(m: "MonsterModel"): normal_awakenings = len(m.awakenings) - m.superawakening_count normal_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[:normal_awakenings]] + return Box(*[Text(e) for e in normal_awakenings_emojis], delimiter=' ') + + @staticmethod + def super_awakenings_row(m: "MonsterModel"): + normal_awakenings = len(m.awakenings) - m.superawakening_count super_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[normal_awakenings:]] + return Box( + Text(get_emoji('sa_questionmark')), + *[Text(e) for e in super_awakenings_emojis], + delimiter=' ') if len(super_awakenings_emojis) > 0 else None + + @staticmethod + def all_awakenings_row(m: "MonsterModel"): + if len(m.awakenings) == 0: + return Box(Text('No Awakenings')) return Box( - Box(*[Text(e) for e in normal_awakenings_emojis], delimiter=' '), - Box( - Text(get_emoji('sa_questionmark')), - *[Text(e) for e in super_awakenings_emojis], - delimiter=' ') if len(super_awakenings_emojis) > 0 else None + IdView.normal_awakenings_row(m), + IdView.super_awakenings_row(m) ) @staticmethod @@ -136,7 +144,7 @@ def embed(state: IdViewState): EmbedField( '/'.join(['{}'.format(t.name) for t in m.types]), Box( - IdView.awakenings_row(m), + IdView.all_awakenings_row(m), IdView.killers_row(m, state.transform_base) ) ), diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py new file mode 100644 index 00000000..a0f2e5aa --- /dev/null +++ b/padinfo/view/transforminfo.py @@ -0,0 +1,85 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView +from discordmenu.embed.text import Text, BoldText + +from padinfo.common.emoji_map import get_awakening_emoji, get_emoji +from padinfo.common.external_links import puzzledragonx +from padinfo.view.components.base import pad_info_footer_with_state +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage +from padinfo.view.id import IdView +from padinfo.view_state.id import IdViewState +from padinfo.view_state.transforminfo import TransformInfoViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +def base_info(m: "MonsterModel"): + return Box( + Box( + '\N{DOWN-POINTING RED TRIANGLE}', + IdView.normal_awakenings_row(m) if len(m.awakenings) != 0 + else Box(Text('No Awakenings')), + delimiter=' ' + ), + IdView.super_awakenings_row(m), + # the transform base of the base is the same + IdView.killers_row(m, m) + ) + +def base_skill(m: "MonsterModel"): + active_skill = m.active_skill + return (" (Base: {} -> {})".format(active_skill.turn_max, active_skill.turn_min) if active_skill + else 'None') + + +class TransformInfoView: + @staticmethod + def embed(state: TransformInfoViewState): + base_mon = state.base_mon + transformed_mon = state.transformed_mon + + fields = [ + EmbedField( + '/'.join(['{}'.format(t.name) for t in transformed_mon.types]), + Box( + IdView.normal_awakenings_row(transformed_mon) + if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), + base_info(base_mon) + ), + ), + EmbedField( + 'Card info', + IdView.misc_info(transformed_mon, state.true_evo_type_raw, state.acquire_raw, + state.base_rarity), + inline=True + ), + EmbedField( + IdView.stats_header(transformed_mon).to_markdown(), + IdView.stats(transformed_mon), + inline=True + ), + EmbedField( + IdView.active_skill_header(transformed_mon).to_markdown() + base_skill(base_mon), + Text(transformed_mon.active_skill.desc if transformed_mon.active_skill else 'None') + ), + EmbedField( + IdView.leader_skill_header(transformed_mon).to_markdown(), + Text(transformed_mon.leader_skill.desc if transformed_mon.leader_skill else 'None') + ) + ] + + return EmbedView( + EmbedMain( + color=state.color, + title=MonsterHeader.long_v2(transformed_mon).to_markdown(), + url=puzzledragonx(transformed_mon) + ), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon)), + embed_footer=pad_info_footer_with_state(state), + embed_fields=fields + ) diff --git a/padinfo/view_state/transforminfo.py b/padinfo/view_state/transforminfo.py new file mode 100644 index 00000000..a15a1302 --- /dev/null +++ b/padinfo/view_state/transforminfo.py @@ -0,0 +1,50 @@ +from padinfo.common.config import UserConfig +from padinfo.core.id import get_monster_misc_info +from padinfo.core.transforminfo import perform_transforminfo_query +from padinfo.view_state.base import ViewState + + +class TransformInfoViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, + base_rarity, acquire_raw, true_evo_type_raw): + super().__init__(original_author_id, menu_type, raw_query, extra_state=None) + self.color = color + self.base_mon = base_mon + self.transformed_mon = transformed_mon + self.base_rarity = base_rarity + self.acquire_raw = acquire_raw + self.true_evo_type_raw = true_evo_type_raw + + def serialize(self): + ret = super().serialize() + ret.update({ + 'b_resolved_monster_id': self.base_mon.monster_id, + 't_resolved_monster_id': self.transformed_mon.monster_id + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + base_mon_id = ims['b_resolved_monster_id'] + transformed_mon_id = ims['t_resolved_monster_id'] + + base_mon = dgcog.get_monster(base_mon_id) + transformed_mon = dgcog.get_monster(transformed_mon_id) + + acquire_raw, base_rarity, true_evo_type_raw = \ + await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + + return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, + base_mon, transformed_mon, base_rarity, acquire_raw, + true_evo_type_raw) + + @staticmethod + async def query(dgcog, base_mon, transformed_mon): + db_context = dgcog.database + acquire_raw, _, base_rarity, _, true_evo_type_raw = \ + await get_monster_misc_info(db_context, transformed_mon) + + return acquire_raw, base_rarity, true_evo_type_raw From 98fa65d664cd5fa3aa1a76cab33429d4241bbd4d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 8 Feb 2021 00:51:30 -0600 Subject: [PATCH 0579/1220] Add full names of killer awakening modifiers (#768) --- dadguide/token_mappings.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 8eade221..c3c26797 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -185,18 +185,18 @@ class Awakenings(Enum): Awakenings.SKILLBINDRES: ('sbr',), Awakenings.ENHANCEDHEAL: ('htpa', 'oeh'), Awakenings.MULTIBOOST: ('multi', 'mb'), - Awakenings.DRAGONKILLER: ('dk', 'drk', 'killer'), - Awakenings.GODKILLER: ('gk', 'gok', 'killer'), - Awakenings.DEVILKILLER: ('vk', 'dek', 'killer'), - Awakenings.MACHINEKILLER: ('mk', 'mak', 'killer'), - Awakenings.BALANCEDKILLER: ('bk', 'bak', 'killer'), - Awakenings.ATTACKERKILLER: ('ak', 'aak', 'killer'), - Awakenings.PHYSICALKILLER: ('pk', 'phk', 'killer'), - Awakenings.HEALERKILLER: ('hk', 'hek', 'killer'), - Awakenings.EVOMATKILLER: ('evok', 'a2killer'), - Awakenings.AWOKENKILLER: ('awok', 'a2killer'), - Awakenings.FODDERKILLER: ('enhk', 'a2killer'), - Awakenings.REDEEMKILLER: ('vendork', 'a2killer'), + Awakenings.DRAGONKILLER: ('dragonkiller', 'dk', 'drk', 'killer'), + Awakenings.GODKILLER: ('godkiller', 'gk', 'gok', 'killer'), + Awakenings.DEVILKILLER: ('devilkiller', 'vk', 'dek', 'killer'), + Awakenings.MACHINEKILLER: ('machinekiller', 'mk', 'mak', 'killer'), + Awakenings.BALANCEDKILLER: ('balancedkiller', 'bk', 'bak', 'killer'), + Awakenings.ATTACKERKILLER: ('attackerkiller', 'ak', 'aak', 'killer'), + Awakenings.PHYSICALKILLER: ('physicalkiller', 'pk', 'phk', 'killer'), + Awakenings.HEALERKILLER: ('healerkiller', 'hk', 'hek', 'killer'), + Awakenings.EVOMATKILLER: ('evokiller', 'evok', 'a2killer'), + Awakenings.AWOKENKILLER: ('awokenkiller', 'awok', 'a2killer'), + Awakenings.FODDERKILLER: ('enhancekiller', 'enhk', 'a2killer'), + Awakenings.REDEEMKILLER: ('vendorkiller', 'vendork', 'a2killer'), Awakenings.ENHCOMBO7C: ('7c',), Awakenings.GUARDBREAK: ('gb',), Awakenings.FUA: ('fua',), From 3fd760463b6432de192ce87b37e2be5c0aef68a1 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 01:13:42 -0700 Subject: [PATCH 0580/1220] Add warning when user uses id1 syntax in an id3 query (#770) --- dadguide/token_mappings.py | 4 ++++ padinfo/padinfo.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index c3c26797..5bcf8baf 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -307,3 +307,7 @@ class MiscModifiers(Enum): HAZARDOUS_IN_NAME_PREFIXES = { "reincarnated" } + +ID1_SUPPORTED = {'hw', 'h', 'x', 'ny', 'gh', 'v', 'np', 'ma', 'a', 'r', 'rr', 'rg', 'rb', 'rl', 'rd', 'rx', 'b', 'br', + 'bg', 'bb', 'bl', 'bd', 'bx', 'g', 'gr', 'gg', 'gb', 'gl', 'gd', 'gx', 'l', 'lr', 'lg', 'lb', 'll', + 'ld', 'lx', 'd', 'dr', 'dg', 'db', 'dl', 'dd', 'dx', 'x', 'xr', 'xg', 'xb', 'xl', 'xd', 'xx'} diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index dbebe3a7..a8979774 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -245,6 +245,27 @@ async def _do_id(self, ctx, query: str, force_id3_pref=None): original_author_id = ctx.message.author.id friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + + goodquery = None + if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.index2.all_name_tokens: + goodquery = [query[0], query[1:]] + elif query[:1] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.index2.all_name_tokens: + goodquery = [query[:1], query[2:]] + + if goodquery: + bad = False + for m in dgcog.index2.all_name_tokens[goodquery[1]]: + for p in dgcog.index2.modifiers[m]: + if p == 'xm' and goodquery[0] == 'x': + goodquery[0] = 'xm' + if p == goodquery[0]: + bad = True + if bad and query not in dgcog.index2.all_name_tokens: + await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" + f" Try using `{' '.join(goodquery)}` (with a space) instead! For more" + f" info about `id3` check out" + f" !") + monster, err, debug_info = await findMonsterCustom2(dgcog, beta_id3, raw_query) From 068b5c18a80e60147eee2a15566a8659b6b87920 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 02:16:32 -0700 Subject: [PATCH 0581/1220] Fix indexes (#772) --- padinfo/padinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a8979774..ea3ce1ed 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -249,8 +249,8 @@ async def _do_id(self, ctx, query: str, force_id3_pref=None): goodquery = None if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.index2.all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:1] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.index2.all_name_tokens: - goodquery = [query[:1], query[2:]] + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.index2.all_name_tokens: + goodquery = [query[:2], query[2:]] if goodquery: bad = False From 1eb463cb5b94051c1a6054b589ceee35d623338c Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 02:21:31 -0700 Subject: [PATCH 0582/1220] Support adding modifiers via sheet (#771) * Add modifiers via sheet * Fix typo --- dadguide/monster_index.py | 61 ++++++++++++++++++++++++++++++------ dadguide/token_mappings.py | 6 ++-- padinfo/core/find_monster.py | 2 +- padinfo/padinfo.py | 10 +++--- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 53c7d91d..11672585 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -16,6 +16,9 @@ GROUP_TREENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') NAME_TOKEN_ALIAS_SHEET = SHEETS_PATTERN.format('1229125459') +MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('2089525837') +TREE_MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('1372419168') + class MonsterIndex2(aobject): @@ -39,11 +42,13 @@ async def __ainit__(self, monsters, db): self.replacement_tokens = defaultdict(set) self.treename_overrides = set() - nickname_data, treenames_data, pantheon_data, nt_alias_data = await asyncio.gather( + nickname_data, treenames_data, pantheon_data, nt_alias_data, mod_data, treemod_data = await asyncio.gather( sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 5), sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 5), sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, 2), - sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2) + sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2), + sheet_to_reader(MODIFIER_OVERRIDE_SHEET, 2), + sheet_to_reader(TREE_MODIFIER_OVERRIDE_SHEET, 2), ) for m_id, name, lp, ov, i in nickname_data: @@ -84,10 +89,39 @@ async def __ainit__(self, monsters, db): for token, alias in nt_alias_data: self.replacement_tokens[token].add(alias) + self.manual_prefixes = defaultdict(set) + for mid, mods in mod_data: + if mid.isdigit(): + mid = int(mid) + for mod in mods.split(","): + mod = mod.strip() + if " " in mod: + self.multi_word_tokens.add(tuple(mod.lower().split(" "))) + mod = mod.lower().replace(" ", "") + self.manual_prefixes[mid].update(get_modifier_aliases(mod)) + + for mid, mods in treemod_data: + if mid.isdigit(): + mid = int(mid) + for mod in mods.split(","): + mod = mod.strip().lower() + if " " in mod: + self.multi_word_tokens.add(tuple(mod.split(" "))) + mod = mod.replace(" ", "") + aliases = get_modifier_aliases(mod) + for emid in self.graph.get_alt_ids_by_id(mid): + self.manual_prefixes[emid].update(aliases) + + self._known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() for x in xs}.union(KNOWN_MODIFIERS) - self.manual = self.name_tokens = self.fluff_tokens = self.modifiers = defaultdict(set) + self.manual_nick = defaultdict(set) + self.manual_tree = defaultdict(set) + self.name_tokens = defaultdict(set) + self.fluff_tokens = defaultdict(set) + self.modifiers = defaultdict(set) + await self._build_monster_index(monsters) self.manual = combine_tokens_dicts(self.manual_nick, self.manual_tree) @@ -99,12 +133,6 @@ async def __ainit__(self, monsters, db): __init__ = __ainit__ async def _build_monster_index(self, monsters): - self.manual_nick = defaultdict(set) - self.manual_tree = defaultdict(set) - self.name_tokens = defaultdict(set) - self.fluff_tokens = defaultdict(set) - self.modifiers = defaultdict(set) - async for m in AsyncIter(monsters): self.modifiers[m] = await self.get_modifiers(m) @@ -217,7 +245,7 @@ def _get_important_tokens(cls, oname, treenames=None): return cls._name_to_tokens(min(n1, n2, key=token_count)) async def get_modifiers(self, m): - modifiers = set() + modifiers = self.manual_prefixes[m.monster_id].copy() basemon = self.graph.get_base_monster(m) @@ -375,6 +403,12 @@ async def sheet_to_reader(url, length=None): return ((line + [None] * length)[:length] for line in csv.reader(file, delimiter=',')) +def copydict(token_dict): + copy = defaultdict(set) + for k, v in token_dict.items(): + copy[k] = v.copy() + return copy + def combine_tokens_dicts(d1, *ds): combined = defaultdict(set, d1.copy()) for d2 in ds: @@ -386,3 +420,10 @@ def combine_tokens_dicts(d1, *ds): def token_count(tstr): tstr = re.sub(r"\(.+\)", "", tstr) return len(re.split(r'\W+', tstr)) + +def get_modifier_aliases(mod): + output = {mod} + for mods in ALL_TOKEN_DICTS: + if mod in mods: + output.update(mods) + return output diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 5bcf8baf..3ed5af6d 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -272,7 +272,7 @@ class MiscModifiers(Enum): 'mega awoken' }} -KNOWN_MODIFIERS = {v for vs in { +ALL_TOKEN_DICTS = { *COLOR_MAP.values(), *SUB_COLOR_MAP.values(), *DUAL_COLOR_MAP.values(), @@ -280,7 +280,9 @@ class MiscModifiers(Enum): *AWOKEN_MAP.values(), *EVO_MAP.values(), *MISC_MAP.values(), -} for v in vs} +} + +KNOWN_MODIFIERS = {v for vs in ALL_TOKEN_DICTS for v in vs} COLOR_TOKENS = { *sum(COLOR_MAP.values(), ()), diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index cc532cdf..b6ab819d 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -207,7 +207,7 @@ def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches, def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, matches=None): if matches is None: - monster_score = defaultdict(MonsterMatch) + matches = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] return max(monsters, diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ea3ce1ed..03f88257 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -885,6 +885,7 @@ async def debugid(self, ctx, *, query): return bm = dgcog.database.graph.get_base_monster(m) pfxs = dgcog.index2.modifiers[m] + manmods = dgcog.index2.manual_prefixes[m.monster_id] EVOANDTYPE = dgcog.token_maps.EVO_TOKENS.union(dgcog.token_maps.TYPE_TOKENS) o = (f"[{m.monster_id}] {m.name_en}\n" f"Base: [{bm.monster_id}] {bm.name_en}\n" @@ -898,7 +899,8 @@ async def debugid(self, ctx, *, query): f" Attribute: {' '.join(sorted(t for t in pfxs if t in dgcog.token_maps.COLOR_TOKENS))}\n" f" Awakening: {' '.join(sorted(t for t in pfxs if t in dgcog.token_maps.AWAKENING_TOKENS))}\n" f" Evo & Type: {' '.join(sorted(t for t in pfxs if t in EVOANDTYPE))}\n" - f" Other: {' '.join(sorted(t for t in pfxs if t not in dgcog.token_maps.OTHER_HIDDEN_TOKENS))}\n") + f" Other: {' '.join(sorted(t for t in pfxs if t not in dgcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" + f"Manually Added: {' '.join(sorted(manmods))}\n") for page in pagify(o): await ctx.send(box(page)) @@ -1022,7 +1024,7 @@ def additmods(ms, om): return "" return "\n\tAlternate names: " + ', '.join(inline(m) for m in ms if m != om) - meanings = [ + meanings = '\n'.join([ *["Evo: " + k.value + additmods(v, token) for k, v in tms.EVO_MAP.items() if token in v], *["Type: " + get_type_emoji(k) + ' ' + k.name + additmods(v, token) @@ -1049,10 +1051,10 @@ def additmods(ms, om): f"{additmods([f'{m}*{d}' for d in v], token)}" for m, ag in re.findall(r"^(\d+)\*{}$".format(awokengroup), token) for a, v in tms.AWOKEN_MAP.items() if ag in v] - ] + ]) if meanings or o: - for page in pagify("\n".join(meanings) + "\n\n" + o.strip()): + for page in pagify(meanings + "\n\n" + o.strip()): await ctx.send(page) else: await ctx.send(f"There are no modifiers that match `{token}`.") From 26b9129812e164269c377025a613eb768a22f09c Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 02:32:04 -0700 Subject: [PATCH 0583/1220] Make monster IDs count as manual tokens (#773) --- dadguide/monster_index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 11672585..d1324f92 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -137,9 +137,9 @@ async def _build_monster_index(self, monsters): self.modifiers[m] = await self.get_modifiers(m) # ID - self.name_tokens[str(m.monster_no_na)].add(m) + self.manual_nick[str(m.monster_no_na)].add(m) if m.monster_id > 10000: - self.name_tokens[str(m.monster_id)].add(m) + self.manual_nick[str(m.monster_id)].add(m) if m.monster_no_na != m.monster_no_jp: self.name_tokens['na' + str(m.monster_no_na)].add(m) self.name_tokens['jp' + str(m.monster_no_jp)].add(m) From a153d04deaaea4942a790910810e1fe5007c5273 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 02:37:20 -0700 Subject: [PATCH 0584/1220] Add exlamation points to idtest :( (#774) After much deliberation, we, The Tsubotki Team, decided that Tsubaki should be boring and not fun, so we decided to remove the last bit of spice she had. We hope to see many un-fun features in the future! --- padinfo/idtest_mixin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index 6bdd6e0b..c128b61d 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -1,4 +1,3 @@ -import random import re from datetime import datetime @@ -376,7 +375,7 @@ async def idt_run(self, ctx): if c != len(suite): o += f"\n\nTests complete. {c}/{len(suite)} succeeded." else: - o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded" + random.choice('.....!!!!!?') + o += "\n\n\N{LARGE GREEN CIRCLE} All tests succeeded!" for page in pagify(o): await ctx.send(box(page)) @@ -462,7 +461,6 @@ async def idt_runall(self, ctx): if qc + fc != len(fsuite) + len(qsuite): o += f"\n\nTests complete. {qc + fc}/{len(fsuite) + len(qsuite)} succeeded." else: - o += "\n\n\N{LARGE GREEN CIRCLE} \N{LARGE GREEN CIRCLE} All tests succeeded" \ - + random.choice(['.'] * 5 + ['!!'] * 5 + ['???']) + o += "\n\n\N{LARGE GREEN CIRCLE} \N{LARGE GREEN CIRCLE} All tests succeeded!!" for page in pagify(o): await ctx.send(box(page)) From bb87226e4d14311177527c54107aa55c27f75950 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 15:18:39 -0700 Subject: [PATCH 0585/1220] Add hlepful text to idhelp (#777) --- padinfo/padinfo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 03f88257..ff0cbb41 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -929,8 +929,10 @@ async def debugiddist(self, ctx, s1, s2): async def idhelp(self, ctx, *, query=""): """Get help with an id query""" await ctx.send( - "See <{0}> for documentation on `{1.prefix}id`! Use `{1.prefix}idmeaning` to query the meaning of any modifier token.".format( - IDGUIDE, ctx)) + "See <{0}> for documentation on `{1.prefix}id`!" + " Use `{1.prefix}idmeaning` to query the meaning of any modifier token." + " Remember that other than `equip`, modifiers must be at the start of the query." + "".format(IDGUIDE, ctx)) if query: await self.debugid(ctx, query=query) From 5f8c1d2d54333991a1c188dd028cb735ebc79948 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 17:30:25 -0700 Subject: [PATCH 0586/1220] Allow negated name token w/o positive name tokens (#776) --- padinfo/core/find_monster.py | 15 +++++++++------ padinfo/padinfo.py | 2 ++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index b6ab819d..32c3722c 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -147,22 +147,22 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], return set(modifiers), negative_modifiers, name, negative_name - def process_name_tokens(self, name_query_tokens, neg_name_tokens, index2, matches): + def process_name_tokens(self, name_query_tokens, neg_name_tokens, dgcog, matches): monstergen = None for t in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(t, index2, matches) + valid = self.get_valid_monsters_from_name_token(t, dgcog.index2, matches) if monstergen is not None: monstergen.intersection_update(valid) else: monstergen = valid for t in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(t, index2, matches, mult=-10) + invalid = self.get_valid_monsters_from_name_token(t, dgcog.index2, matches, mult=-10) if monstergen is not None: monstergen.difference_update(invalid) else: - monstergen = set() + monstergen = set(dgcog.database.get_all_monsters()).difference(invalid) return monstergen @@ -317,6 +317,9 @@ def __init__(self, score=0, name=None, mod=None): if mod is None: self.mod = set() + def __repr__(self): + return str((self.score, [t.split(' - ')[0] for t in self.name], [t.split(' - ')[0] for t in self.mod])) + async def find_monster_search(tokenized_query, dgcog) -> \ Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch]]: @@ -332,10 +335,10 @@ async def find_monster_search(tokenized_query, dgcog) -> \ print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) matches = defaultdict(MonsterMatch) - if name_query_tokens: + if name_query_tokens or neg_name_tokens: monster_gen = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, - dgcog.index2, + dgcog, matches) if not monster_gen: # No monsters match the given name tokens diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ff0cbb41..8b7f2328 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1013,6 +1013,8 @@ def f(m, s): if submwtokens: o += "\n\n[Multi-word Super-tokens]\n" for t in submwtokens: + if not DGCOG.index2.all_name_tokens[''.join(t)]: + continue creators = sorted(DGCOG.index2.mwtoken_creators["".join(t)], key=lambda m: m.monster_id) o += f"{' '.join(t).title()}" o += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' From 4a0975b248f110efaa478a5546b8df6e3023c72c Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 18:22:09 -0700 Subject: [PATCH 0587/1220] Cutover all commands other than id1 itself to id3 (#775) * Remove id1 * Comment --- dadguide/old_monster_index.py | 3 +- padbuildimg/padbuildimg.py | 4 +- padglobal/padglobal.py | 33 ++++++---- padinfo/common/config.py | 6 +- padinfo/core/find_monster.py | 37 ++++------- padinfo/core/leader_skills.py | 12 ++-- padinfo/core/transforminfo.py | 6 +- padinfo/id_menu.py | 2 +- padinfo/padinfo.py | 98 +++++++++++------------------- padinfo/view_state/common.py | 4 +- padinfo/view_state/leader_skill.py | 2 +- pricecheck/pricecheck.py | 85 ++++++++++++++------------ 12 files changed, 132 insertions(+), 160 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 261f097d..9cf32a93 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -252,8 +252,7 @@ def find_monster(self, query): f" Please start using `^id {self.bad_entries[query]}` instead, with a space." " For more information, check out:" " " - " or join the Tsubaki server ()." - " To start using the beta now, type `^idset beta y`!") + " or join the Tsubaki server ().") # handle exact nickname match if query in self.all_entries: diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 4ee1414a..32e5ebf6 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -508,7 +508,7 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card, err, debug_info = await self.padinfo_cog.fm1(tok.value) + ass_card, err, debug_info = await self.padinfo_cog.fm3(tok.value) if ass_card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) elif tok.type == 'REPEAT': @@ -518,7 +518,7 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = await self.padinfo_cog.fm1(tok.value) + card, err, debug_info = await self.padinfo_cog.fm3(tok.value) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) if not card.is_inheritable: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 9d9eda3e..41c3c8dc 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -70,6 +70,13 @@ async def lookup_named_monster(query: str): nm, err, debug_info = await padinfo_cog.fm_(str(query)) return nm, err, debug_info +async def lookup_monster_model(query: str): + padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') + if padinfo_cog is None: + raise Exception("Cog not Loaded") + m, err, debug_info = await padinfo_cog.fm3(str(query)) + return m, err, debug_info + def monster_id_to_monster(monster_id): dg_cog = PADGLOBAL_COG.bot.get_cog('Dadguide') @@ -735,22 +742,25 @@ async def which(self, ctx, *, term: str = None): if name is None or definition is None: return if not success: - await ctx.send('`Which {}`\n{}'.format(name, definition)) + await ctx.send('Which {}\n{}'.format(name, definition)) return - await ctx.send(inline('Which {} - Last Updated {}'.format(name, timestamp))) + await ctx.send('Which {} - Last Updated {}'.format(name, timestamp)) await ctx.send(self.emojify(definition)) async def _resolve_which(self, ctx, term): db_context = self.bot.get_cog('Dadguide').database + padinfo = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') - nm, _, _ = await lookup_named_monster(term) - if nm is None: + m, _, _ = await lookup_monster_model(term) + if m is None: await ctx.send(inline('No monster matched that query')) return None, None, None, None - name = nm.group_computed_treename.title() - monster_id = nm.base_monster_no + m = db_context.graph.get_base_monster(m) + + name = padinfo.get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() + monster_id = m.monster_id definition = self.settings.which().get(monster_id, None) timestamp = "2000-01-01" @@ -785,7 +795,7 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): return if not success: - await ctx.send('`Which {}`\n{}'.format(name, definition)) + await ctx.send('Which {}\n{}'.format(name, definition)) return await self._do_send_which(ctx, to_user, name, definition, timestamp) @@ -1145,12 +1155,13 @@ async def get_guide_text(self, term: str): if term in self.settings.dungeonGuide(): return term, self.settings.dungeonGuide()[term], None - nm, _, _ = await lookup_named_monster(term) - if nm is None: + m, _, _ = await lookup_monster_model(term) + if m is None: return None, None, 'No dungeon or monster matched that query' + m = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) - name = nm.group_computed_treename.title() - definition = self.settings.leaderGuide().get(nm.base_monster_no, None) + name = m.name_en + definition = self.settings.leaderGuide().get(m.monster_id, None) if definition is None: return None, None, 'A monster matched that query but has no guide' diff --git a/padinfo/common/config.py b/padinfo/common/config.py index 211559c3..6db5416b 100644 --- a/padinfo/common/config.py +++ b/padinfo/common/config.py @@ -2,16 +2,14 @@ class UserConfig: - def __init__(self, color, beta_id3): + def __init__(self, color): self.color = color - self.beta_id3 = beta_id3 class BotConfig: @staticmethod async def get_user(config, user_id): user_config = config.user_from_id(user_id) - beta_id3 = await user_config.beta_id3() user_color = await user_config.color() color = user_color_to_discord_color(user_color) - return UserConfig(color, beta_id3) + return UserConfig(color) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 32c3722c..5348803e 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -86,7 +86,7 @@ def _monster_has_token(self, monster, token, matches, monster_mods): else: closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) rat = calc_ratio_modifier(closest, token) - if rat > self.TOKEN_JW_DISTANCE: + if rat > self.MODIFIER_JW_DISTANCE: matches[monster].mod.add(f"{token} - {closest}") matches[monster].score += rat return True @@ -236,26 +236,12 @@ def get_monster_evos(self, database, monster_gen, matches): return monster_evos -async def findMonsterCustom(dgcog, ctx, config, query): - if await config.user(ctx.author).beta_id3(): - m = await findMonster3(dgcog, query) - if m: - return m, "", "" - else: - return None, "Monster not found", "" - else: - return await findMonster1(dgcog, query) - - -async def findMonsterCustom2(dgcog, beta_id3, query): - if beta_id3: - m = await findMonster3(dgcog, query) - if m: - return m, "", "" - else: - return None, "Monster not found", "" +async def findMonsterCustom(dgcog, query): + m = await findMonster3(dgcog, query) + if m: + return m, "", "" else: - return await findMonster1(dgcog, query) + return None, "Monster not found", "" async def findMonster1(dgcog, query): @@ -279,8 +265,7 @@ async def _findMonster(dgcog, query) -> Tuple[Optional["NamedMonster"], Optional prefix = (await dgcog.bot.get_valid_prefixes())[0] return (None, f"Sorry, id1 doesn't support this query and we are no longer" - f" developing id1 features. Please use `{prefix}id3 {query}`! You can" - f" opt into using the beta all the time by running `{prefix}idset beta y`!", + f" developing id1 features. Please use `{prefix}id {query}`!", None) @@ -332,9 +317,9 @@ async def find_monster_search(tokenized_query, dgcog) -> \ if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + #print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + matches = defaultdict(MonsterMatch) - if name_query_tokens or neg_name_tokens: monster_gen = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, @@ -356,8 +341,8 @@ async def find_monster_search(tokenized_query, dgcog) -> \ # no modifiers match any monster in the evo tree return None, {} - print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) - if k in monster_gen}) + #print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) + # if k in monster_gen}) # Return most likely candidate based on query. mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, matches) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 078ccfac..13f8f450 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,6 +1,6 @@ import re -from padinfo.core.find_monster import findMonsterCustom2 +from padinfo.core.find_monster import findMonsterCustom def humanize_number(number, sigfigs=2): @@ -67,7 +67,7 @@ def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): return f"[{format_number(hp)}/{format_number(atk)}/{format_number(rcv)}{resist}] {extras}" -async def perform_leaderskill_query(dgcog, raw_query, beta_id3): +async def perform_leaderskill_query(dgcog, raw_query): # Remove unicode quotation marks query = re.sub("[\u201c\u201d]", '"', raw_query) @@ -82,8 +82,8 @@ async def perform_leaderskill_query(dgcog, raw_query, beta_id3): right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" - nm, err, debug_info = dgcog.index.find_monster(query) - if nm and left_query in nm.prefixes: + m, err, debug_info = await findMonsterCustom(dgcog, query) + if m and left_query in dgcog.index2.modifiers[m]: left_query = query right_query = None @@ -91,9 +91,9 @@ async def perform_leaderskill_query(dgcog, raw_query, beta_id3): else: # no separators left_query, right_query = query, None - left_m, left_err, _ = await findMonsterCustom2(dgcog, beta_id3, left_query) + left_m, left_err, _ = await findMonsterCustom(dgcog, left_query) if right_query: - right_m, right_err, _ = await findMonsterCustom2(dgcog, beta_id3, right_query) + right_m, right_err, _ = await findMonsterCustom(dgcog, right_query) else: right_m, right_err, = left_m, left_err return left_err, left_m, left_query, right_err, right_m, right_query diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index 96003b07..e57b4c8d 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -1,10 +1,10 @@ -from padinfo.core.find_monster import findMonsterCustom2 +from padinfo.core.find_monster import findMonsterCustom -async def perform_transforminfo_query(dgcog, raw_query, beta_id3): +async def perform_transforminfo_query(dgcog, raw_query): db_context = dgcog.database mgraph = dgcog.database.graph - found_monster, err, debug_info = await findMonsterCustom2(dgcog, beta_id3, raw_query) + found_monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not found_monster: return found_monster, err, debug_info, None diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 719fc345..9d3214a6 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -8,7 +8,6 @@ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji -from dadguide.database_context import DbContext from padinfo.pane_names import IdMenuPaneNames from padinfo.view.evos import EvosView from padinfo.view.id import IdView @@ -25,6 +24,7 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel + from dadguide.database_context import DbContext menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8b7f2328..a86a72b3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -26,12 +26,12 @@ from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ - findMonsterCustom, calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom2 + calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom from padinfo.core.historic_lookups import historic_lookups from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query -from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.core.padinfo_settings import settings +from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.id_menu import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.idtest_mixin import IdTest @@ -102,9 +102,11 @@ def __init__(self, bot, *args, **kwargs): self.config.register_user(survey_mode=0, color=None, beta_id3=False, lastaction=None) self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) - self.fm1 = lambda q: fm.findMonster1(bot.get_cog("Dadguide"), q) + self.fm3 = lambda q: fm.findMonsterCustom(bot.get_cog("Dadguide"), q) self.fm_ = lambda q: fm._findMonster(bot.get_cog("Dadguide"), q) + self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster + def cog_unload(self): """Manually nulling out database because the GC for cogs seems to be pretty shitty""" # TODO.... manage historic_lookups??? probably not. @@ -196,7 +198,7 @@ async def test_reaction_add(self, reaction, member): async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) + m, err, debug_info = await findMonsterCustom(dgcog, query) if m is not None: await ctx.send(MonsterHeader.short(m)) await ctx.send(box(m.name_ja)) @@ -234,11 +236,6 @@ async def id3(self, ctx, *, query: str): await self._do_id(ctx, query, force_id3_pref=True) async def _do_id(self, ctx, query: str, force_id3_pref=None): - # dgcog = await self.get_dgcog() - beta_id3 = await self.config.user(ctx.author).beta_id3() - if force_id3_pref is not None: - beta_id3 = force_id3_pref - dgcog = await self.get_dgcog() raw_query = query color = await self.get_user_embed_color(ctx) @@ -266,8 +263,7 @@ async def _do_id(self, ctx, query: str, force_id3_pref=None): f" info about `id3` check out" f" !") - monster, err, debug_info = await findMonsterCustom2(dgcog, beta_id3, - raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not monster: await self.makeFailureMsg(ctx, query, err) @@ -281,13 +277,13 @@ async def send_error(error): asyncio.create_task(send_error(err)) # id3 messaging stuff - if beta_id3 and monster and monster.monster_no_na != monster.monster_no_jp: + if monster and monster.monster_no_na != monster.monster_no_jp: await ctx.send("The NA ID and JP ID of this card differ! " "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) - if beta_id3 and await self.config.do_survey(): + if await self.config.do_survey(): asyncio.create_task(self.send_survey_after(ctx, query, monster)) transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ @@ -349,8 +345,7 @@ async def checkbadness(self, ctx): @checks.bot_has_permissions() async def id2(self, ctx, *, query: str): """Monster info (main tab)""" - await ctx.send("id2 has been discontinued! For an even better searching experience," - " opt into the id3 beta using `{}idset beta y`".format(ctx.prefix)) + await ctx.send("id2 has been discontinued!".format(ctx.prefix)) @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) @@ -363,8 +358,7 @@ async def evos(self, ctx, *, query: str): friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), - raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: await self.makeFailureMsg(ctx, query, err) @@ -388,8 +382,7 @@ async def evomats(self, ctx, *, query: str): original_author_id = ctx.message.author.id friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), - raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not monster: await self.makeFailureMsg(ctx, query, err) @@ -419,8 +412,7 @@ async def pantheon(self, ctx, *, query: str): friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), - raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: await self.makeFailureMsg(ctx, query, err) @@ -489,8 +481,7 @@ async def pic(self, ctx, *, query: str): friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), - raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: await self.makeFailureMsg(ctx, query, err) @@ -507,7 +498,7 @@ async def pic(self, ctx, *, query: str): async def links(self, ctx, *, query: str): """Monster links""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) + m, err, debug_info = await findMonsterCustom(dgcog, query) if m is not None: menu = IdMenuOld(ctx) embed = await menu.make_links_embed(m) @@ -527,8 +518,7 @@ async def otherinfo(self, ctx, *, query: str): friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - monster, err, debug_info = await findMonsterCustom2(dgcog, await self.config.user(ctx.author).beta_id3(), - raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: await self.makeFailureMsg(ctx, query, err) @@ -545,7 +535,7 @@ async def otherinfo(self, ctx, *, query: str): async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" dgcog = await self.get_dgcog() - monster, err, _ = await findMonsterCustom(dgcog, ctx, self.config, query) + monster, err, _ = await findMonsterCustom(dgcog, query) if monster is None: await self.makeFailureMsg(ctx, query, err) return @@ -560,7 +550,7 @@ async def buttoninfo(self, ctx, *, query: str): async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) + m, err, debug_info = await findMonsterCustom(dgcog, query) if m is not None: menu = IdMenuOld(ctx, allowed_emojis=self.get_emojis()) embed = await menu.make_lookup_embed(m) @@ -573,7 +563,7 @@ async def lookup(self, ctx, *, query: str): async def evolist(self, ctx, *, query): """Monster info (for all monsters in the evo tree)""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) + m, err, debug_info = await findMonsterCustom(dgcog, query) if m is not None: await self._do_evolistmenu(ctx, m) else: @@ -590,9 +580,8 @@ async def leaderskill(self, ctx, *, raw_query): [p]ls r sonia "revo lu bu" [p]ls sonia lubu """ - beta_id3 = await self.config.user(ctx.author).beta_id3() dgcog = await self.get_dgcog() - l_err, l_mon, l_query, r_err, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, beta_id3) + l_err, l_mon, l_query, r_err, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, try separating the queries with / or wrap with quotes.' if l_err: @@ -627,7 +616,7 @@ def user_color_to_discord_color(self, color): @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) + m, err, debug_info = await findMonsterCustom(dgcog, query) if err: await ctx.send(err) return @@ -642,18 +631,17 @@ async def leaderskillsingle(self, ctx, *, query): @checks.bot_has_permissions(embed_links=True) async def transforminfo(self, ctx, *, query): """Show info about a transform card, including some helpful details about the base card.""" - beta_id3 = await self.config.user(ctx.author).beta_id3() dgcog = await self.get_dgcog() base_mon, err, debug_info, transformed_mon = \ - await perform_transforminfo_query(dgcog, query, beta_id3) + await perform_transforminfo_query(dgcog, query) if not base_mon: await self.makeFailureMsg(ctx, query, err) return - + if not transformed_mon: await ctx.send('Your query `{}` found [{}] {}, '.format(query, base_mon.monster_id, - base_mon.name_en) + 'which has no evos that transform.') + base_mon.name_en) + 'which has no evos that transform.') return if err: @@ -692,7 +680,7 @@ async def padsay(self, ctx, server, *, query: str = None): query = query.strip().lower() dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, ctx, self.config, query) + m, err, debug_info = await findMonsterCustom(dgcog, query) if m is not None: voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na if voice_id is None: @@ -746,22 +734,6 @@ async def survey(self, ctx, value): else: await ctx.send("value must be `always`, `sometimes`, or `never`") - @idset.command() - async def beta(self, ctx, value: bool = True): - """Opt in (or out D:) to the id3 beta test!""" - await self.config.user(ctx.author).beta_id3.set(value) - await ctx.tick() - - @idset.command() - @checks.is_owner() - async def betacount(self, ctx): - """Check the number of beta testers""" - c = 0 - for v in (await self.config.all_users()).values(): - if v['beta_id3']: - c += 1 - await ctx.send(inline(str(c) + " user(s) have opted in.")) - @is_donor() @idset.command() async def embedcolor(self, ctx, *, color): @@ -783,6 +755,11 @@ async def embedcolor(self, ctx, *, color): return await ctx.tick() + @idset.command() + async def beta(self, ctx, *, text): + """Discontinued""" + await ctx.send("You can no longer opt out of `id3`. Use `{0.prefix}id1` if you need to use it.".format(ctx)) + @commands.group() @checks.is_owner() async def padinfo(self, ctx): @@ -863,17 +840,10 @@ def get_emojis(self): return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] async def makeFailureMsg(self, ctx, query: str, err): - if await self.config.user(ctx.author).beta_id3(): - await ctx.send("Sorry, your query {0} didn't match any results :(\n" - "See <{2}> for " - "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " - "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) - return - msg = ('Lookup failed: {0}\n\n').format(err) - await ctx.send(msg + 'Try opting into the beta of our new id lookup for better results!' - ' Try `{0.prefix}id3 {1}` to use it one time, or `{0.prefix}idset beta y`' - ' to permanently opt into it (you can opt out with `{0.prefix}idset beta n`' - ' if you want to later).'.format(ctx, query)) + await ctx.send("Sorry, your query {0} didn't match any results :(\n" + "See <{2}> for " + "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " + "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) @commands.command(aliases=["iddebug"]) async def debugid(self, ctx, *, query): diff --git a/padinfo/view_state/common.py b/padinfo/view_state/common.py index d1ad9f70..e1d75db9 100644 --- a/padinfo/view_state/common.py +++ b/padinfo/view_state/common.py @@ -1,5 +1,5 @@ from padinfo.common.config import UserConfig -from padinfo.core.find_monster import findMonsterCustom2 +from padinfo.core.find_monster import findMonsterCustom async def get_monster_from_ims(dgcog, user_config: UserConfig, ims: dict): @@ -9,5 +9,5 @@ async def get_monster_from_ims(dgcog, user_config: UserConfig, ims: dict): resolved_monster_id = int(resolved_monster_id_str) if resolved_monster_id_str else None if resolved_monster_id: return dgcog.database.graph.get_monster(resolved_monster_id) - monster, _, _ = await findMonsterCustom2(dgcog, user_config.beta_id3, query) + monster, _, _ = await findMonsterCustom(dgcog, query) return monster diff --git a/padinfo/view_state/leader_skill.py b/padinfo/view_state/leader_skill.py index 374d290b..f7c1c806 100644 --- a/padinfo/view_state/leader_skill.py +++ b/padinfo/view_state/leader_skill.py @@ -26,6 +26,6 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - _, l_mon, l_query, _, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, user_config.beta_id3) + _, l_mon, l_query, _, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, r_query) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 15dd6223..9b20ce30 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -1,7 +1,6 @@ -import discord -import re from io import BytesIO -from redbot.core import Config, checks, commands + +from redbot.core import Config, commands from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check @@ -14,11 +13,14 @@ {foot} """ + def rint(x, p): return str(round(x, p)).rstrip('0').rstrip('.') + class PriceCheck(commands.Cog): """A pricechecking cog for Lumon""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -45,24 +47,26 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def pricecheck(self, ctx, *, query): """Displays pricing data for a tradable non-collab gem.""" padinfo_cog = self.bot.get_cog('PadInfo') - if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + dgcog = self.bot.get_cog("Dadguide") + if padinfo_cog is None or dgcog is None: + await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog.fm_(query) - if not nm: + m, err, debug_info = await padinfo_cog.fm3(query) + if not m: await ctx.send(err) return + base_id = str(dgcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: - if str(nm.base_monster_no) not in pcs: - if nm.sell_mp < 100: - await ctx.send("{} does not have PC data.".format(nm.name_en)) + if base_id not in pcs: + if m.sell_mp < 100: + await ctx.send("{} does not have PC data.".format(m.name_en)) else: - await ctx.send("{} is not tradable.".format(nm.name_en)) + await ctx.send("{} is not tradable.".format(m.name_en)) return - sc, foot = pcs[str(nm.base_monster_no)] - pct = PC_TEXT.format(name=nm.name_en, + sc, foot = pcs[base_id] + pct = PC_TEXT.format(name=m.name_en, stam_cost=rint(sc, 2), pp_val=rint(sc * 83 / 50, 2), points=rint(sc * 83 / 50 / 297, 2), @@ -70,11 +74,11 @@ async def pricecheck(self, ctx, *, query): if await self.config.channel(ctx.channel).dm(): await ctx.send(DISCLAIMER) for page in pagify(pct): - await ctx.author.send(box(page.replace("'","ʼ"), lang='q')) + await ctx.author.send(box(page.replace("'", "ʼ"), lang='q')) else: await ctx.send(DISCLAIMER) for page in pagify(pct): - await ctx.send(box(page.replace("'","ʼ"), lang='q')) + await ctx.send(box(page.replace("'", "ʼ"), lang='q')) @commands.group() @auth_check('pcadmin') @@ -85,61 +89,66 @@ async def pcadmin(self, ctx): async def set(self, ctx, stam_cost: float, *, query): """Adds stamina cost data to a card.""" padinfo_cog = self.bot.get_cog('PadInfo') + dgcog = self.bot.get_cog("Dadguide") if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog.fm_(query) - if not nm: + m, err, debug_info = await padinfo_cog.fm3(query) + if not m: await ctx.send(err) return + base_id = str(dgcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: foot = "" - if str(nm.base_monster_no) in pcs: - foot = pcs[str(nm.base_monster_no)][1] - pcs[str(nm.base_monster_no)] = (stam_cost, foot) - await ctx.send(box("Set {} ({}) to {}".format(nm.name_en, nm.base_monster_no, stam_cost))) + if base_id in pcs: + foot = pcs[base_id][1] + pcs[base_id] = (stam_cost, foot) + await ctx.send(box("Set {} ({}) to {}".format(m.name_en, base_id, stam_cost))) @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) async def setfooter(self, ctx, query, *, footer=""): """Adds notes regarding the stamina cost of a card.""" padinfo_cog = self.bot.get_cog('PadInfo') - if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + dgcog = self.bot.get_cog('Dadguide') + if padinfo_cog is None or dgcog is None: + await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog.fm_(query) - if not nm: + m, err, debug_info = await padinfo_cog.fm3(query) + if not m: await ctx.send(err) return + base_id = str(dgcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: sc = -1 - if str(nm.base_monster_no) in pcs: - sc = pcs[str(nm.base_monster_no)][0] - pcs[str(nm.base_monster_no)] = (sc, footer.strip('`')) - await ctx.send(box("Set {} ({}) footer to '{}'".format(nm.name_en, nm.base_monster_no, footer))) + if base_id in pcs: + sc = pcs[base_id][0] + pcs[base_id] = (sc, footer.strip('`')) + await ctx.send(box("Set {} ({}) footer to '{}'".format(m.name_en, base_id, footer))) @pcadmin.command(aliases=['delete', 'del', 'rm']) async def remove(self, ctx, *, query): """Removes stamina cost data from a card.""" padinfo_cog = self.bot.get_cog('PadInfo') - if padinfo_cog is None: - await ctx.send(inline("Error: PadInfo Cog not loaded. Please alert a bot owner.")) + dgcog = self.bot.get_cog('Dadguide') + if padinfo_cog is None or dgcog is None: + await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - nm, err, debug_info = await padinfo_cog.fm_(query) - if not nm: + m, err, debug_info = await padinfo_cog.fm3(query) + if not m: await ctx.send(err) return async with self.config.pcs() as pcs: - if str(nm.base_monster_no) not in pcs: - await ctx.send("{} does not have PC data.".format(nm.name_en)) + if str(dgcog.database.graph.get_base_id(m)) not in pcs: + await ctx.send("{} does not have PC data.".format(m.name_en)) return - del pcs[str(nm.base_monster_no)] - await ctx.send(inline("Removed PC data from {}.".format(nm.name_en))) + del pcs[str(dgcog.database.graph.get_base_id(m))] + await ctx.send(inline("Removed PC data from {}.".format(m.name_en))) @pcadmin.command() async def setdmonly(self, ctx, value: bool = True): From 8550ba19a2ac563dae613f92a66446c0ccdbac61 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 18:47:54 -0700 Subject: [PATCH 0588/1220] Change id url (#778) --- dadguide/old_monster_index.py | 2 +- padinfo/padinfo.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py index 9cf32a93..e3d5b8f5 100644 --- a/dadguide/old_monster_index.py +++ b/dadguide/old_monster_index.py @@ -251,7 +251,7 @@ def find_monster(self, query): err = ("It looks like this query won't be supported soon!" f" Please start using `^id {self.bad_entries[query]}` instead, with a space." " For more information, check out:" - " " + " " " or join the Tsubaki server ().") # handle exact nickname match diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a86a72b3..f8b78f5f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -54,7 +54,7 @@ EMBED_NOT_GENERATED = -1 -IDGUIDE = "https://github.com/TsubakiBotPad/pad-cogs/wiki/%5Eid-user-guide" +IDGUIDE = "https://github.com/TsubakiBotPad/pad-cogs/wiki/id-user-guide" def _data_file(file_name: str) -> str: @@ -261,7 +261,7 @@ async def _do_id(self, ctx, query: str, force_id3_pref=None): await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" f" Try using `{' '.join(goodquery)}` (with a space) instead! For more" f" info about `id3` check out" - f" !") + f" <{IDGUIDE}>!") monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -756,9 +756,11 @@ async def embedcolor(self, ctx, *, color): await ctx.tick() @idset.command() - async def beta(self, ctx, *, text): + async def beta(self, ctx, *, text=""): """Discontinued""" - await ctx.send("You can no longer opt out of `id3`. Use `{0.prefix}id1` if you need to use it.".format(ctx)) + await ctx.send(f"`id3 `is now enabled globally, see" + f" {IDGUIDE} for more information." + f" `{ctx.prefix}id1` is temporarily available if you need to use it.") @commands.group() @checks.is_owner() From 71f2ce5f497ec0abdefe519d3dcd9e54f42a693e Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 8 Feb 2021 18:54:54 -0700 Subject: [PATCH 0589/1220] Wrap documentation link in idset beta warning (#779) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f8b78f5f..332da572 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -759,7 +759,7 @@ async def embedcolor(self, ctx, *, color): async def beta(self, ctx, *, text=""): """Discontinued""" await ctx.send(f"`id3 `is now enabled globally, see" - f" {IDGUIDE} for more information." + f" <{IDGUIDE}> for more information." f" `{ctx.prefix}id1` is temporarily available if you need to use it.") @commands.group() From 2907e6693f30594b8b72ddbf1215067df29d28c4 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Tue, 9 Feb 2021 00:05:09 -0500 Subject: [PATCH 0590/1220] Update transforminfo (#780) * Fix bug in id where transforms that gain balance type show wrong killers * Make some updates to tfinfo to reflect feedback * Change back a method name in tfinfo view --- padinfo/tf_menu.py | 5 +++-- padinfo/view/id.py | 12 +++++++----- padinfo/view/transforminfo.py | 25 ++++++++++++++++++------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/padinfo/tf_menu.py b/padinfo/tf_menu.py index 19b85cbc..c3086f41 100644 --- a/padinfo/tf_menu.py +++ b/padinfo/tf_menu.py @@ -15,10 +15,11 @@ if TYPE_CHECKING: pass -emoji_button_names = ['\N{HOUSE BUILDING}', '\N{DOWNWARDS BLACK ARROW}', '\N{UPWARDS BLACK ARROW}', - '\N{CROSS MARK}'] +emoji_button_names = ['\N{HOUSE BUILDING}', '\N{DOWN-POINTING RED TRIANGLE}', + '\N{BLACK RIGHT-POINTING TRIANGLE}', '\N{CROSS MARK}'] menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') + class TransformInfoMenu: INITIAL_EMOJI = emoji_button_names[0] MENU_TYPE = 'TransformInfo' diff --git a/padinfo/view/id.py b/padinfo/view/id.py index a43a1af8..d442f6c5 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -71,12 +71,14 @@ def all_awakenings_row(m: "MonsterModel"): @staticmethod def killers_row(m: "MonsterModel", transform_base): - killers_text = 'Any' if 'Any' in m.killers else \ - ' '.join(_killer_latent_emoji(k) for k in transform_base.killers) - available_killer_text = 'Available killers:' if m==transform_base else 'Avail. killers (pre-xform):' + killers = m.killers if m==transform_base else transform_base.killers + killers_text = 'Any' if 'Any' in killers else \ + ' '.join(_killer_latent_emoji(k) for k in killers) return Box( - BoldText(available_killer_text), - Text('[{} slots]'.format(m.latent_slots)), + BoldText('Available killers:'), + Text('\N{DOWN-POINTING RED TRIANGLE}' if m!=transform_base else ''), + Text('[{} slots]'.format(m.latent_slots if m==transform_base \ + else transform_base.latent_slots)), Text(killers_text), delimiter=' ' ) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index a0f2e5aa..d94f9b29 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -17,23 +17,33 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel +BASE_EMOJI = '\N{DOWN-POINTING RED TRIANGLE}' + def base_info(m: "MonsterModel"): return Box( Box( - '\N{DOWN-POINTING RED TRIANGLE}', + BASE_EMOJI, IdView.normal_awakenings_row(m) if len(m.awakenings) != 0 else Box(Text('No Awakenings')), delimiter=' ' ), - IdView.super_awakenings_row(m), - # the transform base of the base is the same - IdView.killers_row(m, m) + IdView.super_awakenings_row(m) + ) + +def transform_skill_header(m: "MonsterModel"): + active_skill = m.active_skill + active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' + return Box( + BoldText('Active Skill'), + BoldText(active_cd), + delimiter=' ' ) def base_skill(m: "MonsterModel"): active_skill = m.active_skill - return (" (Base: {} -> {})".format(active_skill.turn_max, active_skill.turn_min) if active_skill + return (" (" + BASE_EMOJI + " " + + "{} -> {})".format(active_skill.turn_max, active_skill.turn_min) if active_skill else 'None') @@ -49,7 +59,8 @@ def embed(state: TransformInfoViewState): Box( IdView.normal_awakenings_row(transformed_mon) if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), - base_info(base_mon) + base_info(base_mon), + IdView.killers_row(transformed_mon, base_mon) ), ), EmbedField( @@ -64,7 +75,7 @@ def embed(state: TransformInfoViewState): inline=True ), EmbedField( - IdView.active_skill_header(transformed_mon).to_markdown() + base_skill(base_mon), + transform_skill_header(transformed_mon).to_markdown() + base_skill(base_mon), Text(transformed_mon.active_skill.desc if transformed_mon.active_skill else 'None') ), EmbedField( From 8b30eb4f77f840298b7687f9c2cbd616e15703a4 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 9 Feb 2021 02:09:26 -0700 Subject: [PATCH 0591/1220] Force tree to be 2+ monsters for auto-treenaming (#781) * Force tree to be 2+ monsters for auto-treenaming * Special matching for gems --- dadguide/monster_index.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index d1324f92..a66bac0f 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -147,13 +147,14 @@ async def _build_monster_index(self, monsters): # Name Tokens nametokens = self._name_to_tokens(m.name_en) last_token = m.name_en.split(',')[-1].strip() - autotoken = True + alt_monsters = self.graph.get_alt_monsters(m) + autotoken = len(alt_monsters) > 1 for jpt in m.name_ja.split(" "): self.name_tokens[jpt].add(m) # Propagate name tokens throughout all evos - for me in self.graph.get_alt_monsters(m): + for me in alt_monsters: if last_token != me.name_en.split(',')[-1].strip(): autotoken = False for t in self.monster_id_to_nametokens[me.monster_id]: @@ -162,10 +163,15 @@ async def _build_monster_index(self, monsters): # Find likely treenames treenames = set() - for me in self.graph.get_alt_monsters(m): - match = re.match("(?:Awoken|Reincarnated) (.*)", me.name_en) - if match: - treenames.add(match.group(1)) + regexes = [ + r"(?:Awoken|Reincarnated) (.*)", + r".*, (.*'s Gem)", + ] + for me in alt_monsters: + for r in regexes: + match = re.match(r, me.name_en) + if match: + treenames.add(match.group(1)) # Add important tokens for t in self.monster_id_to_nametokens[m.monster_id]: @@ -182,12 +188,12 @@ async def _build_monster_index(self, monsters): self.add_name_token(self.name_tokens, token, m) if m.is_equip: possessives = re.findall(r"(\w+)'s", m.name_en.lower()) - for mevo in self.graph.get_alt_monsters(m): + for mevo in alt_monsters: for token2 in possessives: if token2 in self._name_to_tokens(mevo.name_en.lower()): self.add_name_token(self.name_tokens, token2, mevo) else: - for mevo in self.graph.get_alt_monsters(m): + for mevo in alt_monsters: if token in self._name_to_tokens(mevo.name_en): self.add_name_token(self.name_tokens, token, mevo) @@ -198,7 +204,7 @@ async def _build_monster_index(self, monsters): # significantly more complicated logic in the lookup later on. # Test case: Mizutsune is a nickname for Dark Aurora, ID 4148. Issue: #614 if m.is_equip: - for mevo in self.graph.get_alt_monsters(m): + for mevo in alt_monsters: if not mevo.is_equip: for token2 in self._get_important_tokens(mevo.name_en, treenames): self.add_name_token(self.name_tokens, token2, m) From e9c627acc414fefc25f784caffc7b7d9c062ede8 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 9 Feb 2021 03:11:33 -0700 Subject: [PATCH 0592/1220] Show gem data when base is insignificant (#782) --- padinfo/padinfo.py | 4 ++-- padinfo/view/materials.py | 19 ++++++++++++------- padinfo/view_state/materials.py | 16 +++++++++++----- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 332da572..b107044d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -388,7 +388,7 @@ async def evomats(self, ctx, *, query: str): await self.makeFailureMsg(ctx, query, err) return - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = \ + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ await MaterialsViewState.query(dgcog, monster) if mats is None: @@ -396,7 +396,7 @@ async def evomats(self, ctx, *, query: str): return state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.mats_control) await menu.create(ctx, state) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index f3d45f3e..06a15154 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -2,17 +2,17 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LinkedText +from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.base import pad_info_footer, pad_info_footer_with_state +from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view_state.materials import MaterialsViewState if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + pass MAX_MONS_TO_SHOW = 5 @@ -61,9 +61,14 @@ def embed(state: MaterialsViewState): embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), embed_footer=pad_info_footer_with_state(state), embed_fields=[f for f in [ - mat_use_field(state.mats, "Evo materials") if state.mats or not state.monster.is_stackable else None, - mat_use_field(state.usedin, "Material for", 10) if state.usedin else None, - mat_use_field(state.gemusedin, "Evo gem ({}) is mat for".format(state.gemid)) if state.gemusedin else None, - skillup_field(state.skillups, state.skillup_evo_count, state.link) if not state.monster.is_stackable else None + mat_use_field(state.mats, "Evo materials") + if state.mats or not (state.monster.is_stackable or state.gem_override) else None, + mat_use_field(state.usedin, "Material for", 10) + if state.usedin else None, + mat_use_field(state.gemusedin, "Evo gem ({}) is mat for".format(state.gemid), + 10 if state.gem_override else 5) + if state.gemusedin else None, + skillup_field(state.skillups, state.skillup_evo_count, state.link) + if not (state.monster.is_stackable or state.gem_override) else None ] if f is not None] ) diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 91e37596..b7d46b8d 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -14,7 +14,7 @@ class MaterialsViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, - use_evo_scroll: bool = True, + gem_override: bool, use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.link = link @@ -24,6 +24,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.mats = mats self.usedin = usedin self.gemid = gemid + self.gem_override = gem_override self.query = query self.monster = monster self.color = color @@ -43,7 +44,7 @@ def serialize(self): async def deserialize(dgcog, user_config: UserConfig, ims: dict): monster = await get_monster_from_ims(dgcog, user_config, ims) - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link = \ + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ await MaterialsViewState.query(dgcog, monster) raw_query = ims['raw_query'] @@ -53,7 +54,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' return MaterialsViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, use_evo_scroll=use_evo_scroll, extra_state=ims) @@ -82,8 +83,13 @@ async def query(dgcog, monster): db_context.graph.get_base_id(su) != db_context.graph.get_base_id(monster) and su not in sugs] if monster.active_skill else [] skillup_evo_count = len(sums) - len(vsums) + gem_override = False if not any([mats, usedin, gemusedin, skillups and not monster.is_stackable]): - return None, None, None, None, None, None, None + return None, None, None, None, None, None, None, None + if not any([mats, usedin, skillups and not monster.is_stackable]): + mats, gemusedin, _, usedin, skillups, skillup_evo_count, link, _ \ + = await MaterialsViewState.query(dgcog, evo_gem) + gem_override = True - return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link + return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override From c3e40d9ab7f6250cb3ce231a74c9bc2ede545c23 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 10 Feb 2021 04:30:00 -0600 Subject: [PATCH 0593/1220] Return None in transition func when pane is unsupported (#767) * Add UnsupportedPaneType & let evo pane fail via this * bump required discord-menu reversion * Return None instead of exception --- padinfo/core/find_monster.py | 8 ++++---- padinfo/id_menu.py | 12 +++++++++--- padinfo/info.json | 2 +- padinfo/padinfo.py | 5 +++++ padinfo/view_state/evos.py | 6 ++++++ padinfo/view_state/materials.py | 3 +++ padinfo/view_state/pantheon.py | 3 +++ 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 5348803e..6a8b38a6 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -317,8 +317,8 @@ async def find_monster_search(tokenized_query, dgcog) -> \ if t not in dgcog.index2.all_modifiers: settings.add_typo_mod(t) - #print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) - + # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) + matches = defaultdict(MonsterMatch) if name_query_tokens or neg_name_tokens: monster_gen = find_monster.process_name_tokens(name_query_tokens, @@ -341,8 +341,8 @@ async def find_monster_search(tokenized_query, dgcog) -> \ # no modifiers match any monster in the evo tree return None, {} - #print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) - # if k in monster_gen}) + # print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) + # if k in monster_gen}) # Return most likely candidate based on query. mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, matches) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 9d3214a6..33a92aff 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -163,14 +163,18 @@ def id_control(state: IdViewState): ) @staticmethod - def evos_control(state: EvosViewState): + def evos_control(state: Optional[EvosViewState]): + if state is None: + return None return EmbedControl( [EvosView.embed(state)], [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod - def mats_control(state: MaterialsViewState): + def mats_control(state: Optional[MaterialsViewState]): + if state is None: + return None return EmbedControl( [MaterialsView.embed(state)], [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] @@ -184,7 +188,9 @@ def pic_control(state: PicViewState): ) @staticmethod - def pantheon_control(state: PantheonViewState): + def pantheon_control(state: Optional[PantheonViewState]): + if state is None: + return None return EmbedControl( [PantheonView.embed(state)], [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] diff --git a/padinfo/info.json b/padinfo/info.json index 250e966b..34c2aaf6 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -23,7 +23,7 @@ "tsutils>=3.1.0", "prettytable", "python-levenshtein", - "discord-menu>=0.12.0" + "discord-menu>=0.13.0" ], "tags": [ "PAD" diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b107044d..8cd90771 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -366,6 +366,11 @@ async def evos(self, ctx, *, query: str): alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + if alt_versions is None: + await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, + monster.name_en) + 'which has no alt evos or gems.') + return + state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_versions, gem_versions, use_evo_scroll=settings.checkEvoID(ctx.author.id)) diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index eff0fd8c..9cd6982a 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -37,6 +37,10 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): monster = await get_monster_from_ims(dgcog, user_config, ims) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + if alt_versions is None: + return None + print('def not kittens') + raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -54,4 +58,6 @@ async def query(dgcog, monster): alt_versions = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, key=lambda x: x.monster_id) gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) + if len(alt_versions) == 1 and len(gem_versions) == 0: + return None, None return alt_versions, gem_versions diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index b7d46b8d..8e4e95a7 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -47,6 +47,9 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ await MaterialsViewState.query(dgcog, monster) + if mats is None: + return None + raw_query = ims['raw_query'] query = ims.get('query') or raw_query menu_type = ims['menu_type'] diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index 9c3a636f..b2da095c 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -37,6 +37,9 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): monster = await get_monster_from_ims(dgcog, user_config, ims) pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + if pantheon_list is None: + return None + raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] From 3394cfd19d3de329d2ebcd9bbc23f11b16ae467f Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 10 Feb 2021 21:41:39 -0700 Subject: [PATCH 0594/1220] Support multi-word tokens in NameTokenAliases (#785) * Allow for multi-token token replacements * Undo channelmirror changes --- dadguide/monster_index.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a66bac0f..1c31689e 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,6 +1,8 @@ import asyncio import csv +import inspect import io +import logging import re from collections import defaultdict @@ -19,7 +21,7 @@ MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('2089525837') TREE_MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('1372419168') - +logger = logging.getLogger('red.pad-cogs.dadguide.monster_index') class MonsterIndex2(aobject): async def __ainit__(self, monsters, db): @@ -86,8 +88,8 @@ async def __ainit__(self, monsters, db): self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) next(nt_alias_data) # Skip over heading - for token, alias in nt_alias_data: - self.replacement_tokens[token].add(alias) + for tokens, alias in nt_alias_data: + self.replacement_tokens[frozenset(re.split(r'\W+', tokens))].add(alias) self.manual_prefixes = defaultdict(set) for mid, mods in mod_data: @@ -224,6 +226,20 @@ async def _build_monster_index(self, monsters): for nick in self.monster_id_to_treename[base_id]: self.add_name_token(self.manual_tree, nick, m) + def add_name_token(self, token_dict, token, m): + if len(inspect.stack(0)) > 100: + logger.warning(f"Infinite loop detected in name token replacement with token {token}. Aborting.") + return + + token_dict[token.lower()].add(m) + if token.lower() in self._known_mods and token.lower() not in HAZARDOUS_IN_NAME_PREFIXES: + self.modifiers[m].add(token.lower()) + + # Replacements + for ts in (k for k in self.replacement_tokens if token.lower() in k and all(m in token_dict[t] for t in k)): + for t in self.replacement_tokens[ts]: + self.add_name_token(token_dict, t, m) + @staticmethod def _name_to_tokens(oname): if not oname: @@ -391,12 +407,6 @@ def is_story(m, do_transform=True): return modifiers - def add_name_token(self, token_dict, token, m): - for t in self.replacement_tokens[token.lower()].union({token.lower()}): - token_dict[t].add(m) - if t.lower() in self._known_mods and t.lower() not in HAZARDOUS_IN_NAME_PREFIXES: - self.modifiers[m].add(t.lower()) - # TODO: Move this to TSUtils async def sheet_to_reader(url, length=None): From 006be492f5f935a29cb46b191e8767ccd012d549 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 11 Feb 2021 04:44:33 -0600 Subject: [PATCH 0595/1220] Make id menu emojis optional when panes don't apply (#787) * initial progress that doesn't work yet * Add reaction_list check to home screen * Hide emojis from all id menu commands --- padinfo/id_menu.py | 20 +++++++++----- padinfo/padinfo.py | 47 +++++++++++++++++++++++++++++++-- padinfo/view_state/common.py | 5 ++++ padinfo/view_state/evos.py | 14 +++++++--- padinfo/view_state/id.py | 7 ++++- padinfo/view_state/materials.py | 10 +++++-- padinfo/view_state/otherinfo.py | 9 +++++-- padinfo/view_state/pantheon.py | 7 ++++- padinfo/view_state/pic.py | 10 ++++--- 9 files changed, 107 insertions(+), 22 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 33a92aff..df0f0534 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, List from discord import Message from discordmenu.embed.emoji import EmbedMenuEmojiConfig @@ -157,50 +157,56 @@ async def respond_with_otherinfo(message: Optional[Message], ims, **data): @staticmethod def id_control(state: IdViewState): + reaction_list = state.reaction_list return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def evos_control(state: Optional[EvosViewState]): if state is None: return None + reaction_list = state.reaction_list return EmbedControl( [EvosView.embed(state)], - [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def mats_control(state: Optional[MaterialsViewState]): if state is None: return None + reaction_list = state.reaction_list return EmbedControl( [MaterialsView.embed(state)], - [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def pic_control(state: PicViewState): + reaction_list = state.reaction_list return EmbedControl( [PicView.embed(state)], - [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def pantheon_control(state: Optional[PantheonViewState]): if state is None: return None + reaction_list = state.reaction_list return EmbedControl( [PantheonView.embed(state)], - [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) @staticmethod def otherinfo_control(state: OtherInfoViewState): + reaction_list = state.reaction_list return EmbedControl( [OtherInfoView.embed(state)], - [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] ) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8cd90771..edda7147 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -7,7 +7,7 @@ import urllib.parse from collections import OrderedDict from io import BytesIO -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional import discord import tsutils @@ -288,12 +288,35 @@ async def send_error(error): transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ await get_id_view_state_data(dgcog, monster) + full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, - use_evo_scroll=settings.checkEvoID(ctx.author.id)) + use_evo_scroll=settings.checkEvoID(ctx.author.id), + reaction_list=initial_reaction_list) menu = IdMenu.menu(original_author_id, friends, self.bot.user.id) await menu.create(ctx, state) + @staticmethod + async def _get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", full_reaction_list: List[Optional[str]]): + # hide some panes if we're in evo scroll mode + if not settings.checkEvoID(ctx.author.id): + return full_reaction_list + alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + if alt_versions is None: + full_reaction_list[0] = None + full_reaction_list[1] = None + full_reaction_list[3] = None + pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + if pantheon_list is None: + full_reaction_list[6] = None + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ + await MaterialsViewState.query(dgcog, monster) + if mats is None: + full_reaction_list[4] = None + return list(filter(None, full_reaction_list)) + async def send_survey_after(self, ctx, query, result_monster): dgcog = await self.get_dgcog() sm = await self.config.user(ctx.author).survey_mode() @@ -371,8 +394,12 @@ async def evos(self, ctx, *, query: str): monster.name_en) + 'which has no alt evos or gems.') return + full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_versions, gem_versions, + reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.evos_control) await menu.create(ctx, state) @@ -400,8 +427,12 @@ async def evomats(self, ctx, *, query: str): await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) return + full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, + reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.mats_control) await menu.create(ctx, state) @@ -428,8 +459,12 @@ async def pantheon(self, ctx, *, query: str): await ctx.send(inline('Too many monsters in this series to display')) return + full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, pantheon_list, series_name, + reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pantheon_control) await menu.create(ctx, state) @@ -492,8 +527,12 @@ async def pic(self, ctx, *, query: str): await self.makeFailureMsg(ctx, query, err) return + full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pic_control) await menu.create(ctx, state) @@ -529,8 +568,12 @@ async def otherinfo(self, ctx, *, query: str): await self.makeFailureMsg(ctx, query, err) return + full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.otherinfo_control) await menu.create(ctx, state) diff --git a/padinfo/view_state/common.py b/padinfo/view_state/common.py index e1d75db9..65cf38ca 100644 --- a/padinfo/view_state/common.py +++ b/padinfo/view_state/common.py @@ -11,3 +11,8 @@ async def get_monster_from_ims(dgcog, user_config: UserConfig, ims: dict): return dgcog.database.graph.get_monster(resolved_monster_id) monster, _, _ = await findMonsterCustom(dgcog, query) return monster + + +def get_reaction_list_from_ims(ims): + reaction_list_str = ims.get('reaction_list') + return reaction_list_str.split(',') if reaction_list_str else None diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index 9cd6982a..87784f2d 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -3,18 +3,21 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims +from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel class EvosViewState(ViewState): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + def __init__(self, original_author_id, menu_type, raw_query, query, color, + monster: "MonsterModel", alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], + reaction_list: List[str] = None, use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.reaction_list = reaction_list self.alt_versions = alt_versions self.gem_versions = gem_versions self.query = query @@ -29,6 +32,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret @@ -39,16 +43,18 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): if alt_versions is None: return None - print('def not kittens') raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] + reaction_list = get_reaction_list_from_ims(ims) - return EvosViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return EvosViewState(original_author_id, menu_type, raw_query, query, user_config.color, + monster, alt_versions, gem_versions, + reaction_list=reaction_list, use_evo_scroll=use_evo_scroll, extra_state=ims) diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 1b4902b3..d8716604 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -4,7 +4,7 @@ from padinfo.core.id import get_id_view_state_data from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims +from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -14,6 +14,7 @@ class IdViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], use_evo_scroll: bool = True, + reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) self.acquire_raw = acquire_raw @@ -25,6 +26,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.query = query self.true_evo_type_raw = true_evo_type_raw self.use_evo_scroll = use_evo_scroll + self.reaction_list = reaction_list def serialize(self): ret = super().serialize() @@ -33,6 +35,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret @@ -48,8 +51,10 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' + reaction_list = get_reaction_list_from_ims(ims) return IdViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, extra_state=ims) diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 8e4e95a7..27b2780b 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -4,7 +4,7 @@ from padinfo.common.external_links import ilmina_skill from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims +from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -14,9 +14,12 @@ class MaterialsViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, - gem_override: bool, use_evo_scroll: bool = True, + gem_override: bool, + use_evo_scroll: bool = True, + reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.reaction_list = reaction_list self.link = link self.skillup_evo_count = skillup_evo_count self.skillups = skillups @@ -37,6 +40,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret @@ -55,10 +59,12 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' + reaction_list = get_reaction_list_from_ims(ims) return MaterialsViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, extra_state=ims) @staticmethod diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py index 8ef18d92..081f7e38 100644 --- a/padinfo/view_state/otherinfo.py +++ b/padinfo/view_state/otherinfo.py @@ -1,9 +1,9 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims +from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -12,8 +12,10 @@ class OtherInfoViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", use_evo_scroll: bool = True, + reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.reaction_list = reaction_list self.color = color self.monster = monster self.query = query @@ -26,6 +28,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret @@ -38,7 +41,9 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' + reaction_list = get_reaction_list_from_ims(ims) return OtherInfoViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, extra_state=ims) diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index b2da095c..e3d4cacc 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -3,7 +3,7 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims +from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -13,8 +13,10 @@ class PantheonViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", pantheon_list: List["MonsterModel"], series_name: str, use_evo_scroll: bool = True, + reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.reaction_list = reaction_list self.series_name = series_name self.pantheon_list = pantheon_list self.color = color @@ -29,6 +31,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret @@ -45,10 +48,12 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] + reaction_list = get_reaction_list_from_ims(ims) return PantheonViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, pantheon_list, series_name, use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, extra_state=ims) @staticmethod diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py index b5041239..f6215088 100644 --- a/padinfo/view_state/pic.py +++ b/padinfo/view_state/pic.py @@ -1,9 +1,9 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims +from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -12,8 +12,10 @@ class PicViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", use_evo_scroll: bool = True, + reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.reaction_list = reaction_list self.color = color self.monster = monster self.query = query @@ -26,6 +28,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret @@ -38,7 +41,8 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] + reaction_list = get_reaction_list_from_ims(ims) return PicViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - use_evo_scroll=use_evo_scroll, + use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) From 405ad4087ed52c713e40c2279052383cc4d1d6ae Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 11 Feb 2021 04:14:56 -0700 Subject: [PATCH 0596/1220] Add status manager to dadguide load (#788) --- dadguide/dadguide.py | 24 ++++++++++++------------ dadguide/info.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 5438300a..a1569ad6 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -8,25 +8,24 @@ """ import asyncio import csv -import difflib import json -import logging import os import shutil -import tsutils -from io import BytesIO from collections import defaultdict +from io import BytesIO +from typing import Optional + +import tsutils from redbot.core import checks, data_manager from redbot.core import commands +from . import token_mappings +from .database_loader import load_database from .database_manager import * +from .models.monster_model import MonsterModel from .models.monster_stats import monster_stats, MonsterStatModifierInput -from .old_monster_index import MonsterIndex from .monster_index import MonsterIndex2 -from .database_loader import load_database -from . import token_mappings - -from .models.monster_model import MonsterModel +from .old_monster_index import MonsterIndex logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -80,8 +79,8 @@ def __init__(self, bot, *args, **kwargs): self.translated_names = {} self.database = None - self.index = None # type: MonsterIndex - self.index2 = None # type: MonsterIndex2 + self.index = None # type: Optional[MonsterIndex] + self.index2 = None # type: Optional[MonsterIndex2] self.monster_stats = monster_stats self.MonsterStatModifierInput = MonsterStatModifierInput @@ -144,7 +143,8 @@ async def reload_data_task(self): while self == self.bot.get_cog('Dadguide'): short_wait = False try: - await self.download_and_refresh_nicknames() + async with tsutils.StatusManager(self.bot): + await self.download_and_refresh_nicknames() logger.info('Done refreshing Dadguide, triggering ready') self._is_ready.set() except Exception as ex: diff --git a/dadguide/info.json b/dadguide/info.json index 48de9dd2..554084d7 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils>=3.2.0", + "tsutils>=3.2.1", "pytz", "romkan", "networkx" From 1c039dbfcd24d7568e7d0bfccfcbae459d7bbd1f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 11 Feb 2021 17:26:59 -0600 Subject: [PATCH 0597/1220] Refactor id new menu code (#789) * Create ViewStateBaseId & refactor to use it * Remove user pref from get_monster_from_ims * Move core/id functions to IdViewState.query * Change pantheon VS to classmethod, reformat --- padinfo/core/id.py | 16 --------- padinfo/id_menu.py | 4 +-- padinfo/id_menu_old.py | 5 ++- padinfo/padinfo.py | 3 +- padinfo/view/components/base.py | 4 +-- padinfo/view_state/base.py | 2 +- padinfo/view_state/base_id.py | 51 ++++++++++++++++++++++++++ padinfo/view_state/common.py | 3 +- padinfo/view_state/evos.py | 36 ++++++++----------- padinfo/view_state/id.py | 55 +++++++++++++++++------------ padinfo/view_state/leader_skill.py | 4 +-- padinfo/view_state/materials.py | 33 ++++++++--------- padinfo/view_state/otherinfo.py | 42 ++-------------------- padinfo/view_state/pantheon.py | 34 ++++++++---------- padinfo/view_state/pic.py | 40 ++------------------- padinfo/view_state/transforminfo.py | 12 +++---- 16 files changed, 147 insertions(+), 197 deletions(-) delete mode 100644 padinfo/core/id.py create mode 100644 padinfo/view_state/base_id.py diff --git a/padinfo/core/id.py b/padinfo/core/id.py deleted file mode 100644 index afeaa983..00000000 --- a/padinfo/core/id.py +++ /dev/null @@ -1,16 +0,0 @@ -async def get_id_view_state_data(dgcog, monster): - db_context = dgcog.database - acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ - await get_monster_misc_info(db_context, monster) - - return transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters - - -async def get_monster_misc_info(db_context, monster): - transform_base = db_context.graph.get_transform_base(monster) - true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value - acquire_raw = db_context.graph.monster_acquisition(monster) - base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity - alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, - key=lambda x: x.monster_id) - return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index df0f0534..6a2ac04a 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -51,7 +51,7 @@ def menu(original_author_id, friend_ids, bot_id, initial_control=None): async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] db_context: "DbContext" = dgcog.database - m = db_context.graph.get_monster(ims['resolved_monster_id']) + m = db_context.graph.get_monster(int(ims['resolved_monster_id'])) use_evo_scroll = ims.get('use_evo_scroll') != 'False' new_monster_id = str(IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll)) @@ -76,7 +76,7 @@ def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_ev async def respond_with_right(message: Optional[Message], ims, **data): dgcog = data['dgcog'] db_context: "DbContext" = dgcog.database - m = db_context.graph.get_monster(ims['resolved_monster_id']) + m = db_context.graph.get_monster(int(ims['resolved_monster_id'])) use_evo_scroll = ims.get('use_evo_scroll') != 'False' new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll)) diff --git a/padinfo/id_menu_old.py b/padinfo/id_menu_old.py index 1c831c86..6f602b95 100644 --- a/padinfo/id_menu_old.py +++ b/padinfo/id_menu_old.py @@ -4,10 +4,9 @@ import discord from discord import Color -from padinfo.core.id import get_monster_misc_info from padinfo.core.padinfo_settings import settings from padinfo.view.id import IdView -from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillSingleView +from padinfo.view.leader_skill import LeaderSkillSingleView from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView from padinfo.view_state.id import IdViewState @@ -35,7 +34,7 @@ async def get_user_embed_color(self, pdicog): async def make_id_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ - await get_monster_misc_info(self.db_context, m) + await IdViewState.query(self.db_context, m) state = IdViewState("", "TODO", "todo", "", color, m, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, use_evo_scroll=settings.checkEvoID(self.ctx.author.id)) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index edda7147..6e99353f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -28,7 +28,6 @@ from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom from padinfo.core.historic_lookups import historic_lookups -from padinfo.core.id import get_id_view_state_data from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query @@ -287,7 +286,7 @@ async def send_error(error): asyncio.create_task(self.send_survey_after(ctx, query, monster)) transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ - await get_id_view_state_data(dgcog, monster) + await IdViewState.query(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) diff --git a/padinfo/view/components/base.py b/padinfo/view/components/base.py index 67350b07..d60a6f96 100644 --- a/padinfo/view/components/base.py +++ b/padinfo/view/components/base.py @@ -1,7 +1,7 @@ from discordmenu.embed.components import EmbedFooter from discordmenu.intra_message_state import IntraMessageState -from padinfo.view_state.base import ViewState +from padinfo.view_state.base import ViewStateBase TSUBAKI_FLOWER_ICON_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/tsubaki/tsubakiflower.png' @@ -10,7 +10,7 @@ def pad_info_footer(): return EmbedFooter('Requester may click the reactions below to switch tabs') -def pad_info_footer_with_state(state: ViewState): +def pad_info_footer_with_state(state: ViewStateBase): url = IntraMessageState.serialize(TSUBAKI_FLOWER_ICON_URL, state.serialize()) return EmbedFooter( 'Requester may click the reactions below to switch tabs', diff --git a/padinfo/view_state/base.py b/padinfo/view_state/base.py index afc874c8..20303cf3 100644 --- a/padinfo/view_state/base.py +++ b/padinfo/view_state/base.py @@ -1,4 +1,4 @@ -class ViewState: +class ViewStateBase: def __init__(self, original_author_id, menu_type, raw_query, extra_state=None): self.extra_state = extra_state or {} self.menu_type = menu_type diff --git a/padinfo/view_state/base_id.py b/padinfo/view_state/base_id.py new file mode 100644 index 00000000..177eac8a --- /dev/null +++ b/padinfo/view_state/base_id.py @@ -0,0 +1,51 @@ +from typing import TYPE_CHECKING, List + +from padinfo.common.config import UserConfig +from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class ViewStateBaseId: + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + use_evo_scroll: bool = True, + reaction_list: List[str] = None, + extra_state=None): + self.extra_state = extra_state or {} + self.menu_type = menu_type + self.original_author_id = original_author_id + self.raw_query = raw_query + self.reaction_list = reaction_list + self.color = color + self.monster = monster + self.query = query + self.use_evo_scroll = use_evo_scroll + + def serialize(self): + ret = { + 'raw_query': self.raw_query, + 'menu_type': self.menu_type, + 'original_author_id': self.original_author_id, + 'query': self.query, + 'resolved_monster_id': self.monster.monster_id, + 'use_evo_scroll': str(self.use_evo_scroll), + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, + } + ret.update(self.extra_state) + return ret + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + monster = await get_monster_from_ims(dgcog, ims) + + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + menu_type = ims['menu_type'] + reaction_list = get_reaction_list_from_ims(ims) + + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, + extra_state=ims) diff --git a/padinfo/view_state/common.py b/padinfo/view_state/common.py index 65cf38ca..2eb4b953 100644 --- a/padinfo/view_state/common.py +++ b/padinfo/view_state/common.py @@ -1,8 +1,7 @@ -from padinfo.common.config import UserConfig from padinfo.core.find_monster import findMonsterCustom -async def get_monster_from_ims(dgcog, user_config: UserConfig, ims: dict): +async def get_monster_from_ims(dgcog, ims: dict): query = ims.get('query') or ims['raw_query'] resolved_monster_id_str = ims.get('resolved_monster_id') diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index 87784f2d..19d2e4d7 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -2,43 +2,37 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames -from padinfo.view_state.base import ViewState +from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel -class EvosViewState(ViewState): +class EvosViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], reaction_list: List[str] = None, use_evo_scroll: bool = True, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) - self.reaction_list = reaction_list + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=extra_state) self.alt_versions = alt_versions self.gem_versions = gem_versions - self.query = query - self.monster = monster - self.color = color - self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.evos, - 'query': self.query, - 'resolved_monster_id': self.monster.monster_id, - 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - monster = await get_monster_from_ims(dgcog, user_config, ims) + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + monster = await get_monster_from_ims(dgcog, ims) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) if alt_versions is None: @@ -51,12 +45,12 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] reaction_list = get_reaction_list_from_ims(ims) - return EvosViewState(original_author_id, menu_type, raw_query, query, user_config.color, - monster, - alt_versions, gem_versions, - reaction_list=reaction_list, - use_evo_scroll=use_evo_scroll, - extra_state=ims) + return cls(original_author_id, menu_type, raw_query, query, user_config.color, + monster, + alt_versions, gem_versions, + reaction_list=reaction_list, + use_evo_scroll=use_evo_scroll, + extra_state=ims) @staticmethod async def query(dgcog, monster): diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index d8716604..39892f89 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -1,49 +1,42 @@ from typing import List, TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.core.id import get_id_view_state_data from padinfo.pane_names import IdMenuPaneNames -from padinfo.view_state.base import ViewState +from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel -class IdViewState(ViewState): +class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=extra_state) self.acquire_raw = acquire_raw self.alt_monsters = alt_monsters - self.color = color self.base_rarity = base_rarity self.transform_base = transform_base - self.monster = monster - self.query = query self.true_evo_type_raw = true_evo_type_raw - self.use_evo_scroll = use_evo_scroll - self.reaction_list = reaction_list def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.id, - 'query': self.query, - 'resolved_monster_id': self.monster.monster_id, - 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - monster = await get_monster_from_ims(dgcog, user_config, ims) + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + monster = await get_monster_from_ims(dgcog, ims) transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ - await get_id_view_state_data(dgcog, monster) + await IdViewState.query(dgcog, monster) raw_query = ims['raw_query'] # This is to support the 2 vs 1 monster query difference between ^ls and ^id @@ -53,8 +46,26 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = get_reaction_list_from_ims(ims) - return IdViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=ims) + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ + await IdViewState._get_monster_misc_info(db_context, monster) + + return transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters + + @staticmethod + async def _get_monster_misc_info(db_context, monster): + transform_base = db_context.graph.get_transform_base(monster) + true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value + acquire_raw = db_context.graph.monster_acquisition(monster) + base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity + alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + key=lambda x: x.monster_id) + return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw diff --git a/padinfo/view_state/leader_skill.py b/padinfo/view_state/leader_skill.py index f7c1c806..b7fe3fe5 100644 --- a/padinfo/view_state/leader_skill.py +++ b/padinfo/view_state/leader_skill.py @@ -1,9 +1,9 @@ from padinfo.common.config import UserConfig from padinfo.core.leader_skills import perform_leaderskill_query -from padinfo.view_state.base import ViewState +from padinfo.view_state.base import ViewStateBase -class LeaderSkillViewState(ViewState): +class LeaderSkillViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, l_mon, r_mon, l_query, r_query): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 27b2780b..246acdc5 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -3,14 +3,14 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill from padinfo.pane_names import IdMenuPaneNames -from padinfo.view_state.base import ViewState +from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel -class MaterialsViewState(ViewState): +class MaterialsViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, @@ -18,7 +18,10 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + reaction_list=reaction_list, + use_evo_scroll=use_evo_scroll, + extra_state=extra_state) self.reaction_list = reaction_list self.link = link self.skillup_evo_count = skillup_evo_count @@ -28,26 +31,18 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.usedin = usedin self.gemid = gemid self.gem_override = gem_override - self.query = query - self.monster = monster - self.color = color - self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.materials, - 'query': self.query, - 'resolved_monster_id': self.monster.monster_id, - 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): - monster = await get_monster_from_ims(dgcog, user_config, ims) + monster = await get_monster_from_ims(dgcog, ims) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ await MaterialsViewState.query(dgcog, monster) @@ -61,11 +56,11 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = get_reaction_list_from_ims(ims) - return MaterialsViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=ims) + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=ims) @staticmethod async def query(dgcog, monster): diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py index 081f7e38..289f92c1 100644 --- a/padinfo/view_state/otherinfo.py +++ b/padinfo/view_state/otherinfo.py @@ -1,49 +1,11 @@ -from typing import TYPE_CHECKING, List - -from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames -from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - +from padinfo.view_state.base_id import ViewStateBaseId -class OtherInfoViewState(ViewState): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - use_evo_scroll: bool = True, - reaction_list: List[str] = None, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) - self.reaction_list = reaction_list - self.color = color - self.monster = monster - self.query = query - self.use_evo_scroll = use_evo_scroll +class OtherInfoViewState(ViewStateBaseId): def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.otherinfo, - 'query': self.query, - 'resolved_monster_id': self.monster.monster_id, - 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret - - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - monster = await get_monster_from_ims(dgcog, user_config, ims) - - raw_query = ims['raw_query'] - query = ims.get('query') or raw_query - menu_type = ims['menu_type'] - original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - reaction_list = get_reaction_list_from_ims(ims) - - return OtherInfoViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=ims) diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index e3d4cacc..6b69b860 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -2,42 +2,36 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames -from padinfo.view_state.base import ViewState +from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel -class PantheonViewState(ViewState): +class PantheonViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", pantheon_list: List["MonsterModel"], series_name: str, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) - self.reaction_list = reaction_list + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=extra_state) self.series_name = series_name self.pantheon_list = pantheon_list - self.color = color - self.monster = monster - self.query = query - self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.pantheon, - 'query': self.query, - 'resolved_monster_id': self.monster.monster_id, - 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - monster = await get_monster_from_ims(dgcog, user_config, ims) + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + monster = await get_monster_from_ims(dgcog, ims) pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: @@ -50,11 +44,11 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] reaction_list = get_reaction_list_from_ims(ims) - return PantheonViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - pantheon_list, series_name, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=ims) + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + pantheon_list, series_name, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=ims) @staticmethod async def query(dgcog, monster): diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py index f6215088..b7663165 100644 --- a/padinfo/view_state/pic.py +++ b/padinfo/view_state/pic.py @@ -1,48 +1,12 @@ -from typing import TYPE_CHECKING, List - -from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames -from padinfo.view_state.base import ViewState -from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel +from padinfo.view_state.base_id import ViewStateBaseId -class PicViewState(ViewState): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - use_evo_scroll: bool = True, - reaction_list: List[str] = None, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) - self.reaction_list = reaction_list - self.color = color - self.monster = monster - self.query = query - self.use_evo_scroll = use_evo_scroll +class PicViewState(ViewStateBaseId): def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.pic, - 'query': self.query, - 'resolved_monster_id': self.monster.monster_id, - 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, }) return ret - - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - monster = await get_monster_from_ims(dgcog, user_config, ims) - - raw_query = ims['raw_query'] - query = ims.get('query') or raw_query - original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - menu_type = ims['menu_type'] - reaction_list = get_reaction_list_from_ims(ims) - - return PicViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, - extra_state=ims) diff --git a/padinfo/view_state/transforminfo.py b/padinfo/view_state/transforminfo.py index a15a1302..42e678a4 100644 --- a/padinfo/view_state/transforminfo.py +++ b/padinfo/view_state/transforminfo.py @@ -1,10 +1,8 @@ from padinfo.common.config import UserConfig -from padinfo.core.id import get_monster_misc_info -from padinfo.core.transforminfo import perform_transforminfo_query -from padinfo.view_state.base import ViewState +from padinfo.view_state.base import ViewStateBase -class TransformInfoViewState(ViewState): +class TransformInfoViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, base_rarity, acquire_raw, true_evo_type_raw): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) @@ -44,7 +42,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): @staticmethod async def query(dgcog, base_mon, transformed_mon): db_context = dgcog.database - acquire_raw, _, base_rarity, _, true_evo_type_raw = \ - await get_monster_misc_info(db_context, transformed_mon) - + acquire_raw = db_context.graph.monster_acquisition(transformed_mon) + base_rarity = db_context.graph.get_base_monster_by_id(transformed_mon.monster_no).rarity + true_evo_type_raw = db_context.graph.true_evo_type_by_monster(transformed_mon).value return acquire_raw, base_rarity, true_evo_type_raw From 41af22311440add4265e4e09fb992af2a160430b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 11 Feb 2021 20:24:15 -0600 Subject: [PATCH 0598/1220] Warn when numberscroll reaches nonexistent monster (#791) --- padinfo/id_menu.py | 20 +++++++++++++++----- padinfo/view_state/base_id.py | 2 ++ padinfo/view_state/evos.py | 2 ++ padinfo/view_state/id.py | 3 +++ padinfo/view_state/materials.py | 3 ++- padinfo/view_state/pantheon.py | 2 ++ 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 6a2ac04a..27adaf98 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -54,8 +54,10 @@ async def respond_with_left(message: Optional[Message], ims, **data): m = db_context.graph.get_monster(int(ims['resolved_monster_id'])) use_evo_scroll = ims.get('use_evo_scroll') != 'False' - new_monster_id = str(IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll)) - ims['resolved_monster_id'] = new_monster_id + new_monster_id = IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll) + if new_monster_id is None: + ims['unsupported_transition'] = True + ims['resolved_monster_id'] = str(new_monster_id) if new_monster_id else None pane_type = ims['pane_type'] pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] @@ -70,7 +72,7 @@ def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_ev return new_id else: prev_monster = db_context.graph.numeric_prev_monster(monster) - return prev_monster.monster_id if prev_monster else monster.monster_id + return prev_monster.monster_id if prev_monster else None @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): @@ -80,7 +82,9 @@ async def respond_with_right(message: Optional[Message], ims, **data): use_evo_scroll = ims.get('use_evo_scroll') != 'False' new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll)) - ims['resolved_monster_id'] = new_monster_id + if new_monster_id is None: + ims['unsupported_transition'] = True + ims['resolved_monster_id'] = str(new_monster_id) if new_monster_id else None pane_type = ims.get('pane_type') pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] @@ -99,7 +103,7 @@ def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_ev return new_id else: next_monster = db_context.graph.numeric_next_monster(monster) - return next_monster.monster_id if next_monster else monster.monster_id + return next_monster.monster_id if next_monster else None @staticmethod async def respond_with_current_id(message: Optional[Message], ims, **data): @@ -157,6 +161,8 @@ async def respond_with_otherinfo(message: Optional[Message], ims, **data): @staticmethod def id_control(state: IdViewState): + if state is None: + return None reaction_list = state.reaction_list return EmbedControl( [IdView.embed(state)], @@ -185,6 +191,8 @@ def mats_control(state: Optional[MaterialsViewState]): @staticmethod def pic_control(state: PicViewState): + if state is None: + return None reaction_list = state.reaction_list return EmbedControl( [PicView.embed(state)], @@ -203,6 +211,8 @@ def pantheon_control(state: Optional[PantheonViewState]): @staticmethod def otherinfo_control(state: OtherInfoViewState): + if state is None: + return None reaction_list = state.reaction_list return EmbedControl( [OtherInfoView.embed(state)], diff --git a/padinfo/view_state/base_id.py b/padinfo/view_state/base_id.py index 177eac8a..1c619fa0 100644 --- a/padinfo/view_state/base_id.py +++ b/padinfo/view_state/base_id.py @@ -37,6 +37,8 @@ def serialize(self): @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None monster = await get_monster_from_ims(dgcog, ims) raw_query = ims['raw_query'] diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index 19d2e4d7..9e5d7a82 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -32,6 +32,8 @@ def serialize(self): @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None monster = await get_monster_from_ims(dgcog, ims) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 39892f89..fec2392e 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -34,6 +34,9 @@ def serialize(self): @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + # for numberscroll getting to a gap in monster book, or 1, or last monster + if ims.get('unsupported_transition'): + return None monster = await get_monster_from_ims(dgcog, ims) transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ await IdViewState.query(dgcog, monster) diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 246acdc5..25fdeb6d 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -41,7 +41,8 @@ def serialize(self): @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): - + if ims.get('unsupported_transition'): + return None monster = await get_monster_from_ims(dgcog, ims) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ await MaterialsViewState.query(dgcog, monster) diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index 6b69b860..28ae8cea 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -31,6 +31,8 @@ def serialize(self): @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None monster = await get_monster_from_ims(dgcog, ims) pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) From 4f0ec426c38fda5db2e7a3a6a6f705f6ba9878d5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 11 Feb 2021 21:14:41 -0600 Subject: [PATCH 0599/1220] Remove unused menu1 emojis from padinfo (#792) --- padinfo/padinfo.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6e99353f..3dd48642 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -82,19 +82,9 @@ def __init__(self, bot, *args, **kwargs): self.menu = Menu(bot) # These emojis are the keys into the idmenu submenus - self.id_emoji = '\N{HOUSE BUILDING}' - self.evo_emoji = char_to_emoji('e') - self.mats_emoji = '\N{MEAT ON BONE}' self.ls_emoji = '\N{HOUSE BUILDING}' self.left_emoji = char_to_emoji('l') self.right_emoji = char_to_emoji('r') - self.pantheon_emoji = '\N{CLASSICAL BUILDING}' - self.pic_emoji = '\N{FRAME WITH PICTURE}' - self.other_info_emoji = '\N{SCROLL}' - self.first_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' - self.previous_monster_emoji = '\N{BLACK LEFT-POINTING TRIANGLE}' - self.next_monster_emoji = '\N{BLACK RIGHT-POINTING TRIANGLE}' - self.last_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' self.remove_emoji = self.menu.emoji['no'] self.config = Config.get_conf(self, identifier=9401770) From 950e86a7f305bd215236070fd1eb54e9613b5c4f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 11 Feb 2021 21:43:13 -0600 Subject: [PATCH 0600/1220] Fix bugs in lssingle & evolist from refactoring (#793) --- padinfo/id_menu_old.py | 8 ++++---- padinfo/padinfo.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/padinfo/id_menu_old.py b/padinfo/id_menu_old.py index 6f602b95..e6477762 100644 --- a/padinfo/id_menu_old.py +++ b/padinfo/id_menu_old.py @@ -17,9 +17,9 @@ class IdMenu: - def __init__(self, ctx, db_context: "DbContext" = None, allowed_emojis: list = None): + def __init__(self, ctx, dgcog=None, allowed_emojis: list = None): self.ctx = ctx - self.db_context = db_context + self.dgcog = dgcog self.allowed_emojis = allowed_emojis async def get_user_embed_color(self, pdicog): @@ -33,8 +33,8 @@ async def get_user_embed_color(self, pdicog): async def make_id_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ - await IdViewState.query(self.db_context, m) + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + await IdViewState.query(self.dgcog, m) state = IdViewState("", "TODO", "todo", "", color, m, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, use_evo_scroll=settings.checkEvoID(self.ctx.author.id)) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3dd48642..cfd6b31d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -466,7 +466,7 @@ async def _do_evolistmenu(self, ctx, sm): monsters.sort(key=lambda x: x.monster_id) emoji_to_embed = OrderedDict() - menu = IdMenuOld(ctx, db_context=db_context, allowed_emojis=self.get_emojis()) + menu = IdMenuOld(ctx, dgcog=DGCOG, allowed_emojis=self.get_emojis()) starting_menu_emoji = None for idx, m in enumerate(monsters): chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" @@ -657,7 +657,7 @@ async def leaderskillsingle(self, ctx, *, query): if err: await ctx.send(err) return - menu = IdMenuOld(ctx, db_context=dgcog.database, allowed_emojis=self.get_emojis()) + menu = IdMenuOld(ctx, dgcog=dgcog, allowed_emojis=self.get_emojis()) emoji_to_embed = OrderedDict() emoji_to_embed[self.ls_emoji] = await menu.make_lssingle_embed(m) emoji_to_embed[self.left_emoji] = await menu.make_id_embed(m) From de68f7c5751aa2fcc673cb5c9b5d14792262626c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 12 Feb 2021 17:02:23 -0600 Subject: [PATCH 0601/1220] Replace hardcoded indices with IdMenuPanes (#794) --- padinfo/padinfo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index cfd6b31d..3fc5cc12 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -294,16 +294,16 @@ async def _get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel" return full_reaction_list alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) if alt_versions is None: - full_reaction_list[0] = None - full_reaction_list[1] = None - full_reaction_list[3] = None + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_left][0])] = None + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_right][0])] = None + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_evos][0])] = None pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: - full_reaction_list[6] = None + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_pantheon][0])] = None mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ await MaterialsViewState.query(dgcog, monster) if mats is None: - full_reaction_list[4] = None + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_mats][0])] = None return list(filter(None, full_reaction_list)) async def send_survey_after(self, ctx, query, result_monster): From ca4edccb24f0d327d83ac0cc13ae8a033c82bb44 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 12 Feb 2021 23:45:40 -0600 Subject: [PATCH 0602/1220] Create MonsterListMenu & use to update EvoList (#796) * initial commit, doesn't work tho * It works now! * Add title --- padinfo/MonsterListMenu.py | 156 +++++++++++++++++++++++++++++ padinfo/padinfo.py | 64 ++++++------ padinfo/view/monster_list.py | 32 ++++++ padinfo/view_state/monster_list.py | 51 ++++++++++ 4 files changed, 272 insertions(+), 31 deletions(-) create mode 100644 padinfo/MonsterListMenu.py create mode 100644 padinfo/view/monster_list.py create mode 100644 padinfo/view_state/monster_list.py diff --git a/padinfo/MonsterListMenu.py b/padinfo/MonsterListMenu.py new file mode 100644 index 00000000..5590e9d6 --- /dev/null +++ b/padinfo/MonsterListMenu.py @@ -0,0 +1,156 @@ +from typing import TYPE_CHECKING, Optional + +from discord import Message +from discordmenu.embed.emoji import EmbedMenuEmojiConfig +from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.emoji.emoji_cache import emoji_cache +from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ + MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter +from tsutils import char_to_emoji + +from padinfo.pane_names import IdMenuPaneNames +from padinfo.view.id import IdView +from padinfo.view.monster_list import MonsterListView +from padinfo.view_state.id import IdViewState +from padinfo.view_state.monster_list import MonsterListViewState + +if TYPE_CHECKING: + pass + +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') + + +class MonsterListMenu: + MENU_TYPE = 'MonsterListMenu' + + @staticmethod + def menu(original_author_id, friend_ids, bot_id, initial_control=None): + if initial_control is None: + initial_control = MonsterListMenu.monster_list_control + + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(MonsterListMenuPanes.emoji_names()) + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(bot_id), + MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) + ] + embed = EmbedMenu(reaction_filters, MonsterListMenuPanes.transitions(), initial_control, menu_emoji_config) + return embed + + @staticmethod + async def respond_with_monster_list(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + view_state = await MonsterListViewState.deserialize(dgcog, user_config, ims) + control = MonsterListMenu.monster_list_control(view_state) + return control + + @staticmethod + async def respond_with_n(message: Optional[Message], ims, n, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + ims['resolved_monster_id'] = int(ims['monster_list'][n]) + + view_state = await IdViewState.deserialize(dgcog, user_config, ims) + control = MonsterListMenu.id_control(view_state) + return control + + @staticmethod + async def respond_with_0(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 0, **data) + + @staticmethod + async def respond_with_1(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 1, **data) + + @staticmethod + async def respond_with_2(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 2, **data) + + @staticmethod + async def respond_with_3(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 3, **data) + + @staticmethod + async def respond_with_4(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 4, **data) + + @staticmethod + async def respond_with_5(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 5, **data) + + @staticmethod + async def respond_with_6(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 6, **data) + + @staticmethod + async def respond_with_7(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 7, **data) + + @staticmethod + async def respond_with_8(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 8, **data) + + @staticmethod + async def respond_with_9(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 9, **data) + + @staticmethod + async def respond_with_10(message: Optional[Message], ims, **data): + return await MonsterListMenu.respond_with_n(message, ims, 10, **data) + + @staticmethod + def monster_list_control(state: MonsterListViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [MonsterListView.embed(state)], + reaction_list + ) + + @staticmethod + def id_control(state: IdViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [IdView.embed(state)], + reaction_list + ) + + +class MonsterListMenuPanes: + INITIAL_EMOJI = '\N{HOUSE BUILDING}' + DATA = { + MonsterListMenu.respond_with_monster_list: ('\N{HOUSE BUILDING}', IdMenuPaneNames.evos), + MonsterListMenu.respond_with_0: (char_to_emoji('0'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_1: (char_to_emoji('1'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_2: (char_to_emoji('2'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_3: (char_to_emoji('3'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_4: (char_to_emoji('4'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_5: (char_to_emoji('5'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_6: (char_to_emoji('6'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_7: (char_to_emoji('7'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_8: (char_to_emoji('8'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_9: (char_to_emoji('9'), IdMenuPaneNames.id), + MonsterListMenu.respond_with_10: ('\N{KEYCAP TEN}', IdMenuPaneNames.id), + } + + @classmethod + def emoji_names(cls): + return [v[0] for k, v in cls.DATA.items()] + + @classmethod + def transitions(cls): + return {v[0]: k for k, v in cls.DATA.items()} + + @classmethod + def pane_types(cls): + return {v[1]: k for k, v in cls.DATA.items() if v[1]} + + @staticmethod + def get_initial_reaction_list(number_of_evos: int): + return MonsterListMenuPanes.emoji_names()[:number_of_evos+1] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3fc5cc12..72d2ac90 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -20,6 +20,7 @@ from tabulate import tabulate from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor, rmdiacritics +from padinfo.MonsterListMenu import MonsterListMenu, MonsterListMenuPanes from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ get_attribute_emoji_by_monster @@ -41,6 +42,7 @@ from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState from padinfo.view_state.materials import MaterialsViewState +from padinfo.view_state.monster_list import MonsterListViewState from padinfo.view_state.otherinfo import OtherInfoViewState from padinfo.view_state.pantheon import PantheonViewState from padinfo.view_state.pic import PicViewState @@ -148,6 +150,7 @@ async def test_reaction_add(self, reaction, member): if not (emoji_clicked in ls_menu_emoji_button_names or emoji_clicked in IdMenuPanes.emoji_names() or + emoji_clicked in MonsterListMenuPanes.emoji_names() or emoji_clicked in tf_menu_emoji_button_names): return @@ -161,7 +164,8 @@ async def test_reaction_add(self, reaction, member): menu_map = { LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu.menu, IdMenu.MENU_TYPE: IdMenu.menu, - TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu + TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu, + MonsterListMenu.MENU_TYPE: MonsterListMenu.menu, } menu_func = menu_map.get(menu_type) @@ -288,7 +292,8 @@ async def send_error(error): await menu.create(ctx, state) @staticmethod - async def _get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", full_reaction_list: List[Optional[str]]): + async def _get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", + full_reaction_list: List[Optional[str]]): # hide some panes if we're in evo scroll mode if not settings.checkEvoID(ctx.author.id): return full_reaction_list @@ -458,30 +463,6 @@ async def pantheon(self, ctx, *, query: str): menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pantheon_control) await menu.create(ctx, state) - async def _do_evolistmenu(self, ctx, sm): - DGCOG = self.bot.get_cog("Dadguide") - db_context = DGCOG.database - - monsters = db_context.graph.get_alt_monsters_by_id(sm.monster_id) - monsters.sort(key=lambda x: x.monster_id) - - emoji_to_embed = OrderedDict() - menu = IdMenuOld(ctx, dgcog=DGCOG, allowed_emojis=self.get_emojis()) - starting_menu_emoji = None - for idx, m in enumerate(monsters): - chars = "0123456789\N{KEYCAP TEN}ABCDEFGHI" - if idx > 19: - await ctx.send( - "There are too many evos for this monster to display. Try using `{}evolist`.".format(ctx.prefix)) - return - else: - emoji = char_to_emoji(chars[idx]) - emoji_to_embed[emoji] = await menu.make_id_embed(m) - if m.monster_id == sm.monster_id: - starting_menu_emoji = emoji - - return await self._do_menu(ctx, starting_menu_emoji, EmojiUpdater(emoji_to_embed), timeout=60) - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): if starting_menu_emoji not in emoji_to_embed.emoji_dict: # Selected menu wasn't generated for this monster @@ -598,13 +579,34 @@ async def lookup(self, ctx, *, query: str): @commands.command() @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): - """Monster info (for all monsters in the evo tree)""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if m is not None: - await self._do_evolistmenu(ctx, m) - else: + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + + if monster is None: await self.makeFailureMsg(ctx, query, err) + return + + alt_versions, _ = await EvosViewState.query(dgcog, monster) + + if alt_versions is None: + await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, + monster.name_en) + 'which has no alt evos.') + return + + initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(alt_versions)) + + state = MonsterListViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, query, color, + alt_versions, 'Evolution List', + reaction_list=initial_reaction_list + ) + menu = MonsterListMenu.menu(original_author_id, friends, self.bot.user.id) + await menu.create(ctx, state) @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list.py new file mode 100644 index 00000000..8051122a --- /dev/null +++ b/padinfo/view/monster_list.py @@ -0,0 +1,32 @@ +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView + +from padinfo.view.components.base import pad_info_footer_with_state +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view_state.monster_list import MonsterListViewState + + +def _monster_list(monsters): + if not len(monsters): + return [] + return [ + MonsterHeader.short_with_emoji(mon, link=True) + for mon in sorted(monsters, key=lambda x: int(x.monster_id)) + ] + + +class MonsterListView: + @staticmethod + def embed(state: MonsterListViewState): + fields = [ + EmbedField(state.title, + Box(*_monster_list(state.monster_list))) + ] + + return EmbedView( + EmbedMain( + color=state.color, + ), + embed_footer=pad_info_footer_with_state(state), + embed_fields=fields) diff --git a/padinfo/view_state/monster_list.py b/padinfo/view_state/monster_list.py new file mode 100644 index 00000000..257a44d7 --- /dev/null +++ b/padinfo/view_state/monster_list.py @@ -0,0 +1,51 @@ +from typing import TYPE_CHECKING, List + +from padinfo.common.config import UserConfig +from padinfo.pane_names import IdMenuPaneNames +from padinfo.view_state.base import ViewStateBase +from padinfo.view_state.common import get_reaction_list_from_ims + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class MonsterListViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, query, color, + monster_list: List["MonsterModel"], title, + reaction_list=None, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, + extra_state=extra_state) + self.title = title + self.monster_list = monster_list + self.reaction_list = reaction_list + self.color = color + self.query = query + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': IdMenuPaneNames.id, + 'title': self.title, + 'monster_list': [str(m.monster_no) for m in self.monster_list], + 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None + monster_list = [dgcog.database.graph.get_monster(int(m)) for m in ims['monster_list']] + title = ims['title'] + + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + reaction_list = get_reaction_list_from_ims(ims) + + return MonsterListViewState(original_author_id, menu_type, raw_query, query, user_config.color, + monster_list=monster_list, title=title, + reaction_list=reaction_list, + extra_state=ims) From 312befae9190cbc00dda78827763ef8ee55a9edf Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 06:18:23 -0700 Subject: [PATCH 0603/1220] Fix id1 (#800) --- padinfo/padinfo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 72d2ac90..bd885f6b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -256,7 +256,10 @@ async def _do_id(self, ctx, query: str, force_id3_pref=None): f" info about `id3` check out" f" <{IDGUIDE}>!") - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + if force_id3_pref is not False: + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + else: + monster, err, debug_info = await findMonster1(dgcog, raw_query) if not monster: await self.makeFailureMsg(ctx, query, err) From 3432bfbc7bdbdd2c77166cfdc41d7e9448fbdaa6 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 06:21:53 -0700 Subject: [PATCH 0604/1220] Remove Index1 (Actually) (#799) * Remove Index1 (Actually) * Remove 800 --- dadguide/__init__.py | 2 +- dadguide/dadguide.py | 35 --- dadguide/old_monster_index.py | 521 ---------------------------------- padglobal/padglobal.py | 143 +--------- padinfo/core/find_monster.py | 29 +- padinfo/padinfo.py | 63 +--- padsearch/padsearch.py | 19 +- 7 files changed, 26 insertions(+), 786 deletions(-) delete mode 100644 dadguide/old_monster_index.py diff --git a/dadguide/__init__.py b/dadguide/__init__.py index 1159ca75..dbe92ee4 100644 --- a/dadguide/__init__.py +++ b/dadguide/__init__.py @@ -1,4 +1,4 @@ -from .dadguide import * +from .dadguide import Dadguide __red_end_user_data_statement__ = "No personal data is stored." diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index a1569ad6..55759425 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -8,7 +8,6 @@ """ import asyncio import csv -import json import os import shutil from collections import defaultdict @@ -25,7 +24,6 @@ from .models.monster_model import MonsterModel from .models.monster_stats import monster_stats, MonsterStatModifierInput from .monster_index import MonsterIndex2 -from .old_monster_index import MonsterIndex logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -79,7 +77,6 @@ def __init__(self, bot, *args, **kwargs): self.translated_names = {} self.database = None - self.index = None # type: Optional[MonsterIndex] self.index2 = None # type: Optional[MonsterIndex2] self.monster_stats = monster_stats @@ -107,15 +104,6 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return - async def create_index(self, accept_filter=None): - """Exported function that allows a client cog to create an id1/2 monster index""" - await self.wait_until_ready() - return await MonsterIndex(self.database, - self.nickname_overrides, - self.treename_overrides, - self.panthname_overrides, - accept_filter=accept_filter) - async def create_index2(self): """Exported function that allows a client cog to create an id3 monster index""" await self.wait_until_ready() @@ -196,33 +184,10 @@ async def download_and_refresh_nicknames(self): logger.info('Loading dg database') self.database = load_database(self.database) logger.info('Building dg monster index') - self.index = await MonsterIndex(self.database, self.nickname_overrides, - self.treename_overrides, self.panthname_overrides) self.index2 = await MonsterIndex2(self.database.get_all_monsters(False), self.database) - logger.info('Writing dg monster computed names') - self.write_monster_computed_names() - logger.info('Done refreshing dg data') - def write_monster_computed_names(self): - results = {} - for name, nm in self.index.all_entries.items(): - results[name] = int(tsutils.get_pdx_id_dadguide(nm)) - - with open(NAMES_EXPORT_PATH, 'w', encoding='utf-8') as f: - json.dump(results, f) - - results = {} - for nm in self.index.all_monsters: - entry = {'bn': list(nm.group_treenames)} - if nm.extra_nicknames: - entry['nn'] = list(nm.extra_nicknames) - results[int(tsutils.get_pdx_id_dadguide(nm))] = entry - - with open(TREENAMES_EXPORT_PATH, 'w', encoding='utf-8') as f: - json.dump(results, f) - def _csv_to_tuples(self, file_path: str, cols: int = 2): # Loads a two-column CSV into an array of tuples. results = [] diff --git a/dadguide/old_monster_index.py b/dadguide/old_monster_index.py deleted file mode 100644 index e3d5b8f5..00000000 --- a/dadguide/old_monster_index.py +++ /dev/null @@ -1,521 +0,0 @@ -import difflib -from collections import defaultdict - -import tsutils -from discord.utils import find as find_first -from redbot.core.utils import AsyncIter - -from dadguide.models.enum_types import Attribute, MonsterType -from dadguide.models.monster_model import MonsterModel -from dadguide.models.series_model import SeriesModel -from .database_context import DbContext -from .models.enum_types import EvoType -from .models.enum_types import InternalEvoType - - -class MonsterIndex(tsutils.aobject): - async def __ainit__(self, monster_database: DbContext, nickname_overrides, treename_overrides, - panthname_overrides, accept_filter=None): - # Important not to hold onto anything except IDs here so we don't leak memory - self.db_context = monster_database - base_monster_ids = monster_database.get_monsters_where(monster_database.graph.monster_is_base) - - self.attr_short_prefix_map = { - Attribute.Fire: ['r'], - Attribute.Water: ['b'], - Attribute.Wood: ['g'], - Attribute.Light: ['l'], - Attribute.Dark: ['d'], - Attribute.Unknown: ['h'], - Attribute.Nil: ['x'], - } - self.attr_long_prefix_map = { - Attribute.Fire: ['red', 'fire'], - Attribute.Water: ['blue', 'water'], - Attribute.Wood: ['green', 'wood'], - Attribute.Light: ['light'], - Attribute.Dark: ['dark'], - Attribute.Unknown: ['unknown'], - Attribute.Nil: ['null', 'none'], - } - - self.series_to_prefix_map = { - 130: ['halloween', 'hw', 'h'], - 136: ['xmas', 'christmas', 'x'], - 125: ['summer', 'beach'], - 114: ['school', 'academy', 'gakuen'], - 139: ['new years', 'ny'], - 149: ['wedding', 'bride'], - 154: ['padr'], - 175: ['valentines', 'vday', 'v'], - 183: ['gh', 'gungho'], - 117: ['gh', 'gungho'], - } - - monster_id_to_nicknames = defaultdict(set) - for monster_id, nicknames in nickname_overrides.items(): - monster_id_to_nicknames[monster_id] = nicknames - - named_monsters = [] - async for base_mon in AsyncIter(base_monster_ids): - base_id = base_mon.monster_id - base_monster = monster_database.graph.get_monster(base_id) - series = base_monster.series - group_treename_overrides = treename_overrides.get(base_id, []) - evolution_tree = monster_database.graph.get_alt_monsters_by_id(base_id) - named_mg = NamedMonsterGroup(evolution_tree, group_treename_overrides) - named_evolution_tree = [] - for monster in evolution_tree: - if accept_filter and not accept_filter(monster): - continue - prefixes = self.compute_prefixes(monster, evolution_tree) - extra_nicknames = monster_id_to_nicknames[monster.monster_id] - - # The query mis-handles transforms so we have to fetch base monsters - # from the graph properly ourselves instead of just listening to whatever - # the query says the base monster is above - named_monster = NamedMonster( - monster, named_mg, prefixes, extra_nicknames, series, - base_monster=monster_database.graph.get_base_monster(monster)) - named_monsters.append(named_monster) - named_evolution_tree.append(named_monster) - for named_monster in named_evolution_tree: - named_monster.set_evolution_tree(named_evolution_tree) - - # Sort the NamedMonsters into the opposite order we want to accept their nicknames in - # This order is: - # 1) High priority first - # 2) Larger group sizes - # 3) Minimum ID size in the group - # 4) Monsters with higher ID values - def named_monsters_sort(named_mon: NamedMonster): - return (not named_mon.is_low_priority, named_mon.group_size, -1 * - named_mon.base_monster_no_na, named_mon.monster_no_na) - - named_monsters.sort(key=named_monsters_sort) - - # set up a set of all pantheon names, a set of all pantheon nicknames, and a dictionary of nickname -> full name - # later we will set up a dictionary of pantheon full name -> monsters - self.all_pantheon_names = set() - self.all_pantheon_names.update(panthname_overrides.values()) - - self.pantheon_nick_to_name = panthname_overrides - self.pantheon_nick_to_name.update(panthname_overrides) - - self.all_pantheon_nicknames = set() - self.all_pantheon_nicknames.update(panthname_overrides.keys()) - - self.all_prefixes = set() - self.pantheons = defaultdict(set) - self.all_entries = {} - self.bad_entries = {} - self.two_word_entries = {} - for nm in named_monsters: - self.all_prefixes.update(nm.prefixes) - for nickname in nm.final_nicknames: - self.all_entries[nickname] = nm - for nickname in nm.final_two_word_nicknames: - self.two_word_entries[nickname] = nm - for nickname in nm.bad_nicknames: - self.bad_entries[nickname] = nm.bad_nicknames[nickname] - if nm.series: - for pantheon in self.all_pantheon_names: - if pantheon.lower() == nm.series.lower(): - self.pantheons[pantheon.lower()].add(nm) - - self.all_monsters = named_monsters - self.all_en_name_to_monsters = {m.name_en.lower(): m for m in named_monsters} - self.monster_no_na_to_named_monster = {m.monster_no_na: m for m in named_monsters} - self.monster_id_to_named_monster = {m.monster_id: m for m in named_monsters} - - for monster_id, nicknames in nickname_overrides.items(): - nm = self.monster_id_to_named_monster.get(monster_id) - if nm: - for nickname in nicknames: - self.all_entries[nickname] = nm - - __init__ = __ainit__ - - def init_index(self): - pass - - def compute_prefixes(self, m: MonsterModel, evotree: list): - prefixes = set() - - attr1_short_prefixes = self.attr_short_prefix_map[m.attr1] - attr1_long_prefixes = self.attr_long_prefix_map[m.attr1] - prefixes.update(attr1_short_prefixes) - prefixes.update(attr1_long_prefixes) - - # If no 2nd attribute, use x so we can look those monsters up easier - attr2_short_prefixes = self.attr_short_prefix_map.get(m.attr2, ['x']) - for a1 in attr1_short_prefixes: - for a2 in attr2_short_prefixes: - prefixes.add(a1 + a2) - prefixes.add(a1 + '/' + a2) - - # TODO: add prefixes based on type - - # Chibi monsters have the same NA name, except lowercased - lower_name = m.name_en.lower() - if m.name_en != m.name_ja: - if lower_name == m.name_en: - prefixes.add('chibi') - elif 'ミニ' in m.name_ja: - # Guarding this separately to prevent 'gemini' from triggering (e.g. 2645) - prefixes.add('chibi') - - if 'chibi' in lower_name: - prefixes.add('chibi') - - true_evo_type = self.db_context.graph.true_evo_type_by_monster(m) - awoken = lower_name.startswith('awoken') or '覚醒' in lower_name - revo = true_evo_type == InternalEvoType.Reincarnated - srevo = lower_name.startswith('super reincarnated') or '超転生' in lower_name - mega = lower_name.startswith('mega awoken') or '極醒' in lower_name - awoken_or_revo_or_equip_or_mega = awoken or revo or m.is_equip or mega - - # These clauses need to be separate to handle things like 'Awoken Thoth' which are - # actually Evos but have awoken in the name - if awoken: - prefixes.add('a') - prefixes.add('awoken') - - if revo: - prefixes.add('revo') - prefixes.add('reincarnated') - - if mega: - prefixes.add('mega') - prefixes.add('mega awoken') - prefixes.add('awoken') - prefixes.add('ma') - - if srevo: - prefixes.add('srevo') - prefixes.add('super reincarnated') - - # Prefixes for evo type - cur_evo_type = self.db_context.graph.cur_evo_type_by_monster(m) - if cur_evo_type == EvoType.Base: - prefixes.add('base') - elif cur_evo_type == EvoType.Evo: - prefixes.add('evo') - elif cur_evo_type == EvoType.UvoAwoken and not awoken_or_revo_or_equip_or_mega: - prefixes.add('uvo') - prefixes.add('uevo') - elif cur_evo_type == EvoType.UuvoReincarnated and not awoken_or_revo_or_equip_or_mega: - prefixes.add('uuvo') - prefixes.add('uuevo') - - # Other Prefixes - if self.db_context.graph.monster_is_farmable_evo(m): - prefixes.add('farmable') - - # If any monster in the group is a pixel, add 'nonpixel' to all the versions - # without pixel in the name. Add 'pixel' as a prefix to the ones with pixel in the name. - def is_pixel(n): - n = n.name_en.lower() - return n.startswith('pixel') or n.startswith('ドット') - - for gm in evotree: - if is_pixel(gm): - prefixes.update(['pixel'] if is_pixel(m) else ['np', 'nonpixel']) - break - - if m.is_equip: - prefixes.add('assist') - prefixes.add('equip') - - # Collab prefixes - prefixes.update(self.series_to_prefix_map.get(m.series.series_id, [])) - - return prefixes - - def find_monster(self, query): - query = tsutils.rmdiacritics(query).lower().strip() - - # id search - if query.isdigit(): - m = self.monster_no_na_to_named_monster.get(int(query)) - if m is None: - return None, 'Looks like a monster ID but was not found', None - else: - return m, None, "ID lookup" - # special handling for na/jp - - # TODO: need to handle na_only? - - err = None - if query in self.bad_entries: - err = ("It looks like this query won't be supported soon!" - f" Please start using `^id {self.bad_entries[query]}` instead, with a space." - " For more information, check out:" - " " - " or join the Tsubaki server ().") - - # handle exact nickname match - if query in self.all_entries: - return self.all_entries[query], err, "Exact nickname" - - contains_ja = tsutils.contains_ja(query) - if len(query) < 2 and contains_ja: - return None, 'Japanese queries must be at least 2 characters', None - elif len(query) < 4 and not contains_ja: - return None, 'Your query must be at least 4 letters', None - - # TODO: this should be a length-limited priority queue - matches = set() - - # prefix search for ids, take max id - for nickname, m in self.all_entries.items(): - if query.endswith("base {}".format(m.monster_id)): - matches.add( - find_first(lambda mo: m.base_monster_no == mo.monster_id, self.all_entries.values())) - if len(matches): - return self.pick_best_monster(matches), None, "Base ID match, max of 1".format() - - # prefix search for nicknames, space-preceeded, take max id - for nickname, m in self.all_entries.items(): - if nickname.startswith(query + ' '): - matches.add(m) - if len(matches): - return self.pick_best_monster(matches), err, "Space nickname prefix, max of {}".format(len(matches)) - - # prefix search for nicknames, take max id - for nickname, m in self.all_entries.items(): - if nickname.startswith(query): - matches.add(m) - if len(matches): - all_names = ",".join(map(lambda x: x.name_en, matches)) - return self.pick_best_monster(matches), err, "Nickname prefix, max of {}, matches=({})".format( - len(matches), all_names) - - # prefix search for full name, take max id - for nickname, m in self.all_entries.items(): - if m.name_en.lower().startswith(query) or m.name_ja.lower().startswith(query): - matches.add(m) - if len(matches): - return self.pick_best_monster(matches), err, "Full name, max of {}".format(len(matches)) - - # for nicknames with 2 names, prefix search 2nd word, take max id - if query in self.two_word_entries: - return self.two_word_entries[query], err, "Second-word nickname prefix, max of {}".format(len(matches)) - - # TODO: refactor 2nd search characteristcs for 2nd word - - # full name contains on nickname, take max id - for nickname, m in self.all_entries.items(): - if query in m.name_en.lower() or query in m.name_ja.lower(): - matches.add(m) - if len(matches): - return self.pick_best_monster(matches), err, 'Nickname contains nickname match ({})'.format( - len(matches)) - - # No decent matches. Try near hits on nickname instead - matches = difflib.get_close_matches(query, self.all_entries.keys(), n=1, cutoff=.8) - if len(matches): - match = matches[0] - return self.all_entries[match], err, 'Close nickname match ({})'.format(match) - - # Still no decent matches. Try near hits on full name instead - matches = difflib.get_close_matches( - query, self.all_en_name_to_monsters.keys(), n=1, cutoff=.9) - if len(matches): - match = matches[0] - return self.all_en_name_to_monsters[match], err, 'Close name match ({})'.format(match) - - # About to give up, try matching all words - matches = set() - for nickname, m in self.all_entries.items(): - if (all(map(lambda x: x in m.name_en.lower(), query.split())) or - all(map(lambda x: x in m.name_ja.lower(), query.split()))): - matches.add(m) - if len(matches): - return self.pick_best_monster(matches), err, 'All word match on full name, max of {}'.format( - len(matches)) - - # couldn't find anything - return None, "Could not find a match for: " + query, None - - @staticmethod - def pick_best_monster(named_monster_list): - return max(named_monster_list, key=lambda x: (not x.is_low_priority, x.rarity, x.monster_no_na)) - - -class NamedMonsterGroup(object): - def __init__(self, evolution_tree: list, treename_overrides: list): - base_monster = min(evolution_tree, key=lambda m: m.monster_id) - - self.is_low_priority = ( - self._is_low_priority_monster(base_monster) - or self._is_low_priority_group(evolution_tree)) - - self.group_size = len(evolution_tree) - self.base_monster_no = base_monster.monster_id - self.base_monster_no_na = base_monster.monster_no_na - - self.monster_no_to_treename = { - m.monster_id: self._compute_monster_treename(m) for m in evolution_tree - } - - self.computed_treename = self._compute_group_treename(evolution_tree) - self.computed_treenames = {self.computed_treename} - if '-' in self.computed_treename: - self.computed_treenames.add(self.computed_treename.replace('-', ' ')) - - self.treenames = treename_overrides or self.computed_treenames - - @staticmethod - def _compute_monster_treename(m: MonsterModel): - treename = m.name_en.lower() - if ',' in treename: - name_parts = treename.split(',') - if name_parts[1].strip().startswith('the '): - # handle names like 'xxx, the yyy' where xxx is the name - treename = name_parts[0] - else: - # otherwise, grab the chunk after the last comma - treename = name_parts[-1] - - for x in ['awoken', 'reincarnated']: - if treename.startswith(x): - treename = treename.replace(x, '') - - # Fix for DC collab garbage - treename = treename.replace('(comics)', '') - treename = treename.replace('(film)', '') - - return treename.strip() - - def _compute_group_treename(self, monsters): - """Computes the treename for a group of monsters. - - Prefer a treename with the largest count across the group. If all the - groups have equal size, prefer the lowest monster number treename. - This monster in general has better names, particularly when all the - names are unique, e.g. for male/female hunters.""" - - def count_and_id(): - return [0, 0] - - treename_to_info = defaultdict(count_and_id) - - for m in monsters: - treename = self.monster_no_to_treename[m.monster_id] - entry = treename_to_info[treename] - entry[0] += 1 - entry[1] = max(entry[1], m.monster_id) - - entries = [[count_id[0], -1 * count_id[1], bn] for bn, count_id in treename_to_info.items()] - return max(entries)[2] - - @staticmethod - def _is_low_priority_monster(m: MonsterModel): - lp_types = [MonsterType.Evolve, MonsterType.Enhance, MonsterType.Awoken, MonsterType.Vendor] - lp_substrings = ['tamadra'] - lp_min_rarity = 2 - name = m.name_en.lower() - - failed_type = m.type1 in lp_types - failed_ss = any([x in name for x in lp_substrings]) - failed_rarity = m.rarity < lp_min_rarity - failed_chibi = name == m.name_en and m.name_en != m.name_ja - failed_equip = m.is_equip - return failed_type or failed_ss or failed_rarity or failed_chibi or failed_equip - - @staticmethod - def _is_low_priority_group(mg: list): - lp_grp_min_rarity = 5 - max_rarity = max(m.rarity for m in mg) - failed_max_rarity = max_rarity < lp_grp_min_rarity - return failed_max_rarity - - -class NamedMonster(object): - def __init__(self, monster: MonsterModel, monster_group: NamedMonsterGroup, prefixes: set, extra_nicknames: set, - series: SeriesModel, base_monster: MonsterModel = None): - - self.evolution_tree = None - - # Hold on to the IDs instead - self.monster_id = monster.monster_id - self.monster_no_na = monster.monster_no_na - self.monster_no_jp = monster.monster_no_jp - - # ID of the root of the tree for this monster - self.base_monster_no = base_monster.monster_id - self.base_monster_no_na = base_monster.monster_no_na - - # This stuff is important for nickname generation - self.group_treenames = monster_group.treenames - self.prefixes = prefixes - - # Pantheon - series = series - self.series = series.name if series else None - - # Data used to determine how to rank the nicknames - self.is_low_priority = monster_group.is_low_priority or monster.is_equip - self.group_size = monster_group.group_size - self.rarity = monster.rarity - self.sell_mp = monster.sell_mp - - # Used in fallback searches - self.name_en = monster.name_en - self.name_ja = monster.name_ja - - # These are just extra metadata - self.monster_treename = monster_group.monster_no_to_treename[self.monster_id] - self.group_computed_treename = monster_group.computed_treename - self.extra_nicknames = extra_nicknames - - # Compute any extra prefixes - if self.monster_treename in ('ana', 'ace'): - self.prefixes.add(self.monster_treename) - - # Compute extra treenames by checking for two-word treenames and using the second half - self.two_word_treenames = set() - for treename in self.group_treenames: - treename_words = treename.split(' ') - if len(treename_words) == 2: - self.two_word_treenames.add(treename_words[1]) - - # The primary result nicknames - self.final_nicknames = set() - # Set the configured override nicknames - self.final_nicknames.update(self.extra_nicknames) - # Set the roma subname for JP monsters - if monster.roma_subname: - self.final_nicknames.add(monster.roma_subname) - - self.bad_nicknames = dict() - - # For each treename, add nicknames - for treename in self.group_treenames: - # Add the treename directly - self.final_nicknames.add(treename) - # Add the prefix plus treename, and the prefix with a space between treename - for prefix in self.prefixes: - self.final_nicknames.add(prefix + treename) - self.final_nicknames.add(prefix + ' ' + treename) - self.bad_nicknames[prefix + treename] = prefix + ' ' + treename - - self.final_two_word_nicknames = set() - - # Slightly different process for two-word treenames. Does this make sense? Who knows. - for treename in self.two_word_treenames: - self.final_two_word_nicknames.add(treename) - # Add the prefix plus treename, and the prefix with a space between treename - for prefix in self.prefixes: - self.final_two_word_nicknames.add(prefix + treename) - self.final_two_word_nicknames.add(prefix + ' ' + treename) - self.bad_nicknames[prefix + treename] = prefix + ' ' + treename - - def set_evolution_tree(self, evolution_tree): - """ - Set the evolution tree to a list of NamedMonsters so that we can have - nice things like prefix lookups on the entire tree in id2 and not cry - about Diablos equip - """ - self.evolution_tree = evolution_tree diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 41c3c8dc..3ebe32fa 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1,19 +1,18 @@ import asyncio -import csv import datetime import difflib import json import logging import os import re +import time from collections import defaultdict -from io import StringIO, BytesIO +from io import BytesIO import aiohttp import discord import prettytable import pytz -import time import tsutils from redbot.core import checks, data_manager from redbot.core import commands, errors @@ -63,13 +62,6 @@ def mod_help(self, ctx, help_type): commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, "short_doc") -async def lookup_named_monster(query: str): - padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') - if padinfo_cog is None: - raise Exception("Cog not Loaded") - nm, err, debug_info = await padinfo_cog.fm_(str(query)) - return nm, err, debug_info - async def lookup_monster_model(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') if padinfo_cog is None: @@ -85,13 +77,6 @@ def monster_id_to_monster(monster_id): return dg_cog.get_monster(monster_id) -def monster_id_to_named_monster(monster_id): - dg_cog = PADGLOBAL_COG.bot.get_cog('Dadguide') - if dg_cog is None: - return None - return dg_cog.index.monster_id_to_named_monster.get(monster_id, None) - - async def check_enabled(ctx): """If the server is disabled, raise a warning and return False""" if PADGLOBAL_COG.settings.checkDisabled(ctx.message): @@ -205,100 +190,6 @@ async def togglepadglobal(self, ctx): status = 'disabled' if self.settings.checkDisabled(ctx.message) else 'enabled' await ctx.send(inline('PAD Global commands {} on this server').format(status)) - @commands.command() - @auth_check('contentadmin') - async def debugid1dump(self, ctx): - dg_cog = self.bot.get_cog('Dadguide') - mi = dg_cog.index - - async def write_send(nn_map, file_name): - data_holder = StringIO() - writer = csv.writer(data_holder) - for nn, nm in nn_map.items(): - writer.writerow([nn, nm.monster_no_na, nm.name_en]) - bytes_data = BytesIO(data_holder.getvalue().encode()) - await ctx.channel.send(file=discord.File(bytes_data, file_name)) - - await write_send(mi.all_entries, 'all_entries.csv') - await write_send(mi.two_word_entries, 'two_word_entries.csv') - - @commands.command(aliases=['iddebug1']) - @auth_check('contentadmin') - async def debugid1(self, ctx, *, query): - dg_cog = self.bot.get_cog('Dadguide') - # m is a named monster - m, err, debug_info = await lookup_named_monster(query) - - if m is None: - await ctx.send(box('No match: ' + err)) - return - - msg = "{}. {}".format(m.monster_no_na, m.name_en) - msg += "\nLookup type: {}".format(debug_info) - - def list_or_none(li): - if len(li) == 1: - return '\n\t{}'.format(''.join(li)) - elif len(li): - return '\n\t' + '\n\t'.join(sorted(li)) - else: - return 'NONE' - - msg += "\n\nNickname original components:" - msg += "\n monster_treename: {}".format(m.monster_treename) - msg += "\n group_computed_treename: {}".format(m.group_computed_treename) - msg += "\n extra_nicknames: {}".format(list_or_none(m.extra_nicknames)) - - msg += "\n\nNickname final components:" - msg += "\n treenames: {}".format(list_or_none(m.group_treenames)) - msg += "\n prefixes: {}".format(list_or_none(m.prefixes)) - - msg += "\n\nAccepted nickname entries:" - accepted_nn = list(filter(lambda nn: m.monster_id == dg_cog.index.all_entries[nn].monster_id, - m.final_nicknames)) - accepted_twnn = list(filter(lambda nn: m.monster_id == dg_cog.index.two_word_entries[nn].monster_id, - m.final_two_word_nicknames)) - - msg += "\n nicknames: {}".format(list_or_none(accepted_nn)) - msg += "\n two_word_nicknames: {}".format(list_or_none(accepted_twnn)) - - msg += "\n\nOverwritten nickname entries:" - replaced_nn = list(filter(lambda nn: nn not in accepted_nn, - m.final_nicknames)) - - replaced_twnn = list(filter(lambda nn: nn not in accepted_twnn, - m.final_two_word_nicknames)) - - replaced_nn_info = map(lambda nn: ( - nn, dg_cog.index.all_entries[nn]), replaced_nn) - replaced_twnn_info = map( - lambda nn: (nn, dg_cog.index.two_word_entries[nn]), replaced_twnn) - - replaced_nn_text = list(map(lambda nn_info: '{} : {}. {}'.format( - nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_en), - replaced_nn_info)) - - replaced_twnn_text = list(map(lambda nn_info: '{} : {}. {}'.format( - nn_info[0], nn_info[1].monster_no_na, nn_info[1].name_en), - replaced_twnn_info)) - - msg += "\n nicknames: {}".format(list_or_none(replaced_nn_text)) - msg += "\n two_word_nicknames: {}".format(list_or_none(replaced_twnn_text)) - - msg += "\n\nNickname entry sort parts:" - msg += "\n (is_low_priority, group_size, monster_no_na) : ({}, {}, {})".format( - m.is_low_priority, m.group_size, m.monster_no_na) - - msg += "\n\nMatch selection sort parts:" - msg += "\n (is_low_priority, rarity, monster_no_na) : ({}, {}, {})".format( - m.is_low_priority, m.rarity, m.monster_no_na) - - sent_messages = [] - for page in pagify(msg): - sent_messages.append(await ctx.send(box(page))) - await tsutils.await_and_remove(self.bot, sent_messages[-1], ctx.author, - delete_msgs=sent_messages, timeout=30) - @commands.command(aliases=['fir']) @auth_check('contentadmin') async def forceindexreload(self, ctx): @@ -803,10 +694,9 @@ def which_to_text(self): monsters = defaultdict(list) for monster_id in self.settings.which(): m = monster_id_to_monster(monster_id) - nm = monster_id_to_named_monster(monster_id) - if m is None or nm is None: + if m is None: continue - name = nm.group_computed_treename.title() + name = m.name_en.split(", ")[-1] grp = m.series.name monsters[grp].append(name) @@ -880,8 +770,8 @@ async def getwhich(self, ctx): for w in self.settings.which(): w %= 10000 - nm = monster_id_to_named_monster(w) - name = nm.group_computed_treename.title() + m = monster_id_to_monster(w) + name = m.name_en.split(', ')[-1] result = self.settings.which()[w] if isinstance(result, list): @@ -901,19 +791,6 @@ async def getwhich(self, ctx): for page in pagify(msg): await ctx.send(box(page)) - @commands.command(aliases=['lookupdebug']) - @auth_check('contentadmin') - async def debuglookup(self, ctx, *, term: str): - """Shows why a query matches to a monster""" - term = term.lower().replace('?', '') - nm, err, deb = await lookup_named_monster(term) - base = nm.group_computed_treename.title() if nm else nm - name = nm.name_en if nm else nm - monster_id = nm.monster_id if nm else nm - definition = self.settings.which().get(monster_id, None) - await ctx.send('Which Debug:\n```Base: {}\nName: {}\nID: {}\nError: {}\nDebug: {}```' - .format(base, name, monster_id, err, deb)) - @padglobal.command() @checks.is_owner() async def addadmin(self, ctx, user: discord.Member): @@ -1181,10 +1058,10 @@ def guide_to_text(self): msg += '\n\n__**Leader Guides**__' for monster_id, definition in self.settings.leaderGuide().items(): - nm = monster_id_to_named_monster(monster_id) - if nm is None: + m = monster_id_to_monster(monster_id) + if m is None: continue - name = nm.group_computed_treename.title() + name = m.name_en.split(', ')[-1].title() msg += '\n**{}** :\n{}\n'.format(name, definition) return msg @@ -1310,7 +1187,7 @@ async def daychange(self, ctx): else: # earliest possible date of the second Sunday in March dstthresh = pst.replace(month=3, day=8) - + # calculate the day DST changes if dstthresh < pst: dstthresh = dstthresh.replace(year=(dstthresh.year + 1)) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 6a8b38a6..df8fb93b 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -6,13 +6,11 @@ from Levenshtein import jaro_winkler from tsutils import rmdiacritics -from padinfo.core.historic_lookups import historic_lookups, historic_lookups_file_path, historic_lookups_id3, \ - historic_lookups_file_path_id3 +from padinfo.core.historic_lookups import historic_lookups_id3, historic_lookups_file_path_id3 from padinfo.core.padinfo_settings import settings if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel - from dadguide.old_monster_index import NamedMonster SERIES_TYPE_PRIORITY = { "regular": 4, @@ -244,31 +242,6 @@ async def findMonsterCustom(dgcog, query): return None, "Monster not found", "" -async def findMonster1(dgcog, query): - query = rmdiacritics(query) - nm, err, debug_info = await _findMonster(dgcog, query) - - monster_no = nm.monster_id if nm else -1 - historic_lookups[query] = monster_no - json.dump(historic_lookups, open(historic_lookups_file_path, "w+")) - - m = dgcog.get_monster(nm.monster_id) if nm else None - - return m, err, debug_info - - -async def _findMonster(dgcog, query) -> Tuple[Optional["NamedMonster"], Optional[str], Optional[str]]: - await dgcog.wait_until_ready() - try: - return dgcog.index.find_monster(query) - except Exception: - prefix = (await dgcog.bot.get_valid_prefixes())[0] - return (None, - f"Sorry, id1 doesn't support this query and we are no longer" - f" developing id1 features. Please use `{prefix}id {query}`!", - None) - - async def findMonster3(dgcog, query): m = await _findMonster3(dgcog, query) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bd885f6b..5cc87909 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,5 +1,4 @@ import asyncio -import json import logging import os import random @@ -15,7 +14,6 @@ from discordmenu.emoji.emoji_cache import emoji_cache from discordmenu.intra_message_state import IntraMessageState from redbot.core import checks, commands, data_manager, Config -from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor, rmdiacritics @@ -26,7 +24,7 @@ get_attribute_emoji_by_monster from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info -from padinfo.core.find_monster import find_monster, findMonster1, findMonster3, \ +from padinfo.core.find_monster import find_monster, findMonster3, \ calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom from padinfo.core.historic_lookups import historic_lookups from padinfo.core.leader_skills import perform_leaderskill_query @@ -49,7 +47,7 @@ from padinfo.view_state.transforminfo import TransformInfoViewState if TYPE_CHECKING: - pass + from dadguide.models.monster_model import MonsterModel logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -94,7 +92,6 @@ def __init__(self, bot, *args, **kwargs): self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) self.fm3 = lambda q: fm.findMonsterCustom(bot.get_cog("Dadguide"), q) - self.fm_ = lambda q: fm._findMonster(bot.get_cog("Dadguide"), q) self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster @@ -204,31 +201,25 @@ async def _id(self, ctx, *, query: str): """Monster info (main tab)""" await self._do_id(ctx, query) - @commands.command(aliases=["idold", "oldid"]) - @checks.bot_has_permissions(embed_links=True) - async def id1(self, ctx, *, query): - """Do a search via id1""" - await self._do_id(ctx, query, force_id3_pref=False) - @commands.command() @checks.bot_has_permissions(embed_links=True) async def idna(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" - await self._do_id(ctx, "inna " + query, force_id3_pref=True) + await self._do_id(ctx, "inna " + query) @commands.command() @checks.bot_has_permissions(embed_links=True) async def idjp(self, ctx, *, query: str): """Monster info (limited to JP monsters ONLY)""" - await self._do_id(ctx, "injp " + query, force_id3_pref=True) + await self._do_id(ctx, "injp " + query) @commands.command() @checks.bot_has_permissions(embed_links=True) async def id3(self, ctx, *, query: str): """Monster info (main tab)""" - await self._do_id(ctx, query, force_id3_pref=True) + await self._do_id(ctx, query) - async def _do_id(self, ctx, query: str, force_id3_pref=None): + async def _do_id(self, ctx, query: str): dgcog = await self.get_dgcog() raw_query = query color = await self.get_user_embed_color(ctx) @@ -256,10 +247,7 @@ async def _do_id(self, ctx, query: str, force_id3_pref=None): f" info about `id3` check out" f" <{IDGUIDE}>!") - if force_id3_pref is not False: - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) - else: - monster, err, debug_info = await findMonster1(dgcog, raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not monster: await self.makeFailureMsg(ctx, query, err) @@ -319,8 +307,9 @@ async def send_survey_after(self, ctx, query, result_monster): sm = await self.config.user(ctx.author).survey_mode() sms = [1, await self.config.sometimes_perc() / 100, 0][sm] if random.random() < sms: - m1, _, _ = await findMonster1(dgcog, query) - id1res = f"{m1.name_en} ({m1.monster_id})" if m1 else "None" + mid1 = historic_lookups.get(query) + m1 = mid1 and dgcog.get_monster(mid1) + id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" params = urllib.parse.urlencode( {'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': id3res, 'entry.1016180044': id1res}) @@ -801,8 +790,7 @@ async def embedcolor(self, ctx, *, color): async def beta(self, ctx, *, text=""): """Discontinued""" await ctx.send(f"`id3 `is now enabled globally, see" - f" <{IDGUIDE}> for more information." - f" `{ctx.prefix}id1` is temporarily available if you need to use it.") + f" <{IDGUIDE}> for more information.") @commands.group() @checks.is_owner() @@ -850,35 +838,6 @@ async def setvoicepath(self, ctx, *, path=''): settings.setVoiceDir(path) await ctx.tick() - @checks.is_owner() - @padinfo.command() - async def iddiff(self, ctx): - """Runs the diff checker for id and id3""" - await ctx.send("Running diff checker...") - hist_aggreg = list(historic_lookups) - s = 0 - f = [] - dgcog = await self.get_dgcog() - async for c, query in AsyncIter(enumerate(hist_aggreg)): - m1, err1, debug_info1 = await findMonster1(dgcog, query) - m2, err2, debug_info2 = await findMonster3(dgcog, query) - if c % 50 == 0: - await ctx.send(inline("{}/{} complete.".format(c, len(hist_aggreg)))) - if m1 == m2 or (m1 and m2 and m1.monster_id == m2.monster_id): - s += 1 - continue - - f.append((query, - [m1.monster_id if m1 else None, m2.monster_id if m2 else None], - [err1, err2], - [debug_info1, debug_info2] - )) - if m1 and m2: - await ctx.send("Major Discrepency: `{}` -> {}/{}".format(query, m1.name_en, m2.name_en)) - await ctx.send("Done running diff checker. {}/{} passed.".format(s, len(hist_aggreg))) - file = discord.File(BytesIO(json.dumps(f).encode()), filename="diff.json") - await ctx.send(file=file) - def get_emojis(self): server_ids = [int(sid) for sid in settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index e8070c69..f31a4212 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -1,10 +1,9 @@ -import json import logging import re -from io import BytesIO from fnmatch import fnmatch +from io import BytesIO + from ply import lex -from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify from tsutils import timeout_after @@ -759,7 +758,7 @@ async def search(self, ctx, *, filter_spec: str): # If it still failed, raise the original exception raise ex dg_cog = self.bot.get_cog('Dadguide') - if dg_cog == None: + if dg_cog is None: await ctx.send("Dadguide cog not loaded.") return monsters = dg_cog.database.get_all_monsters() @@ -809,15 +808,3 @@ def _make_search_config(self, input): lexer.input(input) db_context = self.bot.get_cog("Dadguide").database return SearchConfig(lexer, db_context) - - @commands.command() - @checks.is_owner() - async def debugsearch(self, ctx, *, query): - padinfo_cog = self.bot.get_cog('PadInfo') - m, err, debug_info = await padinfo_cog.findMonster1(query) - - if m is None: - await ctx.send(box('No match: ' + err)) - return - - await ctx.send(box(json.dumps(m.search, indent=2, default=lambda o: o.__dict__))) From 38e322b6113ecef1814f3dde1bd0755e33af418f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 13 Feb 2021 16:32:40 -0600 Subject: [PATCH 0605/1220] Fix inline for pg 'which' failure msg cos emoji (#802) --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 3ebe32fa..f9865aa0 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -671,7 +671,7 @@ async def _resolve_which(self, ctx, term): top_monster = db_context.graph.get_numerical_sort_top_monster_by_id(monster.monster_no) return name, SIMPLE_TREE_MSG.format(top_monster.monster_no, top_monster.name_en), None, False else: - await ctx.send(inline('No which info for {} (#{})'.format(name, monster_id))) + await ctx.send('No which info for {} (#{})'.format(name, monster_id)) return None, None, None, None @commands.command() From 0ffcae353419c672689cc209c1b9c986de2cd8bf Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 17:59:06 -0700 Subject: [PATCH 0606/1220] Move LeaderSkillSingle to new menu (#803) * Move LeaderSkillSingle to new menu * Remove Old Menu * Split files --- padinfo/id_menu_old.py | 16 +----- padinfo/ls_menu.py | 17 +++--- padinfo/lss_menu.py | 66 +++++++++++++++++++++++ padinfo/padinfo.py | 43 ++++++--------- padinfo/view/leader_skill.py | 19 ++----- padinfo/view/leader_skill_single.py | 28 ++++++++++ padinfo/view_state/leader_skill_single.py | 19 +++++++ 7 files changed, 140 insertions(+), 68 deletions(-) create mode 100644 padinfo/lss_menu.py create mode 100644 padinfo/view/leader_skill_single.py create mode 100644 padinfo/view_state/leader_skill_single.py diff --git a/padinfo/id_menu_old.py b/padinfo/id_menu_old.py index e6477762..88486f58 100644 --- a/padinfo/id_menu_old.py +++ b/padinfo/id_menu_old.py @@ -6,7 +6,7 @@ from padinfo.core.padinfo_settings import settings from padinfo.view.id import IdView -from padinfo.view.leader_skill import LeaderSkillSingleView +from padinfo.view.leader_skill_single import LeaderSkillSingleView from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView from padinfo.view_state.id import IdViewState @@ -31,16 +31,6 @@ async def get_user_embed_color(self, pdicog): else: return discord.Color(color) - async def make_id_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ - await IdViewState.query(self.dgcog, m) - state = IdViewState("", "TODO", "todo", "", color, m, transform_base, true_evo_type_raw, acquire_raw, - base_rarity, alt_monsters, - use_evo_scroll=settings.checkEvoID(self.ctx.author.id)) - e = IdView.embed(state) - return e.to_embed() - async def make_lookup_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return LookupView.embed(m, color).to_embed() @@ -48,7 +38,3 @@ async def make_lookup_embed(self, m: "MonsterModel"): async def make_links_embed(self, m: "MonsterModel"): color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) return LinksView.embed(m, color).to_embed() - - async def make_lssingle_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return LeaderSkillSingleView.embed(m, color).to_embed() diff --git a/padinfo/ls_menu.py b/padinfo/ls_menu.py index 4a5d53b2..de7191ad 100644 --- a/padinfo/ls_menu.py +++ b/padinfo/ls_menu.py @@ -16,20 +16,19 @@ if TYPE_CHECKING: pass -emoji_button_names = ['\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r'), '\N{CROSS MARK}'] menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') class LeaderSkillMenu: - INITIAL_EMOJI = emoji_button_names[0] MENU_TYPE = 'LeaderSkill' + EMOJI_BUTTON_NAMES = ('\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r'), '\N{CROSS MARK}') - @staticmethod - def menu(original_author_id, friend_ids, bot_id): + @classmethod + def menu(cls, original_author_id, friend_ids, bot_id): transitions = { - LeaderSkillMenu.INITIAL_EMOJI: LeaderSkillMenu.respond_to_house, - emoji_button_names[1]: LeaderSkillMenu.respond_to_l, - emoji_button_names[2]: LeaderSkillMenu.respond_to_r, + cls.EMOJI_BUTTON_NAMES[0]: LeaderSkillMenu.respond_to_house, + cls.EMOJI_BUTTON_NAMES[1]: LeaderSkillMenu.respond_to_l, + cls.EMOJI_BUTTON_NAMES[2]: LeaderSkillMenu.respond_to_r, } valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) @@ -78,12 +77,12 @@ async def respond_to_house(message: Optional[Message], ims, **data): def ls_control(state: LeaderSkillViewState): return EmbedControl( [LeaderSkillView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in LeaderSkillMenu.EMOJI_BUTTON_NAMES] ) @staticmethod def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in LeaderSkillMenu.EMOJI_BUTTON_NAMES] ) diff --git a/padinfo/lss_menu.py b/padinfo/lss_menu.py new file mode 100644 index 00000000..d53dd052 --- /dev/null +++ b/padinfo/lss_menu.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING, Optional + +from discord import Message +from discordmenu.embed.emoji import EmbedMenuEmojiConfig +from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.emoji.emoji_cache import emoji_cache +from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ + MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter + +from padinfo.view.id import IdView +from padinfo.view.leader_skill_single import LeaderSkillSingleView +from padinfo.view_state.id import IdViewState +from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState + +if TYPE_CHECKING: + pass + +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') + + +class LeaderSkillSingleMenu: + EMOJI_BUTTON_NAMES = ('\N{HOUSE BUILDING}', '\N{SQUARED ID}', '\N{CROSS MARK}') + MENU_TYPE = 'LeaderSkillSingle' + + @classmethod + def menu(cls, original_author_id, friend_ids, bot_id): + transitions = { + cls.EMOJI_BUTTON_NAMES[0]: cls.respond_to_house, + cls.EMOJI_BUTTON_NAMES[1]: cls.view_ls, + } + + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(bot_id), + MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) + ] + + return EmbedMenu(reaction_filters, transitions, cls.ls_control, menu_emoji_config) + + @classmethod + async def view_ls(cls, message: Optional[Message], ims, *, dgcog, user_config, **data): + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = cls.id_control(id_view_state) + return id_control + + @classmethod + async def respond_to_house(cls, message: Optional[Message], ims, *, dgcog, user_config, **data): + ls_view_state = await LeaderSkillSingleViewState.deserialize(dgcog, user_config, ims) + ls_control = cls.ls_control(ls_view_state) + return ls_control + + @classmethod + def ls_control(cls, state: LeaderSkillSingleViewState): + return EmbedControl( + [LeaderSkillSingleView.embed(state)], + [emoji_cache.get_by_name(e) for e in cls.EMOJI_BUTTON_NAMES] + ) + + @classmethod + def id_control(cls, state: IdViewState): + return EmbedControl( + [IdView.embed(state)], + [emoji_cache.get_by_name(e) for e in cls.EMOJI_BUTTON_NAMES] + ) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5cc87909..833124c1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -4,7 +4,6 @@ import random import re import urllib.parse -from collections import OrderedDict from io import BytesIO from typing import TYPE_CHECKING, List, Optional @@ -16,7 +15,7 @@ from redbot.core import checks, commands, data_manager, Config from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate -from tsutils import EmojiUpdater, Menu, char_to_emoji, is_donor, rmdiacritics +from tsutils import char_to_emoji, is_donor, rmdiacritics from padinfo.MonsterListMenu import MonsterListMenu, MonsterListMenuPanes from padinfo.common.config import BotConfig @@ -33,12 +32,14 @@ from padinfo.id_menu import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.idtest_mixin import IdTest -from padinfo.ls_menu import LeaderSkillMenu, emoji_button_names as ls_menu_emoji_button_names +from padinfo.ls_menu import LeaderSkillMenu +from padinfo.lss_menu import LeaderSkillSingleMenu from padinfo.tf_menu import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState +from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState from padinfo.view_state.materials import MaterialsViewState from padinfo.view_state.monster_list import MonsterListViewState from padinfo.view_state.otherinfo import OtherInfoViewState @@ -79,13 +80,12 @@ class PadInfo(commands.Cog, IdTest): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.menu = Menu(bot) # These emojis are the keys into the idmenu submenus self.ls_emoji = '\N{HOUSE BUILDING}' self.left_emoji = char_to_emoji('l') self.right_emoji = char_to_emoji('r') - self.remove_emoji = self.menu.emoji['no'] + self.remove_emoji = '\N{CROSS MARK}' self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False, lastaction=None) @@ -145,7 +145,8 @@ async def test_reaction_add(self, reaction, member): else: emoji_clicked = emoji_obj.name - if not (emoji_clicked in ls_menu_emoji_button_names or + if not (emoji_clicked in LeaderSkillMenu.EMOJI_BUTTON_NAMES or + emoji_clicked in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES or emoji_clicked in IdMenuPanes.emoji_names() or emoji_clicked in MonsterListMenuPanes.emoji_names() or emoji_clicked in tf_menu_emoji_button_names): @@ -160,6 +161,7 @@ async def test_reaction_add(self, reaction, member): menu_type = ims['menu_type'] menu_map = { LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu.menu, + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu.menu, IdMenu.MENU_TYPE: IdMenu.menu, TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu, MonsterListMenu.MENU_TYPE: MonsterListMenu.menu, @@ -455,23 +457,6 @@ async def pantheon(self, ctx, *, query: str): menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pantheon_control) await menu.create(ctx, state) - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=30): - if starting_menu_emoji not in emoji_to_embed.emoji_dict: - # Selected menu wasn't generated for this monster - return EMBED_NOT_GENERATED - - emoji_to_embed.emoji_dict[self.remove_emoji] = self.menu.reaction_delete_message - - try: - result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, - starting_menu_emoji, timeout=timeout) - if result_msg and result_embed: - # Message is finished but not deleted, clear the footer - result_embed.set_footer(text=discord.Embed.Empty) - await result_msg.edit(embed=result_embed) - except Exception as ex: - logger.error('Menu failure', exc_info=True) - @commands.command(aliases=['img']) @checks.bot_has_permissions(embed_links=True) async def pic(self, ctx, *, query: str): @@ -651,12 +636,14 @@ async def leaderskillsingle(self, ctx, *, query): if err: await ctx.send(err) return - menu = IdMenuOld(ctx, dgcog=dgcog, allowed_emojis=self.get_emojis()) - emoji_to_embed = OrderedDict() - emoji_to_embed[self.ls_emoji] = await menu.make_lssingle_embed(m) - emoji_to_embed[self.left_emoji] = await menu.make_id_embed(m) - await self._do_menu(ctx, self.ls_emoji, EmojiUpdater(emoji_to_embed)) + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, color, m) + menu = LeaderSkillSingleMenu.menu(original_author_id, friends, self.bot.user.id) + await menu.create(ctx, state) @commands.command(aliases=['tfinfo', 'xforminfo']) @checks.bot_has_permissions(embed_links=True) diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index e572bebc..fd1cf067 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -2,16 +2,16 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain -from discordmenu.embed.view import EmbedView from discordmenu.embed.text import BoldText, Text +from discordmenu.embed.view import EmbedView -from padinfo.core.leader_skills import createMultiplierText, createSingleMultiplierText +from padinfo.core.leader_skills import createMultiplierText from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.leader_skill import LeaderSkillViewState if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + pass class LeaderSkillView: @@ -29,16 +29,3 @@ def embed(state: LeaderSkillViewState): Text(rls.desc if rls else 'None')), color=state.color), embed_footer=pad_info_footer_with_state(state)) - - -class LeaderSkillSingleView: - @staticmethod - def embed(m: "MonsterModel", color): - ls = m.leader_skill - return EmbedView( - EmbedMain( - title=createSingleMultiplierText(ls), - description=Box( - BoldText(MonsterHeader.name(m, link=True, show_jp=True)), - Text(ls.desc if ls else 'None')), - color=color)) diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py new file mode 100644 index 00000000..bafc9e76 --- /dev/null +++ b/padinfo/view/leader_skill_single.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain +from discordmenu.embed.text import BoldText, Text +from discordmenu.embed.view import EmbedView + +from padinfo.core.leader_skills import createSingleMultiplierText +from padinfo.view.components.base import pad_info_footer_with_state +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState + +if TYPE_CHECKING: + pass + + +class LeaderSkillSingleView: + @staticmethod + def embed(state: LeaderSkillSingleViewState): + ls = state.mon.leader_skill + return EmbedView( + embed_main=EmbedMain( + title=createSingleMultiplierText(ls), + description=Box( + BoldText(MonsterHeader.name(state.mon, link=True, show_jp=True)), + Text(ls.desc if ls else 'None')), + color=state.color), + embed_footer=pad_info_footer_with_state(state)) diff --git a/padinfo/view_state/leader_skill_single.py b/padinfo/view_state/leader_skill_single.py new file mode 100644 index 00000000..a3ce3256 --- /dev/null +++ b/padinfo/view_state/leader_skill_single.py @@ -0,0 +1,19 @@ +from padinfo.common.config import UserConfig +from padinfo.view_state.base import ViewStateBase +from padinfo.view_state.common import get_monster_from_ims + + +class LeaderSkillSingleViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, color, mon): + super().__init__(original_author_id, menu_type, raw_query, extra_state=None) + self.color = color + self.mon = mon + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + mon = await get_monster_from_ims(dgcog, ims) + return LeaderSkillSingleViewState(original_author_id, menu_type, raw_query, user_config.color, mon) From 85328ef8c4ad54b072e4f554bb821d253f9ab992 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 19:30:07 -0700 Subject: [PATCH 0607/1220] Don't inspect the stack while recursing through name tokens (#804) * Make recursion faster * Remove imports --- dadguide/monster_index.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 1c31689e..aa349782 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -1,6 +1,5 @@ import asyncio import csv -import inspect import io import logging import re @@ -226,9 +225,9 @@ async def _build_monster_index(self, monsters): for nick in self.monster_id_to_treename[base_id]: self.add_name_token(self.manual_tree, nick, m) - def add_name_token(self, token_dict, token, m): - if len(inspect.stack(0)) > 100: - logger.warning(f"Infinite loop detected in name token replacement with token {token}. Aborting.") + def add_name_token(self, token_dict, token, m, depth=5): + if depth <= 0: + logger.warning(f"Depth exceeded with token {token}. Aborting.") return token_dict[token.lower()].add(m) @@ -238,7 +237,7 @@ def add_name_token(self, token_dict, token, m): # Replacements for ts in (k for k in self.replacement_tokens if token.lower() in k and all(m in token_dict[t] for t in k)): for t in self.replacement_tokens[ts]: - self.add_name_token(token_dict, t, m) + self.add_name_token(token_dict, t, m, depth-1) @staticmethod def _name_to_tokens(oname): From d4322554380b993ff58292c55f2ca0ccd3eb605b Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 20:01:43 -0700 Subject: [PATCH 0608/1220] Allow queries to raise exceptions in idtest (#805) --- padinfo/idtest_mixin.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index c128b61d..2c39e979 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -364,9 +364,13 @@ async def idt_run(self, ctx): async with ctx.typing(): for i, qr in enumerate(sorted(suite.items())): q, r = qr - m = await findMonster3(dgcog, q) - mid = m and m.monster_id - if m is not None and m.monster_id != r['result'] or m is None and r['result'] >= 0: + try: + m = await findMonster3(dgcog, q) or -1 + except Exception: + m = -2 + mid = getattr(m, "monster_id", m) + + if mid != r['result']: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' o += f"{i}. {q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" @@ -426,9 +430,13 @@ async def idt_runall(self, ctx): ml = len(max(qsuite, key=len)) + 2 async with ctx.typing(): for c, q in enumerate(sorted(qsuite)): - m = await findMonster3(dgcog, q) - mid = m and m.monster_id - if m is not None and m.monster_id != qsuite[q]['result'] or m is None and qsuite[q]['result'] >= 0: + try: + m = await findMonster3(dgcog, q) or -1 + except Exception: + m = -2 + mid = getattr(m, "monster_id", m) + + if mid != r['result']: reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' qq = '"' + q + '"' qo += (f"{str(c).rjust(4)}. {qq.ljust(ml)} - {rcircle} " From 2395c4a413a7446c27ef4be9fb1696f4bfb0ca22 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 20:09:17 -0700 Subject: [PATCH 0609/1220] Fix error in idtest runall when there are no modifiers (#806) * Allow queries to raise exceptions in idtest * Update idtest_mixin.py --- padinfo/idtest_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index 2c39e979..ff007eff 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -436,7 +436,7 @@ async def idt_runall(self, ctx): m = -2 mid = getattr(m, "monster_id", m) - if mid != r['result']: + if mid != qsuite[q]['result']: reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' qq = '"' + q + '"' qo += (f"{str(c).rjust(4)}. {qq.ljust(ml)} - {rcircle} " From 49d4f23b5803eb845369f9557f6f86ef484575f3 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 22:16:57 -0700 Subject: [PATCH 0610/1220] Fully remove all id1 related things (#807) --- dadguide/dadguide.py | 68 ------------------------------------------ padglobal/padglobal.py | 2 +- 2 files changed, 1 insertion(+), 69 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 55759425..c4ffad29 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -7,10 +7,8 @@ entire database could be leaked when the module is reloaded. """ import asyncio -import csv import os import shutil -from collections import defaultdict from io import BytesIO from typing import Optional @@ -65,17 +63,6 @@ def __init__(self, bot, *args, **kwargs): self.settings = DadguideSettings("dadguide") - # A string -> int mapping, nicknames to monster_id_na - self.nickname_overrides = {} - - # An int -> set(string), monster_id_na to set of treename overrides - self.treename_overrides = defaultdict(set) - - self.panthname_overrides = defaultdict(set) - - # Map of google-translated JP names to EN names - self.translated_names = {} - self.database = None self.index2 = None # type: Optional[MonsterIndex2] @@ -146,12 +133,6 @@ async def reload_data_task(self): logger.exception("dadguide data wait loop failed: %s", ex) raise ex - async def reload_config_files(self): - os.remove(NICKNAME_FILE_PATTERN) - os.remove(TREENAME_FILE_PATTERN) - os.remove(PANTHNAME_FILE_PATTERN) - await self.download_and_refresh_nicknames() - async def download_and_refresh_nicknames(self): if self.settings.data_file(): logger.info('Copying dg data file') @@ -160,27 +141,6 @@ async def download_and_refresh_nicknames(self): logger.info('Downloading dg data files') await self._download_files() - logger.info('Downloading dg name override files') - await self._download_override_files() - - logger.info('Loading dg name overrides') - nickname_overrides = self._csv_to_tuples(NICKNAME_FILE_PATTERN, 6) - treename_overrides = self._csv_to_tuples(TREENAME_FILE_PATTERN, 6) - panthname_overrides = self._csv_to_tuples(PANTHNAME_FILE_PATTERN, 3) - - self.nickname_overrides = defaultdict(set) - for id, nick, _, _, _, i in nickname_overrides: - if id.isdigit() and not i: - self.nickname_overrides[int(id)].add(nick.lower()) - - self.treename_overrides = defaultdict(set) - for id, treename, _, _, _, i in treename_overrides: - if id.isdigit() and not i: - self.treename_overrides[int(id)].add(treename.lower()) - - self.panthname_overrides = {x[1].lower(): x[2].lower() for x in panthname_overrides} - self.panthname_overrides.update({v: v for _, v in self.panthname_overrides.items()}) - logger.info('Loading dg database') self.database = load_database(self.database) logger.info('Building dg monster index') @@ -188,38 +148,10 @@ async def download_and_refresh_nicknames(self): logger.info('Done refreshing dg data') - def _csv_to_tuples(self, file_path: str, cols: int = 2): - # Loads a two-column CSV into an array of tuples. - results = [] - with open(file_path, encoding='utf-8') as f: - file_reader = csv.reader(f, delimiter=',') - for row in file_reader: - if len(row) < 2: - continue - - data = [None] * cols - for i in range(0, min(cols, len(row))): - data[i] = row[i].strip() - - if not len(data[0]): - continue - - results.append(data) - return results - async def _download_files(self): one_hour_secs = 1 * 60 * 60 await tsutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, one_hour_secs) - async def _download_override_files(self): - one_hour_secs = 1 * 60 * 60 - await tsutils.async_cached_plain_request( - NICKNAME_FILE_PATTERN, NICKNAME_OVERRIDES_SHEET, one_hour_secs) - await tsutils.async_cached_plain_request( - TREENAME_FILE_PATTERN, GROUP_TREENAMES_OVERRIDES_SHEET, one_hour_secs) - await tsutils.async_cached_plain_request( - PANTHNAME_FILE_PATTERN, PANTHNAME_OVERRIDES_SHEET, one_hour_secs) - @commands.group() @checks.is_owner() async def dadguide(self, ctx): diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index f9865aa0..c40236c3 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -201,7 +201,7 @@ async def forceindexreload(self, ctx): start = time.perf_counter() await ctx.send('Starting reload...') dadguide_cog = self.bot.get_cog('Dadguide') - await dadguide_cog.reload_config_files() + await dadguide_cog.download_and_refresh_nicknames() await dadguide_cog.wait_until_ready() await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) From 1b210bbc34ff687a56018be302ca31e446ee8ef3 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Feb 2021 23:11:54 -0700 Subject: [PATCH 0611/1220] Make padinfo new menu reaction listener raw (#808) --- padinfo/padinfo.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 833124c1..d2666ecb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -137,13 +137,9 @@ def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') return dg_cog.get_monster(monster_id) - @commands.Cog.listener('on_reaction_add') - async def test_reaction_add(self, reaction, member): - emoji_obj = reaction.emoji - if isinstance(emoji_obj, str): - emoji_clicked = emoji_obj - else: - emoji_clicked = emoji_obj.name + @commands.Cog.listener('on_raw_reaction_add') + async def on_raw_reaction_add(self, payload): + emoji_clicked = payload.emoji.name if not (emoji_clicked in LeaderSkillMenu.EMOJI_BUTTON_NAMES or emoji_clicked in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES or @@ -152,7 +148,9 @@ async def test_reaction_add(self, reaction, member): emoji_clicked in tf_menu_emoji_button_names): return - message = reaction.message + message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) + reaction = discord.utils.find(lambda r: getattr(r.emoji, "name", r.emoji) == emoji_clicked, message.reactions) + user = self.bot.get_user(payload.user_id) ims = message.embeds and IntraMessageState.extract_data(message.embeds[0]) if not ims: return @@ -175,7 +173,7 @@ async def test_reaction_add(self, reaction, member): friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] embed_menu = menu_func(original_author_id, friends, self.bot.user.id) - if not (await embed_menu.should_respond(message, reaction, member)): + if not (await embed_menu.should_respond(message, reaction, user)): return dgcog = await self.get_dgcog() @@ -184,7 +182,7 @@ async def test_reaction_add(self, reaction, member): 'dgcog': dgcog, 'user_config': user_config } - await embed_menu.transition(message, ims, emoji_clicked, member, **data) + await embed_menu.transition(message, ims, emoji_clicked, user, **data) @commands.command() async def jpname(self, ctx, *, query: str): From 7e77997e636645957726d78ae0beba6d283e7372 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 14 Feb 2021 00:16:19 -0600 Subject: [PATCH 0612/1220] Revert "Make padinfo new menu reaction listener raw (#808)" (#809) This reverts commit 1b210bbc34ff687a56018be302ca31e446ee8ef3. --- padinfo/padinfo.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d2666ecb..833124c1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -137,9 +137,13 @@ def get_monster(self, monster_id: int): dg_cog = self.bot.get_cog('Dadguide') return dg_cog.get_monster(monster_id) - @commands.Cog.listener('on_raw_reaction_add') - async def on_raw_reaction_add(self, payload): - emoji_clicked = payload.emoji.name + @commands.Cog.listener('on_reaction_add') + async def test_reaction_add(self, reaction, member): + emoji_obj = reaction.emoji + if isinstance(emoji_obj, str): + emoji_clicked = emoji_obj + else: + emoji_clicked = emoji_obj.name if not (emoji_clicked in LeaderSkillMenu.EMOJI_BUTTON_NAMES or emoji_clicked in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES or @@ -148,9 +152,7 @@ async def on_raw_reaction_add(self, payload): emoji_clicked in tf_menu_emoji_button_names): return - message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) - reaction = discord.utils.find(lambda r: getattr(r.emoji, "name", r.emoji) == emoji_clicked, message.reactions) - user = self.bot.get_user(payload.user_id) + message = reaction.message ims = message.embeds and IntraMessageState.extract_data(message.embeds[0]) if not ims: return @@ -173,7 +175,7 @@ async def on_raw_reaction_add(self, payload): friend_cog = self.bot.get_cog("Friend") friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] embed_menu = menu_func(original_author_id, friends, self.bot.user.id) - if not (await embed_menu.should_respond(message, reaction, user)): + if not (await embed_menu.should_respond(message, reaction, member)): return dgcog = await self.get_dgcog() @@ -182,7 +184,7 @@ async def on_raw_reaction_add(self, payload): 'dgcog': dgcog, 'user_config': user_config } - await embed_menu.transition(message, ims, emoji_clicked, user, **data) + await embed_menu.transition(message, ims, emoji_clicked, member, **data) @commands.command() async def jpname(self, ctx, *, query: str): From 2a39ca74570704c6d037ed6f51320cdf0f87b1a4 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Feb 2021 02:37:42 -0700 Subject: [PATCH 0613/1220] Log invalid id3 queries (#811) * Allow invalid queries to be searched with a warning * Log bad queries * Add pause before uh oh --- padinfo/padinfo.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 833124c1..e899a44f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -89,7 +89,8 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=9401770) self.config.register_user(survey_mode=0, color=None, beta_id3=False, lastaction=None) - self.config.register_global(sometimes_perc=20, good=0, bad=0, do_survey=False, test_suite={}, fluff_suite=[]) + self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False, test_suite={}, + fluff_suite=[]) self.fm3 = lambda q: fm.findMonsterCustom(bot.get_cog("Dadguide"), q) @@ -244,10 +245,15 @@ async def _do_id(self, ctx, query: str): if p == goodquery[0]: bad = True if bad and query not in dgcog.index2.all_name_tokens: - await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" - f" Try using `{' '.join(goodquery)}` (with a space) instead! For more" - f" info about `id3` check out" - f" <{IDGUIDE}>!") + async def send_message(): + await asyncio.sleep(1) + await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" + f" Try using `{' '.join(goodquery)}` (with a space) instead! For more" + f" info about the new id changes, check out" + f" <{IDGUIDE}>!") + asyncio.create_task(send_message()) + async with self.config.bad_queries() as bq: + bq.append((raw_query, ctx.author.id)) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) From 73ef407dd20554b66eafeb4af405a5759758d752 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Feb 2021 04:11:40 -0700 Subject: [PATCH 0614/1220] Make get_priority_tuple "public" in find_monster (#812) --- padinfo/core/find_monster.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index df8fb93b..50beba5f 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -203,24 +203,32 @@ def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches, return potential_evos + def get_priority_tuple(self, m, dgcog, tokenized_query=None, matches=None): + if matches is None: + matches = defaultdict(MonsterMatch) + if tokenized_query is None: + tokenized_query = [] + + return (matches[m].score, + not m.is_equip, + # Match na on id overlap + bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), + SERIES_TYPE_PRIORITY.get(m.series.series_type), + m.on_na if m.series.series_type == "collab" else 0, + dgcog.database.graph.monster_is_rem_evo(m), + not all(t.value in [0, 12, 14, 15] for t in m.types), + not any(t.value in [0, 12, 14, 15] for t in m.types), + -dgcog.database.graph.get_base_id(m), + m.rarity, + m.monster_no_na) + def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, matches=None): if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] - return max(monsters, - key=lambda m: (matches[m].score, - not m.is_equip, - # Match na on id overlap - bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), - SERIES_TYPE_PRIORITY.get(m.series.series_type), - m.on_na if m.series.series_type == "collab" else 0, - dgcog.database.graph.monster_is_rem_evo(m), - not all(t.value in [0, 12, 14, 15] for t in m.types), - not any(t.value in [0, 12, 14, 15] for t in m.types), - -dgcog.database.graph.get_base_id(m), - m.rarity, - m.monster_no_na)) + + return max(monsters, key=lambda m: self.get_priority_tuple(m, dgcog, tokenized_query, matches)) def get_monster_evos(self, database, monster_gen, matches): monster_evos = set() From 220df7fd9f0f86e92ce04b1998265d09dcce497f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 14 Feb 2021 21:49:34 -0600 Subject: [PATCH 0615/1220] Add MonsterList linked menus for evolist & idsearch (#797) * It kinda works but emojis are out of order * it works except for emoji order * custom delete reaction & fix child message * Change debugging Z back to E * Remove print statement * Fix deletion behavior * Reset the parent when showing a child * Fix typo in comment * Fix two-action behavior * omg i suck at naming files, also use id emoj * Fix titles & custom del fn * Update discordmenu version, checkout this version to compare * add comment * Improve logic for idsearch * fix functions * more fix ids function * Fix house emoji * delete incorrect comment * Send child message sooner --- padinfo/id_menu.py | 47 +++++- padinfo/info.json | 2 +- ...onsterListMenu.py => monster_list_menu.py} | 96 +++++++----- padinfo/padinfo.py | 142 +++++++++++++----- padinfo/pane_names.py | 14 ++ padinfo/reaction_list.py | 34 +++++ padinfo/view/components/monster/header.py | 3 +- padinfo/view/monster_list.py | 5 +- padinfo/view_state/id.py | 5 + padinfo/view_state/monster_list.py | 11 +- 10 files changed, 273 insertions(+), 86 deletions(-) rename padinfo/{MonsterListMenu.py => monster_list_menu.py} (57%) create mode 100644 padinfo/reaction_list.py diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index 27adaf98..dea59815 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, List +from typing import TYPE_CHECKING, Optional from discord import Message from discordmenu.embed.emoji import EmbedMenuEmojiConfig @@ -44,7 +44,8 @@ def menu(original_author_id, friend_ids, bot_id, initial_control=None): BotAuthoredMessageReactionFilter(bot_id), MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) ] - embed = EmbedMenu(reaction_filters, IdMenuPanes.transitions(), initial_control, menu_emoji_config) + embed = EmbedMenu(reaction_filters, IdMenuPanes.transitions(), initial_control, menu_emoji_config, + delete_func=IdMenu.respond_with_delete) return embed @staticmethod @@ -105,6 +106,21 @@ def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_ev next_monster = db_context.graph.numeric_next_monster(monster) return next_monster.monster_id if next_monster else None + @staticmethod + async def respond_with_refresh(message: Optional[Message], ims, **data): + # This is used by disambig screen & other multi-message embeds, where we need to deserialize & then + # re-serialize the ims, with the same information in place + pane_type = ims.get('pane_type') + pane_type_to_func_map = IdMenuPanes.pane_types() + response_func = pane_type_to_func_map[pane_type] + return await response_func(message, ims, **data) + + @staticmethod + async def respond_with_delete(message: Optional[Message], ims, **data): + if ims.get('is_child'): + return await message.edit(embed=None) + return await message.delete() + @staticmethod async def respond_with_current_id(message: Optional[Message], ims, **data): dgcog = data['dgcog'] @@ -226,16 +242,23 @@ class IdMenuPanes: IdMenu.respond_with_left: ('\N{BLACK LEFT-POINTING TRIANGLE}', None), IdMenu.respond_with_right: ('\N{BLACK RIGHT-POINTING TRIANGLE}', None), IdMenu.respond_with_current_id: ('\N{HOUSE BUILDING}', IdMenuPaneNames.id), - IdMenu.respond_with_evos: (char_to_emoji('e'), IdMenuPaneNames.evos), + IdMenu.respond_with_evos: (char_to_emoji('E'), IdMenuPaneNames.evos), IdMenu.respond_with_mats: ('\N{MEAT ON BONE}', IdMenuPaneNames.materials), IdMenu.respond_with_picture: ('\N{FRAME WITH PICTURE}', IdMenuPaneNames.pic), IdMenu.respond_with_pantheon: ('\N{CLASSICAL BUILDING}', IdMenuPaneNames.pantheon), IdMenu.respond_with_otherinfo: ('\N{SCROLL}', IdMenuPaneNames.otherinfo), + IdMenu.respond_with_refresh: ( + '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', IdMenuPaneNames.refresh), + IdMenu.respond_with_delete: ('\N{CROSS MARK}', IdMenuPaneNames.delete), } + HIDDEN_EMOJIS = [ + IdMenuPaneNames.refresh, + IdMenuPaneNames.delete, + ] @classmethod def emoji_names(cls): - return [v[0] for k, v in cls.DATA.items()] + return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] @classmethod def transitions(cls): @@ -243,4 +266,18 @@ def transitions(cls): @classmethod def pane_types(cls): - return {v[1]: k for k, v in cls.DATA.items() if v[1]} + return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} + + @staticmethod + def emoji_name_to_emoji(name: str): + for _, data_pair in IdMenuPanes.DATA.items(): + if data_pair[1] == name: + return data_pair[0] + return None + + @staticmethod + def emoji_name_to_function(name: str): + for _, data_pair in IdMenuPanes.DATA.items(): + if data_pair[1] == name: + return data_pair[1] + return None diff --git a/padinfo/info.json b/padinfo/info.json index 34c2aaf6..c653e91b 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -23,7 +23,7 @@ "tsutils>=3.1.0", "prettytable", "python-levenshtein", - "discord-menu>=0.13.0" + "discord-menu>=0.14.0" ], "tags": [ "PAD" diff --git a/padinfo/MonsterListMenu.py b/padinfo/monster_list_menu.py similarity index 57% rename from padinfo/MonsterListMenu.py rename to padinfo/monster_list_menu.py index 5590e9d6..e5e00d39 100644 --- a/padinfo/MonsterListMenu.py +++ b/padinfo/monster_list_menu.py @@ -8,10 +8,9 @@ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji -from padinfo.pane_names import IdMenuPaneNames -from padinfo.view.id import IdView +from padinfo.id_menu import IdMenu, IdMenuPanes +from padinfo.pane_names import IdMenuPaneNames, MonsterListPaneNames, global_emoji_responses from padinfo.view.monster_list import MonsterListView -from padinfo.view_state.id import IdViewState from padinfo.view_state.monster_list import MonsterListViewState if TYPE_CHECKING: @@ -22,6 +21,7 @@ class MonsterListMenu: MENU_TYPE = 'MonsterListMenu' + CHILD_MENU_TYPE = 'IdMenu' @staticmethod def menu(original_author_id, friend_ids, bot_id, initial_control=None): @@ -38,6 +38,18 @@ def menu(original_author_id, friend_ids, bot_id, initial_control=None): embed = EmbedMenu(reaction_filters, MonsterListMenuPanes.transitions(), initial_control, menu_emoji_config) return embed + @staticmethod + async def respond_with_refresh(message: Optional[Message], ims, **data): + # this is only called once on message load + if data.get('child_message_id'): + ims['child_message_id'] = data['child_message_id'] + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_reset(message: Optional[Message], ims, **data): + # replace with the overview list after the child menu changes + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + @staticmethod async def respond_with_monster_list(message: Optional[Message], ims, **data): dgcog = data['dgcog'] @@ -48,14 +60,16 @@ async def respond_with_monster_list(message: Optional[Message], ims, **data): @staticmethod async def respond_with_n(message: Optional[Message], ims, n, **data): - dgcog = data['dgcog'] - user_config = data['user_config'] - ims['resolved_monster_id'] = int(ims['monster_list'][n]) - - view_state = await IdViewState.deserialize(dgcog, user_config, ims) - control = MonsterListMenu.id_control(view_state) - return control + reaction_list = IdMenuPanes.emoji_names() + if not data.get('child_message_ims'): + # default to the overview screen if we weren't already on a screen + ims['reaction_list'] = ','.join(reaction_list) + ims['is_child'] = 'True' + return await IdMenu.respond_with_current_id(message, ims, **data) + data['child_message_ims']['reaction_list'] = ','.join(reaction_list) + data['child_message_ims']['resolved_monster_id'] = int(ims['resolved_monster_id']) + return await IdMenu.respond_with_refresh(message, data['child_message_ims'], **data) @staticmethod async def respond_with_0(message: Optional[Message], ims, **data): @@ -111,37 +125,35 @@ def monster_list_control(state: MonsterListViewState): reaction_list ) - @staticmethod - def id_control(state: IdViewState): - if state is None: - return None - reaction_list = state.reaction_list - return EmbedControl( - [IdView.embed(state)], - reaction_list - ) - class MonsterListMenuPanes: INITIAL_EMOJI = '\N{HOUSE BUILDING}' DATA = { - MonsterListMenu.respond_with_monster_list: ('\N{HOUSE BUILDING}', IdMenuPaneNames.evos), - MonsterListMenu.respond_with_0: (char_to_emoji('0'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_1: (char_to_emoji('1'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_2: (char_to_emoji('2'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_3: (char_to_emoji('3'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_4: (char_to_emoji('4'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_5: (char_to_emoji('5'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_6: (char_to_emoji('6'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_7: (char_to_emoji('7'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_8: (char_to_emoji('8'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_9: (char_to_emoji('9'), IdMenuPaneNames.id), - MonsterListMenu.respond_with_10: ('\N{KEYCAP TEN}', IdMenuPaneNames.id), + MonsterListMenu.respond_with_monster_list: ('\N{HOUSE BUILDING}', MonsterListPaneNames.home), + MonsterListMenu.respond_with_0: (char_to_emoji('0'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_1: (char_to_emoji('1'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_2: (char_to_emoji('2'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_3: (char_to_emoji('3'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_4: (char_to_emoji('4'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_5: (char_to_emoji('5'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_6: (char_to_emoji('6'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_7: (char_to_emoji('7'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_8: (char_to_emoji('8'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_9: (char_to_emoji('9'), MonsterListPaneNames.id), + MonsterListMenu.respond_with_10: ('\N{KEYCAP TEN}', MonsterListPaneNames.id), + MonsterListMenu.respond_with_refresh: ( + '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', MonsterListPaneNames.refresh), + MonsterListMenu.respond_with_reset: (global_emoji_responses['reset'], MonsterListPaneNames.reset) } + HIDDEN_EMOJIS = [ + MonsterListPaneNames.home, + MonsterListPaneNames.refresh, + MonsterListPaneNames.reset, + ] @classmethod def emoji_names(cls): - return [v[0] for k, v in cls.DATA.items()] + return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] @classmethod def transitions(cls): @@ -149,8 +161,22 @@ def transitions(cls): @classmethod def pane_types(cls): - return {v[1]: k for k, v in cls.DATA.items() if v[1]} + return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} @staticmethod def get_initial_reaction_list(number_of_evos: int): - return MonsterListMenuPanes.emoji_names()[:number_of_evos+1] + return MonsterListMenuPanes.emoji_names()[:number_of_evos + 1] + + @staticmethod + def emoji_name_to_emoji(name: str): + for _, data_pair in MonsterListMenuPanes.DATA.items(): + if data_pair[1] == name: + return data_pair[0] + return None + + @staticmethod + def emoji_name_to_function(name: str): + for _, data_pair in MonsterListMenuPanes.DATA.items(): + if data_pair[1] == name: + return data_pair[1] + return None diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e899a44f..82a82c37 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -5,7 +5,7 @@ import re import urllib.parse from io import BytesIO -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List import discord import tsutils @@ -17,7 +17,7 @@ from tabulate import tabulate from tsutils import char_to_emoji, is_donor, rmdiacritics -from padinfo.MonsterListMenu import MonsterListMenu, MonsterListMenuPanes +from padinfo.monster_list_menu import MonsterListMenu, MonsterListMenuPanes from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ get_attribute_emoji_by_monster @@ -32,10 +32,12 @@ from padinfo.id_menu import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.idtest_mixin import IdTest +from padinfo.pane_names import global_emoji_responses from padinfo.ls_menu import LeaderSkillMenu from padinfo.lss_menu import LeaderSkillSingleMenu from padinfo.tf_menu import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader +from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState @@ -168,6 +170,21 @@ async def test_reaction_add(self, reaction, member): MonsterListMenu.MENU_TYPE: MonsterListMenu.menu, } + # If true then the top menu will also respond on reaction + respond_with_child = { + (MonsterListMenu.MENU_TYPE, char_to_emoji(0)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(1)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(2)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(3)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(4)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(5)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(6)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(7)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(8)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(9)): False, + (MonsterListMenu.MENU_TYPE, char_to_emoji(10)): False, + } + menu_func = menu_map.get(menu_type) if not menu_func: @@ -185,6 +202,26 @@ async def test_reaction_add(self, reaction, member): 'dgcog': dgcog, 'user_config': user_config } + if ims.get('child_message_id') and (ims['menu_type'], emoji_clicked) in respond_with_child.keys(): + await message.remove_reaction(emoji_clicked, member) + fctx = await self.bot.get_context(message) + child_message = await fctx.fetch_message(int(ims['child_message_id'])) + previous_child_ims = child_message.embeds and IntraMessageState.extract_data(child_message.embeds[0]) + if previous_child_ims: + data['child_message_ims'] = previous_child_ims + next_child_ims = ims.copy() + next_child_ims['menu_type'] = IdMenu.MENU_TYPE + next_child_ims['is_child'] = True + + # The order here is really important!! The set of emojis attached to the ims is going to be changed in + # the second transition, so it's VITAL that we reset prior to showing the child menu. + # It's also better from a perceived performance standpoint becuase the emojis are so rate-limited and + # the reset wouldn't happen until all emojis showed up in the child, so this way it feels like everything + # happens faster, but regardless the reset must happen first. + if respond_with_child[(ims['menu_type'], emoji_clicked)]: + await embed_menu.transition(message, ims, global_emoji_responses['reset'], member, **data) + await embed_menu.transition(child_message, next_child_ims, emoji_clicked, member, **data) + return await embed_menu.transition(message, ims, emoji_clicked, member, **data) @commands.command() @@ -281,7 +318,7 @@ async def send_error(error): transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ await IdViewState.query(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] - initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, @@ -290,26 +327,6 @@ async def send_error(error): menu = IdMenu.menu(original_author_id, friends, self.bot.user.id) await menu.create(ctx, state) - @staticmethod - async def _get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", - full_reaction_list: List[Optional[str]]): - # hide some panes if we're in evo scroll mode - if not settings.checkEvoID(ctx.author.id): - return full_reaction_list - alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) - if alt_versions is None: - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_left][0])] = None - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_right][0])] = None - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_evos][0])] = None - pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) - if pantheon_list is None: - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_pantheon][0])] = None - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ - await MaterialsViewState.query(dgcog, monster) - if mats is None: - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_mats][0])] = None - return list(filter(None, full_reaction_list)) - async def send_survey_after(self, ctx, query, result_monster): dgcog = await self.get_dgcog() sm = await self.config.user(ctx.author).survey_mode() @@ -389,7 +406,7 @@ async def evos(self, ctx, *, query: str): return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] - initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_versions, gem_versions, @@ -422,7 +439,7 @@ async def evomats(self, ctx, *, query: str): return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] - initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, @@ -454,7 +471,7 @@ async def pantheon(self, ctx, *, query: str): return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] - initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, pantheon_list, series_name, @@ -481,7 +498,7 @@ async def pic(self, ctx, *, query: str): return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] - initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, @@ -522,7 +539,7 @@ async def otherinfo(self, ctx, *, query: str): return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] - initial_reaction_list = await self._get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, @@ -563,33 +580,48 @@ async def lookup(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): dgcog = await self.get_dgcog() - raw_query = query - color = await self.get_user_embed_color(ctx) - original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + monster, err, debug_info = await findMonsterCustom(dgcog, query) if monster is None: await self.makeFailureMsg(ctx, query, err) return alt_versions, _ = await EvosViewState.query(dgcog, monster) - if alt_versions is None: await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, monster.name_en) + 'which has no alt evos.') return + await self._do_monster_list(ctx, dgcog, query, alt_versions, 'Evolution List') - initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(alt_versions)) + async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], title): + raw_query = query + original_author_id = ctx.message.author.id + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + color = await self.get_user_embed_color(ctx) + initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) state = MonsterListViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, query, color, - alt_versions, 'Evolution List', + monster_list, title, reaction_list=initial_reaction_list ) - menu = MonsterListMenu.menu(original_author_id, friends, self.bot.user.id) - await menu.create(ctx, state) + parent_menu = MonsterListMenu.menu(original_author_id, friends, self.bot.user.id) + message = await ctx.send('Setting up!') + child_message = await ctx.send( + 'Click a reaction to see monster details!') + ims = state.serialize() + user_config = await BotConfig.get_user(self.config, ctx.author.id) + data = { + 'dgcog': dgcog, + 'user_config': user_config, + 'child_message_id': child_message.id, + } + try: + await parent_menu.transition(message, ims, MonsterListMenuPanes.emoji_name_to_emoji('refresh'), ctx.author, **data) + await message.edit(content=None) + except discord.errors.NotFound: + # The user could delete the menu before we can do this + pass @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) @@ -1085,3 +1117,33 @@ async def idtraceback(self, ctx, *, query): f"**Matched Name Tokens**:\n{ntokenstr}\n\n" f"**Matched Mod Tokens**:\n{mtokenstr}\n\n" + (f"**Equally Scoring Matches**:\n{lpstr}" if lower_prio else "")) + + @commands.command(aliases=["ids"]) + async def idsearch(self, ctx, query): + dgcog = self.bot.get_cog("Dadguide") + await dgcog.wait_until_ready() + + query = rmdiacritics(query).strip().lower().replace(",", "") + tokenized_query = query.split() + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) + + best_match, matches = max( + await find_monster_search(tokenized_query, dgcog), + await find_monster_search(mw_tokenized_query, dgcog) + if tokenized_query != mw_tokenized_query else (None, {}), + key=lambda t: t[1][t[0]].score if t[0] else 0 + ) + if not best_match: + await ctx.send("No monster matched.") + return + + used = [best_match.monster_id] + monster_list = [] + for m in sorted(matches, key=lambda mon: find_monster.get_priority_tuple(mon, dgcog, matches=matches)): + bmid = dgcog.database.graph.get_base_monster(m).monster_id + if bmid not in used: + used.append(bmid) + monster_list.append(m) + monster_list.reverse() + monster_list = [best_match] + monster_list[:10] + await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results') diff --git a/padinfo/pane_names.py b/padinfo/pane_names.py index 7a165018..674dbf6a 100644 --- a/padinfo/pane_names.py +++ b/padinfo/pane_names.py @@ -5,3 +5,17 @@ class IdMenuPaneNames: otherinfo = 'otherinfo' pantheon = 'pantheon' pic = 'pic' + refresh = 'refresh' + delete = 'delete' + + +class MonsterListPaneNames: + home = 'home' + id = 'id' + refresh = 'refresh' + reset = 'reset' + + +global_emoji_responses = { + 'reset': '\N{WHITE MEDIUM STAR}' +} diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py new file mode 100644 index 00000000..c39a1f63 --- /dev/null +++ b/padinfo/reaction_list.py @@ -0,0 +1,34 @@ +from typing import List, Optional, TYPE_CHECKING + +from discordmenu.emoji.emoji_cache import emoji_cache + +from padinfo.core.padinfo_settings import settings +from padinfo.id_menu import IdMenuPanes, IdMenu +from padinfo.view_state.evos import EvosViewState +from padinfo.view_state.materials import MaterialsViewState +from padinfo.view_state.pantheon import PantheonViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +async def get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", + full_reaction_list: List[Optional[str]] = None, force_evoscroll=False): + # hide some panes if we're in evo scroll mode + if not full_reaction_list: + full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + if not force_evoscroll and not settings.checkEvoID(ctx.author.id): + return full_reaction_list + alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + if alt_versions is None: + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_left][0])] = None + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_right][0])] = None + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_evos][0])] = None + pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + if pantheon_list is None: + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_pantheon][0])] = None + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ + await MaterialsViewState.query(dgcog, monster) + if mats is None: + full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_mats][0])] = None + return list(filter(None, full_reaction_list)) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 18f50860..08742844 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -45,9 +45,10 @@ def long_v2(m: "MonsterModel", link=False): return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) @staticmethod - def short_with_emoji(m: "MonsterModel", link=True): + def short_with_emoji(m: "MonsterModel", link=True, prefix=None): msg = f"{m.monster_no_na} - {m.name_en}" return Box( + prefix, Text(get_attribute_emoji_by_monster(m)), LinkedText(msg, puzzledragonx(m)) if link else Text(msg), Text(MonsterHeader.jp_suffix(m, False)), diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list.py index 8051122a..f110d2fa 100644 --- a/padinfo/view/monster_list.py +++ b/padinfo/view/monster_list.py @@ -1,6 +1,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView +from tsutils import char_to_emoji from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader @@ -11,8 +12,8 @@ def _monster_list(monsters): if not len(monsters): return [] return [ - MonsterHeader.short_with_emoji(mon, link=True) - for mon in sorted(monsters, key=lambda x: int(x.monster_id)) + MonsterHeader.short_with_emoji(mon, link=True, prefix=char_to_emoji(i)) + for i, mon in enumerate(monsters) ] diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index fec2392e..62174766 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -14,11 +14,13 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], use_evo_scroll: bool = True, reaction_list: List[str] = None, + is_child: bool = False, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) + self.is_child = is_child self.acquire_raw = acquire_raw self.alt_monsters = alt_monsters self.base_rarity = base_rarity @@ -29,6 +31,7 @@ def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.id, + 'is_child': str(self.is_child) }) return ret @@ -48,11 +51,13 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = get_reaction_list_from_ims(ims) + is_child = bool(ims.get('is_child')) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, + is_child=is_child, extra_state=ims) @staticmethod diff --git a/padinfo/view_state/monster_list.py b/padinfo/view_state/monster_list.py index 257a44d7..ba289d5b 100644 --- a/padinfo/view_state/monster_list.py +++ b/padinfo/view_state/monster_list.py @@ -13,9 +13,12 @@ class MonsterListViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster_list: List["MonsterModel"], title, reaction_list=None, - extra_state=None): + extra_state=None, + child_message_id=None + ): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.child_message_id = child_message_id self.title = title self.monster_list = monster_list self.reaction_list = reaction_list @@ -29,6 +32,7 @@ def serialize(self): 'title': self.title, 'monster_list': [str(m.monster_no) for m in self.monster_list], 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, + 'child_message_id': self.child_message_id }) return ret @@ -44,8 +48,11 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] reaction_list = get_reaction_list_from_ims(ims) + child_message_id = ims.get('child_message_id') return MonsterListViewState(original_author_id, menu_type, raw_query, query, user_config.color, monster_list=monster_list, title=title, reaction_list=reaction_list, - extra_state=ims) + extra_state=ims, + child_message_id=child_message_id + ) From 900f4d69834004ea8bad8e91868bf2ce7725418b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 14 Feb 2021 22:06:01 -0600 Subject: [PATCH 0616/1220] Update tsutils version (#814) --- padinfo/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/info.json b/padinfo/info.json index c653e91b..7d98cb59 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.1.0", + "tsutils>=3.2.2", "prettytable", "python-levenshtein", "discord-menu>=0.14.0" From 22a6f944a810eeb9f852d38741df937dae707670 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 14 Feb 2021 22:17:08 -0600 Subject: [PATCH 0617/1220] Fix number of emojis after home button was deleted (#815) --- padinfo/monster_list_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/monster_list_menu.py b/padinfo/monster_list_menu.py index e5e00d39..49f02a39 100644 --- a/padinfo/monster_list_menu.py +++ b/padinfo/monster_list_menu.py @@ -165,7 +165,7 @@ def pane_types(cls): @staticmethod def get_initial_reaction_list(number_of_evos: int): - return MonsterListMenuPanes.emoji_names()[:number_of_evos + 1] + return MonsterListMenuPanes.emoji_names()[:number_of_evos] @staticmethod def emoji_name_to_emoji(name: str): From 6dda28d2765e778357a0a4633750df72c1745358 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 14 Feb 2021 23:31:07 -0600 Subject: [PATCH 0618/1220] Fix custom deletion in IdMenu (#816) --- padinfo/id_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index dea59815..b9af9be3 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -117,7 +117,7 @@ async def respond_with_refresh(message: Optional[Message], ims, **data): @staticmethod async def respond_with_delete(message: Optional[Message], ims, **data): - if ims.get('is_child'): + if ims.get('is_child') == 'True': return await message.edit(embed=None) return await message.delete() From 467e40e271c0a853755513875943b3fa0a8df2f9 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Mon, 15 Feb 2021 01:34:24 -0500 Subject: [PATCH 0619/1220] Show correct base killers for cards that transform multiple times (#817) --- dadguide/monster_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 55309cb1..4da36810 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -352,7 +352,7 @@ def get_transform_base_id_by_id(self, monster_id): curr = monster_id while curr not in seen: seen.add(curr) - next_ids = self.get_prev_transform_ids_by_monster_id(monster_id) + next_ids = self.get_prev_transform_ids_by_monster_id(curr) if next_ids: curr = next_ids.pop() else: From ae54b3cc62de58edc8ac38c856aa90ee53734ade Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Mon, 15 Feb 2021 01:56:37 -0500 Subject: [PATCH 0620/1220] Add extra info about transforming cards to id (#810) * Add transform base awakenings (and triangles) to id view * Have id use condensed cd-only format for transform cards --- padinfo/view/id.py | 33 ++++++++++++++++++++++++++------- padinfo/view/transforminfo.py | 11 +---------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index d442f6c5..9d12e5e1 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -60,13 +60,22 @@ def super_awakenings_row(m: "MonsterModel"): delimiter=' ') if len(super_awakenings_emojis) > 0 else None @staticmethod - def all_awakenings_row(m: "MonsterModel"): + def all_awakenings_row(m: "MonsterModel", transform_base): if len(m.awakenings) == 0: return Box(Text('No Awakenings')) return Box( - IdView.normal_awakenings_row(m), - IdView.super_awakenings_row(m) + Box( + '\N{UP-POINTING RED TRIANGLE}' if m!=transform_base else '', + IdView.normal_awakenings_row(m), + delimiter=' ' + ), + Box( + '\N{DOWN-POINTING RED TRIANGLE}', + IdView.all_awakenings_row(transform_base, transform_base), + delimiter=' ' + ) if m!=transform_base else None, + IdView.super_awakenings_row(m), ) @staticmethod @@ -122,9 +131,19 @@ def stats_header(m: "MonsterModel"): return header @staticmethod - def active_skill_header(m: "MonsterModel"): + def active_skill_header(m: "MonsterModel", transform_base): active_skill = m.active_skill - active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) if active_skill else 'None' + if m==transform_base: + active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) \ + if active_skill else 'None' + else: + base_skill = transform_base.active_skill + base_cd = ' (\N{DOWN-POINTING RED TRIANGLE} {} -> {})'.format(base_skill.turn_max, + base_skill.turn_min) \ + if base_skill else 'None' + + active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' + active_cd += base_cd return Box( BoldText('Active Skill'), BoldText(active_cd), @@ -146,7 +165,7 @@ def embed(state: IdViewState): EmbedField( '/'.join(['{}'.format(t.name) for t in m.types]), Box( - IdView.all_awakenings_row(m), + IdView.all_awakenings_row(m, state.transform_base), IdView.killers_row(m, state.transform_base) ) ), @@ -161,7 +180,7 @@ def embed(state: IdViewState): inline=True ), EmbedField( - IdView.active_skill_header(m).to_markdown(), + IdView.active_skill_header(m, state.transform_base).to_markdown(), Text(m.active_skill.desc if m.active_skill else 'None') ), EmbedField( diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index d94f9b29..5b2eb5c7 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -31,15 +31,6 @@ def base_info(m: "MonsterModel"): IdView.super_awakenings_row(m) ) -def transform_skill_header(m: "MonsterModel"): - active_skill = m.active_skill - active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' - return Box( - BoldText('Active Skill'), - BoldText(active_cd), - delimiter=' ' - ) - def base_skill(m: "MonsterModel"): active_skill = m.active_skill return (" (" + BASE_EMOJI + " " @@ -75,7 +66,7 @@ def embed(state: TransformInfoViewState): inline=True ), EmbedField( - transform_skill_header(transformed_mon).to_markdown() + base_skill(base_mon), + IdView.active_skill_header(transformed_mon, base_mon).to_markdown(), Text(transformed_mon.active_skill.desc if transformed_mon.active_skill else 'None') ), EmbedField( From 5f7fb4f8b46db325ef795563ffd8b0937a82a979 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 15 Feb 2021 01:15:23 -0600 Subject: [PATCH 0621/1220] Fix serialization of is_child for id menu (#818) --- padinfo/view_state/id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 62174766..865d15db 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -31,7 +31,7 @@ def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.id, - 'is_child': str(self.is_child) + 'is_child': 'True' if self.is_child else '' }) return ret From ba915fe53067224687ca95e1352a6b3e32ac4ba8 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 15 Feb 2021 04:29:57 -0700 Subject: [PATCH 0622/1220] Fix IDSearch (#819) I'm going to pretend this branch was a single commit --- padinfo/core/find_monster.py | 8 ++++---- padinfo/padinfo.py | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 50beba5f..baf9cc80 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -288,7 +288,7 @@ def __repr__(self): async def find_monster_search(tokenized_query, dgcog) -> \ - Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch]]: + Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"]]: mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ find_monster.interpret_query(tokenized_query, dgcog.index2) @@ -308,7 +308,7 @@ async def find_monster_search(tokenized_query, dgcog) -> \ matches) if not monster_gen: # No monsters match the given name tokens - return None, {} + return None, {}, set() monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, matches) else: # There are no name tokens in the query @@ -320,7 +320,7 @@ async def find_monster_search(tokenized_query, dgcog) -> \ dgcog.index2.modifiers) if not monster_gen: # no modifiers match any monster in the evo tree - return None, {} + return None, {}, set() # print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) # if k in monster_gen}) @@ -328,7 +328,7 @@ async def find_monster_search(tokenized_query, dgcog) -> \ # Return most likely candidate based on query. mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, matches) - return mon, matches + return mon, matches, monster_gen find_monster = FindMonster() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 82a82c37..52e3f4e5 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1079,7 +1079,7 @@ async def idtraceback(self, ctx, *, query): tokenized_query = query.split() mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) - bestmatch, matches = max( + bestmatch, matches, _ = max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) if tokenized_query != mw_tokenized_query else (None, {}), @@ -1119,7 +1119,7 @@ async def idtraceback(self, ctx, *, query): (f"**Equally Scoring Matches**:\n{lpstr}" if lower_prio else "")) @commands.command(aliases=["ids"]) - async def idsearch(self, ctx, query): + async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") await dgcog.wait_until_ready() @@ -1127,23 +1127,24 @@ async def idsearch(self, ctx, query): tokenized_query = query.split() mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) - best_match, matches = max( + success, matches, mgen = max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) if tokenized_query != mw_tokenized_query else (None, {}), key=lambda t: t[1][t[0]].score if t[0] else 0 ) - if not best_match: + if not success: await ctx.send("No monster matched.") return - used = [best_match.monster_id] + used = set() monster_list = [] - for m in sorted(matches, key=lambda mon: find_monster.get_priority_tuple(mon, dgcog, matches=matches)): - bmid = dgcog.database.graph.get_base_monster(m).monster_id + for m in sorted(mgen, key=lambda m: find_monster.get_priority_tuple(m, dgcog, matches=matches), reverse=True): + print(m) + bmid = dgcog.database.graph.get_base_id(m) if bmid not in used: - used.append(bmid) + used.add(bmid) monster_list.append(m) - monster_list.reverse() - monster_list = [best_match] + monster_list[:10] + monster_list = monster_list[:11] + await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results') From af6555c7a3c3ee32301ab4fb7229379329606c99 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 15 Feb 2021 04:52:37 -0700 Subject: [PATCH 0623/1220] Delete errant print (#820) --- padinfo/padinfo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 52e3f4e5..8666fdc3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1140,7 +1140,6 @@ async def idsearch(self, ctx, *, query): used = set() monster_list = [] for m in sorted(mgen, key=lambda m: find_monster.get_priority_tuple(m, dgcog, matches=matches), reverse=True): - print(m) bmid = dgcog.database.graph.get_base_id(m) if bmid not in used: used.add(bmid) From 593eb4b41ac8f3e65371ddb1df83ffb0a907b4c4 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 15 Feb 2021 21:31:03 -0700 Subject: [PATCH 0624/1220] Show JW Distance in TbID (#824) --- padinfo/core/find_monster.py | 14 +++++++------- padinfo/padinfo.py | 15 ++++++++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index baf9cc80..3f100ecb 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -78,14 +78,14 @@ def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): def _monster_has_token(self, monster, token, matches, monster_mods): if len(token) < 6: if token in monster_mods: - matches[monster].mod.add(f"{token} - {token}") + matches[monster].mod.add((token, token)) matches[monster].score += 1 return True else: closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) rat = calc_ratio_modifier(closest, token) if rat > self.MODIFIER_JW_DISTANCE: - matches[monster].mod.add(f"{token} - {closest}") + matches[monster].mod.add((token, closest)) matches[monster].score += rat return True return False @@ -173,17 +173,17 @@ def get_valid_monsters_from_name_token(self, t, index2, matches, mult=1): score = calc_ratio_name(t, match, index2) for m in index2.manual[match]: if m not in valid: - matches[m].name.add(f"{t} - {match}") + matches[m].name.add((t, match, '')) matches[m].score += (score + .001) * mult valid.add(m) for m in index2.name_tokens[match]: if m not in valid: - matches[m].name.add(f"{t} - {match}") + matches[m].name.add((t, match, '')) matches[m].score += score * mult valid.add(m) for m in index2.fluff_tokens[match]: if m not in valid: - matches[m].name.add(f"{t} - {match}") + matches[m].name.add((t, match, '')) matches[m].score += score * mult / 2 valid.add(m) @@ -236,7 +236,7 @@ def get_monster_evos(self, database, monster_gen, matches): for evo in database.graph.get_alt_monsters(m): monster_evos.add(evo) if matches[evo].score < matches[m].score: - matches[evo].name = {t + f" (from evo {m.monster_id})" for t in matches[m].name} + matches[evo].name = {(t[0], t[1], f'(from evo {m.monster_id})') for t in matches[m].name} matches[evo].score = matches[m].score - .003 return monster_evos @@ -284,7 +284,7 @@ def __init__(self, score=0, name=None, mod=None): self.mod = set() def __repr__(self): - return str((self.score, [t.split(' - ')[0] for t in self.name], [t.split(' - ')[0] for t in self.mod])) + return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) async def find_monster_search(tokenized_query, dgcog) -> \ diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8666fdc3..2dcd33b2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -873,7 +873,7 @@ async def makeFailureMsg(self, ctx, query: str, err): "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) - @commands.command(aliases=["iddebug"]) + @commands.command(aliases=["iddebug", "dbid", "iddb"]) async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" dgcog = self.bot.get_cog("Dadguide") @@ -971,7 +971,7 @@ async def exportmodifiers(self, ctx): await ctx.send(file=text_to_file(o, filename="table.md")) - @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod"]) + @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod", "luid", "idlu"]) async def idmeaning(self, ctx, *, token): """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") @@ -1108,12 +1108,17 @@ async def idtraceback(self, ctx, *, query): else: lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) - mtokenstr = '\n'.join(sorted(mtokens)) - ntokenstr = '\n'.join(sorted(ntokens)) + mtokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" + f" ({round(calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" + for t in sorted(mtokens)) + ntokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" + f" ({round(calc_ratio_name(t[0], t[1], dgcog.index2), 2) if t[0] != t[1] else 'exact'})" + f" {t[2]}" + for t in sorted(ntokens)) await ctx.send(f"**Monster matched**: " f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})\n" - f"**Total Score**: {score}\n\n" + f"**Total Score**: {round(score, 2)}\n\n" f"**Matched Name Tokens**:\n{ntokenstr}\n\n" f"**Matched Mod Tokens**:\n{mtokenstr}\n\n" + (f"**Equally Scoring Matches**:\n{lpstr}" if lower_prio else "")) From 5f29cc17c1f111de368f4638855c2bea978a1b51 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 16 Feb 2021 01:18:32 -0600 Subject: [PATCH 0625/1220] Make child message a fully-deletable embed (#822) * Fix number of emojis after home button was deleted * Initial progress * omg it works * rm print statements * reformat code --- padinfo/id_menu.py | 6 +- padinfo/message_menu.py | 103 +++++++++++++++++++++++++++++ padinfo/monster_list_menu.py | 8 +-- padinfo/padinfo.py | 30 ++++++--- padinfo/view/evos.py | 3 +- padinfo/view/id.py | 10 +-- padinfo/view/links.py | 2 +- padinfo/view/message.py | 17 +++++ padinfo/view/otherinfo.py | 6 +- padinfo/view/pic.py | 2 +- padinfo/view/transforminfo.py | 11 ++- padinfo/view_state/base.py | 8 ++- padinfo/view_state/base_id.py | 6 +- padinfo/view_state/common.py | 5 -- padinfo/view_state/evos.py | 4 +- padinfo/view_state/id.py | 13 ++-- padinfo/view_state/materials.py | 4 +- padinfo/view_state/message.py | 21 ++++++ padinfo/view_state/monster_list.py | 14 ++-- padinfo/view_state/pantheon.py | 4 +- 20 files changed, 222 insertions(+), 55 deletions(-) create mode 100644 padinfo/message_menu.py create mode 100644 padinfo/view/message.py create mode 100644 padinfo/view_state/message.py diff --git a/padinfo/id_menu.py b/padinfo/id_menu.py index b9af9be3..aca5d9ce 100644 --- a/padinfo/id_menu.py +++ b/padinfo/id_menu.py @@ -8,6 +8,7 @@ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji +from padinfo.message_menu import MessageMenu from padinfo.pane_names import IdMenuPaneNames from padinfo.view.evos import EvosView from padinfo.view.id import IdView @@ -117,7 +118,10 @@ async def respond_with_refresh(message: Optional[Message], ims, **data): @staticmethod async def respond_with_delete(message: Optional[Message], ims, **data): - if ims.get('is_child') == 'True': + if ims.get('is_child'): + if ims.get('message'): + ims['menu_type'] = MessageMenu.MENU_TYPE + return await MessageMenu.respond_with_message(message, ims, **data) return await message.edit(embed=None) return await message.delete() diff --git a/padinfo/message_menu.py b/padinfo/message_menu.py new file mode 100644 index 00000000..1fe1d9ba --- /dev/null +++ b/padinfo/message_menu.py @@ -0,0 +1,103 @@ +from typing import TYPE_CHECKING, Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.emoji import EmbedMenuEmojiConfig +from discordmenu.embed.menu import EmbedMenu +from discordmenu.emoji.emoji_cache import emoji_cache +from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ + MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter + +from padinfo.view.message import MessageView +from padinfo.view_state.message import MessageViewState + +if TYPE_CHECKING: + pass + +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') + + +class MessagePaneNames: + home = 'home' + + +class MessageMenu: + MENU_TYPE = 'MessageMenu' + message = None + + @staticmethod + def menu(original_author_id, friend_ids, bot_id): + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(MessageMenuPanes.emoji_names()) + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(bot_id), + MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) + ] + embed = EmbedMenu(reaction_filters, MessageMenuPanes.transitions(), MessageMenu.message_control, + menu_emoji_config, + delete_func=MessageMenu.respond_with_delete) + return embed + + @staticmethod + async def respond_with_message(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + user_config = data.get('user_config') + view_state = await MessageViewState.deserialize(dgcog, user_config, ims) + control = MessageMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_delete(message: Optional[Message], ims, **data): + return await message.delete() + + @staticmethod + def message_control(state: MessageViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [MessageView.embed(state)], + reaction_list + ) + + +class MessageMenuPanes: + INITIAL_EMOJI = '\N{HOUSE BUILDING}' + DATA = { + MessageMenu.respond_with_message: ('\N{HOUSE BUILDING}', MessagePaneNames.home), + + } + HIDDEN_EMOJIS = [ + MessagePaneNames.home, + ] + + @classmethod + def emoji_names(cls): + return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] + + @classmethod + def transitions(cls): + return {v[0]: k for k, v in cls.DATA.items()} + + @classmethod + def pane_types(cls): + return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} + + @staticmethod + def get_initial_reaction_list(number_of_evos: int): + return MessageMenuPanes.emoji_names()[:number_of_evos] + + @staticmethod + def emoji_name_to_emoji(name: str): + for _, data_pair in MessageMenuPanes.DATA.items(): + if data_pair[1] == name: + return data_pair[0] + return None + + @staticmethod + def emoji_name_to_function(name: str): + for _, data_pair in MessageMenuPanes.DATA.items(): + if data_pair[1] == name: + return data_pair[1] + return None diff --git a/padinfo/monster_list_menu.py b/padinfo/monster_list_menu.py index 49f02a39..d407e783 100644 --- a/padinfo/monster_list_menu.py +++ b/padinfo/monster_list_menu.py @@ -62,12 +62,12 @@ async def respond_with_monster_list(message: Optional[Message], ims, **data): async def respond_with_n(message: Optional[Message], ims, n, **data): ims['resolved_monster_id'] = int(ims['monster_list'][n]) reaction_list = IdMenuPanes.emoji_names() - if not data.get('child_message_ims'): + if not data.get('child_message_ims') or data['child_message_ims']['menu_type'] != IdMenu.MENU_TYPE: # default to the overview screen if we weren't already on a screen - ims['reaction_list'] = ','.join(reaction_list) - ims['is_child'] = 'True' + ims['reaction_list'] = reaction_list + ims['is_child'] = True return await IdMenu.respond_with_current_id(message, ims, **data) - data['child_message_ims']['reaction_list'] = ','.join(reaction_list) + data['child_message_ims']['reaction_list'] = reaction_list data['child_message_ims']['resolved_monster_id'] = int(ims['resolved_monster_id']) return await IdMenu.respond_with_refresh(message, data['child_message_ims'], **data) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2dcd33b2..d067f060 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -17,7 +17,6 @@ from tabulate import tabulate from tsutils import char_to_emoji, is_donor, rmdiacritics -from padinfo.monster_list_menu import MonsterListMenu, MonsterListMenuPanes from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ get_attribute_emoji_by_monster @@ -32,17 +31,20 @@ from padinfo.id_menu import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.idtest_mixin import IdTest -from padinfo.pane_names import global_emoji_responses from padinfo.ls_menu import LeaderSkillMenu from padinfo.lss_menu import LeaderSkillSingleMenu +from padinfo.message_menu import MessageMenu, MessageMenuPanes +from padinfo.monster_list_menu import MonsterListMenu, MonsterListMenuPanes +from padinfo.pane_names import global_emoji_responses +from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.tf_menu import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader -from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState from padinfo.view_state.materials import MaterialsViewState +from padinfo.view_state.message import MessageViewState from padinfo.view_state.monster_list import MonsterListViewState from padinfo.view_state.otherinfo import OtherInfoViewState from padinfo.view_state.pantheon import PantheonViewState @@ -152,6 +154,7 @@ async def test_reaction_add(self, reaction, member): emoji_clicked in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES or emoji_clicked in IdMenuPanes.emoji_names() or emoji_clicked in MonsterListMenuPanes.emoji_names() or + emoji_clicked in MessageMenuPanes.emoji_names() or emoji_clicked in tf_menu_emoji_button_names): return @@ -168,6 +171,7 @@ async def test_reaction_add(self, reaction, member): IdMenu.MENU_TYPE: IdMenu.menu, TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu, MonsterListMenu.MENU_TYPE: MonsterListMenu.menu, + MessageMenu.MENU_TYPE: MessageMenu.menu, } # If true then the top menu will also respond on reaction @@ -222,6 +226,7 @@ async def test_reaction_add(self, reaction, member): await embed_menu.transition(message, ims, global_emoji_responses['reset'], member, **data) await embed_menu.transition(child_message, next_child_ims, emoji_clicked, member, **data) return + await embed_menu.transition(message, ims, emoji_clicked, member, **data) @commands.command() @@ -288,6 +293,7 @@ async def send_message(): f" Try using `{' '.join(goodquery)}` (with a space) instead! For more" f" info about the new id changes, check out" f" <{IDGUIDE}>!") + asyncio.create_task(send_message()) async with self.config.bad_queries() as bq: bq.append((raw_query, ctx.author.id)) @@ -600,24 +606,32 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] color = await self.get_user_embed_color(ctx) initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) + instruction_message = 'Click a reaction to see monster details!' state = MonsterListViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, query, color, - monster_list, title, + monster_list, title, instruction_message, reaction_list=initial_reaction_list ) parent_menu = MonsterListMenu.menu(original_author_id, friends, self.bot.user.id) message = await ctx.send('Setting up!') - child_message = await ctx.send( - 'Click a reaction to see monster details!') + ims = state.serialize() user_config = await BotConfig.get_user(self.config, ctx.author.id) data = { 'dgcog': dgcog, 'user_config': user_config, - 'child_message_id': child_message.id, } + child_state = MessageViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, color, + instruction_message, + reaction_list=[] + ) + child_menu = MessageMenu.menu(original_author_id, friends, self.bot.user.id) + child_message = await child_menu.create(ctx, child_state) + + data['child_message_id'] = child_message.id try: - await parent_menu.transition(message, ims, MonsterListMenuPanes.emoji_name_to_emoji('refresh'), ctx.author, **data) + await parent_menu.transition(message, ims, MonsterListMenuPanes.emoji_name_to_emoji('refresh'), ctx.author, + **data) await message.edit(content=None) except discord.errors.NotFound: # The user could delete the menu before we can do this diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 13cfd01c..39960f6d 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -29,7 +29,8 @@ def embed(state: EvosViewState): if state.gem_versions: fields.append( EmbedField( - ("{} evolve gem" if len(state.gem_versions) == 1 else "{} evolve gems").format(len(state.gem_versions)), + ("{} evolve gem" if len(state.gem_versions) == 1 else "{} evolve gems").format( + len(state.gem_versions)), Box(*_evo_lines(state.gem_versions, state.monster)))) return EmbedView( diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 9d12e5e1..a23e7fa8 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -2,8 +2,8 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.view import EmbedView from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText +from discordmenu.embed.view import EmbedView from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx @@ -80,14 +80,14 @@ def all_awakenings_row(m: "MonsterModel", transform_base): @staticmethod def killers_row(m: "MonsterModel", transform_base): - killers = m.killers if m==transform_base else transform_base.killers + killers = m.killers if m == transform_base else transform_base.killers killers_text = 'Any' if 'Any' in killers else \ ' '.join(_killer_latent_emoji(k) for k in killers) return Box( BoldText('Available killers:'), - Text('\N{DOWN-POINTING RED TRIANGLE}' if m!=transform_base else ''), - Text('[{} slots]'.format(m.latent_slots if m==transform_base \ - else transform_base.latent_slots)), + Text('\N{DOWN-POINTING RED TRIANGLE}' if m != transform_base else ''), + Text('[{} slots]'.format(m.latent_slots if m == transform_base \ + else transform_base.latent_slots)), Text(killers_text), delimiter=' ' ) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 8a82ae94..fa792faf 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -2,8 +2,8 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain -from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LinkedText +from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina from padinfo.view.components.monster.header import MonsterHeader diff --git a/padinfo/view/message.py b/padinfo/view/message.py new file mode 100644 index 00000000..816a6aaf --- /dev/null +++ b/padinfo/view/message.py @@ -0,0 +1,17 @@ +from discordmenu.embed.components import EmbedMain +from discordmenu.embed.view import EmbedView + +from padinfo.view.components.base import pad_info_footer_with_state +from padinfo.view_state.message import MessageViewState + + +class MessageView: + @staticmethod + def embed(state: MessageViewState): + return EmbedView( + EmbedMain( + color=state.color, + description=state.message + ), + embed_footer=pad_info_footer_with_state(state), + ) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 9cf64b48..ecdf7ab4 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -3,18 +3,18 @@ import prettytable from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.text import LabeledText, Text from discordmenu.embed.view import EmbedView -from discordmenu.embed.text import LabeledText, LinkedText, Text from redbot.core.utils.chat_formatting import box -from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina +from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.links import LinksView from padinfo.view_state.otherinfo import OtherInfoViewState if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + pass def statsbox(m): diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 43b8c925..0430b45b 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -1,7 +1,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage -from discordmenu.embed.view import EmbedView from discordmenu.embed.text import LinkedText, Text +from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 5b2eb5c7..db73bf21 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -2,16 +2,14 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.view import EmbedView from discordmenu.embed.text import Text, BoldText +from discordmenu.embed.view import EmbedView -from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.id import IdView -from padinfo.view_state.id import IdViewState from padinfo.view_state.transforminfo import TransformInfoViewState if TYPE_CHECKING: @@ -25,17 +23,18 @@ def base_info(m: "MonsterModel"): Box( BASE_EMOJI, IdView.normal_awakenings_row(m) if len(m.awakenings) != 0 - else Box(Text('No Awakenings')), + else Box(Text('No Awakenings')), delimiter=' ' ), IdView.super_awakenings_row(m) ) + def base_skill(m: "MonsterModel"): active_skill = m.active_skill return (" (" + BASE_EMOJI + " " + "{} -> {})".format(active_skill.turn_max, active_skill.turn_min) if active_skill - else 'None') + else 'None') class TransformInfoView: @@ -49,7 +48,7 @@ def embed(state: TransformInfoViewState): '/'.join(['{}'.format(t.name) for t in transformed_mon.types]), Box( IdView.normal_awakenings_row(transformed_mon) - if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), + if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), base_info(base_mon), IdView.killers_row(transformed_mon, base_mon) ), diff --git a/padinfo/view_state/base.py b/padinfo/view_state/base.py index 20303cf3..ee53e216 100644 --- a/padinfo/view_state/base.py +++ b/padinfo/view_state/base.py @@ -1,9 +1,13 @@ +from typing import List + + class ViewStateBase: - def __init__(self, original_author_id, menu_type, raw_query, extra_state=None): + def __init__(self, original_author_id, menu_type, raw_query, extra_state=None, reaction_list: List = None): self.extra_state = extra_state or {} self.menu_type = menu_type self.original_author_id = original_author_id self.raw_query = raw_query + self.reaction_list = reaction_list def serialize(self): ret = { @@ -12,4 +16,6 @@ def serialize(self): 'original_author_id': self.original_author_id, } ret.update(self.extra_state) + if self.reaction_list is not None: + ret['reaction_list'] = self.reaction_list return ret diff --git a/padinfo/view_state/base_id.py b/padinfo/view_state/base_id.py index 1c619fa0..5550a9c9 100644 --- a/padinfo/view_state/base_id.py +++ b/padinfo/view_state/base_id.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig -from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -30,7 +30,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, + 'reaction_list': self.reaction_list } ret.update(self.extra_state) return ret @@ -46,7 +46,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] - reaction_list = get_reaction_list_from_ims(ims) + reaction_list = ims.get('reaction_list') return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, diff --git a/padinfo/view_state/common.py b/padinfo/view_state/common.py index 2eb4b953..1a89945e 100644 --- a/padinfo/view_state/common.py +++ b/padinfo/view_state/common.py @@ -10,8 +10,3 @@ async def get_monster_from_ims(dgcog, ims: dict): return dgcog.database.graph.get_monster(resolved_monster_id) monster, _, _ = await findMonsterCustom(dgcog, query) return monster - - -def get_reaction_list_from_ims(ims): - reaction_list_str = ims.get('reaction_list') - return reaction_list_str.split(',') if reaction_list_str else None diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index 9e5d7a82..3dc445d8 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -3,7 +3,7 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -45,7 +45,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] - reaction_list = get_reaction_list_from_ims(ims) + reaction_list = ims.get('reaction_list') return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 865d15db..01705508 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -3,7 +3,7 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -12,6 +12,7 @@ class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], + fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, is_child: bool = False, @@ -20,6 +21,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) + self.fallback_message = fallback_message self.is_child = is_child self.acquire_raw = acquire_raw self.alt_monsters = alt_monsters @@ -31,7 +33,8 @@ def serialize(self): ret = super().serialize() ret.update({ 'pane_type': IdMenuPaneNames.id, - 'is_child': 'True' if self.is_child else '' + 'is_child': self.is_child, + 'message': self.fallback_message, }) return ret @@ -50,11 +53,13 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' - reaction_list = get_reaction_list_from_ims(ims) - is_child = bool(ims.get('is_child')) + reaction_list = ims.get('reaction_list') + fallback_message = ims.get('message') + is_child = ims.get('is_child') return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + fallback_message=fallback_message, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, is_child=is_child, diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 25fdeb6d..565af499 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -4,7 +4,7 @@ from padinfo.common.external_links import ilmina_skill from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -55,7 +55,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' - reaction_list = get_reaction_list_from_ims(ims) + reaction_list = ims.get('reaction_list') return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, diff --git a/padinfo/view_state/message.py b/padinfo/view_state/message.py new file mode 100644 index 00000000..035baf1d --- /dev/null +++ b/padinfo/view_state/message.py @@ -0,0 +1,21 @@ +from typing import List + +from padinfo.common.config import UserConfig +from padinfo.view_state.base import ViewStateBase + + +class MessageViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, + color, message, + reaction_list: List[str] = None): + super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) + self.message = message + self.color = color + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + raw_query = ims.get('raw_query') + return cls(original_author_id, menu_type, raw_query, user_config.color, ims.get('message'), + reaction_list=ims.get('reaction_list')) diff --git a/padinfo/view_state/monster_list.py b/padinfo/view_state/monster_list.py index ba289d5b..221d52f2 100644 --- a/padinfo/view_state/monster_list.py +++ b/padinfo/view_state/monster_list.py @@ -3,7 +3,6 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewStateBase -from padinfo.view_state.common import get_reaction_list_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -11,13 +10,14 @@ class MonsterListViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, query, color, - monster_list: List["MonsterModel"], title, + monster_list: List["MonsterModel"], title, message, reaction_list=None, extra_state=None, child_message_id=None ): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.message = message self.child_message_id = child_message_id self.title = title self.monster_list = monster_list @@ -31,8 +31,9 @@ def serialize(self): 'pane_type': IdMenuPaneNames.id, 'title': self.title, 'monster_list': [str(m.monster_no) for m in self.monster_list], - 'reaction_list': ','.join(self.reaction_list) if self.reaction_list else None, - 'child_message_id': self.child_message_id + 'reaction_list': self.reaction_list, + 'child_message_id': self.child_message_id, + 'message': self.message, }) return ret @@ -47,11 +48,12 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - reaction_list = get_reaction_list_from_ims(ims) + reaction_list = ims.get('reaction_list') child_message_id = ims.get('child_message_id') + message = ims.get('message') return MonsterListViewState(original_author_id, menu_type, raw_query, query, user_config.color, - monster_list=monster_list, title=title, + monster_list, title, message, reaction_list=reaction_list, extra_state=ims, child_message_id=child_message_id diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index 28ae8cea..bfa13bdc 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -3,7 +3,7 @@ from padinfo.common.config import UserConfig from padinfo.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims, get_reaction_list_from_ims +from padinfo.view_state.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -44,7 +44,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] - reaction_list = get_reaction_list_from_ims(ims) + reaction_list = ims.get('reaction_list') return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, pantheon_list, series_name, From 279dce2f82e62d381f1611f262b2688a75d855e0 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 16 Feb 2021 02:25:28 -0600 Subject: [PATCH 0626/1220] Rename message -> simple text & make menu folder (#825) * Rename message -> simple text & make menu folder * Move pane_names into menu folder --- padinfo/menu/__init__.py | 0 padinfo/{id_menu.py => menu/id.py} | 8 +++--- padinfo/{ls_menu.py => menu/leader_skill.py} | 0 .../leader_skill_single.py} | 0 .../monster_list.py} | 4 +-- padinfo/{ => menu}/pane_names.py | 0 .../{message_menu.py => menu/simple_text.py} | 26 ++++++++--------- padinfo/{tf_menu.py => menu/transforminfo.py} | 0 padinfo/padinfo.py | 28 +++++++++---------- padinfo/reaction_list.py | 2 +- padinfo/view/{message.py => simple_text.py} | 6 ++-- padinfo/view_state/evos.py | 2 +- padinfo/view_state/id.py | 2 +- padinfo/view_state/materials.py | 2 +- padinfo/view_state/monster_list.py | 2 +- padinfo/view_state/otherinfo.py | 2 +- padinfo/view_state/pantheon.py | 2 +- padinfo/view_state/pic.py | 2 +- .../view_state/{message.py => simple_text.py} | 2 +- 19 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 padinfo/menu/__init__.py rename padinfo/{id_menu.py => menu/id.py} (97%) rename padinfo/{ls_menu.py => menu/leader_skill.py} (100%) rename padinfo/{lss_menu.py => menu/leader_skill_single.py} (100%) rename padinfo/{monster_list_menu.py => menu/monster_list.py} (98%) rename padinfo/{ => menu}/pane_names.py (100%) rename padinfo/{message_menu.py => menu/simple_text.py} (80%) rename padinfo/{tf_menu.py => menu/transforminfo.py} (100%) rename padinfo/view/{message.py => simple_text.py} (75%) rename padinfo/view_state/{message.py => simple_text.py} (95%) diff --git a/padinfo/menu/__init__.py b/padinfo/menu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/id_menu.py b/padinfo/menu/id.py similarity index 97% rename from padinfo/id_menu.py rename to padinfo/menu/id.py index aca5d9ce..b7518c9b 100644 --- a/padinfo/id_menu.py +++ b/padinfo/menu/id.py @@ -8,8 +8,8 @@ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji -from padinfo.message_menu import MessageMenu -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.simple_text import SimpleTextMenu +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view.evos import EvosView from padinfo.view.id import IdView from padinfo.view.materials import MaterialsView @@ -120,8 +120,8 @@ async def respond_with_refresh(message: Optional[Message], ims, **data): async def respond_with_delete(message: Optional[Message], ims, **data): if ims.get('is_child'): if ims.get('message'): - ims['menu_type'] = MessageMenu.MENU_TYPE - return await MessageMenu.respond_with_message(message, ims, **data) + ims['menu_type'] = SimpleTextMenu.MENU_TYPE + return await SimpleTextMenu.respond_with_message(message, ims, **data) return await message.edit(embed=None) return await message.delete() diff --git a/padinfo/ls_menu.py b/padinfo/menu/leader_skill.py similarity index 100% rename from padinfo/ls_menu.py rename to padinfo/menu/leader_skill.py diff --git a/padinfo/lss_menu.py b/padinfo/menu/leader_skill_single.py similarity index 100% rename from padinfo/lss_menu.py rename to padinfo/menu/leader_skill_single.py diff --git a/padinfo/monster_list_menu.py b/padinfo/menu/monster_list.py similarity index 98% rename from padinfo/monster_list_menu.py rename to padinfo/menu/monster_list.py index d407e783..e81a642c 100644 --- a/padinfo/monster_list_menu.py +++ b/padinfo/menu/monster_list.py @@ -8,8 +8,8 @@ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji -from padinfo.id_menu import IdMenu, IdMenuPanes -from padinfo.pane_names import IdMenuPaneNames, MonsterListPaneNames, global_emoji_responses +from padinfo.menu.id import IdMenu, IdMenuPanes +from padinfo.menu.pane_names import MonsterListPaneNames, global_emoji_responses from padinfo.view.monster_list import MonsterListView from padinfo.view_state.monster_list import MonsterListViewState diff --git a/padinfo/pane_names.py b/padinfo/menu/pane_names.py similarity index 100% rename from padinfo/pane_names.py rename to padinfo/menu/pane_names.py diff --git a/padinfo/message_menu.py b/padinfo/menu/simple_text.py similarity index 80% rename from padinfo/message_menu.py rename to padinfo/menu/simple_text.py index 1fe1d9ba..fe85a714 100644 --- a/padinfo/message_menu.py +++ b/padinfo/menu/simple_text.py @@ -8,8 +8,8 @@ from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter -from padinfo.view.message import MessageView -from padinfo.view_state.message import MessageViewState +from padinfo.view.simple_text import SimpleTextView +from padinfo.view_state.simple_text import SimpleTextViewState if TYPE_CHECKING: pass @@ -17,12 +17,12 @@ menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') -class MessagePaneNames: +class SimpleTextNames: home = 'home' -class MessageMenu: - MENU_TYPE = 'MessageMenu' +class SimpleTextMenu: + MENU_TYPE = 'SimpleTextMenu' message = None @staticmethod @@ -34,17 +34,17 @@ def menu(original_author_id, friend_ids, bot_id): BotAuthoredMessageReactionFilter(bot_id), MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) ] - embed = EmbedMenu(reaction_filters, MessageMenuPanes.transitions(), MessageMenu.message_control, + embed = EmbedMenu(reaction_filters, MessageMenuPanes.transitions(), SimpleTextMenu.message_control, menu_emoji_config, - delete_func=MessageMenu.respond_with_delete) + delete_func=SimpleTextMenu.respond_with_delete) return embed @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): dgcog = data.get('dgcog') user_config = data.get('user_config') - view_state = await MessageViewState.deserialize(dgcog, user_config, ims) - control = MessageMenu.message_control(view_state) + view_state = await SimpleTextViewState.deserialize(dgcog, user_config, ims) + control = SimpleTextMenu.message_control(view_state) return control @staticmethod @@ -52,12 +52,12 @@ async def respond_with_delete(message: Optional[Message], ims, **data): return await message.delete() @staticmethod - def message_control(state: MessageViewState): + def message_control(state: SimpleTextViewState): if state is None: return None reaction_list = state.reaction_list return EmbedControl( - [MessageView.embed(state)], + [SimpleTextView.embed(state)], reaction_list ) @@ -65,11 +65,11 @@ def message_control(state: MessageViewState): class MessageMenuPanes: INITIAL_EMOJI = '\N{HOUSE BUILDING}' DATA = { - MessageMenu.respond_with_message: ('\N{HOUSE BUILDING}', MessagePaneNames.home), + SimpleTextMenu.respond_with_message: ('\N{HOUSE BUILDING}', SimpleTextNames.home), } HIDDEN_EMOJIS = [ - MessagePaneNames.home, + SimpleTextNames.home, ] @classmethod diff --git a/padinfo/tf_menu.py b/padinfo/menu/transforminfo.py similarity index 100% rename from padinfo/tf_menu.py rename to padinfo/menu/transforminfo.py diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d067f060..7b7ae2dd 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -28,23 +28,23 @@ from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query -from padinfo.id_menu import IdMenu, IdMenuPanes +from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.idtest_mixin import IdTest -from padinfo.ls_menu import LeaderSkillMenu -from padinfo.lss_menu import LeaderSkillSingleMenu -from padinfo.message_menu import MessageMenu, MessageMenuPanes -from padinfo.monster_list_menu import MonsterListMenu, MonsterListMenuPanes -from padinfo.pane_names import global_emoji_responses +from padinfo.menu.leader_skill import LeaderSkillMenu +from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu +from padinfo.menu.simple_text import SimpleTextMenu, MessageMenuPanes +from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes +from padinfo.menu.pane_names import global_emoji_responses from padinfo.reaction_list import get_id_menu_initial_reaction_list -from padinfo.tf_menu import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names +from padinfo.menu.transforminfo import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names from padinfo.view.components.monster.header import MonsterHeader from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState from padinfo.view_state.materials import MaterialsViewState -from padinfo.view_state.message import MessageViewState +from padinfo.view_state.simple_text import SimpleTextViewState from padinfo.view_state.monster_list import MonsterListViewState from padinfo.view_state.otherinfo import OtherInfoViewState from padinfo.view_state.pantheon import PantheonViewState @@ -171,7 +171,7 @@ async def test_reaction_add(self, reaction, member): IdMenu.MENU_TYPE: IdMenu.menu, TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu, MonsterListMenu.MENU_TYPE: MonsterListMenu.menu, - MessageMenu.MENU_TYPE: MessageMenu.menu, + SimpleTextMenu.MENU_TYPE: SimpleTextMenu.menu, } # If true then the top menu will also respond on reaction @@ -621,11 +621,11 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM 'dgcog': dgcog, 'user_config': user_config, } - child_state = MessageViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, color, - instruction_message, - reaction_list=[] - ) - child_menu = MessageMenu.menu(original_author_id, friends, self.bot.user.id) + child_state = SimpleTextViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, color, + instruction_message, + reaction_list=[] + ) + child_menu = SimpleTextMenu.menu(original_author_id, friends, self.bot.user.id) child_message = await child_menu.create(ctx, child_state) data['child_message_id'] = child_message.id diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index c39a1f63..3b7b030e 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -3,7 +3,7 @@ from discordmenu.emoji.emoji_cache import emoji_cache from padinfo.core.padinfo_settings import settings -from padinfo.id_menu import IdMenuPanes, IdMenu +from padinfo.menu.id import IdMenuPanes, IdMenu from padinfo.view_state.evos import EvosViewState from padinfo.view_state.materials import MaterialsViewState from padinfo.view_state.pantheon import PantheonViewState diff --git a/padinfo/view/message.py b/padinfo/view/simple_text.py similarity index 75% rename from padinfo/view/message.py rename to padinfo/view/simple_text.py index 816a6aaf..481c128c 100644 --- a/padinfo/view/message.py +++ b/padinfo/view/simple_text.py @@ -2,12 +2,12 @@ from discordmenu.embed.view import EmbedView from padinfo.view.components.base import pad_info_footer_with_state -from padinfo.view_state.message import MessageViewState +from padinfo.view_state.simple_text import SimpleTextViewState -class MessageView: +class SimpleTextView: @staticmethod - def embed(state: MessageViewState): + def embed(state: SimpleTextViewState): return EmbedView( EmbedMain( color=state.color, diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py index 3dc445d8..b824d16c 100644 --- a/padinfo/view_state/evos.py +++ b/padinfo/view_state/evos.py @@ -1,7 +1,7 @@ from typing import List, TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py index 01705508..5853c377 100644 --- a/padinfo/view_state/id.py +++ b/padinfo/view_state/id.py @@ -1,7 +1,7 @@ from typing import List, TYPE_CHECKING from padinfo.common.config import UserConfig -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py index 565af499..c389a7ba 100644 --- a/padinfo/view_state/materials.py +++ b/padinfo/view_state/materials.py @@ -2,7 +2,7 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims diff --git a/padinfo/view_state/monster_list.py b/padinfo/view_state/monster_list.py index 221d52f2..bbc06d11 100644 --- a/padinfo/view_state/monster_list.py +++ b/padinfo/view_state/monster_list.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view_state.base import ViewStateBase if TYPE_CHECKING: diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py index 289f92c1..8e6d6295 100644 --- a/padinfo/view_state/otherinfo.py +++ b/padinfo/view_state/otherinfo.py @@ -1,4 +1,4 @@ -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py index bfa13bdc..7419a2b3 100644 --- a/padinfo/view_state/pantheon.py +++ b/padinfo/view_state/pantheon.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId from padinfo.view_state.common import get_monster_from_ims diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py index b7663165..d59373ff 100644 --- a/padinfo/view_state/pic.py +++ b/padinfo/view_state/pic.py @@ -1,4 +1,4 @@ -from padinfo.pane_names import IdMenuPaneNames +from padinfo.menu.pane_names import IdMenuPaneNames from padinfo.view_state.base_id import ViewStateBaseId diff --git a/padinfo/view_state/message.py b/padinfo/view_state/simple_text.py similarity index 95% rename from padinfo/view_state/message.py rename to padinfo/view_state/simple_text.py index 035baf1d..33dba6ee 100644 --- a/padinfo/view_state/message.py +++ b/padinfo/view_state/simple_text.py @@ -4,7 +4,7 @@ from padinfo.view_state.base import ViewStateBase -class MessageViewState(ViewStateBase): +class SimpleTextViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, message, reaction_list: List[str] = None): From 0f2aad95d3b6a7d3a29f1a05899d81baa1a347f4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 17 Feb 2021 06:21:57 -0600 Subject: [PATCH 0627/1220] Delete id_menu_old (#827) --- padinfo/id_menu_old.py | 40 --------------------------- padinfo/padinfo.py | 62 ++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 72 deletions(-) delete mode 100644 padinfo/id_menu_old.py diff --git a/padinfo/id_menu_old.py b/padinfo/id_menu_old.py deleted file mode 100644 index 88486f58..00000000 --- a/padinfo/id_menu_old.py +++ /dev/null @@ -1,40 +0,0 @@ -import random -from typing import TYPE_CHECKING - -import discord -from discord import Color - -from padinfo.core.padinfo_settings import settings -from padinfo.view.id import IdView -from padinfo.view.leader_skill_single import LeaderSkillSingleView -from padinfo.view.links import LinksView -from padinfo.view.lookup import LookupView -from padinfo.view_state.id import IdViewState - -if TYPE_CHECKING: - from dadguide.database_context import DbContext - from dadguide.models.monster_model import MonsterModel - - -class IdMenu: - def __init__(self, ctx, dgcog=None, allowed_emojis: list = None): - self.ctx = ctx - self.dgcog = dgcog - self.allowed_emojis = allowed_emojis - - async def get_user_embed_color(self, pdicog): - color = await pdicog.config.user(self.ctx.author).color() - if color is None: - return Color.default() - elif color == "random": - return Color(random.randint(0x000000, 0xffffff)) - else: - return discord.Color(color) - - async def make_lookup_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return LookupView.embed(m, color).to_embed() - - async def make_links_embed(self, m: "MonsterModel"): - color = await self.get_user_embed_color(self.ctx.bot.get_cog("PadInfo")) - return LinksView.embed(m, color).to_embed() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7b7ae2dd..5e6e0727 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -28,27 +28,28 @@ from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query -from padinfo.menu.id import IdMenu, IdMenuPanes -from padinfo.id_menu_old import IdMenu as IdMenuOld from padinfo.idtest_mixin import IdTest +from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu -from padinfo.menu.simple_text import SimpleTextMenu, MessageMenuPanes from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes from padinfo.menu.pane_names import global_emoji_responses -from padinfo.reaction_list import get_id_menu_initial_reaction_list +from padinfo.menu.simple_text import SimpleTextMenu, MessageMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names +from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.links import LinksView +from padinfo.view.lookup import LookupView from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState from padinfo.view_state.materials import MaterialsViewState -from padinfo.view_state.simple_text import SimpleTextViewState from padinfo.view_state.monster_list import MonsterListViewState from padinfo.view_state.otherinfo import OtherInfoViewState from padinfo.view_state.pantheon import PantheonViewState from padinfo.view_state.pic import PicViewState +from padinfo.view_state.simple_text import SimpleTextViewState from padinfo.view_state.transforminfo import TransformInfoViewState if TYPE_CHECKING: @@ -513,20 +514,6 @@ async def pic(self, ctx, *, query: str): menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pic_control) await menu.create(ctx, state) - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def links(self, ctx, *, query: str): - """Monster links""" - dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if m is not None: - menu = IdMenuOld(ctx) - embed = await menu.make_links_embed(m) - await ctx.send(embed=embed) - - else: - await self.makeFailureMsg(ctx, query, err) - @commands.command(aliases=['stats']) @checks.bot_has_permissions(embed_links=True) async def otherinfo(self, ctx, *, query: str): @@ -554,6 +541,30 @@ async def otherinfo(self, ctx, *, query: str): menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.otherinfo_control) await menu.create(ctx, state) + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def links(self, ctx, *, query: str): + """Monster links""" + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, query) + if m is None: + await self.makeFailureMsg(ctx, query, err) + color = await self.get_user_embed_color(ctx) + embed = LinksView.embed(m, color).to_embed() + await ctx.send(embed=embed) + + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def lookup(self, ctx, *, query: str): + """Short info results for a monster query""" + dgcog = await self.get_dgcog() + m, err, debug_info = await findMonsterCustom(dgcog, query) + if m is None: + await self.makeFailureMsg(ctx, query, err) + color = await self.get_user_embed_color(ctx) + embed = LookupView.embed(m, color).to_embed() + await ctx.send(embed=embed) + @commands.command() @checks.bot_has_permissions(embed_links=True) async def buttoninfo(self, ctx, *, query: str): @@ -569,19 +580,6 @@ async def buttoninfo(self, ctx, *, query: str): for page in pagify(info_str): await ctx.send(box(page)) - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def lookup(self, ctx, *, query: str): - """Short info results for a monster query""" - dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if m is not None: - menu = IdMenuOld(ctx, allowed_emojis=self.get_emojis()) - embed = await menu.make_lookup_embed(m) - await ctx.send(embed=embed) - else: - await self.makeFailureMsg(ctx, query, err) - @commands.command() @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): From 11cb078ffed844737a013c92768a796ee2adbd93 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 17 Feb 2021 06:36:25 -0600 Subject: [PATCH 0628/1220] Rename send_id_failure_message in padinfo (#828) * Rename padinfo id faiulre message method * return after failure msg in links & lookup * rename again --- padinfo/padinfo.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5e6e0727..b85c23e9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -239,7 +239,7 @@ async def jpname(self, ctx, *, query: str): await ctx.send(MonsterHeader.short(m)) await ctx.send(box(m.name_ja)) else: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) @commands.command(name="id", aliases=["iD", "Id", "ID"]) @checks.bot_has_permissions(embed_links=True) @@ -302,7 +302,7 @@ async def send_message(): monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not monster: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return async def send_error(error): @@ -402,7 +402,7 @@ async def evos(self, ctx, *, query: str): monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) @@ -435,7 +435,7 @@ async def evomats(self, ctx, *, query: str): monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not monster: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ @@ -469,7 +469,7 @@ async def pantheon(self, ctx, *, query: str): monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) @@ -501,7 +501,7 @@ async def pic(self, ctx, *, query: str): monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] @@ -528,7 +528,7 @@ async def otherinfo(self, ctx, *, query: str): monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if monster is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] @@ -548,7 +548,8 @@ async def links(self, ctx, *, query: str): dgcog = await self.get_dgcog() m, err, debug_info = await findMonsterCustom(dgcog, query) if m is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) + return color = await self.get_user_embed_color(ctx) embed = LinksView.embed(m, color).to_embed() await ctx.send(embed=embed) @@ -560,7 +561,8 @@ async def lookup(self, ctx, *, query: str): dgcog = await self.get_dgcog() m, err, debug_info = await findMonsterCustom(dgcog, query) if m is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) + return color = await self.get_user_embed_color(ctx) embed = LookupView.embed(m, color).to_embed() await ctx.send(embed=embed) @@ -572,7 +574,7 @@ async def buttoninfo(self, ctx, *, query: str): dgcog = await self.get_dgcog() monster, err, _ = await findMonsterCustom(dgcog, query) if monster is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return DGCOG = self.bot.get_cog("Dadguide") info = button_info.get_info(DGCOG, monster) @@ -587,7 +589,7 @@ async def evolist(self, ctx, *, query): monster, err, debug_info = await findMonsterCustom(dgcog, query) if monster is None: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return alt_versions, _ = await EvosViewState.query(dgcog, monster) @@ -704,7 +706,7 @@ async def transforminfo(self, ctx, *, query): await perform_transforminfo_query(dgcog, query) if not base_mon: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) return if not transformed_mon: @@ -763,7 +765,7 @@ async def padsay(self, ctx, server, *, query: str = None): await ctx.send('Speaking for ' + header) await speech_cog.play_path(channel, voice_file) else: - await self.makeFailureMsg(ctx, query, err) + await self.send_id_failure_message(ctx, query, err) @commands.group(aliases=['idmode']) async def idset(self, ctx): @@ -879,7 +881,8 @@ def get_emojis(self): server_ids = [int(sid) for sid in settings.emojiServers()] return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] - async def makeFailureMsg(self, ctx, query: str, err): + @staticmethod + async def send_id_failure_message(ctx, query: str, err): await ctx.send("Sorry, your query {0} didn't match any results :(\n" "See <{2}> for " "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " From 614ae68f1422640034f85f5e6462666c0d43f02e Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 17 Feb 2021 14:34:21 -0700 Subject: [PATCH 0629/1220] Allow ids to work in dms (#830) --- padinfo/padinfo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b85c23e9..35908e64 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -208,7 +208,10 @@ async def test_reaction_add(self, reaction, member): 'user_config': user_config } if ims.get('child_message_id') and (ims['menu_type'], emoji_clicked) in respond_with_child.keys(): - await message.remove_reaction(emoji_clicked, member) + try: + await message.remove_reaction(emoji_clicked, member) + except discord.Forbidden: + pass fctx = await self.bot.get_context(message) child_message = await fctx.fetch_message(int(ims['child_message_id'])) previous_child_ims = child_message.embeds and IntraMessageState.extract_data(child_message.embeds[0]) From 371a9f5779ca596ce437c3b331648fc0af30b2fc Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 17 Feb 2021 17:25:38 -0600 Subject: [PATCH 0630/1220] Closable embed menu & use it for IdTraceback (#829) * Kwargs-based closable-embed menu * refactor friends * move mons name to title * show up to 20 monsters --- padinfo/menu/closable_embed.py | 41 ++++++++++++++++++ padinfo/padinfo.py | 62 ++++++++++++++-------------- padinfo/view/id_traceback.py | 46 +++++++++++++++++++++ padinfo/view_state/closable_embed.py | 10 +++++ 4 files changed, 129 insertions(+), 30 deletions(-) create mode 100644 padinfo/menu/closable_embed.py create mode 100644 padinfo/view/id_traceback.py create mode 100644 padinfo/view_state/closable_embed.py diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py new file mode 100644 index 00000000..a423500c --- /dev/null +++ b/padinfo/menu/closable_embed.py @@ -0,0 +1,41 @@ +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.emoji import EmbedMenuEmojiConfig +from discordmenu.embed.menu import EmbedMenu +from discordmenu.emoji.emoji_cache import emoji_cache +from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ + MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter + +from padinfo.view.id_traceback import IdTracebackView +from padinfo.view_state.closable_embed import ClosableEmbedViewState + +menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') + +view_types = { + IdTracebackView.VIEW_TYPE: IdTracebackView +} + + +class ClosableEmbedMenu: + MENU_TYPE = 'ClosableEmbedMenu' + message = None + + @staticmethod + def menu(original_author_id, friend_ids, bot_id): + valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(bot_id), + MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) + ] + embed = EmbedMenu(reaction_filters, {}, ClosableEmbedMenu.message_control, + menu_emoji_config) + return embed + + @staticmethod + def message_control(state: ClosableEmbedViewState): + view = view_types[state.view_type] + return EmbedControl( + [view.embed(state, **state.kwargs)], + ['\N{CROSS MARK}'] + ) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 35908e64..73492b67 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -29,6 +29,7 @@ from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.idtest_mixin import IdTest +from padinfo.menu.closable_embed import ClosableEmbedMenu from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu @@ -38,8 +39,10 @@ from padinfo.menu.transforminfo import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.id_traceback import IdTracebackView from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView +from padinfo.view_state.closable_embed import ClosableEmbedViewState from padinfo.view_state.evos import EvosViewState from padinfo.view_state.id import IdViewState from padinfo.view_state.leader_skill import LeaderSkillViewState @@ -173,6 +176,7 @@ async def test_reaction_add(self, reaction, member): TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu, MonsterListMenu.MENU_TYPE: MonsterListMenu.menu, SimpleTextMenu.MENU_TYPE: SimpleTextMenu.menu, + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu.menu, } # If true then the top menu will also respond on reaction @@ -195,8 +199,7 @@ async def test_reaction_add(self, reaction, member): if not menu_func: return - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) embed_menu = menu_func(original_author_id, friends, self.bot.user.id) if not (await embed_menu.should_respond(message, reaction, member)): return @@ -273,8 +276,7 @@ async def _do_id(self, ctx, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) goodquery = None if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.index2.all_name_tokens: @@ -399,8 +401,7 @@ async def evos(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -433,8 +434,7 @@ async def evomats(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not monster: @@ -466,8 +466,7 @@ async def pantheon(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -498,8 +497,7 @@ async def pic(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -525,8 +523,7 @@ async def otherinfo(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -605,8 +602,7 @@ async def evolist(self, ctx, *, query): async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], title): raw_query = query original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) color = await self.get_user_embed_color(ctx) initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' @@ -664,8 +660,7 @@ async def leaderskill(self, ctx, *, raw_query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = friend_cog and (await friend_cog.get_friends(original_author_id)) + friends = await self.get_user_friends(original_author_id) state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, color, l_mon, r_mon, l_query, r_query) menu = LeaderSkillMenu.menu(original_author_id, friends, self.bot.user.id) @@ -675,7 +670,8 @@ async def get_user_embed_color(self, ctx): color = await self.config.user(ctx.author).color() return self.user_color_to_discord_color(color) - def user_color_to_discord_color(self, color): + @staticmethod + def user_color_to_discord_color(color): if color is None: return Color.default() elif color == "random": @@ -683,6 +679,11 @@ def user_color_to_discord_color(self, color): else: return discord.Color(color) + async def get_user_friends(self, original_author_id): + friend_cog = self.bot.get_cog("Friend") + friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + return friends + @commands.command(aliases=['lssingle']) @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): @@ -694,8 +695,7 @@ async def leaderskillsingle(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, color, m) menu = LeaderSkillSingleMenu.menu(original_author_id, friends, self.bot.user.id) await menu.create(ctx, state) @@ -723,8 +723,7 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + friends = await self.get_user_friends(original_author_id) acquire_raw, base_rarity, true_evo_type_raw = \ await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, @@ -1121,7 +1120,7 @@ async def idtraceback(self, ctx, *, query): ntokens = matches[monster].name mtokens = matches[monster].mod lower_prio = {m for m in matches if matches[m].score == matches[monster].score}.difference({monster}) - if len(lower_prio) > 10: + if len(lower_prio) > 20: lpstr = f"{len(lower_prio)} other monsters." else: lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) @@ -1134,12 +1133,15 @@ async def idtraceback(self, ctx, *, query): f" {t[2]}" for t in sorted(ntokens)) - await ctx.send(f"**Monster matched**: " - f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})\n" - f"**Total Score**: {round(score, 2)}\n\n" - f"**Matched Name Tokens**:\n{ntokenstr}\n\n" - f"**Matched Mod Tokens**:\n{mtokenstr}\n\n" + - (f"**Equally Scoring Matches**:\n{lpstr}" if lower_prio else "")) + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + friends = await self.get_user_friends(original_author_id) + menu = ClosableEmbedMenu.menu(original_author_id, friends, self.bot.user.id) + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, color, + IdTracebackView.VIEW_TYPE, + monster=monster, score=score, name_tokens=ntokenstr, modifier_tokens=mtokenstr, + lower_priority_monsters=lpstr if lower_prio else "None") + await menu.create(ctx, state) @commands.command(aliases=["ids"]) async def idsearch(self, ctx, *, query): diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py new file mode 100644 index 00000000..253362d9 --- /dev/null +++ b/padinfo/view/id_traceback.py @@ -0,0 +1,46 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.text import LabeledText +from discordmenu.embed.view import EmbedView + +from padinfo.common.emoji_map import get_attribute_emoji_by_monster +from padinfo.view.components.base import pad_info_footer_with_state + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +def get_title(monster: "MonsterModel"): + return f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})" + + +def get_description(score: int): + return Box( + LabeledText( + 'Total score', + str(round(score, 2)) + ) + ) + + +class IdTracebackView: + VIEW_TYPE = 'IdTraceback' + + @staticmethod + def embed(state, monster: "MonsterModel" = None, score: int = None, name_tokens: str = None, + modifier_tokens: str = None, lower_priority_monsters: str = None): + fields = [ + EmbedField('Matched Name Tokens', Box(name_tokens)), + EmbedField('Matched Modifier Tokens', Box(modifier_tokens)), + EmbedField('Equally-scoring matches', Box(lower_priority_monsters)), + ] + return EmbedView( + EmbedMain( + color=state.color, + title=get_title(monster), + description=get_description(score) + ), + embed_footer=pad_info_footer_with_state(state), + embed_fields=fields) diff --git a/padinfo/view_state/closable_embed.py b/padinfo/view_state/closable_embed.py new file mode 100644 index 00000000..af5305bb --- /dev/null +++ b/padinfo/view_state/closable_embed.py @@ -0,0 +1,10 @@ +from padinfo.view_state.base import ViewStateBase + + +class ClosableEmbedViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, + color, view_type, **kwargs): + super().__init__(original_author_id, menu_type, raw_query) + self.color = color + self.view_type = view_type + self.kwargs = kwargs From ec797cde96cb5ae07d037a0db78134b7a24c2897 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 17 Feb 2021 19:08:50 -0600 Subject: [PATCH 0631/1220] use IdTracebackViewProps instead of kwargs (#832) --- padinfo/menu/closable_embed.py | 2 +- padinfo/padinfo.py | 8 ++++---- padinfo/view/id_traceback.py | 23 ++++++++++++++++------- padinfo/view_state/closable_embed.py | 4 ++-- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index a423500c..7e364ffc 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -36,6 +36,6 @@ def menu(original_author_id, friend_ids, bot_id): def message_control(state: ClosableEmbedViewState): view = view_types[state.view_type] return EmbedControl( - [view.embed(state, **state.kwargs)], + [view.embed(state, state.props)], ['\N{CROSS MARK}'] ) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 73492b67..b214498e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -39,7 +39,7 @@ from padinfo.menu.transforminfo import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.id_traceback import IdTracebackView +from padinfo.view.id_traceback import IdTracebackView, IdTracebackViewProps from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView from padinfo.view_state.closable_embed import ClosableEmbedViewState @@ -1137,10 +1137,10 @@ async def idtraceback(self, ctx, *, query): original_author_id = ctx.message.author.id friends = await self.get_user_friends(original_author_id) menu = ClosableEmbedMenu.menu(original_author_id, friends, self.bot.user.id) + props = IdTracebackViewProps(monster=monster, score=score, name_tokens=ntokenstr, modifier_tokens=mtokenstr, + lower_priority_monsters=lpstr if lower_prio else "None") state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, color, - IdTracebackView.VIEW_TYPE, - monster=monster, score=score, name_tokens=ntokenstr, modifier_tokens=mtokenstr, - lower_priority_monsters=lpstr if lower_prio else "None") + IdTracebackView.VIEW_TYPE, props) await menu.create(ctx, state) @commands.command(aliases=["ids"]) diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index 253362d9..c39df622 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -12,6 +12,16 @@ from dadguide.models.monster_model import MonsterModel +class IdTracebackViewProps: + def __init__(self, monster: "MonsterModel", score: int, name_tokens: str, + modifier_tokens: str, lower_priority_monsters: str): + self.lower_priority_monsters = lower_priority_monsters + self.modifier_tokens = modifier_tokens + self.name_tokens = name_tokens + self.score = score + self.monster = monster + + def get_title(monster: "MonsterModel"): return f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})" @@ -29,18 +39,17 @@ class IdTracebackView: VIEW_TYPE = 'IdTraceback' @staticmethod - def embed(state, monster: "MonsterModel" = None, score: int = None, name_tokens: str = None, - modifier_tokens: str = None, lower_priority_monsters: str = None): + def embed(state, props: IdTracebackViewProps): fields = [ - EmbedField('Matched Name Tokens', Box(name_tokens)), - EmbedField('Matched Modifier Tokens', Box(modifier_tokens)), - EmbedField('Equally-scoring matches', Box(lower_priority_monsters)), + EmbedField('Matched Name Tokens', Box(props.name_tokens)), + EmbedField('Matched Modifier Tokens', Box(props.modifier_tokens)), + EmbedField('Equally-scoring matches', Box(props.lower_priority_monsters)), ] return EmbedView( EmbedMain( color=state.color, - title=get_title(monster), - description=get_description(score) + title=get_title(props.monster), + description=get_description(props.score) ), embed_footer=pad_info_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view_state/closable_embed.py b/padinfo/view_state/closable_embed.py index af5305bb..28236a8e 100644 --- a/padinfo/view_state/closable_embed.py +++ b/padinfo/view_state/closable_embed.py @@ -3,8 +3,8 @@ class ClosableEmbedViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, - color, view_type, **kwargs): + color, view_type, props): super().__init__(original_author_id, menu_type, raw_query) self.color = color self.view_type = view_type - self.kwargs = kwargs + self.props = props From 00f78369a3ed3bbb97eb3d0ef2bc0ef45387c545 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 17 Feb 2021 21:10:25 -0600 Subject: [PATCH 0632/1220] Refactor reaction list - req discord-menu update (#833) * Refactor reaction list * bump discordmenu version * emoji button names on tfinfo object --- padinfo/info.json | 2 +- padinfo/menu/closable_embed.py | 19 ++----- padinfo/menu/id.py | 15 +---- padinfo/menu/leader_skill.py | 22 ++------ padinfo/menu/leader_skill_single.py | 45 ++++++--------- padinfo/menu/monster_list.py | 15 +---- padinfo/menu/simple_text.py | 22 ++------ padinfo/menu/transforminfo.py | 16 +----- padinfo/padinfo.py | 88 ++++++++++++++++------------- 9 files changed, 93 insertions(+), 151 deletions(-) diff --git a/padinfo/info.json b/padinfo/info.json index 7d98cb59..14363409 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -23,7 +23,7 @@ "tsutils>=3.2.2", "prettytable", "python-levenshtein", - "discord-menu>=0.14.0" + "discord-menu>=0.16.0" ], "tags": [ "PAD" diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index 7e364ffc..ef4c7d3e 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -1,13 +1,11 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu -from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from padinfo.view.id_traceback import IdTracebackView from padinfo.view_state.closable_embed import ClosableEmbedViewState +emoji_button_names = ['\N{CROSS MARK}'] menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') view_types = { @@ -17,19 +15,12 @@ class ClosableEmbedMenu: MENU_TYPE = 'ClosableEmbedMenu' + EMOJI_BUTTON_NAMES = emoji_button_names message = None @staticmethod - def menu(original_author_id, friend_ids, bot_id): - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] - reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(bot_id), - MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) - ] - embed = EmbedMenu(reaction_filters, {}, ClosableEmbedMenu.message_control, - menu_emoji_config) + def menu(): + embed = EmbedMenu({}, ClosableEmbedMenu.message_control, menu_emoji_config) return embed @staticmethod @@ -37,5 +28,5 @@ def message_control(state: ClosableEmbedViewState): view = view_types[state.view_type] return EmbedControl( [view.embed(state, state.props)], - ['\N{CROSS MARK}'] + ClosableEmbedMenu.EMOJI_BUTTON_NAMES ) diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index b7518c9b..8d31de6d 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -4,12 +4,10 @@ from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji -from padinfo.menu.simple_text import SimpleTextMenu from padinfo.menu.pane_names import IdMenuPaneNames +from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.evos import EvosView from padinfo.view.id import IdView from padinfo.view.materials import MaterialsView @@ -34,18 +32,11 @@ class IdMenu: MENU_TYPE = 'IdMenu' @staticmethod - def menu(original_author_id, friend_ids, bot_id, initial_control=None): + def menu(initial_control=None): if initial_control is None: initial_control = IdMenu.id_control - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(IdMenuPanes.emoji_names()) - reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(bot_id), - MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) - ] - embed = EmbedMenu(reaction_filters, IdMenuPanes.transitions(), initial_control, menu_emoji_config, + embed = EmbedMenu(IdMenuPanes.transitions(), initial_control, menu_emoji_config, delete_func=IdMenu.respond_with_delete) return embed diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index de7191ad..1f64b3fa 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -4,8 +4,6 @@ from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji from padinfo.view.id import IdView @@ -23,23 +21,15 @@ class LeaderSkillMenu: MENU_TYPE = 'LeaderSkill' EMOJI_BUTTON_NAMES = ('\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r'), '\N{CROSS MARK}') - @classmethod - def menu(cls, original_author_id, friend_ids, bot_id): + @staticmethod + def menu(): transitions = { - cls.EMOJI_BUTTON_NAMES[0]: LeaderSkillMenu.respond_to_house, - cls.EMOJI_BUTTON_NAMES[1]: LeaderSkillMenu.respond_to_l, - cls.EMOJI_BUTTON_NAMES[2]: LeaderSkillMenu.respond_to_r, + LeaderSkillMenu.EMOJI_BUTTON_NAMES[0]: LeaderSkillMenu.respond_to_house, + LeaderSkillMenu.EMOJI_BUTTON_NAMES[1]: LeaderSkillMenu.respond_to_l, + LeaderSkillMenu.EMOJI_BUTTON_NAMES[2]: LeaderSkillMenu.respond_to_r, } - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) - reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(bot_id), - MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) - ] - - return EmbedMenu(reaction_filters, transitions, LeaderSkillMenu.ls_control, menu_emoji_config) + return EmbedMenu(transitions, LeaderSkillMenu.ls_control, menu_emoji_config) @staticmethod async def respond_to_r(message: Optional[Message], ims, **data): diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index d53dd052..7df0b33b 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -4,8 +4,6 @@ from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from padinfo.view.id import IdView from padinfo.view.leader_skill_single import LeaderSkillSingleView @@ -22,45 +20,36 @@ class LeaderSkillSingleMenu: EMOJI_BUTTON_NAMES = ('\N{HOUSE BUILDING}', '\N{SQUARED ID}', '\N{CROSS MARK}') MENU_TYPE = 'LeaderSkillSingle' - @classmethod - def menu(cls, original_author_id, friend_ids, bot_id): + @staticmethod + def menu(): transitions = { - cls.EMOJI_BUTTON_NAMES[0]: cls.respond_to_house, - cls.EMOJI_BUTTON_NAMES[1]: cls.view_ls, + LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES[0]: LeaderSkillSingleMenu.respond_to_house, + LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES[1]: LeaderSkillSingleMenu.view_ls, } + return EmbedMenu(transitions, LeaderSkillSingleMenu.ls_control, menu_emoji_config) - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) - reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(bot_id), - MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) - ] - - return EmbedMenu(reaction_filters, transitions, cls.ls_control, menu_emoji_config) - - @classmethod - async def view_ls(cls, message: Optional[Message], ims, *, dgcog, user_config, **data): + @staticmethod + async def view_ls(message: Optional[Message], ims, *, dgcog, user_config, **data): id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) - id_control = cls.id_control(id_view_state) + id_control = LeaderSkillSingleMenu.id_control(id_view_state) return id_control - @classmethod - async def respond_to_house(cls, message: Optional[Message], ims, *, dgcog, user_config, **data): + @staticmethod + async def respond_to_house(message: Optional[Message], ims, *, dgcog, user_config, **data): ls_view_state = await LeaderSkillSingleViewState.deserialize(dgcog, user_config, ims) - ls_control = cls.ls_control(ls_view_state) + ls_control = LeaderSkillSingleMenu.ls_control(ls_view_state) return ls_control - @classmethod - def ls_control(cls, state: LeaderSkillSingleViewState): + @staticmethod + def ls_control(state: LeaderSkillSingleViewState): return EmbedControl( [LeaderSkillSingleView.embed(state)], - [emoji_cache.get_by_name(e) for e in cls.EMOJI_BUTTON_NAMES] + [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES] ) - @classmethod - def id_control(cls, state: IdViewState): + @staticmethod + def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in cls.EMOJI_BUTTON_NAMES] + [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES] ) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index e81a642c..ae48cd8e 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -3,9 +3,6 @@ from discord import Message from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl -from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from tsutils import char_to_emoji from padinfo.menu.id import IdMenu, IdMenuPanes @@ -24,18 +21,10 @@ class MonsterListMenu: CHILD_MENU_TYPE = 'IdMenu' @staticmethod - def menu(original_author_id, friend_ids, bot_id, initial_control=None): + def menu(initial_control=None): if initial_control is None: initial_control = MonsterListMenu.monster_list_control - - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(MonsterListMenuPanes.emoji_names()) - reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(bot_id), - MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) - ] - embed = EmbedMenu(reaction_filters, MonsterListMenuPanes.transitions(), initial_control, menu_emoji_config) + embed = EmbedMenu(MonsterListMenuPanes.transitions(), initial_control, menu_emoji_config) return embed @staticmethod diff --git a/padinfo/menu/simple_text.py b/padinfo/menu/simple_text.py index fe85a714..31b3983a 100644 --- a/padinfo/menu/simple_text.py +++ b/padinfo/menu/simple_text.py @@ -4,9 +4,6 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu -from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from padinfo.view.simple_text import SimpleTextView from padinfo.view_state.simple_text import SimpleTextViewState @@ -26,15 +23,8 @@ class SimpleTextMenu: message = None @staticmethod - def menu(original_author_id, friend_ids, bot_id): - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(MessageMenuPanes.emoji_names()) - reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(bot_id), - MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) - ] - embed = EmbedMenu(reaction_filters, MessageMenuPanes.transitions(), SimpleTextMenu.message_control, + def menu(): + embed = EmbedMenu(SimpleTextMenuPanes.transitions(), SimpleTextMenu.message_control, menu_emoji_config, delete_func=SimpleTextMenu.respond_with_delete) return embed @@ -62,7 +52,7 @@ def message_control(state: SimpleTextViewState): ) -class MessageMenuPanes: +class SimpleTextMenuPanes: INITIAL_EMOJI = '\N{HOUSE BUILDING}' DATA = { SimpleTextMenu.respond_with_message: ('\N{HOUSE BUILDING}', SimpleTextNames.home), @@ -86,18 +76,18 @@ def pane_types(cls): @staticmethod def get_initial_reaction_list(number_of_evos: int): - return MessageMenuPanes.emoji_names()[:number_of_evos] + return SimpleTextMenuPanes.emoji_names()[:number_of_evos] @staticmethod def emoji_name_to_emoji(name: str): - for _, data_pair in MessageMenuPanes.DATA.items(): + for _, data_pair in SimpleTextMenuPanes.DATA.items(): if data_pair[1] == name: return data_pair[0] return None @staticmethod def emoji_name_to_function(name: str): - for _, data_pair in MessageMenuPanes.DATA.items(): + for _, data_pair in SimpleTextMenuPanes.DATA.items(): if data_pair[1] == name: return data_pair[1] return None diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index c3086f41..7c39e16c 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -4,8 +4,6 @@ from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.reaction_filter import ValidEmojiReactionFilter, NotPosterEmojiReactionFilter, \ - MessageOwnerReactionFilter, FriendReactionFilter, BotAuthoredMessageReactionFilter from padinfo.view.id import IdView from padinfo.view.transforminfo import TransformInfoView @@ -23,25 +21,17 @@ class TransformInfoMenu: INITIAL_EMOJI = emoji_button_names[0] MENU_TYPE = 'TransformInfo' + EMOJI_BUTTON_NAMES = emoji_button_names @staticmethod - def menu(original_author_id, friend_ids, bot_id): + def menu(): transitions = { TransformInfoMenu.INITIAL_EMOJI: TransformInfoMenu.respond_with_overview, emoji_button_names[1]: TransformInfoMenu.respond_with_base, emoji_button_names[2]: TransformInfoMenu.respond_with_transform, } - valid_emoji_names = [e.name for e in emoji_cache.custom_emojis] + list(transitions.keys()) - reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(bot_id), - MessageOwnerReactionFilter(original_author_id, - FriendReactionFilter(original_author_id, friend_ids)) - ] - - return EmbedMenu(reaction_filters, transitions, TransformInfoMenu.tf_control, + return EmbedMenu(transitions, TransformInfoMenu.tf_control, menu_emoji_config) @staticmethod diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b214498e..626b85ff 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -12,6 +12,8 @@ from discord import Color from discordmenu.emoji.emoji_cache import emoji_cache from discordmenu.intra_message_state import IntraMessageState +from discordmenu.reaction_filter import FriendReactionFilter, MessageOwnerReactionFilter, \ + BotAuthoredMessageReactionFilter, NotPosterEmojiReactionFilter, ValidEmojiReactionFilter from redbot.core import checks, commands, data_manager, Config from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate @@ -35,8 +37,8 @@ from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes from padinfo.menu.pane_names import global_emoji_responses -from padinfo.menu.simple_text import SimpleTextMenu, MessageMenuPanes -from padinfo.menu.transforminfo import TransformInfoMenu, emoji_button_names as tf_menu_emoji_button_names +from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes +from padinfo.menu.transforminfo import TransformInfoMenu from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.id_traceback import IdTracebackView, IdTracebackViewProps @@ -154,12 +156,21 @@ async def test_reaction_add(self, reaction, member): else: emoji_clicked = emoji_obj.name - if not (emoji_clicked in LeaderSkillMenu.EMOJI_BUTTON_NAMES or - emoji_clicked in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES or - emoji_clicked in IdMenuPanes.emoji_names() or - emoji_clicked in MonsterListMenuPanes.emoji_names() or - emoji_clicked in MessageMenuPanes.emoji_names() or - emoji_clicked in tf_menu_emoji_button_names): + menu_to_emoji_list_map = { + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu.EMOJI_BUTTON_NAMES, + IdMenu.MENU_TYPE: IdMenuPanes.emoji_names(), + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu.EMOJI_BUTTON_NAMES, + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES, + MonsterListMenu.MENU_TYPE: MonsterListMenuPanes.emoji_names(), + SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes.emoji_names(), + TransformInfoMenu.MENU_TYPE: TransformInfoMenu.EMOJI_BUTTON_NAMES + } + + emoji_found = False + for menu_type, emoji_list in menu_to_emoji_list_map.items(): + if emoji_clicked in emoji_list: + emoji_found = True + if not emoji_found: return message = reaction.message @@ -199,9 +210,10 @@ async def test_reaction_add(self, reaction, member): if not menu_func: return - friends = await self.get_user_friends(original_author_id) - embed_menu = menu_func(original_author_id, friends, self.bot.user.id) - if not (await embed_menu.should_respond(message, reaction, member)): + embed_menu = menu_func() + if not (await embed_menu.should_respond( + message, reaction, await self.get_reaction_filters( + original_author_id, menu_to_emoji_list_map[menu_type]), member)): return dgcog = await self.get_dgcog() @@ -236,6 +248,17 @@ async def test_reaction_add(self, reaction, member): await embed_menu.transition(message, ims, emoji_clicked, member, **data) + async def get_reaction_filters(self, original_author_id, valid_emoji_names): + friend_cog = self.bot.get_cog("Friend") + friend_ids = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] + reaction_filters = [ + ValidEmojiReactionFilter(valid_emoji_names), + NotPosterEmojiReactionFilter(), + BotAuthoredMessageReactionFilter(self.bot.user.id), + MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) + ] + return reaction_filters + @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" @@ -276,7 +299,6 @@ async def _do_id(self, ctx, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) goodquery = None if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.index2.all_name_tokens: @@ -336,7 +358,7 @@ async def send_error(error): monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, use_evo_scroll=settings.checkEvoID(ctx.author.id), reaction_list=initial_reaction_list) - menu = IdMenu.menu(original_author_id, friends, self.bot.user.id) + menu = IdMenu.menu() await menu.create(ctx, state) async def send_survey_after(self, ctx, query, result_monster): @@ -389,7 +411,7 @@ async def checkbadness(self, ctx): @commands.command() @checks.bot_has_permissions() - async def id2(self, ctx, *, query: str): + async def id2(self, ctx): """Monster info (main tab)""" await ctx.send("id2 has been discontinued!".format(ctx.prefix)) @@ -401,7 +423,6 @@ async def evos(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -423,7 +444,7 @@ async def evos(self, ctx, *, query: str): monster, alt_versions, gem_versions, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) - menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.evos_control) + menu = IdMenu.menu(initial_control=IdMenu.evos_control) await menu.create(ctx, state) @commands.command(name="mats", aliases=['evomats', 'evomat', 'skillups']) @@ -434,7 +455,6 @@ async def evomats(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) if not monster: @@ -455,7 +475,7 @@ async def evomats(self, ctx, *, query: str): mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) - menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.mats_control) + menu = IdMenu.menu(initial_control=IdMenu.mats_control) await menu.create(ctx, state) @commands.command(aliases=["series"]) @@ -466,7 +486,6 @@ async def pantheon(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -486,7 +505,7 @@ async def pantheon(self, ctx, *, query: str): monster, pantheon_list, series_name, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) - menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pantheon_control) + menu = IdMenu.menu(initial_control=IdMenu.pantheon_control) await menu.create(ctx, state) @commands.command(aliases=['img']) @@ -497,7 +516,6 @@ async def pic(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -512,7 +530,7 @@ async def pic(self, ctx, *, query: str): monster, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) - menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.pic_control) + menu = IdMenu.menu(initial_control=IdMenu.pic_control) await menu.create(ctx, state) @commands.command(aliases=['stats']) @@ -523,7 +541,6 @@ async def otherinfo(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) @@ -538,7 +555,7 @@ async def otherinfo(self, ctx, *, query: str): monster, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) - menu = IdMenu.menu(original_author_id, friends, self.bot.user.id, initial_control=IdMenu.otherinfo_control) + menu = IdMenu.menu(initial_control=IdMenu.otherinfo_control) await menu.create(ctx, state) @commands.command() @@ -602,7 +619,6 @@ async def evolist(self, ctx, *, query): async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], title): raw_query = query original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) color = await self.get_user_embed_color(ctx) initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' @@ -611,7 +627,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM monster_list, title, instruction_message, reaction_list=initial_reaction_list ) - parent_menu = MonsterListMenu.menu(original_author_id, friends, self.bot.user.id) + parent_menu = MonsterListMenu.menu() message = await ctx.send('Setting up!') ims = state.serialize() @@ -624,7 +640,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM instruction_message, reaction_list=[] ) - child_menu = SimpleTextMenu.menu(original_author_id, friends, self.bot.user.id) + child_menu = SimpleTextMenu.menu() child_message = await child_menu.create(ctx, child_state) data['child_message_id'] = child_message.id @@ -660,10 +676,9 @@ async def leaderskill(self, ctx, *, raw_query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, color, l_mon, r_mon, l_query, r_query) - menu = LeaderSkillMenu.menu(original_author_id, friends, self.bot.user.id) + menu = LeaderSkillMenu.menu() await menu.create(ctx, state) async def get_user_embed_color(self, ctx): @@ -695,9 +710,8 @@ async def leaderskillsingle(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, color, m) - menu = LeaderSkillSingleMenu.menu(original_author_id, friends, self.bot.user.id) + menu = LeaderSkillSingleMenu.menu() await menu.create(ctx, state) @commands.command(aliases=['tfinfo', 'xforminfo']) @@ -723,13 +737,12 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) acquire_raw, base_rarity, true_evo_type_raw = \ await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, color, base_mon, transformed_mon, base_rarity, acquire_raw, true_evo_type_raw) - menu = TransformInfoMenu.menu(original_author_id, friends, self.bot.user.id) + menu = TransformInfoMenu.menu() await menu.create(ctx, state) @commands.command() @@ -828,7 +841,7 @@ async def embedcolor(self, ctx, *, color): await ctx.tick() @idset.command() - async def beta(self, ctx, *, text=""): + async def beta(self, ctx): """Discontinued""" await ctx.send(f"`id3 `is now enabled globally, see" f" <{IDGUIDE}> for more information.") @@ -1125,18 +1138,17 @@ async def idtraceback(self, ctx, *, query): else: lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) - mtokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" + mtokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" f" ({round(calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" for t in sorted(mtokens)) - ntokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" + ntokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" f" ({round(calc_ratio_name(t[0], t[1], dgcog.index2), 2) if t[0] != t[1] else 'exact'})" f" {t[2]}" for t in sorted(ntokens)) color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - friends = await self.get_user_friends(original_author_id) - menu = ClosableEmbedMenu.menu(original_author_id, friends, self.bot.user.id) + menu = ClosableEmbedMenu.menu() props = IdTracebackViewProps(monster=monster, score=score, name_tokens=ntokenstr, modifier_tokens=mtokenstr, lower_priority_monsters=lpstr if lower_prio else "None") state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, color, From df708b4231721a32b170873738a2ad4831039b67 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 18 Feb 2021 00:25:44 -0600 Subject: [PATCH 0633/1220] Combine view & view state files (#834) * combine all view & view state files * combine all view & view state files * Convert rest of emojis except delete is broken * Fix delete button * rm unused function * Finish combining view_state folder into view --- padinfo/menu/closable_embed.py | 7 +- padinfo/menu/common.py | 42 ++++++++ padinfo/menu/id.py | 66 ++++-------- padinfo/menu/leader_skill.py | 34 +++--- padinfo/menu/leader_skill_single.py | 41 +++---- padinfo/menu/monster_list.py | 75 +++++-------- padinfo/menu/pane_names.py | 21 ---- padinfo/menu/simple_text.py | 36 +------ padinfo/menu/transforminfo.py | 37 +++---- padinfo/padinfo.py | 50 ++++----- padinfo/reaction_list.py | 6 +- .../{view_state => view}/closable_embed.py | 2 +- padinfo/{view_state => view}/common.py | 0 padinfo/view/components/base.py | 7 +- .../components/view_state_base.py} | 0 .../components/view_state_base_id.py} | 2 +- padinfo/view/evos.py | 85 +++++++++++++-- padinfo/view/id.py | 91 ++++++++++++++-- padinfo/view/leader_skill.py | 34 +++++- padinfo/view/leader_skill_single.py | 22 +++- padinfo/view/links.py | 2 + padinfo/view/lookup.py | 2 + padinfo/view/materials.py | 101 +++++++++++++++++- padinfo/view/monster_list.py | 62 ++++++++++- padinfo/view/otherinfo.py | 17 ++- padinfo/view/pantheon.py | 69 +++++++++++- padinfo/view/pic.py | 14 ++- padinfo/view/simple_text.py | 24 ++++- padinfo/view/transforminfo.py | 60 +++++++++-- padinfo/view_state/__init__.py | 0 padinfo/view_state/evos.py | 65 ----------- padinfo/view_state/id.py | 84 --------------- padinfo/view_state/leader_skill.py | 31 ------ padinfo/view_state/leader_skill_single.py | 19 ---- padinfo/view_state/materials.py | 100 ----------------- padinfo/view_state/monster_list.py | 60 ----------- padinfo/view_state/otherinfo.py | 11 -- padinfo/view_state/pantheon.py | 67 ------------ padinfo/view_state/pic.py | 12 --- padinfo/view_state/simple_text.py | 21 ---- padinfo/view_state/transforminfo.py | 48 --------- 41 files changed, 729 insertions(+), 798 deletions(-) create mode 100644 padinfo/menu/common.py delete mode 100644 padinfo/menu/pane_names.py rename padinfo/{view_state => view}/closable_embed.py (82%) rename padinfo/{view_state => view}/common.py (100%) rename padinfo/{view_state/base.py => view/components/view_state_base.py} (100%) rename padinfo/{view_state/base_id.py => view/components/view_state_base_id.py} (97%) delete mode 100644 padinfo/view_state/__init__.py delete mode 100644 padinfo/view_state/evos.py delete mode 100644 padinfo/view_state/id.py delete mode 100644 padinfo/view_state/leader_skill.py delete mode 100644 padinfo/view_state/leader_skill_single.py delete mode 100644 padinfo/view_state/materials.py delete mode 100644 padinfo/view_state/monster_list.py delete mode 100644 padinfo/view_state/otherinfo.py delete mode 100644 padinfo/view_state/pantheon.py delete mode 100644 padinfo/view_state/pic.py delete mode 100644 padinfo/view_state/simple_text.py delete mode 100644 padinfo/view_state/transforminfo.py diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index ef4c7d3e..df6fb82d 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -2,8 +2,9 @@ from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu +from padinfo.menu.common import MenuPanes from padinfo.view.id_traceback import IdTracebackView -from padinfo.view_state.closable_embed import ClosableEmbedViewState +from padinfo.view.closable_embed import ClosableEmbedViewState emoji_button_names = ['\N{CROSS MARK}'] menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') @@ -30,3 +31,7 @@ def message_control(state: ClosableEmbedViewState): [view.embed(state, state.props)], ClosableEmbedMenu.EMOJI_BUTTON_NAMES ) + + +class ClosableEmbedMenuPanes(MenuPanes): + pass diff --git a/padinfo/menu/common.py b/padinfo/menu/common.py new file mode 100644 index 00000000..6ab83862 --- /dev/null +++ b/padinfo/menu/common.py @@ -0,0 +1,42 @@ +emoji_buttons = { + 'home': '\N{HOUSE BUILDING}', + 'reset': '\N{WHITE MEDIUM STAR}', +} + + +class MenuPanes: + INITIAL_EMOJI = emoji_buttons['home'], + DATA = { + } + HIDDEN_EMOJIS = [ + ] + + @classmethod + def emoji_names(cls): + return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] + + @classmethod + def transitions(cls): + return {v[0]: k for k, v in cls.DATA.items()} + + @classmethod + def pane_types(cls): + return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} + + @classmethod + def get_initial_reaction_list(cls, number_of_evos: int): + return cls.emoji_names()[:number_of_evos] + + @classmethod + def emoji_name_to_emoji(cls, name: str): + for _, data_pair in cls.DATA.items(): + if data_pair[1] == name: + return data_pair[0] + return None + + @classmethod + def emoji_name_to_function(cls, name: str): + for _, data_pair in cls.DATA.items(): + if data_pair[1] == name: + return data_pair[1] + return None diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 8d31de6d..46985423 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -6,20 +6,14 @@ from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from padinfo.menu.pane_names import IdMenuPaneNames +from padinfo.menu.common import MenuPanes from padinfo.menu.simple_text import SimpleTextMenu -from padinfo.view.evos import EvosView -from padinfo.view.id import IdView -from padinfo.view.materials import MaterialsView -from padinfo.view.otherinfo import OtherInfoView -from padinfo.view.pantheon import PantheonView -from padinfo.view.pic import PicView -from padinfo.view_state.evos import EvosViewState -from padinfo.view_state.id import IdViewState -from padinfo.view_state.materials import MaterialsViewState -from padinfo.view_state.otherinfo import OtherInfoViewState -from padinfo.view_state.pantheon import PantheonViewState -from padinfo.view_state.pic import PicViewState +from padinfo.view.evos import EvosView, EvosViewState +from padinfo.view.id import IdView, IdViewState +from padinfo.view.materials import MaterialsView, MaterialsViewState +from padinfo.view.otherinfo import OtherInfoView, OtherInfoViewState +from padinfo.view.pantheon import PantheonView, PantheonViewState +from padinfo.view.pic import PicView, PicViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -231,17 +225,21 @@ def otherinfo_control(state: OtherInfoViewState): ) -class IdMenuPanes: - INITIAL_EMOJI = '\N{HOUSE BUILDING}' +class IdMenuPaneNames: + refresh = 'refresh' + delete = 'delete' + + +class IdMenuPanes(MenuPanes): DATA = { IdMenu.respond_with_left: ('\N{BLACK LEFT-POINTING TRIANGLE}', None), IdMenu.respond_with_right: ('\N{BLACK RIGHT-POINTING TRIANGLE}', None), - IdMenu.respond_with_current_id: ('\N{HOUSE BUILDING}', IdMenuPaneNames.id), - IdMenu.respond_with_evos: (char_to_emoji('E'), IdMenuPaneNames.evos), - IdMenu.respond_with_mats: ('\N{MEAT ON BONE}', IdMenuPaneNames.materials), - IdMenu.respond_with_picture: ('\N{FRAME WITH PICTURE}', IdMenuPaneNames.pic), - IdMenu.respond_with_pantheon: ('\N{CLASSICAL BUILDING}', IdMenuPaneNames.pantheon), - IdMenu.respond_with_otherinfo: ('\N{SCROLL}', IdMenuPaneNames.otherinfo), + IdMenu.respond_with_current_id: ('\N{HOUSE BUILDING}', IdView.VIEW_TYPE), + IdMenu.respond_with_evos: (char_to_emoji('E'), EvosView.VIEW_TYPE), + IdMenu.respond_with_mats: ('\N{MEAT ON BONE}', MaterialsView.VIEW_TYPE), + IdMenu.respond_with_picture: ('\N{FRAME WITH PICTURE}', PicView.VIEW_TYPE), + IdMenu.respond_with_pantheon: ('\N{CLASSICAL BUILDING}', PantheonView.VIEW_TYPE), + IdMenu.respond_with_otherinfo: ('\N{SCROLL}', OtherInfoView.VIEW_TYPE), IdMenu.respond_with_refresh: ( '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', IdMenuPaneNames.refresh), IdMenu.respond_with_delete: ('\N{CROSS MARK}', IdMenuPaneNames.delete), @@ -250,29 +248,3 @@ class IdMenuPanes: IdMenuPaneNames.refresh, IdMenuPaneNames.delete, ] - - @classmethod - def emoji_names(cls): - return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] - - @classmethod - def transitions(cls): - return {v[0]: k for k, v in cls.DATA.items()} - - @classmethod - def pane_types(cls): - return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} - - @staticmethod - def emoji_name_to_emoji(name: str): - for _, data_pair in IdMenuPanes.DATA.items(): - if data_pair[1] == name: - return data_pair[0] - return None - - @staticmethod - def emoji_name_to_function(name: str): - for _, data_pair in IdMenuPanes.DATA.items(): - if data_pair[1] == name: - return data_pair[1] - return None diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 1f64b3fa..07a8787c 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import Optional from discord import Message from discordmenu.embed.emoji import EmbedMenuEmojiConfig @@ -6,30 +6,19 @@ from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from padinfo.view.id import IdView -from padinfo.view.leader_skill import LeaderSkillView -from padinfo.view_state.id import IdViewState -from padinfo.view_state.leader_skill import LeaderSkillViewState - -if TYPE_CHECKING: - pass +from padinfo.menu.common import emoji_buttons, MenuPanes +from padinfo.view.id import IdView, IdViewState +from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillViewState menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') class LeaderSkillMenu: MENU_TYPE = 'LeaderSkill' - EMOJI_BUTTON_NAMES = ('\N{HOUSE BUILDING}', char_to_emoji('l'), char_to_emoji('r'), '\N{CROSS MARK}') @staticmethod def menu(): - transitions = { - LeaderSkillMenu.EMOJI_BUTTON_NAMES[0]: LeaderSkillMenu.respond_to_house, - LeaderSkillMenu.EMOJI_BUTTON_NAMES[1]: LeaderSkillMenu.respond_to_l, - LeaderSkillMenu.EMOJI_BUTTON_NAMES[2]: LeaderSkillMenu.respond_to_r, - } - - return EmbedMenu(transitions, LeaderSkillMenu.ls_control, menu_emoji_config) + return EmbedMenu(LeaderSkillMenuPanes.transitions(), LeaderSkillMenu.ls_control, menu_emoji_config) @staticmethod async def respond_to_r(message: Optional[Message], ims, **data): @@ -67,12 +56,21 @@ async def respond_to_house(message: Optional[Message], ims, **data): def ls_control(state: LeaderSkillViewState): return EmbedControl( [LeaderSkillView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillMenu.EMOJI_BUTTON_NAMES] + [emoji_cache.get_by_name(e) for e in LeaderSkillMenuPanes.emoji_names()] ) @staticmethod def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillMenu.EMOJI_BUTTON_NAMES] + [emoji_cache.get_by_name(e) for e in LeaderSkillMenuPanes.emoji_names()] ) + + +class LeaderSkillMenuPanes(MenuPanes): + INITIAL_EMOJI = emoji_buttons['home'] + DATA = { + LeaderSkillMenu.respond_to_house: (emoji_buttons['home'], None), + LeaderSkillMenu.respond_to_l: (char_to_emoji('l'), IdView.VIEW_TYPE), + LeaderSkillMenu.respond_to_r: (char_to_emoji('r'), IdView.VIEW_TYPE), + } diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index 7df0b33b..322b6a27 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -5,10 +5,9 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from padinfo.view.id import IdView -from padinfo.view.leader_skill_single import LeaderSkillSingleView -from padinfo.view_state.id import IdViewState -from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState +from padinfo.menu.common import MenuPanes, emoji_buttons +from padinfo.view.id import IdView, IdViewState +from padinfo.view.leader_skill_single import LeaderSkillSingleView, LeaderSkillSingleViewState if TYPE_CHECKING: pass @@ -17,39 +16,43 @@ class LeaderSkillSingleMenu: - EMOJI_BUTTON_NAMES = ('\N{HOUSE BUILDING}', '\N{SQUARED ID}', '\N{CROSS MARK}') MENU_TYPE = 'LeaderSkillSingle' @staticmethod def menu(): - transitions = { - LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES[0]: LeaderSkillSingleMenu.respond_to_house, - LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES[1]: LeaderSkillSingleMenu.view_ls, - } - return EmbedMenu(transitions, LeaderSkillSingleMenu.ls_control, menu_emoji_config) + return EmbedMenu(LeaderSkillSingleMenuPanes.transitions(), + LeaderSkillSingleMenu.ls_control, menu_emoji_config) @staticmethod - async def view_ls(message: Optional[Message], ims, *, dgcog, user_config, **data): - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) - id_control = LeaderSkillSingleMenu.id_control(id_view_state) - return id_control - - @staticmethod - async def respond_to_house(message: Optional[Message], ims, *, dgcog, user_config, **data): + async def respond_with_ls(message: Optional[Message], ims, *, dgcog, user_config, **data): ls_view_state = await LeaderSkillSingleViewState.deserialize(dgcog, user_config, ims) ls_control = LeaderSkillSingleMenu.ls_control(ls_view_state) return ls_control + @staticmethod + async def respond_with_id(message: Optional[Message], ims, *, dgcog, user_config, **data): + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_control = LeaderSkillSingleMenu.id_control(id_view_state) + return id_control + @staticmethod def ls_control(state: LeaderSkillSingleViewState): return EmbedControl( [LeaderSkillSingleView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES] + [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenuPanes.emoji_names()] ) @staticmethod def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES] + [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenuPanes.emoji_names()] ) + + +class LeaderSkillSingleMenuPanes(MenuPanes): + INITIAL_EMOJI = emoji_buttons['home'] + DATA = { + LeaderSkillSingleMenu.respond_with_ls: (emoji_buttons['home'], None), + LeaderSkillSingleMenu.respond_with_id: ('\N{SQUARED ID}', IdView.VIEW_TYPE), + } diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index ae48cd8e..1869179a 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -1,21 +1,24 @@ -from typing import TYPE_CHECKING, Optional +from typing import Optional from discord import Message from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji +from padinfo.menu.common import emoji_buttons, MenuPanes from padinfo.menu.id import IdMenu, IdMenuPanes -from padinfo.menu.pane_names import MonsterListPaneNames, global_emoji_responses -from padinfo.view.monster_list import MonsterListView -from padinfo.view_state.monster_list import MonsterListViewState - -if TYPE_CHECKING: - pass +from padinfo.view.id import IdView +from padinfo.view.monster_list import MonsterListView, MonsterListViewState menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') +class MonsterListPaneNames: + home = 'home' + refresh = 'refresh' + reset = 'reset' + + class MonsterListMenu: MENU_TYPE = 'MonsterListMenu' CHILD_MENU_TYPE = 'IdMenu' @@ -115,57 +118,27 @@ def monster_list_control(state: MonsterListViewState): ) -class MonsterListMenuPanes: +class MonsterListMenuPanes(MenuPanes): INITIAL_EMOJI = '\N{HOUSE BUILDING}' DATA = { - MonsterListMenu.respond_with_monster_list: ('\N{HOUSE BUILDING}', MonsterListPaneNames.home), - MonsterListMenu.respond_with_0: (char_to_emoji('0'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_1: (char_to_emoji('1'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_2: (char_to_emoji('2'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_3: (char_to_emoji('3'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_4: (char_to_emoji('4'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_5: (char_to_emoji('5'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_6: (char_to_emoji('6'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_7: (char_to_emoji('7'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_8: (char_to_emoji('8'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_9: (char_to_emoji('9'), MonsterListPaneNames.id), - MonsterListMenu.respond_with_10: ('\N{KEYCAP TEN}', MonsterListPaneNames.id), + MonsterListMenu.respond_with_monster_list: (emoji_buttons[MonsterListPaneNames.home], MonsterListPaneNames.home), + MonsterListMenu.respond_with_0: (char_to_emoji('0'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_1: (char_to_emoji('1'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_2: (char_to_emoji('2'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_3: (char_to_emoji('3'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_4: (char_to_emoji('4'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_5: (char_to_emoji('5'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_6: (char_to_emoji('6'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_7: (char_to_emoji('7'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_8: (char_to_emoji('8'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_9: (char_to_emoji('9'), IdView.VIEW_TYPE), + MonsterListMenu.respond_with_10: ('\N{KEYCAP TEN}', IdView.VIEW_TYPE), MonsterListMenu.respond_with_refresh: ( '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', MonsterListPaneNames.refresh), - MonsterListMenu.respond_with_reset: (global_emoji_responses['reset'], MonsterListPaneNames.reset) + MonsterListMenu.respond_with_reset: (emoji_buttons['reset'], MonsterListPaneNames.reset) } HIDDEN_EMOJIS = [ MonsterListPaneNames.home, MonsterListPaneNames.refresh, MonsterListPaneNames.reset, ] - - @classmethod - def emoji_names(cls): - return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] - - @classmethod - def transitions(cls): - return {v[0]: k for k, v in cls.DATA.items()} - - @classmethod - def pane_types(cls): - return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} - - @staticmethod - def get_initial_reaction_list(number_of_evos: int): - return MonsterListMenuPanes.emoji_names()[:number_of_evos] - - @staticmethod - def emoji_name_to_emoji(name: str): - for _, data_pair in MonsterListMenuPanes.DATA.items(): - if data_pair[1] == name: - return data_pair[0] - return None - - @staticmethod - def emoji_name_to_function(name: str): - for _, data_pair in MonsterListMenuPanes.DATA.items(): - if data_pair[1] == name: - return data_pair[1] - return None diff --git a/padinfo/menu/pane_names.py b/padinfo/menu/pane_names.py deleted file mode 100644 index 674dbf6a..00000000 --- a/padinfo/menu/pane_names.py +++ /dev/null @@ -1,21 +0,0 @@ -class IdMenuPaneNames: - id = 'id' - evos = 'evos' - materials = 'materials' - otherinfo = 'otherinfo' - pantheon = 'pantheon' - pic = 'pic' - refresh = 'refresh' - delete = 'delete' - - -class MonsterListPaneNames: - home = 'home' - id = 'id' - refresh = 'refresh' - reset = 'reset' - - -global_emoji_responses = { - 'reset': '\N{WHITE MEDIUM STAR}' -} diff --git a/padinfo/menu/simple_text.py b/padinfo/menu/simple_text.py index 31b3983a..aca73d46 100644 --- a/padinfo/menu/simple_text.py +++ b/padinfo/menu/simple_text.py @@ -5,8 +5,8 @@ from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu -from padinfo.view.simple_text import SimpleTextView -from padinfo.view_state.simple_text import SimpleTextViewState +from padinfo.menu.common import MenuPanes +from padinfo.view.simple_text import SimpleTextView, SimpleTextViewState if TYPE_CHECKING: pass @@ -52,7 +52,7 @@ def message_control(state: SimpleTextViewState): ) -class SimpleTextMenuPanes: +class SimpleTextMenuPanes(MenuPanes): INITIAL_EMOJI = '\N{HOUSE BUILDING}' DATA = { SimpleTextMenu.respond_with_message: ('\N{HOUSE BUILDING}', SimpleTextNames.home), @@ -61,33 +61,3 @@ class SimpleTextMenuPanes: HIDDEN_EMOJIS = [ SimpleTextNames.home, ] - - @classmethod - def emoji_names(cls): - return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] - - @classmethod - def transitions(cls): - return {v[0]: k for k, v in cls.DATA.items()} - - @classmethod - def pane_types(cls): - return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} - - @staticmethod - def get_initial_reaction_list(number_of_evos: int): - return SimpleTextMenuPanes.emoji_names()[:number_of_evos] - - @staticmethod - def emoji_name_to_emoji(name: str): - for _, data_pair in SimpleTextMenuPanes.DATA.items(): - if data_pair[1] == name: - return data_pair[0] - return None - - @staticmethod - def emoji_name_to_function(name: str): - for _, data_pair in SimpleTextMenuPanes.DATA.items(): - if data_pair[1] == name: - return data_pair[1] - return None diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 7c39e16c..fa85ebcf 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -1,37 +1,24 @@ -from typing import TYPE_CHECKING, Optional +from typing import Optional from discord import Message from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from padinfo.view.id import IdView -from padinfo.view.transforminfo import TransformInfoView -from padinfo.view_state.id import IdViewState -from padinfo.view_state.transforminfo import TransformInfoViewState +from padinfo.menu.closable_embed import emoji_button_names +from padinfo.menu.common import MenuPanes, emoji_buttons +from padinfo.view.id import IdView, IdViewState +from padinfo.view.transforminfo import TransformInfoView, TransformInfoViewState -if TYPE_CHECKING: - pass - -emoji_button_names = ['\N{HOUSE BUILDING}', '\N{DOWN-POINTING RED TRIANGLE}', - '\N{BLACK RIGHT-POINTING TRIANGLE}', '\N{CROSS MARK}'] menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') class TransformInfoMenu: - INITIAL_EMOJI = emoji_button_names[0] MENU_TYPE = 'TransformInfo' - EMOJI_BUTTON_NAMES = emoji_button_names @staticmethod def menu(): - transitions = { - TransformInfoMenu.INITIAL_EMOJI: TransformInfoMenu.respond_with_overview, - emoji_button_names[1]: TransformInfoMenu.respond_with_base, - emoji_button_names[2]: TransformInfoMenu.respond_with_transform, - } - - return EmbedMenu(transitions, TransformInfoMenu.tf_control, + return EmbedMenu(TransformInfoMenuPanes.transitions(), TransformInfoMenu.tf_control, menu_emoji_config) @staticmethod @@ -68,12 +55,20 @@ async def respond_with_overview(message: Optional[Message], ims, **data): def tf_control(state: TransformInfoViewState): return EmbedControl( [TransformInfoView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in TransformInfoMenuPanes.emoji_names()] ) @staticmethod def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in emoji_button_names] + [emoji_cache.get_by_name(e) for e in TransformInfoMenuPanes.emoji_names()] ) + + +class TransformInfoMenuPanes(MenuPanes): + DATA = { + TransformInfoMenu.respond_with_overview: (emoji_buttons['home'], TransformInfoView.VIEW_TYPE), + TransformInfoMenu.respond_with_base: ('\N{DOWN-POINTING RED TRIANGLE}', IdView.VIEW_TYPE), + TransformInfoMenu.respond_with_transform: ('\N{BLACK RIGHT-POINTING TRIANGLE}', IdView.VIEW_TYPE), + } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 626b85ff..febe837a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -10,6 +10,7 @@ import discord import tsutils from discord import Color +from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.emoji.emoji_cache import emoji_cache from discordmenu.intra_message_state import IntraMessageState from discordmenu.reaction_filter import FriendReactionFilter, MessageOwnerReactionFilter, \ @@ -31,31 +32,30 @@ from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.idtest_mixin import IdTest -from padinfo.menu.closable_embed import ClosableEmbedMenu +from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes from padinfo.menu.id import IdMenu, IdMenuPanes -from padinfo.menu.leader_skill import LeaderSkillMenu -from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu +from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes +from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes -from padinfo.menu.pane_names import global_emoji_responses from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes -from padinfo.menu.transforminfo import TransformInfoMenu +from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.id_traceback import IdTracebackView, IdTracebackViewProps from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView -from padinfo.view_state.closable_embed import ClosableEmbedViewState -from padinfo.view_state.evos import EvosViewState -from padinfo.view_state.id import IdViewState -from padinfo.view_state.leader_skill import LeaderSkillViewState -from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState -from padinfo.view_state.materials import MaterialsViewState -from padinfo.view_state.monster_list import MonsterListViewState -from padinfo.view_state.otherinfo import OtherInfoViewState -from padinfo.view_state.pantheon import PantheonViewState -from padinfo.view_state.pic import PicViewState -from padinfo.view_state.simple_text import SimpleTextViewState -from padinfo.view_state.transforminfo import TransformInfoViewState +from padinfo.view.closable_embed import ClosableEmbedViewState +from padinfo.view.evos import EvosViewState +from padinfo.view.id import IdViewState +from padinfo.view.leader_skill import LeaderSkillViewState +from padinfo.view.leader_skill_single import LeaderSkillSingleViewState +from padinfo.view.materials import MaterialsViewState +from padinfo.view.monster_list import MonsterListViewState +from padinfo.view.otherinfo import OtherInfoViewState +from padinfo.view.pantheon import PantheonViewState +from padinfo.view.pic import PicViewState +from padinfo.view.simple_text import SimpleTextViewState +from padinfo.view.transforminfo import TransformInfoViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -157,20 +157,20 @@ async def test_reaction_add(self, reaction, member): emoji_clicked = emoji_obj.name menu_to_emoji_list_map = { - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu.EMOJI_BUTTON_NAMES, + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenuPanes.emoji_names(), IdMenu.MENU_TYPE: IdMenuPanes.emoji_names(), - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu.EMOJI_BUTTON_NAMES, - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu.EMOJI_BUTTON_NAMES, + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes.emoji_names(), + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenuPanes.emoji_names(), MonsterListMenu.MENU_TYPE: MonsterListMenuPanes.emoji_names(), SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes.emoji_names(), - TransformInfoMenu.MENU_TYPE: TransformInfoMenu.EMOJI_BUTTON_NAMES + TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes.emoji_names(), } - emoji_found = False + emoji_recognized = emoji_clicked in EmbedMenuEmojiConfig().to_list() for menu_type, emoji_list in menu_to_emoji_list_map.items(): if emoji_clicked in emoji_list: - emoji_found = True - if not emoji_found: + emoji_recognized = True + if not emoji_recognized: return message = reaction.message @@ -242,7 +242,7 @@ async def test_reaction_add(self, reaction, member): # the reset wouldn't happen until all emojis showed up in the child, so this way it feels like everything # happens faster, but regardless the reset must happen first. if respond_with_child[(ims['menu_type'], emoji_clicked)]: - await embed_menu.transition(message, ims, global_emoji_responses['reset'], member, **data) + await embed_menu.transition(message, ims, emoji_clicked, member, **data) await embed_menu.transition(child_message, next_child_ims, emoji_clicked, member, **data) return diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index 3b7b030e..a7a39c24 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -4,9 +4,9 @@ from padinfo.core.padinfo_settings import settings from padinfo.menu.id import IdMenuPanes, IdMenu -from padinfo.view_state.evos import EvosViewState -from padinfo.view_state.materials import MaterialsViewState -from padinfo.view_state.pantheon import PantheonViewState +from padinfo.view.evos import EvosViewState +from padinfo.view.materials import MaterialsViewState +from padinfo.view.pantheon import PantheonViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel diff --git a/padinfo/view_state/closable_embed.py b/padinfo/view/closable_embed.py similarity index 82% rename from padinfo/view_state/closable_embed.py rename to padinfo/view/closable_embed.py index 28236a8e..1c2f4ab7 100644 --- a/padinfo/view_state/closable_embed.py +++ b/padinfo/view/closable_embed.py @@ -1,4 +1,4 @@ -from padinfo.view_state.base import ViewStateBase +from padinfo.view.components.view_state_base import ViewStateBase class ClosableEmbedViewState(ViewStateBase): diff --git a/padinfo/view_state/common.py b/padinfo/view/common.py similarity index 100% rename from padinfo/view_state/common.py rename to padinfo/view/common.py diff --git a/padinfo/view/components/base.py b/padinfo/view/components/base.py index d60a6f96..9f74b341 100644 --- a/padinfo/view/components/base.py +++ b/padinfo/view/components/base.py @@ -1,7 +1,10 @@ +from typing import Union + from discordmenu.embed.components import EmbedFooter from discordmenu.intra_message_state import IntraMessageState -from padinfo.view_state.base import ViewStateBase +from padinfo.view.components.view_state_base import ViewStateBase +from padinfo.view.components.view_state_base_id import ViewStateBaseId TSUBAKI_FLOWER_ICON_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/tsubaki/tsubakiflower.png' @@ -10,7 +13,7 @@ def pad_info_footer(): return EmbedFooter('Requester may click the reactions below to switch tabs') -def pad_info_footer_with_state(state: ViewStateBase): +def pad_info_footer_with_state(state: Union[ViewStateBase, ViewStateBaseId]): url = IntraMessageState.serialize(TSUBAKI_FLOWER_ICON_URL, state.serialize()) return EmbedFooter( 'Requester may click the reactions below to switch tabs', diff --git a/padinfo/view_state/base.py b/padinfo/view/components/view_state_base.py similarity index 100% rename from padinfo/view_state/base.py rename to padinfo/view/components/view_state_base.py diff --git a/padinfo/view_state/base_id.py b/padinfo/view/components/view_state_base_id.py similarity index 97% rename from padinfo/view_state/base_id.py rename to padinfo/view/components/view_state_base_id.py index 5550a9c9..8edecac2 100644 --- a/padinfo/view_state/base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, List from padinfo.common.config import UserConfig -from padinfo.view_state.common import get_monster_from_ims +from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 39960f6d..c46c8ed7 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -1,37 +1,102 @@ +from typing import List, TYPE_CHECKING + from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view_state.evos import EvosViewState +from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.common import get_monster_from_ims + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class EvosViewState(ViewStateBaseId): + def __init__(self, original_author_id, menu_type, raw_query, query, color, + monster: "MonsterModel", + alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], + reaction_list: List[str] = None, + use_evo_scroll: bool = True, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=extra_state) + self.alt_versions = alt_versions + self.gem_versions = gem_versions + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': EvosView.VIEW_TYPE, + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None + monster = await get_monster_from_ims(dgcog, ims) + alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + if alt_versions is None: + return None -def _evo_lines(monsters, current_monster): - if not len(monsters): - return [] - return [ - MonsterHeader.short_with_emoji(ae, link=ae.monster_id != current_monster.monster_id) - for ae in sorted(monsters, key=lambda x: int(x.monster_id)) - ] + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + menu_type = ims['menu_type'] + reaction_list = ims.get('reaction_list') + + return cls(original_author_id, menu_type, raw_query, query, user_config.color, + monster, + alt_versions, gem_versions, + reaction_list=reaction_list, + use_evo_scroll=use_evo_scroll, + extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + alt_versions = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + key=lambda x: x.monster_id) + gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) + if len(alt_versions) == 1 and len(gem_versions) == 0: + return None, None + return alt_versions, gem_versions class EvosView: + VIEW_TYPE = 'Evos' + + @staticmethod + def _evo_lines(monsters, current_monster): + if not len(monsters): + return [] + return [ + MonsterHeader.short_with_emoji(ae, link=ae.monster_id != current_monster.monster_id) + for ae in sorted(monsters, key=lambda x: int(x.monster_id)) + ] + @staticmethod def embed(state: EvosViewState): fields = [ EmbedField( ("{} evolution" if len(state.alt_versions) == 1 else "{} evolutions").format(len(state.alt_versions)), - Box(*_evo_lines(state.alt_versions, state.monster)))] + Box(*EvosView._evo_lines(state.alt_versions, state.monster)))] if state.gem_versions: fields.append( EmbedField( ("{} evolve gem" if len(state.gem_versions) == 1 else "{} evolve gems").format( len(state.gem_versions)), - Box(*_evo_lines(state.gem_versions, state.monster)))) + Box(*EvosView._evo_lines(state.gem_versions, state.monster)))) return EmbedView( EmbedMain( diff --git a/padinfo/view/id.py b/padinfo/view/id.py index a23e7fa8..53783353 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,23 +1,100 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createMultiplierText from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view_state.id import IdViewState +from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel from dadguide.models.awakening_model import AwakeningModel +class IdViewState(ViewStateBaseId): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], + fallback_message: str = None, + use_evo_scroll: bool = True, + reaction_list: List[str] = None, + is_child: bool = False, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=extra_state) + self.fallback_message = fallback_message + self.is_child = is_child + self.acquire_raw = acquire_raw + self.alt_monsters = alt_monsters + self.base_rarity = base_rarity + self.transform_base = transform_base + self.true_evo_type_raw = true_evo_type_raw + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': IdView.VIEW_TYPE, + 'is_child': self.is_child, + 'message': self.fallback_message, + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + # for numberscroll getting to a gap in monster book, or 1, or last monster + if ims.get('unsupported_transition'): + return None + monster = await get_monster_from_ims(dgcog, ims) + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + await IdViewState.query(dgcog, monster) + + raw_query = ims['raw_query'] + # This is to support the 2 vs 1 monster query difference between ^ls and ^id + query = ims.get('query') or raw_query + menu_type = ims['menu_type'] + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + reaction_list = ims.get('reaction_list') + fallback_message = ims.get('message') + is_child = ims.get('is_child') + + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + fallback_message=fallback_message, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + is_child=is_child, + extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ + await IdViewState._get_monster_misc_info(db_context, monster) + + return transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters + + @staticmethod + async def _get_monster_misc_info(db_context, monster): + transform_base = db_context.graph.get_transform_base(monster) + true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value + acquire_raw = db_context.graph.monster_acquisition(monster) + base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity + alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + key=lambda x: x.monster_id) + return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw + + def _get_awakening_text(awakening: "AwakeningModel"): return get_awakening_emoji(awakening.awoken_skill_id, awakening.name) @@ -44,6 +121,8 @@ def _monster_is_enhance(m: "MonsterModel"): class IdView: + VIEW_TYPE = 'Id' + @staticmethod def normal_awakenings_row(m: "MonsterModel"): normal_awakenings = len(m.awakenings) - m.superawakening_count @@ -66,7 +145,7 @@ def all_awakenings_row(m: "MonsterModel", transform_base): return Box( Box( - '\N{UP-POINTING RED TRIANGLE}' if m!=transform_base else '', + '\N{UP-POINTING RED TRIANGLE}' if m != transform_base else '', IdView.normal_awakenings_row(m), delimiter=' ' ), @@ -74,7 +153,7 @@ def all_awakenings_row(m: "MonsterModel", transform_base): '\N{DOWN-POINTING RED TRIANGLE}', IdView.all_awakenings_row(transform_base, transform_base), delimiter=' ' - ) if m!=transform_base else None, + ) if m != transform_base else None, IdView.super_awakenings_row(m), ) @@ -133,13 +212,13 @@ def stats_header(m: "MonsterModel"): @staticmethod def active_skill_header(m: "MonsterModel", transform_base): active_skill = m.active_skill - if m==transform_base: + if m == transform_base: active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) \ if active_skill else 'None' else: base_skill = transform_base.active_skill base_cd = ' (\N{DOWN-POINTING RED TRIANGLE} {} -> {})'.format(base_skill.turn_max, - base_skill.turn_min) \ + base_skill.turn_min) \ if base_skill else 'None' active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index fd1cf067..c235f3c0 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -5,16 +5,48 @@ from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig from padinfo.core.leader_skills import createMultiplierText +from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view_state.leader_skill import LeaderSkillViewState +from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: pass +class LeaderSkillViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, color, l_mon, r_mon, l_query, r_query): + super().__init__(original_author_id, menu_type, raw_query, extra_state=None) + self.color = color + self.l_mon = l_mon + self.l_query = l_query + self.r_mon = r_mon + self.r_query = r_query + + def serialize(self): + ret = super().serialize() + ret.update({ + 'l_query': self.l_query, + 'r_query': self.r_query, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + _, l_mon, l_query, _, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) + return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, + r_query) + + class LeaderSkillView: + VIEW_TYPE = 'LeaderSkill' + @staticmethod def embed(state: LeaderSkillViewState): lls = state.l_mon.leader_skill diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index bafc9e76..3e91a6bd 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -5,16 +5,36 @@ from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig from padinfo.core.leader_skills import createSingleMultiplierText from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view_state.leader_skill_single import LeaderSkillSingleViewState +from padinfo.view.components.view_state_base import ViewStateBase +from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: pass +class LeaderSkillSingleViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, color, mon): + super().__init__(original_author_id, menu_type, raw_query, extra_state=None) + self.color = color + self.mon = mon + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + + mon = await get_monster_from_ims(dgcog, ims) + return LeaderSkillSingleViewState(original_author_id, menu_type, raw_query, user_config.color, mon) + + class LeaderSkillSingleView: + VIEW_TYPE = 'LeaderSkillSingle' + @staticmethod def embed(state: LeaderSkillSingleViewState): ls = state.mon.leader_skill diff --git a/padinfo/view/links.py b/padinfo/view/links.py index fa792faf..977601a9 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -14,6 +14,8 @@ class LinksView: + VIEW_TYPE = 'Links' + @staticmethod def linksbox(m): return Box( diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index f7bb290a..3d4f44d2 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -12,6 +12,8 @@ class LookupView: + VIEW_TYPE = 'Lookup' + @staticmethod def embed(m: "MonsterModel", color): return EmbedView( diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 06a15154..a852b388 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -1,22 +1,115 @@ -from typing import TYPE_CHECKING +from typing import List, TYPE_CHECKING, Optional from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig +from padinfo.common.external_links import ilmina_skill from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view_state.materials import MaterialsViewState +from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: - pass + from dadguide.models.monster_model import MonsterModel MAX_MONS_TO_SHOW = 5 +class MaterialsViewState(ViewStateBaseId): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], + gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, + gem_override: bool, + use_evo_scroll: bool = True, + reaction_list: List[str] = None, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + reaction_list=reaction_list, + use_evo_scroll=use_evo_scroll, + extra_state=extra_state) + self.reaction_list = reaction_list + self.link = link + self.skillup_evo_count = skillup_evo_count + self.skillups = skillups + self.gemusedin = gemusedin + self.mats = mats + self.usedin = usedin + self.gemid = gemid + self.gem_override = gem_override + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': MaterialsView.VIEW_TYPE, + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None + monster = await get_monster_from_ims(dgcog, ims) + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ + await MaterialsViewState.query(dgcog, monster) + + if mats is None: + return None + + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + menu_type = ims['menu_type'] + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + reaction_list = ims.get('reaction_list') + + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + mats = db_context.graph.evo_mats_by_monster(monster) + usedin = db_context.graph.material_of_monsters(monster) + evo_gem = db_context.graph.evo_gem_monster(monster) + gemid = str(evo_gem.monster_no_na) if evo_gem else None + gemusedin = db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] + skillups = [] + skillup_evo_count = 0 + link = ilmina_skill(monster) + + if monster.active_skill: + sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id) + if db_context.graph.monster_is_farmable_evo(m)] + sugs = [db_context.graph.evo_gem_monster(su) for su in sums] + vsums = [] + for su in sums: + if not any(susu in vsums for susu in db_context.graph.get_alt_monsters(su)): + vsums.append(su) + skillups = [su for su in vsums + if db_context.graph.monster_is_farmable_evo(su) and + db_context.graph.get_base_id(su) != db_context.graph.get_base_id(monster) and + su not in sugs] if monster.active_skill else [] + skillup_evo_count = len(sums) - len(vsums) + gem_override = False + + if not any([mats, usedin, gemusedin, skillups and not monster.is_stackable]): + return None, None, None, None, None, None, None, None + if not any([mats, usedin, skillups and not monster.is_stackable]): + mats, gemusedin, _, usedin, skillups, skillup_evo_count, link, _ \ + = await MaterialsViewState.query(dgcog, evo_gem) + gem_override = True + + return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override + + def mat_use_field(mons, title, max_mons=MAX_MONS_TO_SHOW): text = None if len(mons) == 0: @@ -49,6 +142,8 @@ def skillup_field(mons, sec, link): class MaterialsView: + VIEW_TYPE = 'Materials' + @staticmethod def embed(state: MaterialsViewState): # m: "MonsterModel", color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list.py index f110d2fa..b08bd439 100644 --- a/padinfo/view/monster_list.py +++ b/padinfo/view/monster_list.py @@ -1,11 +1,69 @@ +from typing import TYPE_CHECKING, List + from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView from tsutils import char_to_emoji +from padinfo.common.config import UserConfig from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view_state.monster_list import MonsterListViewState +from padinfo.view.components.view_state_base import ViewStateBase + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class MonsterListViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, query, color, + monster_list: List["MonsterModel"], title, message, + reaction_list=None, + extra_state=None, + child_message_id=None + ): + super().__init__(original_author_id, menu_type, raw_query, + extra_state=extra_state) + self.message = message + self.child_message_id = child_message_id + self.title = title + self.monster_list = monster_list + self.reaction_list = reaction_list + self.color = color + self.query = query + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': MonsterListView.VIEW_TYPE, + 'title': self.title, + 'monster_list': [str(m.monster_no) for m in self.monster_list], + 'reaction_list': self.reaction_list, + 'child_message_id': self.child_message_id, + 'message': self.message, + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None + monster_list = [dgcog.database.graph.get_monster(int(m)) for m in ims['monster_list']] + title = ims['title'] + + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + reaction_list = ims.get('reaction_list') + child_message_id = ims.get('child_message_id') + message = ims.get('message') + + return MonsterListViewState(original_author_id, menu_type, raw_query, query, user_config.color, + monster_list, title, message, + reaction_list=reaction_list, + extra_state=ims, + child_message_id=child_message_id + ) def _monster_list(monsters): @@ -18,6 +76,8 @@ def _monster_list(monsters): class MonsterListView: + VIEW_TYPE = 'MonsterList' + @staticmethod def embed(state: MonsterListViewState): fields = [ diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index ecdf7ab4..c19794a1 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -11,10 +11,19 @@ from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.links import LinksView -from padinfo.view_state.otherinfo import OtherInfoViewState +from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: - pass + from dadguide.models.monster_model import MonsterModel + + +class OtherInfoViewState(ViewStateBaseId): + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': OtherInfoView.VIEW_TYPE, + }) + return ret def statsbox(m): @@ -33,9 +42,11 @@ def statsbox(m): class OtherInfoView: + VIEW_TYPE = 'OtherInfo' + @staticmethod def embed(state: OtherInfoViewState): - m = state.monster + m: "MonsterModel" = state.monster return EmbedView( EmbedMain( color=state.color, diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 24c8606e..d8be6c5d 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -1,15 +1,82 @@ +from typing import TYPE_CHECKING, List + from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view_state.pantheon import PantheonViewState +from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.common import get_monster_from_ims + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class PantheonViewState(ViewStateBaseId): + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + pantheon_list: List["MonsterModel"], series_name: str, + use_evo_scroll: bool = True, + reaction_list: List[str] = None, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=extra_state) + self.series_name = series_name + self.pantheon_list = pantheon_list + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': PantheonView.VIEW_TYPE, + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None + monster = await get_monster_from_ims(dgcog, ims) + pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + + if pantheon_list is None: + return None + + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + original_author_id = ims['original_author_id'] + use_evo_scroll = ims.get('use_evo_scroll') != 'False' + menu_type = ims['menu_type'] + reaction_list = ims.get('reaction_list') + + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + pantheon_list, series_name, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, + extra_state=ims) + + @staticmethod + async def query(dgcog, monster): + db_context = dgcog.database + full_pantheon = db_context.get_monsters_by_series(monster.series_id) + if not full_pantheon: + return None, None + pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) + if len(pantheon_list) == 0 or len(pantheon_list) > 20: + return None, None + + series_name = monster.series.name_en + + return pantheon_list, series_name class PantheonView: + VIEW_TYPE = 'Pantheon' + @staticmethod def embed(state: PantheonViewState): fields = [EmbedField( diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 0430b45b..cd401b5e 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -7,10 +7,22 @@ from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view_state.pic import PicViewState +from padinfo.view.components.view_state_base_id import ViewStateBaseId + + +class PicViewState(ViewStateBaseId): + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': PicView.VIEW_TYPE, + }) + return ret class PicView: + VIEW_TYPE = 'Pic' + @staticmethod def embed(state: PicViewState): url = MonsterImage.picture(state.monster) diff --git a/padinfo/view/simple_text.py b/padinfo/view/simple_text.py index 481c128c..cee08bd7 100644 --- a/padinfo/view/simple_text.py +++ b/padinfo/view/simple_text.py @@ -1,11 +1,33 @@ +from typing import List + from discordmenu.embed.components import EmbedMain from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig from padinfo.view.components.base import pad_info_footer_with_state -from padinfo.view_state.simple_text import SimpleTextViewState +from padinfo.view.components.view_state_base import ViewStateBase + + +class SimpleTextViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, + color, message, + reaction_list: List[str] = None): + super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) + self.message = message + self.color = color + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + raw_query = ims.get('raw_query') + return cls(original_author_id, menu_type, raw_query, user_config.color, ims.get('message'), + reaction_list=ims.get('reaction_list')) class SimpleTextView: + VIEW_TYPE = 'SimpleText' + @staticmethod def embed(state: SimpleTextViewState): return EmbedView( diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index db73bf21..addbdaa5 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -2,15 +2,16 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.text import Text, BoldText +from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView +from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.id import IdView -from padinfo.view_state.transforminfo import TransformInfoViewState +from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -18,6 +19,52 @@ BASE_EMOJI = '\N{DOWN-POINTING RED TRIANGLE}' +class TransformInfoViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, + base_rarity, acquire_raw, true_evo_type_raw): + super().__init__(original_author_id, menu_type, raw_query, extra_state=None) + self.color = color + self.base_mon = base_mon + self.transformed_mon = transformed_mon + self.base_rarity = base_rarity + self.acquire_raw = acquire_raw + self.true_evo_type_raw = true_evo_type_raw + + def serialize(self): + ret = super().serialize() + ret.update({ + 'b_resolved_monster_id': self.base_mon.monster_id, + 't_resolved_monster_id': self.transformed_mon.monster_id + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + base_mon_id = ims['b_resolved_monster_id'] + transformed_mon_id = ims['t_resolved_monster_id'] + + base_mon = dgcog.get_monster(base_mon_id) + transformed_mon = dgcog.get_monster(transformed_mon_id) + + acquire_raw, base_rarity, true_evo_type_raw = \ + await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + + return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, + base_mon, transformed_mon, base_rarity, acquire_raw, + true_evo_type_raw) + + @staticmethod + async def query(dgcog, base_mon, transformed_mon): + db_context = dgcog.database + acquire_raw = db_context.graph.monster_acquisition(transformed_mon) + base_rarity = db_context.graph.get_base_monster_by_id(transformed_mon.monster_no).rarity + true_evo_type_raw = db_context.graph.true_evo_type_by_monster(transformed_mon).value + return acquire_raw, base_rarity, true_evo_type_raw + + def base_info(m: "MonsterModel"): return Box( Box( @@ -30,14 +77,9 @@ def base_info(m: "MonsterModel"): ) -def base_skill(m: "MonsterModel"): - active_skill = m.active_skill - return (" (" + BASE_EMOJI + " " - + "{} -> {})".format(active_skill.turn_max, active_skill.turn_min) if active_skill - else 'None') - - class TransformInfoView: + VIEW_TYPE = 'TransformInfo' + @staticmethod def embed(state: TransformInfoViewState): base_mon = state.base_mon diff --git a/padinfo/view_state/__init__.py b/padinfo/view_state/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/padinfo/view_state/evos.py b/padinfo/view_state/evos.py deleted file mode 100644 index b824d16c..00000000 --- a/padinfo/view_state/evos.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import List, TYPE_CHECKING - -from padinfo.common.config import UserConfig -from padinfo.menu.pane_names import IdMenuPaneNames -from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - - -class EvosViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, - monster: "MonsterModel", - alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], - reaction_list: List[str] = None, - use_evo_scroll: bool = True, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=extra_state) - self.alt_versions = alt_versions - self.gem_versions = gem_versions - - def serialize(self): - ret = super().serialize() - ret.update({ - 'pane_type': IdMenuPaneNames.evos, - }) - return ret - - @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): - if ims.get('unsupported_transition'): - return None - monster = await get_monster_from_ims(dgcog, ims) - alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) - - if alt_versions is None: - return None - - raw_query = ims['raw_query'] - query = ims.get('query') or raw_query - original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - menu_type = ims['menu_type'] - reaction_list = ims.get('reaction_list') - - return cls(original_author_id, menu_type, raw_query, query, user_config.color, - monster, - alt_versions, gem_versions, - reaction_list=reaction_list, - use_evo_scroll=use_evo_scroll, - extra_state=ims) - - @staticmethod - async def query(dgcog, monster): - db_context = dgcog.database - alt_versions = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, - key=lambda x: x.monster_id) - gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) - if len(alt_versions) == 1 and len(gem_versions) == 0: - return None, None - return alt_versions, gem_versions diff --git a/padinfo/view_state/id.py b/padinfo/view_state/id.py deleted file mode 100644 index 5853c377..00000000 --- a/padinfo/view_state/id.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import List, TYPE_CHECKING - -from padinfo.common.config import UserConfig -from padinfo.menu.pane_names import IdMenuPaneNames -from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - - -class IdViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], - fallback_message: str = None, - use_evo_scroll: bool = True, - reaction_list: List[str] = None, - is_child: bool = False, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=extra_state) - self.fallback_message = fallback_message - self.is_child = is_child - self.acquire_raw = acquire_raw - self.alt_monsters = alt_monsters - self.base_rarity = base_rarity - self.transform_base = transform_base - self.true_evo_type_raw = true_evo_type_raw - - def serialize(self): - ret = super().serialize() - ret.update({ - 'pane_type': IdMenuPaneNames.id, - 'is_child': self.is_child, - 'message': self.fallback_message, - }) - return ret - - @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): - # for numberscroll getting to a gap in monster book, or 1, or last monster - if ims.get('unsupported_transition'): - return None - monster = await get_monster_from_ims(dgcog, ims) - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ - await IdViewState.query(dgcog, monster) - - raw_query = ims['raw_query'] - # This is to support the 2 vs 1 monster query difference between ^ls and ^id - query = ims.get('query') or raw_query - menu_type = ims['menu_type'] - original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - reaction_list = ims.get('reaction_list') - fallback_message = ims.get('message') - is_child = ims.get('is_child') - - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, - fallback_message=fallback_message, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - is_child=is_child, - extra_state=ims) - - @staticmethod - async def query(dgcog, monster): - db_context = dgcog.database - acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ - await IdViewState._get_monster_misc_info(db_context, monster) - - return transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters - - @staticmethod - async def _get_monster_misc_info(db_context, monster): - transform_base = db_context.graph.get_transform_base(monster) - true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value - acquire_raw = db_context.graph.monster_acquisition(monster) - base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity - alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, - key=lambda x: x.monster_id) - return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw diff --git a/padinfo/view_state/leader_skill.py b/padinfo/view_state/leader_skill.py deleted file mode 100644 index b7fe3fe5..00000000 --- a/padinfo/view_state/leader_skill.py +++ /dev/null @@ -1,31 +0,0 @@ -from padinfo.common.config import UserConfig -from padinfo.core.leader_skills import perform_leaderskill_query -from padinfo.view_state.base import ViewStateBase - - -class LeaderSkillViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, l_mon, r_mon, l_query, r_query): - super().__init__(original_author_id, menu_type, raw_query, extra_state=None) - self.color = color - self.l_mon = l_mon - self.l_query = l_query - self.r_mon = r_mon - self.r_query = r_query - - def serialize(self): - ret = super().serialize() - ret.update({ - 'l_query': self.l_query, - 'r_query': self.r_query, - }) - return ret - - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - - _, l_mon, l_query, _, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) - return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, - r_query) diff --git a/padinfo/view_state/leader_skill_single.py b/padinfo/view_state/leader_skill_single.py deleted file mode 100644 index a3ce3256..00000000 --- a/padinfo/view_state/leader_skill_single.py +++ /dev/null @@ -1,19 +0,0 @@ -from padinfo.common.config import UserConfig -from padinfo.view_state.base import ViewStateBase -from padinfo.view_state.common import get_monster_from_ims - - -class LeaderSkillSingleViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, mon): - super().__init__(original_author_id, menu_type, raw_query, extra_state=None) - self.color = color - self.mon = mon - - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - - mon = await get_monster_from_ims(dgcog, ims) - return LeaderSkillSingleViewState(original_author_id, menu_type, raw_query, user_config.color, mon) diff --git a/padinfo/view_state/materials.py b/padinfo/view_state/materials.py deleted file mode 100644 index c389a7ba..00000000 --- a/padinfo/view_state/materials.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import List, TYPE_CHECKING, Optional - -from padinfo.common.config import UserConfig -from padinfo.common.external_links import ilmina_skill -from padinfo.menu.pane_names import IdMenuPaneNames -from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - - -class MaterialsViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], - gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, - gem_override: bool, - use_evo_scroll: bool = True, - reaction_list: List[str] = None, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - reaction_list=reaction_list, - use_evo_scroll=use_evo_scroll, - extra_state=extra_state) - self.reaction_list = reaction_list - self.link = link - self.skillup_evo_count = skillup_evo_count - self.skillups = skillups - self.gemusedin = gemusedin - self.mats = mats - self.usedin = usedin - self.gemid = gemid - self.gem_override = gem_override - - def serialize(self): - ret = super().serialize() - ret.update({ - 'pane_type': IdMenuPaneNames.materials, - }) - return ret - - @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): - if ims.get('unsupported_transition'): - return None - monster = await get_monster_from_ims(dgcog, ims) - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ - await MaterialsViewState.query(dgcog, monster) - - if mats is None: - return None - - raw_query = ims['raw_query'] - query = ims.get('query') or raw_query - menu_type = ims['menu_type'] - original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - reaction_list = ims.get('reaction_list') - - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=ims) - - @staticmethod - async def query(dgcog, monster): - db_context = dgcog.database - mats = db_context.graph.evo_mats_by_monster(monster) - usedin = db_context.graph.material_of_monsters(monster) - evo_gem = db_context.graph.evo_gem_monster(monster) - gemid = str(evo_gem.monster_no_na) if evo_gem else None - gemusedin = db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] - skillups = [] - skillup_evo_count = 0 - link = ilmina_skill(monster) - - if monster.active_skill: - sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id) - if db_context.graph.monster_is_farmable_evo(m)] - sugs = [db_context.graph.evo_gem_monster(su) for su in sums] - vsums = [] - for su in sums: - if not any(susu in vsums for susu in db_context.graph.get_alt_monsters(su)): - vsums.append(su) - skillups = [su for su in vsums - if db_context.graph.monster_is_farmable_evo(su) and - db_context.graph.get_base_id(su) != db_context.graph.get_base_id(monster) and - su not in sugs] if monster.active_skill else [] - skillup_evo_count = len(sums) - len(vsums) - gem_override = False - - if not any([mats, usedin, gemusedin, skillups and not monster.is_stackable]): - return None, None, None, None, None, None, None, None - if not any([mats, usedin, skillups and not monster.is_stackable]): - mats, gemusedin, _, usedin, skillups, skillup_evo_count, link, _ \ - = await MaterialsViewState.query(dgcog, evo_gem) - gem_override = True - - return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override diff --git a/padinfo/view_state/monster_list.py b/padinfo/view_state/monster_list.py deleted file mode 100644 index bbc06d11..00000000 --- a/padinfo/view_state/monster_list.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import TYPE_CHECKING, List - -from padinfo.common.config import UserConfig -from padinfo.menu.pane_names import IdMenuPaneNames -from padinfo.view_state.base import ViewStateBase - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - - -class MonsterListViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, query, color, - monster_list: List["MonsterModel"], title, message, - reaction_list=None, - extra_state=None, - child_message_id=None - ): - super().__init__(original_author_id, menu_type, raw_query, - extra_state=extra_state) - self.message = message - self.child_message_id = child_message_id - self.title = title - self.monster_list = monster_list - self.reaction_list = reaction_list - self.color = color - self.query = query - - def serialize(self): - ret = super().serialize() - ret.update({ - 'pane_type': IdMenuPaneNames.id, - 'title': self.title, - 'monster_list': [str(m.monster_no) for m in self.monster_list], - 'reaction_list': self.reaction_list, - 'child_message_id': self.child_message_id, - 'message': self.message, - }) - return ret - - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - if ims.get('unsupported_transition'): - return None - monster_list = [dgcog.database.graph.get_monster(int(m)) for m in ims['monster_list']] - title = ims['title'] - - raw_query = ims['raw_query'] - query = ims.get('query') or raw_query - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - reaction_list = ims.get('reaction_list') - child_message_id = ims.get('child_message_id') - message = ims.get('message') - - return MonsterListViewState(original_author_id, menu_type, raw_query, query, user_config.color, - monster_list, title, message, - reaction_list=reaction_list, - extra_state=ims, - child_message_id=child_message_id - ) diff --git a/padinfo/view_state/otherinfo.py b/padinfo/view_state/otherinfo.py deleted file mode 100644 index 8e6d6295..00000000 --- a/padinfo/view_state/otherinfo.py +++ /dev/null @@ -1,11 +0,0 @@ -from padinfo.menu.pane_names import IdMenuPaneNames -from padinfo.view_state.base_id import ViewStateBaseId - - -class OtherInfoViewState(ViewStateBaseId): - def serialize(self): - ret = super().serialize() - ret.update({ - 'pane_type': IdMenuPaneNames.otherinfo, - }) - return ret diff --git a/padinfo/view_state/pantheon.py b/padinfo/view_state/pantheon.py deleted file mode 100644 index 7419a2b3..00000000 --- a/padinfo/view_state/pantheon.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import TYPE_CHECKING, List - -from padinfo.common.config import UserConfig -from padinfo.menu.pane_names import IdMenuPaneNames -from padinfo.view_state.base_id import ViewStateBaseId -from padinfo.view_state.common import get_monster_from_ims - -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - - -class PantheonViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - pantheon_list: List["MonsterModel"], series_name: str, - use_evo_scroll: bool = True, - reaction_list: List[str] = None, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=extra_state) - self.series_name = series_name - self.pantheon_list = pantheon_list - - def serialize(self): - ret = super().serialize() - ret.update({ - 'pane_type': IdMenuPaneNames.pantheon, - }) - return ret - - @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): - if ims.get('unsupported_transition'): - return None - monster = await get_monster_from_ims(dgcog, ims) - pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) - - if pantheon_list is None: - return None - - raw_query = ims['raw_query'] - query = ims.get('query') or raw_query - original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - menu_type = ims['menu_type'] - reaction_list = ims.get('reaction_list') - - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - pantheon_list, series_name, - use_evo_scroll=use_evo_scroll, - reaction_list=reaction_list, - extra_state=ims) - - @staticmethod - async def query(dgcog, monster): - db_context = dgcog.database - full_pantheon = db_context.get_monsters_by_series(monster.series_id) - if not full_pantheon: - return None, None - pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 20: - return None, None - - series_name = monster.series.name_en - - return pantheon_list, series_name diff --git a/padinfo/view_state/pic.py b/padinfo/view_state/pic.py deleted file mode 100644 index d59373ff..00000000 --- a/padinfo/view_state/pic.py +++ /dev/null @@ -1,12 +0,0 @@ -from padinfo.menu.pane_names import IdMenuPaneNames -from padinfo.view_state.base_id import ViewStateBaseId - - -class PicViewState(ViewStateBaseId): - - def serialize(self): - ret = super().serialize() - ret.update({ - 'pane_type': IdMenuPaneNames.pic, - }) - return ret diff --git a/padinfo/view_state/simple_text.py b/padinfo/view_state/simple_text.py deleted file mode 100644 index 33dba6ee..00000000 --- a/padinfo/view_state/simple_text.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import List - -from padinfo.common.config import UserConfig -from padinfo.view_state.base import ViewStateBase - - -class SimpleTextViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, - color, message, - reaction_list: List[str] = None): - super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) - self.message = message - self.color = color - - @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - raw_query = ims.get('raw_query') - return cls(original_author_id, menu_type, raw_query, user_config.color, ims.get('message'), - reaction_list=ims.get('reaction_list')) diff --git a/padinfo/view_state/transforminfo.py b/padinfo/view_state/transforminfo.py deleted file mode 100644 index 42e678a4..00000000 --- a/padinfo/view_state/transforminfo.py +++ /dev/null @@ -1,48 +0,0 @@ -from padinfo.common.config import UserConfig -from padinfo.view_state.base import ViewStateBase - - -class TransformInfoViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, - base_rarity, acquire_raw, true_evo_type_raw): - super().__init__(original_author_id, menu_type, raw_query, extra_state=None) - self.color = color - self.base_mon = base_mon - self.transformed_mon = transformed_mon - self.base_rarity = base_rarity - self.acquire_raw = acquire_raw - self.true_evo_type_raw = true_evo_type_raw - - def serialize(self): - ret = super().serialize() - ret.update({ - 'b_resolved_monster_id': self.base_mon.monster_id, - 't_resolved_monster_id': self.transformed_mon.monster_id - }) - return ret - - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): - raw_query = ims['raw_query'] - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - base_mon_id = ims['b_resolved_monster_id'] - transformed_mon_id = ims['t_resolved_monster_id'] - - base_mon = dgcog.get_monster(base_mon_id) - transformed_mon = dgcog.get_monster(transformed_mon_id) - - acquire_raw, base_rarity, true_evo_type_raw = \ - await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) - - return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, - base_mon, transformed_mon, base_rarity, acquire_raw, - true_evo_type_raw) - - @staticmethod - async def query(dgcog, base_mon, transformed_mon): - db_context = dgcog.database - acquire_raw = db_context.graph.monster_acquisition(transformed_mon) - base_rarity = db_context.graph.get_base_monster_by_id(transformed_mon.monster_no).rarity - true_evo_type_raw = db_context.graph.true_evo_type_by_monster(transformed_mon).value - return acquire_raw, base_rarity, true_evo_type_raw From 0d1245892396d5e5765635e548feb3a2fa22663c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 18 Feb 2021 01:55:08 -0600 Subject: [PATCH 0634/1220] Fix ide warnings in padinfo (#835) --- padinfo/padinfo.py | 69 +++++++++++++++++++++++----------------------- requirements.txt | 1 + 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index febe837a..adf37316 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -40,15 +40,15 @@ from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list -from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.id_traceback import IdTracebackView, IdTracebackViewProps -from padinfo.view.links import LinksView -from padinfo.view.lookup import LookupView from padinfo.view.closable_embed import ClosableEmbedViewState +from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.evos import EvosViewState from padinfo.view.id import IdViewState +from padinfo.view.id_traceback import IdTracebackView, IdTracebackViewProps from padinfo.view.leader_skill import LeaderSkillViewState from padinfo.view.leader_skill_single import LeaderSkillSingleViewState +from padinfo.view.links import LinksView +from padinfo.view.lookup import LookupView from padinfo.view.materials import MaterialsViewState from padinfo.view.monster_list import MonsterListViewState from padinfo.view.otherinfo import OtherInfoViewState @@ -1014,42 +1014,43 @@ async def idmeaning(self, ctx, *, token): awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} - o = "" + ret = "" - def write_name_token(dict, type, mwtoken=False): + def write_name_token(token_dict, token_type, is_multiword=False): def f(m, s): return bold(s) if DGCOG.database.graph.monster_is_base(m) else s - o = "" + token_ret = "" so = [] - for m in sorted(dict[token], key=lambda m: m.monster_id): - if (m in DGCOG.index2.mwtoken_creators[token]) == mwtoken: - so.append(m) + for mon in sorted(token_dict[token], key=lambda m: m.monster_id): + if (mon in DGCOG.index2.mwtoken_creators[token]) == is_multiword: + so.append(mon) if len(so) > 5: - o += f"\n\n{type}\n" + ", ".join(f(m, str(m.monster_id)) for m in so[:10]) - o += f"... ({len(so)} total)" if len(so) > 10 else "" + token_ret += f"\n\n{token_type}\n" + ", ".join(f(m, str(m.monster_id)) for m in so[:10]) + token_ret += f"... ({len(so)} total)" if len(so) > 10 else "" elif so: - o += f"\n\n{type}\n" + "\n".join(f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) - return o + token_ret += f"\n\n{token_type}\n" + "\n".join( + f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) + return token_ret - o += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) - o += write_name_token(DGCOG.index2.manual, "[Manual Tokens]") - o += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") - o += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") + ret += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) + ret += write_name_token(DGCOG.index2.manual, "[Manual Tokens]") + ret += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") + ret += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") submwtokens = [t for t in DGCOG.index2.multi_word_tokens if token in t] if submwtokens: - o += "\n\n[Multi-word Super-tokens]\n" + ret += "\n\n[Multi-word Super-tokens]\n" for t in submwtokens: if not DGCOG.index2.all_name_tokens[''.join(t)]: continue creators = sorted(DGCOG.index2.mwtoken_creators["".join(t)], key=lambda m: m.monster_id) - o += f"{' '.join(t).title()}" - o += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' - o += (" ( \u2014> " + - str(find_monster.get_most_eligable_monster(DGCOG.index2.all_name_tokens[''.join(t)], - DGCOG).monster_id) - + ")\n") + ret += f"{' '.join(t).title()}" + ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' + ret += (" ( \u2014> " + + str(find_monster.get_most_eligable_monster(DGCOG.index2.all_name_tokens[''.join(t)], + DGCOG).monster_id) + + ")\n") def additmods(ms, om): if len(ms) == 1: @@ -1085,8 +1086,8 @@ def additmods(ms, om): for a, v in tms.AWOKEN_MAP.items() if ag in v] ]) - if meanings or o: - for page in pagify(meanings + "\n\n" + o.strip()): + if meanings or ret: + for page in pagify(meanings + "\n\n" + ret.strip()): await ctx.send(page) else: await ctx.send(f"There are no modifiers that match `{token}`.") @@ -1164,23 +1165,23 @@ async def idsearch(self, ctx, *, query): tokenized_query = query.split() mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) - success, matches, mgen = max( + best_match, matches, mgen = max( await find_monster_search(tokenized_query, dgcog), await find_monster_search(mw_tokenized_query, dgcog) if tokenized_query != mw_tokenized_query else (None, {}), key=lambda t: t[1][t[0]].score if t[0] else 0 ) - if not success: + if not best_match: await ctx.send("No monster matched.") return used = set() monster_list = [] - for m in sorted(mgen, key=lambda m: find_monster.get_priority_tuple(m, dgcog, matches=matches), reverse=True): - bmid = dgcog.database.graph.get_base_id(m) - if bmid not in used: - used.add(bmid) - monster_list.append(m) + for mon in sorted(mgen, key=lambda m: find_monster.get_priority_tuple(m, dgcog, matches=matches), reverse=True): + base_id = dgcog.database.graph.get_base_id(mon) + if base_id not in used: + used.add(base_id) + monster_list.append(mon) monster_list = monster_list[:11] await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results') diff --git a/requirements.txt b/requirements.txt index 69c03afb..edd52c3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ aiofiles aiohttp aioodbc +discord discord-menu git+git://github.com/TsubakiBotPad/python-romkan networkx From 6c8f4c52f77e9000c867bfd4924b4137f26bebf9 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 18 Feb 2021 02:13:19 -0600 Subject: [PATCH 0635/1220] Remove last compiler warning (in padinfo)! (#836) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index adf37316..dc527b58 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -897,7 +897,7 @@ def get_emojis(self): return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] @staticmethod - async def send_id_failure_message(ctx, query: str, err): + async def send_id_failure_message(ctx, query: str, _err): await ctx.send("Sorry, your query {0} didn't match any results :(\n" "See <{2}> for " "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " From 75b094f0fd4971b436de40d3e28b0be41cae4421 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Feb 2021 03:30:15 -0700 Subject: [PATCH 0636/1220] Refactor PadInfo (#837) * Refactor padinfo * Refactor find_monster * Add settings attribute back to PadInfo * Fix regression --- padbuildimg/padbuildimg.py | 43 +++--- padevents/padevents.py | 12 +- padglobal/padglobal.py | 14 +- padinfo/core/find_monster.py | 190 ++++++++++++------------ padinfo/core/leader_skills.py | 12 +- padinfo/core/transforminfo.py | 12 +- padinfo/padinfo.py | 267 ++++++++++++++++------------------ padinfo/view/common.py | 4 +- padinfo/view/leader_skill.py | 2 +- padsearch/padsearch.py | 6 +- pricecheck/pricecheck.py | 16 +- 11 files changed, 284 insertions(+), 294 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 32e5ebf6..9f674fad 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -255,7 +255,7 @@ def lstripalpha(s): return s -class PaDTeamLexer(object): +class PaDTeamLexer: tokens = [ 'ID', 'ASSIST', @@ -288,11 +288,11 @@ def t_ASSIST(self, t): def t_LATENT(self, t): r'\[.+?\]' # words in [] - t.value = [l.strip().lower() for l in t.value.strip('[]').split(',')] + t.value = [lat.strip().lower() for lat in t.value.strip('[]').split(',')] for v in t.value.copy(): if '*' not in v: continue - tmp = [l.strip() for l in v.split('*')] + tmp = [lat.strip() for lat in v.split('*')] if len(tmp[0]) == 1 and tmp[0].isdigit(): count = int(tmp[0]) latent = tmp[1] @@ -306,7 +306,7 @@ def t_LATENT(self, t): for i in range(count): t.value.insert(idx, latent) t.value = t.value[0:MAX_LATENTS] - t.value = [REVERSE_LATENTS_MAP[l] for l in t.value if l in REVERSE_LATENTS_MAP] + t.value = [REVERSE_LATENTS_MAP[lat] for lat in t.value if lat in REVERSE_LATENTS_MAP] return t def t_STATS(self, t): @@ -382,6 +382,9 @@ def t_error(self, t): raise commands.UserFeedbackCheckFailure( "Parse Error: Unknown text '{}' at position {}".format(t.value, t.lexpos)) + def __init__(self): + self.lexer = None + def build(self, **kwargs): # pass debug=1 to enable verbose output self.lexer = lex.lex(module=self) @@ -404,7 +407,7 @@ def validate_latents(card_dict, card, ass_card): latents[idx] = None if l in AWO_RES_LATENT_TO_AWO_MAP and AWO_RES_LATENT_TO_AWO_MAP[l] not in awos: latents[idx] = -latents[idx] - latents = [l for l in latents if l is not None] + latents = [lat for lat in latents if lat is not None] return latents if len(latents) > 0 else None @@ -508,9 +511,9 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card, err, debug_info = await self.padinfo_cog.fm3(tok.value) + ass_card = await self.padinfo_cog.fm3(tok.value) if ass_card is None: - raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) + raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') elif tok.type == 'REPEAT': repeat = min(tok.value, MAX_LATENTS) elif tok.type == 'ID': @@ -518,9 +521,9 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card, err, debug_info = await self.padinfo_cog.fm3(tok.value) + card = await self.padinfo_cog.fm3(tok.value) if card is None: - raise commands.UserFeedbackCheckFailure('Lookup Error: {}'.format(err)) + raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') if not card.is_inheritable: if is_assist: return None, None @@ -596,16 +599,16 @@ def combine_latents(self, card): y_offset = 0 row_count = 0 one_slot, two_slot, six_slot = [], [], [] - for l in latents: - if 100 <= l < 200: - one_slot.append(l) - elif 200 <= l < 300: - two_slot.append(l) - elif 600 <= l < 700: - six_slot.append(l) + for lat in latents: + if 100 <= lat < 200: + one_slot.append(lat) + elif 200 <= lat < 300: + two_slot.append(lat) + elif 600 <= lat < 700: + six_slot.append(lat) six_slot.append(1) - elif -700 < l <= -600: - six_slot.append(l) + elif -700 < lat <= -600: + six_slot.append(lat) six_slot.append(2) sorted_latents = [] if len(one_slot) > len(two_slot): @@ -617,8 +620,8 @@ def combine_latents(self, card): sorted_latents.extend(two_slot) sorted_latents.extend(one_slot) last_height = 0 - for l in sorted_latents: - latent_icon = Image.open(self.params.ASSETS_DIR + 'lat/' + LATENTS_MAP[l] + '.png') + for lat in sorted_latents: + latent_icon = Image.open(self.params.ASSETS_DIR + 'lat/' + LATENTS_MAP[lat] + '.png') if x_offset + latent_icon.size[0] > self.params.PORTRAIT_WIDTH: row_count += 1 x_offset = 0 diff --git a/padevents/padevents.py b/padevents/padevents.py index ac2efcd9..db760e14 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -84,9 +84,9 @@ async def reload_padevents(self): await asyncio.sleep(60 * 60 * 1) async def refresh_data(self): - dg_cog = self.bot.get_cog('Dadguide') - await dg_cog.wait_until_ready() - scheduled_events = dg_cog.database.get_all_events() + dgcog = self.bot.get_cog('Dadguide') + await dgcog.wait_until_ready() + scheduled_events = dgcog.database.get_all_events() new_events = [] for se in scheduled_events: @@ -218,10 +218,10 @@ async def testevent(self, ctx, server, seconds: int = 0): await ctx.send("Unsupported server, pick one of NA, KR, JP") return - dg_cog = self.bot.get_cog('Dadguide') - await dg_cog.wait_until_ready() + dgcog = self.bot.get_cog('Dadguide') + await dgcog.wait_until_ready() # TODO: Don't use this awful importing hack - dg_module = __import__('.'.join(dg_cog.__module__.split('.')[:-1]) + ".models.scheduled_event_model") + dg_module = __import__('.'.join(dgcog.__module__.split('.')[:-1]) + ".models.scheduled_event_model") timestamp = int((datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds)).timestamp()) self.fake_uid -= 1 diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index c40236c3..8df82034 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -66,15 +66,15 @@ async def lookup_monster_model(query: str): padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') if padinfo_cog is None: raise Exception("Cog not Loaded") - m, err, debug_info = await padinfo_cog.fm3(str(query)) - return m, err, debug_info + m = await padinfo_cog.fm3(str(query)) + return m def monster_id_to_monster(monster_id): - dg_cog = PADGLOBAL_COG.bot.get_cog('Dadguide') - if dg_cog is None: + dgcog = PADGLOBAL_COG.bot.get_cog('Dadguide') + if dgcog is None: return None - return dg_cog.get_monster(monster_id) + return dgcog.get_monster(monster_id) async def check_enabled(ctx): @@ -643,7 +643,7 @@ async def _resolve_which(self, ctx, term): padinfo = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') - m, _, _ = await lookup_monster_model(term) + m = await lookup_monster_model(term) if m is None: await ctx.send(inline('No monster matched that query')) return None, None, None, None @@ -1032,7 +1032,7 @@ async def get_guide_text(self, term: str): if term in self.settings.dungeonGuide(): return term, self.settings.dungeonGuide()[term], None - m, _, _ = await lookup_monster_model(term) + m = await lookup_monster_model(term) if m is None: return None, None, 'No dungeon or monster matched that query' m = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 3f100ecb..6aefd74e 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -52,30 +52,30 @@ class FindMonster: def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result = [] - s = 0 + skip = 0 multi_word_tokens_sorted = sorted(valid_multi_word_tokens, key=lambda x: (len(x), len(''.join(x))), reverse=True) - for c1, token in enumerate(tokens): - if s: - s -= 1 + for c1, token1 in enumerate(tokens): + if skip: + skip -= 1 continue for mwt in multi_word_tokens_sorted: if len(mwt) > len(tokens) - c1: continue - for c2, t in enumerate(mwt): - if (tokens[c1 + c2] != t and len(t) < 5) \ - or calc_ratio_modifier(tokens[c1 + c2], t) < self.TOKEN_JW_DISTANCE: + for c2, token2 in enumerate(mwt): + if (tokens[c1 + c2] != token2 and len(token2) < 5) \ + or calc_ratio_modifier(tokens[c1 + c2], token2) < self.TOKEN_JW_DISTANCE: break else: - s = len(mwt) - 1 + skip = len(mwt) - 1 result.append("".join(mwt)) break else: - result.append(token) + result.append(token1) return result - def _monster_has_token(self, monster, token, matches, monster_mods): + def _monster_has_modifier(self, monster, token, matches, monster_mods): if len(token) < 6: if token in monster_mods: matches[monster].mod.add((token, token)) @@ -83,10 +83,10 @@ def _monster_has_token(self, monster, token, matches, monster_mods): return True else: closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) - rat = calc_ratio_modifier(closest, token) - if rat > self.MODIFIER_JW_DISTANCE: + ratio = calc_ratio_modifier(closest, token) + if ratio > self.MODIFIER_JW_DISTANCE: matches[monster].mod.add((token, closest)) - matches[monster].score += rat + matches[monster].score += ratio return True return False @@ -107,7 +107,7 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], else: modifiers.append(token) else: - if i: + if i != 0: tokenized_query = tokenized_query[:-i] break @@ -146,81 +146,82 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], return set(modifiers), negative_modifiers, name, negative_name def process_name_tokens(self, name_query_tokens, neg_name_tokens, dgcog, matches): - monstergen = None + matched_mons = None - for t in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(t, dgcog.index2, matches) - if monstergen is not None: - monstergen.intersection_update(valid) + for pos_name in name_query_tokens: + valid = self.get_valid_monsters_from_name_token(pos_name, dgcog.index2, matches) + if matched_mons is not None: + matched_mons.intersection_update(valid) else: - monstergen = valid + matched_mons = valid - for t in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(t, dgcog.index2, matches, mult=-10) - if monstergen is not None: - monstergen.difference_update(invalid) + for neg_name in neg_name_tokens: + invalid = self.get_valid_monsters_from_name_token(neg_name, dgcog.index2, matches, mult=-10) + if matched_mons is not None: + matched_mons.difference_update(invalid) else: - monstergen = set(dgcog.database.get_all_monsters()).difference(invalid) - - return monstergen - - def get_valid_monsters_from_name_token(self, t, index2, matches, mult=1): - valid = set() - ms = sorted([nt for nt in index2.all_name_tokens if calc_ratio_name(t, nt, index2) > self.TOKEN_JW_DISTANCE], - key=lambda nt: calc_ratio_name(t, nt, index2), reverse=True) - ms += [token for token in index2.all_name_tokens if token.startswith(t)] - for match in ms: - score = calc_ratio_name(t, match, index2) - for m in index2.manual[match]: - if m not in valid: - matches[m].name.add((t, match, '')) - matches[m].score += (score + .001) * mult - valid.add(m) - for m in index2.name_tokens[match]: - if m not in valid: - matches[m].name.add((t, match, '')) - matches[m].score += score * mult - valid.add(m) - for m in index2.fluff_tokens[match]: - if m not in valid: - matches[m].name.add((t, match, '')) - matches[m].score += score * mult / 2 - valid.add(m) - - return valid + matched_mons = set(dgcog.database.get_all_monsters()).difference(invalid) + + return matched_mons + + def get_valid_monsters_from_name_token(self, token, index2, matches, mult=1): + valid_monsters = set() + matched_tokens = sorted((nt for nt in index2.all_name_tokens + if calc_ratio_name(token, nt, index2) > self.TOKEN_JW_DISTANCE), + key=lambda nt: calc_ratio_name(token, nt, index2), reverse=True) + matched_tokens += [t for t in index2.all_name_tokens if t.startswith(token)] + for match in matched_tokens: + score = calc_ratio_name(token, match, index2) + for matched_monster in index2.manual[match]: + if matched_monster not in valid_monsters: + matches[matched_monster].name.add((token, match, '(manual)')) + matches[matched_monster].score += (score + .001) * mult + valid_monsters.add(matched_monster) + for matched_monster in index2.name_tokens[match]: + if matched_monster not in valid_monsters: + matches[matched_monster].name.add((token, match, '(name)')) + matches[matched_monster].score += score * mult + valid_monsters.add(matched_monster) + for matched_monster in index2.fluff_tokens[match]: + if matched_monster not in valid_monsters: + matches[matched_monster].name.add((token, match, '(fluff)')) + matches[matched_monster].score += score * mult / 2 + valid_monsters.add(matched_monster) + + return valid_monsters def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches, monster_mods): - for t in mod_tokens: + for pos_mod_token in mod_tokens: potential_evos = {m for m in potential_evos if - self._monster_has_token(m, t, matches, monster_mods[m])} + self._monster_has_modifier(m, pos_mod_token, matches, monster_mods[m])} if not potential_evos: return None - for t in neg_mod_tokens: + for neg_mod_token in neg_mod_tokens: potential_evos = {m for m in potential_evos if - not self._monster_has_token(m, t, matches, monster_mods[m])} + not self._monster_has_modifier(m, neg_mod_token, matches, monster_mods[m])} if not potential_evos: return None return potential_evos - def get_priority_tuple(self, m, dgcog, tokenized_query=None, matches=None): + def get_priority_tuple(self, monster, dgcog, tokenized_query=None, matches=None): if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] - return (matches[m].score, - not m.is_equip, + return (matches[monster].score, + not monster.is_equip, # Match na on id overlap - bool(m.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), - SERIES_TYPE_PRIORITY.get(m.series.series_type), - m.on_na if m.series.series_type == "collab" else 0, - dgcog.database.graph.monster_is_rem_evo(m), - not all(t.value in [0, 12, 14, 15] for t in m.types), - not any(t.value in [0, 12, 14, 15] for t in m.types), - -dgcog.database.graph.get_base_id(m), - m.rarity, - m.monster_no_na) + bool(monster.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), + SERIES_TYPE_PRIORITY.get(monster.series.series_type), + monster.on_na if monster.series.series_type == "collab" else 0, + dgcog.database.graph.monster_is_rem_evo(monster), + not all(t.value in [0, 12, 14, 15] for t in monster.types), + not any(t.value in [0, 12, 14, 15] for t in monster.types), + -dgcog.database.graph.get_base_id(monster), + monster.rarity, + monster.monster_no_na) def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, matches=None): if matches is None: @@ -230,34 +231,27 @@ def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, match return max(monsters, key=lambda m: self.get_priority_tuple(m, dgcog, tokenized_query, matches)) - def get_monster_evos(self, database, monster_gen, matches): + def get_monster_evos(self, database, matched_mons, matches): monster_evos = set() - for m in sorted(monster_gen, key=lambda m: matches[m].score, reverse=True): - for evo in database.graph.get_alt_monsters(m): + for monster in sorted(matched_mons, key=lambda m: matches[m].score, reverse=True): + for evo in database.graph.get_alt_monsters(monster): monster_evos.add(evo) - if matches[evo].score < matches[m].score: - matches[evo].name = {(t[0], t[1], f'(from evo {m.monster_id})') for t in matches[m].name} - matches[evo].score = matches[m].score - .003 + if matches[evo].score < matches[monster].score: + matches[evo].name = {(t[0], t[1], + f'(from evo {monster.monster_id})') for t in matches[monster].name} + matches[evo].score = matches[monster].score - .003 return monster_evos -async def findMonsterCustom(dgcog, query): - m = await findMonster3(dgcog, query) - if m: - return m, "", "" - else: - return None, "Monster not found", "" - - -async def findMonster3(dgcog, query): - m = await _findMonster3(dgcog, query) +async def findMonster3(dgcog, query) -> Optional["MonsterModel"]: + matched_monster = await _findMonster3(dgcog, query) - monster_no = m.monster_id if m else -1 + monster_no = matched_monster.monster_id if matched_monster else -1 historic_lookups_id3[query] = monster_no json.dump(historic_lookups_id3, open(historic_lookups_file_path_id3, "w+")) - return m + return matched_monster async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: @@ -294,41 +288,41 @@ async def find_monster_search(tokenized_query, dgcog) -> \ name_query_tokens.difference_update({'|'}) - for t in mod_tokens.union(neg_mod_tokens): - if t not in dgcog.index2.all_modifiers: - settings.add_typo_mod(t) + for mod_token in mod_tokens.union(neg_mod_tokens): + if mod_token not in dgcog.index2.all_modifiers: + settings.add_typo_mod(mod_token) # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) matches = defaultdict(MonsterMatch) if name_query_tokens or neg_name_tokens: - monster_gen = find_monster.process_name_tokens(name_query_tokens, + matched_mons = find_monster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog, matches) - if not monster_gen: + if not matched_mons: # No monsters match the given name tokens return None, {}, set() - monster_gen = find_monster.get_monster_evos(dgcog.database, monster_gen, matches) + matched_mons = find_monster.get_monster_evos(dgcog.database, matched_mons, matches) else: # There are no name tokens in the query - monster_gen = {*dgcog.database.get_all_monsters()} + matched_mons = {*dgcog.database.get_all_monsters()} monster_score = defaultdict(int) # Expand search to the evo tree - monster_gen = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, monster_gen, matches, + matched_mons = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, matched_mons, matches, dgcog.index2.modifiers) - if not monster_gen: + if not matched_mons: # no modifiers match any monster in the evo tree return None, {}, set() # print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) - # if k in monster_gen}) + # if k in matched_mons}) # Return most likely candidate based on query. - mon = find_monster.get_most_eligable_monster(monster_gen, dgcog, tokenized_query, matches) + mon = find_monster.get_most_eligable_monster(matched_mons, dgcog, tokenized_query, matches) - return mon, matches, monster_gen + return mon, matches, matched_mons find_monster = FindMonster() diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 13f8f450..e1603950 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,6 +1,6 @@ import re -from padinfo.core.find_monster import findMonsterCustom +from padinfo.core.find_monster import findMonster3 def humanize_number(number, sigfigs=2): @@ -82,7 +82,7 @@ async def perform_leaderskill_query(dgcog, raw_query): right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" - m, err, debug_info = await findMonsterCustom(dgcog, query) + m = await findMonster3(dgcog, query) if m and left_query in dgcog.index2.modifiers[m]: left_query = query right_query = None @@ -91,9 +91,9 @@ async def perform_leaderskill_query(dgcog, raw_query): else: # no separators left_query, right_query = query, None - left_m, left_err, _ = await findMonsterCustom(dgcog, left_query) + left_m = await findMonster3(dgcog, left_query) if right_query: - right_m, right_err, _ = await findMonsterCustom(dgcog, right_query) + right_m = await findMonster3(dgcog, right_query) else: - right_m, right_err, = left_m, left_err - return left_err, left_m, left_query, right_err, right_m, right_query + right_m = left_m + return left_m, left_query, right_m, right_query diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index e57b4c8d..bb9b1203 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -1,14 +1,16 @@ -from padinfo.core.find_monster import findMonsterCustom +from padinfo.core.find_monster import findMonster3 async def perform_transforminfo_query(dgcog, raw_query): db_context = dgcog.database mgraph = dgcog.database.graph - found_monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + found_monster = await findMonster3(dgcog, raw_query) if not found_monster: - return found_monster, err, debug_info, None + return None, None + transformed_mon = None + base_mon = None altversions = mgraph.process_alt_versions(found_monster.monster_id) for mon_id in sorted(altversions): if mgraph.monster_is_transform_base_by_id(mon_id): @@ -18,6 +20,6 @@ async def perform_transforminfo_query(dgcog, raw_query): break if not transformed_mon: - return found_monster, err, debug_info, transformed_mon + return found_monster, transformed_mon - return base_mon, err, debug_info, transformed_mon + return base_mon, transformed_mon diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index dc527b58..6e9eadfc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -26,7 +26,7 @@ from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import find_monster, findMonster3, \ - calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonsterCustom + calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonster3 from padinfo.core.historic_lookups import historic_lookups from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings @@ -102,9 +102,10 @@ def __init__(self, bot, *args, **kwargs): self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False, test_suite={}, fluff_suite=[]) - self.fm3 = lambda q: fm.findMonsterCustom(bot.get_cog("Dadguide"), q) + self.fm3 = lambda q: fm.findMonster3(bot.get_cog("Dadguide"), q) self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster + self.settings = settings def cog_unload(self): """Manually nulling out database because the GC for cogs seems to be pretty shitty""" @@ -129,8 +130,8 @@ async def reload_nicknames(self): try: emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) - dg_cog = self.bot.get_cog('Dadguide') - await dg_cog.wait_until_ready() + dgcog = self.bot.get_cog('Dadguide') + await dgcog.wait_until_ready() except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) @@ -145,8 +146,8 @@ async def get_dgcog(self): return dgcog def get_monster(self, monster_id: int): - dg_cog = self.bot.get_cog('Dadguide') - return dg_cog.get_monster(monster_id) + dgcog = self.bot.get_cog('Dadguide') + return dgcog.get_monster(monster_id) @commands.Cog.listener('on_reaction_add') async def test_reaction_add(self, reaction, member): @@ -212,8 +213,8 @@ async def test_reaction_add(self, reaction, member): embed_menu = menu_func() if not (await embed_menu.should_respond( - message, reaction, await self.get_reaction_filters( - original_author_id, menu_to_emoji_list_map[menu_type]), member)): + message, reaction, await self.get_reaction_filters( + original_author_id, menu_to_emoji_list_map[menu_type]), member)): return dgcog = await self.get_dgcog() @@ -263,12 +264,12 @@ async def get_reaction_filters(self, original_author_id, valid_emoji_names): async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if m is not None: - await ctx.send(MonsterHeader.short(m)) - await ctx.send(box(m.name_ja)) + monster = await findMonster3(dgcog, query) + if monster is not None: + await ctx.send(MonsterHeader.short(monster)) + await ctx.send(box(monster.name_ja)) else: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) @commands.command(name="id", aliases=["iD", "Id", "ID"]) @checks.bot_has_permissions(embed_links=True) @@ -308,11 +309,11 @@ async def _do_id(self, ctx, query: str): if goodquery: bad = False - for m in dgcog.index2.all_name_tokens[goodquery[1]]: - for p in dgcog.index2.modifiers[m]: - if p == 'xm' and goodquery[0] == 'x': + for monster in dgcog.index2.all_name_tokens[goodquery[1]]: + for modifier in dgcog.index2.modifiers[monster]: + if modifier == 'xm' and goodquery[0] == 'x': goodquery[0] = 'xm' - if p == goodquery[0]: + if modifier == goodquery[0]: bad = True if bad and query not in dgcog.index2.all_name_tokens: async def send_message(): @@ -323,22 +324,15 @@ async def send_message(): f" <{IDGUIDE}>!") asyncio.create_task(send_message()) - async with self.config.bad_queries() as bq: - bq.append((raw_query, ctx.author.id)) + async with self.config.bad_queries() as bad_queries: + bad_queries.append((raw_query, ctx.author.id)) - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + monster = await findMonster3(dgcog, raw_query) if not monster: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return - async def send_error(error): - if error: - await asyncio.sleep(1) - await ctx.send(error) - - asyncio.create_task(send_error(err)) - # id3 messaging stuff if monster and monster.monster_no_na != monster.monster_no_jp: await ctx.send("The NA ID and JP ID of this card differ! " @@ -363,9 +357,9 @@ async def send_error(error): async def send_survey_after(self, ctx, query, result_monster): dgcog = await self.get_dgcog() - sm = await self.config.user(ctx.author).survey_mode() - sms = [1, await self.config.sometimes_perc() / 100, 0][sm] - if random.random() < sms: + survey_mode = await self.config.user(ctx.author).survey_mode() + survey_chance = (1, await self.config.sometimes_perc() / 100, 0)[survey_mode] + if random.random() < survey_chance: mid1 = historic_lookups.get(query) m1 = mid1 and dgcog.get_monster(mid1) id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" @@ -380,11 +374,11 @@ async def send_survey_after(self, ctx, query, result_monster): await self.config.good.set(await self.config.good() + 1) elif userres is False: await self.config.bad.set(await self.config.bad() + 1) - m = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" - f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idset" - f" survey` to adjust how often this shows.\n\n<{url}>") + message = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" + f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idset" + f" survey` to adjust how often this shows.\n\n<{url}>") await asyncio.sleep(15) - await m.delete() + await message.delete() @commands.group() async def idsurvey(self, ctx): @@ -424,10 +418,10 @@ async def evos(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + monster = await findMonster3(dgcog, raw_query) if monster is None: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) @@ -455,10 +449,10 @@ async def evomats(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + monster = await findMonster3(dgcog, raw_query) if not monster: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ @@ -487,10 +481,10 @@ async def pantheon(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + monster = await findMonster3(dgcog, raw_query) if monster is None: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) @@ -517,10 +511,10 @@ async def pic(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + monster = await findMonster3(dgcog, raw_query) if monster is None: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] @@ -542,10 +536,10 @@ async def otherinfo(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster, err, debug_info = await findMonsterCustom(dgcog, raw_query) + monster = await findMonster3(dgcog, raw_query) if monster is None: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] @@ -563,12 +557,12 @@ async def otherinfo(self, ctx, *, query: str): async def links(self, ctx, *, query: str): """Monster links""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if m is None: - await self.send_id_failure_message(ctx, query, err) + monster = await findMonster3(dgcog, query) + if monster is None: + await self.send_id_failure_message(ctx, query) return color = await self.get_user_embed_color(ctx) - embed = LinksView.embed(m, color).to_embed() + embed = LinksView.embed(monster, color).to_embed() await ctx.send(embed=embed) @commands.command() @@ -576,12 +570,12 @@ async def links(self, ctx, *, query: str): async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if m is None: - await self.send_id_failure_message(ctx, query, err) + monster = await findMonster3(dgcog, query) + if monster is None: + await self.send_id_failure_message(ctx, query) return color = await self.get_user_embed_color(ctx) - embed = LookupView.embed(m, color).to_embed() + embed = LookupView.embed(monster, color).to_embed() await ctx.send(embed=embed) @commands.command() @@ -589,9 +583,9 @@ async def lookup(self, ctx, *, query: str): async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" dgcog = await self.get_dgcog() - monster, err, _ = await findMonsterCustom(dgcog, query) + monster = await findMonster3(dgcog, query) if monster is None: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return DGCOG = self.bot.get_cog("Dadguide") info = button_info.get_info(DGCOG, monster) @@ -603,10 +597,10 @@ async def buttoninfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): dgcog = await self.get_dgcog() - monster, err, debug_info = await findMonsterCustom(dgcog, query) + monster = await findMonster3(dgcog, query) if monster is None: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return alt_versions, _ = await EvosViewState.query(dgcog, monster) @@ -664,13 +658,14 @@ async def leaderskill(self, ctx, *, raw_query): [p]ls sonia lubu """ dgcog = await self.get_dgcog() - l_err, l_mon, l_query, r_err, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) + l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) - err_msg = '{} query failed to match a monster: [ {} ]. If your query is multiple words, try separating the queries with / or wrap with quotes.' - if l_err: + err_msg = ('{} query failed to match a monster: [ {} ]. If your query is multiple words,' + ' try separating the queries with / or wrap with quotes.') + if l_mon is None: await ctx.send(inline(err_msg.format('Left', l_query))) return - if r_err: + if r_mon is None: await ctx.send(inline(err_msg.format('Right', r_query))) return @@ -703,14 +698,14 @@ async def get_user_friends(self, original_author_id): @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if err: - await ctx.send(err) + monster = await findMonster3(dgcog, query) + if not monster: + await self.send_id_failure_message(ctx, query) return color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, color, m) + state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, color, monster) menu = LeaderSkillSingleMenu.menu() await menu.create(ctx, state) @@ -719,20 +714,16 @@ async def leaderskillsingle(self, ctx, *, query): async def transforminfo(self, ctx, *, query): """Show info about a transform card, including some helpful details about the base card.""" dgcog = await self.get_dgcog() - base_mon, err, debug_info, transformed_mon = \ - await perform_transforminfo_query(dgcog, query) + base_mon, transformed_mon = await perform_transforminfo_query(dgcog, query) if not base_mon: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) return if not transformed_mon: - await ctx.send('Your query `{}` found [{}] {}, '.format(query, base_mon.monster_id, - base_mon.name_en) + 'which has no evos that transform.') - return - - if err: - await ctx.send(err) + await ctx.send( + 'Your query `{}` found [{}] {}, '.format(query, base_mon.monster_id, + base_mon.name_en) + 'which has no evos that transform.') return color = await self.get_user_embed_color(ctx) @@ -765,22 +756,22 @@ async def padsay(self, ctx, server, *, query: str = None): query = query.strip().lower() dgcog = await self.get_dgcog() - m, err, debug_info = await findMonsterCustom(dgcog, query) - if m is not None: - voice_id = m.voice_id_jp if server == 'jp' else m.voice_id_na + monster = await findMonster3(dgcog, query) + if monster is not None: + voice_id = monster.voice_id_jp if server == 'jp' else monster.voice_id_na if voice_id is None: - await ctx.send(inline("No voice file found for " + m.name_en)) + await ctx.send(inline("No voice file found for " + monster.name_en)) return base_dir = settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) - header = '{} ({})'.format(MonsterHeader.short(m), server) + header = '{} ({})'.format(MonsterHeader.short(monster), server) if not os.path.exists(voice_file): await ctx.send(inline('Could not find voice for ' + header)) return await ctx.send('Speaking for ' + header) await speech_cog.play_path(channel, voice_file) else: - await self.send_id_failure_message(ctx, query, err) + await self.send_id_failure_message(ctx, query) @commands.group(aliases=['idmode']) async def idset(self, ctx): @@ -860,9 +851,9 @@ async def emojiservers(self, ctx): @checks.is_owner() async def es_add(self, ctx, server_id: int): """Add the emoji server by ID""" - ess = settings.emojiServers() - if server_id not in ess: - ess.append(server_id) + emoji_servers = settings.emojiServers() + if server_id not in emoji_servers: + emoji_servers.append(server_id) settings.save_settings() await ctx.tick() @@ -870,11 +861,11 @@ async def es_add(self, ctx, server_id: int): @checks.is_owner() async def es_rm(self, ctx, server_id: int): """Remove the emoji server by ID""" - ess = settings.emojiServers() - if server_id not in ess: + emoji_servers = settings.emojiServers() + if server_id not in emoji_servers: await ctx.send("That emoji server is not set.") return - ess.remove(server_id) + emoji_servers.remove(server_id) settings.save_settings() await ctx.tick() @@ -882,8 +873,8 @@ async def es_rm(self, ctx, server_id: int): @checks.is_owner() async def es_show(self, ctx): """List the emoji servers by ID""" - ess = settings.emojiServers() - await ctx.send(box("\n".join(str(s) for s in ess))) + emoji_servers = settings.emojiServers() + await ctx.send(box("\n".join(str(s) for s in emoji_servers))) @padinfo.command() @checks.is_owner() @@ -897,7 +888,7 @@ def get_emojis(self): return [e for g in self.bot.guilds if g.id in server_ids for e in g.emojis] @staticmethod - async def send_id_failure_message(ctx, query: str, _err): + async def send_id_failure_message(ctx, query: str): await ctx.send("Sorry, your query {0} didn't match any results :(\n" "See <{2}> for " "documentation on `{1.prefix}id`! You can also run `{1.prefix}idhelp ` to get " @@ -907,29 +898,29 @@ async def send_id_failure_message(ctx, query: str, _err): async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" dgcog = self.bot.get_cog("Dadguide") - m = await findMonster3(dgcog, query) - if m is None: + mon = await findMonster3(dgcog, query) + if mon is None: await ctx.send(box("Your query didn't match any monsters.")) return - bm = dgcog.database.graph.get_base_monster(m) - pfxs = dgcog.index2.modifiers[m] - manmods = dgcog.index2.manual_prefixes[m.monster_id] + base_monster = dgcog.database.graph.get_base_monster(mon) + mods = dgcog.index2.modifiers[mon] + manual_modifiers = dgcog.index2.manual_prefixes[mon.monster_id] EVOANDTYPE = dgcog.token_maps.EVO_TOKENS.union(dgcog.token_maps.TYPE_TOKENS) - o = (f"[{m.monster_id}] {m.name_en}\n" - f"Base: [{bm.monster_id}] {bm.name_en}\n" - f"Series: {m.series.name_en} ({m.series_id}, {m.series.series_type})\n\n" - f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.name_tokens.items() if m in ms))}\n" - f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.fluff_tokens.items() if m in ms))}\n\n" - f"[Manual Tokens]\n" - f" Treenames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_tree.items() if m in ms))}\n" - f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_nick.items() if m in ms))}\n\n" - f"[Modifier Tokens]\n" - f" Attribute: {' '.join(sorted(t for t in pfxs if t in dgcog.token_maps.COLOR_TOKENS))}\n" - f" Awakening: {' '.join(sorted(t for t in pfxs if t in dgcog.token_maps.AWAKENING_TOKENS))}\n" - f" Evo & Type: {' '.join(sorted(t for t in pfxs if t in EVOANDTYPE))}\n" - f" Other: {' '.join(sorted(t for t in pfxs if t not in dgcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" - f"Manually Added: {' '.join(sorted(manmods))}\n") - for page in pagify(o): + ret = (f"[{mon.monster_id}] {mon.name_en}\n" + f"Base: [{base_monster.monster_id}] {base_monster.name_en}\n" + f"Series: {mon.series.name_en} ({mon.series_id}, {mon.series.series_type})\n\n" + f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.name_tokens.items() if mon in ms))}\n" + f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.fluff_tokens.items() if mon in ms))}\n\n" + f"[Manual Tokens]\n" + f" Treenames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_tree.items() if mon in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_nick.items() if mon in ms))}\n\n" + f"[Modifier Tokens]\n" + f" Attribute: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.COLOR_TOKENS))}\n" + f" Awakening: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.AWAKENING_TOKENS))}\n" + f" Evo & Type: {' '.join(sorted(t for t in mods if t in EVOANDTYPE))}\n" + f" Other: {' '.join(sorted(t for t in mods if t not in dgcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" + f"Manually Added: {' '.join(sorted(manual_modifiers))}\n") + for page in pagify(ret): await ctx.send(box(page)) @commands.command() @@ -967,39 +958,39 @@ async def idhelp(self, ctx, *, query=""): @commands.command() async def exportmodifiers(self, ctx): DGCOG = self.bot.get_cog("Dadguide") - tms = DGCOG.token_maps + maps = DGCOG.token_maps awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} - o = ("Jump to:\n\n" - "* [Types](#types)\n" - "* [Evolutions](#evolutions)\n" - "* [Misc](#misc)\n" - "* [Awakenings](#awakenings)\n" - "* [Series](#series)\n" - "* [Attributes](#attributes)\n\n\n\n") - - etable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.EVO_MAP.items()] - o += "\n\n### Evolutions\n\n" + tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") - ttable = [(k.name, ", ".join(map(inline, v))) for k, v in tms.TYPE_MAP.items()] - o += "\n\n### Types\n\n" + tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") - mtable = [(k.value, ", ".join(map(inline, v))) for k, v in tms.MISC_MAP.items()] - o += "\n\n### Misc\n\n" + tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") - atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in tms.AWOKEN_MAP.items()] - o += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") + ret = ("Jump to:\n\n" + "* [Types](#types)\n" + "* [Evolutions](#evolutions)\n" + "* [Misc](#misc)\n" + "* [Awakenings](#awakenings)\n" + "* [Series](#series)\n" + "* [Attributes](#attributes)\n\n\n\n") + + etable = [(k.value, ", ".join(map(inline, v))) for k, v in maps.EVO_MAP.items()] + ret += "\n\n### Evolutions\n\n" + tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") + ttable = [(k.name, ", ".join(map(inline, v))) for k, v in maps.TYPE_MAP.items()] + ret += "\n\n### Types\n\n" + tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") + mtable = [(k.value, ", ".join(map(inline, v))) for k, v in maps.MISC_MAP.items()] + ret += "\n\n### Misc\n\n" + tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") + atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_MAP.items()] + ret += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") stable = [(series[k].name_en, ", ".join(map(inline, v))) for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items()] - o += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") - ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in tms.COLOR_MAP.items()] + ret += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") + ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in maps.COLOR_MAP.items()] ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in - tms.SUB_COLOR_MAP.items()] - for k, v in tms.DUAL_COLOR_MAP.items(): + maps.SUB_COLOR_MAP.items()] + for k, v in maps.DUAL_COLOR_MAP.items(): k0name = k[0].name.replace("Nil", "None") k1name = k[1].name.replace("Nil", "None") ctable.append((k0name + "/" + k1name, ", ".join(map(inline, v)))) - o += "### Attributes\n\n" + tabulate(ctable, headers=["Meaning", "Tokens"], tablefmt="github") + ret += "### Attributes\n\n" + tabulate(ctable, headers=["Meaning", "Tokens"], tablefmt="github") - await ctx.send(file=text_to_file(o, filename="table.md")) + await ctx.send(file=text_to_file(ret, filename="table.md")) @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod", "luid", "idlu"]) async def idmeaning(self, ctx, *, token): @@ -1095,13 +1086,13 @@ def additmods(ms, om): @commands.command(aliases=["tracebackid", "tbid", "idtb"]) async def idtraceback(self, ctx, *, query): """Get the traceback of an id query""" - mid = None + selected_monster_id = None if "/" in query: - query, mid = query.split("/", 1) - if not mid.strip().isdigit(): + query, selected_monster_id = query.split("/", 1) + if not selected_monster_id.strip().isdigit(): await ctx.send("Monster id must be an int.") return - mid = int(mid.strip()) + selected_monster_id = int(selected_monster_id.strip()) dgcog = self.bot.get_cog("Dadguide") await dgcog.wait_until_ready() @@ -1121,8 +1112,8 @@ async def idtraceback(self, ctx, *, query): await ctx.send("No monster matched.") return - if mid is not None: - selected = {m for m in matches if m.monster_id == mid} + if selected_monster_id is not None: + selected = {m for m in matches if m.monster_id == selected_monster_id} if not selected: await ctx.send("The requested monster was not found as a result of the query.") return diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 1a89945e..c4ec83cd 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -1,4 +1,4 @@ -from padinfo.core.find_monster import findMonsterCustom +from padinfo.core.find_monster import findMonster3 async def get_monster_from_ims(dgcog, ims: dict): @@ -8,5 +8,5 @@ async def get_monster_from_ims(dgcog, ims: dict): resolved_monster_id = int(resolved_monster_id_str) if resolved_monster_id_str else None if resolved_monster_id: return dgcog.database.graph.get_monster(resolved_monster_id) - monster, _, _ = await findMonsterCustom(dgcog, query) + monster = await findMonster3(dgcog, query) return monster diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index c235f3c0..ca58a9b1 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -39,7 +39,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - _, l_mon, l_query, _, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) + l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, r_query) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index f31a4212..cd7349dd 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -757,11 +757,11 @@ async def search(self, ctx, *, filter_spec: str): except: # If it still failed, raise the original exception raise ex - dg_cog = self.bot.get_cog('Dadguide') - if dg_cog is None: + dgcog = self.bot.get_cog('Dadguide') + if dgcog is None: await ctx.send("Dadguide cog not loaded.") return - monsters = dg_cog.database.get_all_monsters() + monsters = dgcog.database.get_all_monsters() rmvGemFilter = self._make_search_config('remove( gem)') monsters = list(filter(rmvGemFilter.check_filters, monsters)) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 9b20ce30..30f4e536 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -53,9 +53,9 @@ async def pricecheck(self, ctx, *, query): return if "gem" not in query.lower(): query += " gem" - m, err, debug_info = await padinfo_cog.fm3(query) + m = await padinfo_cog.fm3(query) if not m: - await ctx.send(err) + await ctx.send("Monster not found.") return base_id = str(dgcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: @@ -95,9 +95,9 @@ async def set(self, ctx, stam_cost: float, *, query): return if "gem" not in query.lower(): query += " gem" - m, err, debug_info = await padinfo_cog.fm3(query) + m = await padinfo_cog.fm3(query) if not m: - await ctx.send(err) + await ctx.send("Monster not found.") return base_id = str(dgcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: @@ -117,9 +117,9 @@ async def setfooter(self, ctx, query, *, footer=""): return if "gem" not in query.lower(): query += " gem" - m, err, debug_info = await padinfo_cog.fm3(query) + m = await padinfo_cog.fm3(query) if not m: - await ctx.send(err) + await ctx.send("Monster not found.") return base_id = str(dgcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: @@ -139,9 +139,9 @@ async def remove(self, ctx, *, query): return if "gem" not in query.lower(): query += " gem" - m, err, debug_info = await padinfo_cog.fm3(query) + m = await padinfo_cog.fm3(query) if not m: - await ctx.send(err) + await ctx.send("Monster not found.") return async with self.config.pcs() as pcs: if str(dgcog.database.graph.get_base_id(m)) not in pcs: From 9ad76a09154ed8ae2b4240b2da3f9bd5c72bdb98 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Feb 2021 04:21:34 -0700 Subject: [PATCH 0637/1220] Remove attribution from multiedit channels (#838) * Remove attribution when multiedit * Whoops --- channelmirror/channelmirror.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index 26aaca92..17651efd 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -33,7 +33,8 @@ def __init__(self, bot, *args, **kwargs): self.config.register_channel(multiedit=False, mirroredit_target=None, nodeletion=False) GACOG = self.bot.get_cog("GlobalAdmin") - if GACOG: GACOG.register_perm("channelmirror") + if GACOG: + GACOG.register_perm("channelmirror") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -249,6 +250,8 @@ async def mirror_msg(self, message): last_spoke_timestamp) if last_spoke_timestamp else now_time attribution_required = last_spoke != author.id attribution_required |= (now_time - last_spoke_time).total_seconds() > ATTRIBUTION_TIME_SECONDS + attribution_required &= not await self.config.channel(message.channel).multiedit() + self.settings.set_last_spoke(channel.id, author.id) attachment_bytes = None From e5ac9371952a102f9b6888b835d98b09477ec92d Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Feb 2021 04:34:40 -0700 Subject: [PATCH 0638/1220] Don't recalculate name token distance when possible (#839) * Very very slightly cut down on time * Remove one more instance --- padinfo/core/find_monster.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 6aefd74e..6bd7d67c 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -166,12 +166,13 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, dgcog, matches def get_valid_monsters_from_name_token(self, token, index2, matches, mult=1): valid_monsters = set() - matched_tokens = sorted((nt for nt in index2.all_name_tokens - if calc_ratio_name(token, nt, index2) > self.TOKEN_JW_DISTANCE), - key=lambda nt: calc_ratio_name(token, nt, index2), reverse=True) + name_ratios = {nt: calc_ratio_name(token, nt, index2) for nt in index2.all_name_tokens} + matched_tokens = sorted((nt for nt in name_ratios + if name_ratios[nt] > self.TOKEN_JW_DISTANCE), + key=lambda nt: name_ratios[nt], reverse=True) matched_tokens += [t for t in index2.all_name_tokens if t.startswith(token)] for match in matched_tokens: - score = calc_ratio_name(token, match, index2) + score = name_ratios[match] for matched_monster in index2.manual[match]: if matched_monster not in valid_monsters: matches[matched_monster].name.add((token, match, '(manual)')) From de5cafd4b606fd06549f6f8c9d640a37eb42a856 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Feb 2021 04:46:51 -0700 Subject: [PATCH 0639/1220] Clarify variable name (#841) --- padinfo/core/find_monster.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 6bd7d67c..2fac0d30 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -166,13 +166,13 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, dgcog, matches def get_valid_monsters_from_name_token(self, token, index2, matches, mult=1): valid_monsters = set() - name_ratios = {nt: calc_ratio_name(token, nt, index2) for nt in index2.all_name_tokens} - matched_tokens = sorted((nt for nt in name_ratios - if name_ratios[nt] > self.TOKEN_JW_DISTANCE), - key=lambda nt: name_ratios[nt], reverse=True) + all_monsters_name_tokens_scores = {nt: calc_ratio_name(token, nt, index2) for nt in index2.all_name_tokens} + matched_tokens = sorted((nt for nt in all_monsters_name_tokens_scores + if all_monsters_name_tokens_scores[nt] > self.TOKEN_JW_DISTANCE), + key=lambda nt: all_monsters_name_tokens_scores[nt], reverse=True) matched_tokens += [t for t in index2.all_name_tokens if t.startswith(token)] for match in matched_tokens: - score = name_ratios[match] + score = all_monsters_name_tokens_scores[match] for matched_monster in index2.manual[match]: if matched_monster not in valid_monsters: matches[matched_monster].name.add((token, match, '(manual)')) From 7da77f52948bf5a8eff545c324f658e14bf28f61 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Feb 2021 05:04:20 -0700 Subject: [PATCH 0640/1220] Change MonsterIndex2 to MonsterIndex (#842) --- dadguide/dadguide.py | 10 +++---- dadguide/monster_index.py | 2 +- padglobal/padglobal.py | 2 +- padinfo/core/find_monster.py | 12 ++++---- padinfo/core/leader_skills.py | 2 +- padinfo/idtest_mixin.py | 8 +++--- padinfo/padinfo.py | 52 +++++++++++++++++------------------ 7 files changed, 44 insertions(+), 44 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index c4ffad29..1f6f4ff3 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -21,7 +21,7 @@ from .database_manager import * from .models.monster_model import MonsterModel from .models.monster_stats import monster_stats, MonsterStatModifierInput -from .monster_index import MonsterIndex2 +from .monster_index import MonsterIndex logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -64,7 +64,7 @@ def __init__(self, bot, *args, **kwargs): self.settings = DadguideSettings("dadguide") self.database = None - self.index2 = None # type: Optional[MonsterIndex2] + self.index = None # type: Optional[MonsterIndex] self.monster_stats = monster_stats self.MonsterStatModifierInput = MonsterStatModifierInput @@ -91,10 +91,10 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return - async def create_index2(self): + async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" await self.wait_until_ready() - return await MonsterIndex2(self.database.get_all_monsters(False), self.database) + self.index = await MonsterIndex(self.database.get_all_monsters(False), self.database) def get_monster(self, monster_id: int) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" @@ -144,7 +144,7 @@ async def download_and_refresh_nicknames(self): logger.info('Loading dg database') self.database = load_database(self.database) logger.info('Building dg monster index') - self.index2 = await MonsterIndex2(self.database.get_all_monsters(False), self.database) + await self.create_index() logger.info('Done refreshing dg data') diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index aa349782..a8b43ff1 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -22,7 +22,7 @@ logger = logging.getLogger('red.pad-cogs.dadguide.monster_index') -class MonsterIndex2(aobject): +class MonsterIndex(aobject): async def __ainit__(self, monsters, db): self.graph = db.graph diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8df82034..f8581672 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -215,7 +215,7 @@ async def forceindexreload3(self, ctx): async with ctx.typing(), self.fir3_lock: start = time.perf_counter() dadguide_cog = self.bot.get_cog('Dadguide') - dadguide_cog.index2 = await dadguide_cog.create_index2() + dadguide_cog.index = await dadguide_cog.create_index() await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) @commands.group(aliases=['pdg']) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 2fac0d30..528e6448 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -149,14 +149,14 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, dgcog, matches matched_mons = None for pos_name in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(pos_name, dgcog.index2, matches) + valid = self.get_valid_monsters_from_name_token(pos_name, dgcog.index, matches) if matched_mons is not None: matched_mons.intersection_update(valid) else: matched_mons = valid for neg_name in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(neg_name, dgcog.index2, matches, mult=-10) + invalid = self.get_valid_monsters_from_name_token(neg_name, dgcog.index, matches, mult=-10) if matched_mons is not None: matched_mons.difference_update(invalid) else: @@ -260,7 +260,7 @@ async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: query = rmdiacritics(query).lower().replace(",", "") tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) return max( await find_monster_search(tokenized_query, dgcog), @@ -285,12 +285,12 @@ def __repr__(self): async def find_monster_search(tokenized_query, dgcog) -> \ Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"]]: mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ - find_monster.interpret_query(tokenized_query, dgcog.index2) + find_monster.interpret_query(tokenized_query, dgcog.index) name_query_tokens.difference_update({'|'}) for mod_token in mod_tokens.union(neg_mod_tokens): - if mod_token not in dgcog.index2.all_modifiers: + if mod_token not in dgcog.index.all_modifiers: settings.add_typo_mod(mod_token) # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) @@ -312,7 +312,7 @@ async def find_monster_search(tokenized_query, dgcog) -> \ # Expand search to the evo tree matched_mons = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, matched_mons, matches, - dgcog.index2.modifiers) + dgcog.index.modifiers) if not matched_mons: # no modifiers match any monster in the evo tree return None, {}, set() diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index e1603950..fecca625 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -83,7 +83,7 @@ async def perform_leaderskill_query(dgcog, raw_query): if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" m = await findMonster3(dgcog, query) - if m and left_query in dgcog.index2.modifiers[m]: + if m and left_query in dgcog.index.modifiers[m]: left_query = query right_query = None diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index ff007eff..3410a7e5 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -402,8 +402,8 @@ async def norf_run(self, ctx): rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' async with ctx.typing(): for i, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) @@ -449,8 +449,8 @@ async def idt_runall(self, ctx): fc = 0 async with ctx.typing(): for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index2.name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6e9eadfc..dd3fcf66 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -302,20 +302,20 @@ async def _do_id(self, ctx, query: str): original_author_id = ctx.message.author.id goodquery = None - if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.index2.all_name_tokens: + if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.index.all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.index2.all_name_tokens: + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.index.all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: bad = False - for monster in dgcog.index2.all_name_tokens[goodquery[1]]: - for modifier in dgcog.index2.modifiers[monster]: + for monster in dgcog.index.all_name_tokens[goodquery[1]]: + for modifier in dgcog.index.modifiers[monster]: if modifier == 'xm' and goodquery[0] == 'x': goodquery[0] = 'xm' if modifier == goodquery[0]: bad = True - if bad and query not in dgcog.index2.all_name_tokens: + if bad and query not in dgcog.index.all_name_tokens: async def send_message(): await asyncio.sleep(1) await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" @@ -903,17 +903,17 @@ async def debugid(self, ctx, *, query): await ctx.send(box("Your query didn't match any monsters.")) return base_monster = dgcog.database.graph.get_base_monster(mon) - mods = dgcog.index2.modifiers[mon] - manual_modifiers = dgcog.index2.manual_prefixes[mon.monster_id] + mods = dgcog.index.modifiers[mon] + manual_modifiers = dgcog.index.manual_prefixes[mon.monster_id] EVOANDTYPE = dgcog.token_maps.EVO_TOKENS.union(dgcog.token_maps.TYPE_TOKENS) ret = (f"[{mon.monster_id}] {mon.name_en}\n" f"Base: [{base_monster.monster_id}] {base_monster.name_en}\n" f"Series: {mon.series.name_en} ({mon.series_id}, {mon.series.series_type})\n\n" - f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.name_tokens.items() if mon in ms))}\n" - f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.index2.fluff_tokens.items() if mon in ms))}\n\n" + f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.index.name_tokens.items() if mon in ms))}\n" + f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.index.fluff_tokens.items() if mon in ms))}\n\n" f"[Manual Tokens]\n" - f" Treenames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_tree.items() if mon in ms))}\n" - f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.index2.manual_nick.items() if mon in ms))}\n\n" + f" Treenames: {' '.join(sorted(t for t, ms in dgcog.index.manual_tree.items() if mon in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.index.manual_nick.items() if mon in ms))}\n\n" f"[Modifier Tokens]\n" f" Attribute: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.COLOR_TOKENS))}\n" f" Awakening: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.AWAKENING_TOKENS))}\n" @@ -933,7 +933,7 @@ async def debugiddist(self, ctx, s1, s2): dgcog = self.bot.get_cog("Dadguide") dist = calc_ratio_modifier(s1, s2) - dist2 = calc_ratio_name(s1, s2, dgcog.index2) + dist2 = calc_ratio_name(s1, s2, dgcog.index) yes = '\N{WHITE HEAVY CHECK MARK}' no = '\N{CROSS MARK}' await ctx.send(f"Printing info for {inline(s1)}, {inline(s2)}\n" + @@ -979,7 +979,7 @@ async def exportmodifiers(self, ctx): atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_MAP.items()] ret += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") stable = [(series[k].name_en, ", ".join(map(inline, v))) - for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items()] + for k, v in DGCOG.index.series_id_to_pantheon_nickname.items()] ret += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in maps.COLOR_MAP.items()] ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in @@ -1014,7 +1014,7 @@ def f(m, s): token_ret = "" so = [] for mon in sorted(token_dict[token], key=lambda m: m.monster_id): - if (mon in DGCOG.index2.mwtoken_creators[token]) == is_multiword: + if (mon in DGCOG.index.mwtoken_creators[token]) == is_multiword: so.append(mon) if len(so) > 5: token_ret += f"\n\n{token_type}\n" + ", ".join(f(m, str(m.monster_id)) for m in so[:10]) @@ -1024,22 +1024,22 @@ def f(m, s): f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) return token_ret - ret += write_name_token(DGCOG.index2.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) - ret += write_name_token(DGCOG.index2.manual, "[Manual Tokens]") - ret += write_name_token(DGCOG.index2.name_tokens, "[Name Tokens]") - ret += write_name_token(DGCOG.index2.fluff_tokens, "[Fluff Tokens]") + ret += write_name_token(DGCOG.index.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) + ret += write_name_token(DGCOG.index.manual, "[Manual Tokens]") + ret += write_name_token(DGCOG.index.name_tokens, "[Name Tokens]") + ret += write_name_token(DGCOG.index.fluff_tokens, "[Fluff Tokens]") - submwtokens = [t for t in DGCOG.index2.multi_word_tokens if token in t] + submwtokens = [t for t in DGCOG.index.multi_word_tokens if token in t] if submwtokens: ret += "\n\n[Multi-word Super-tokens]\n" for t in submwtokens: - if not DGCOG.index2.all_name_tokens[''.join(t)]: + if not DGCOG.index.all_name_tokens[''.join(t)]: continue - creators = sorted(DGCOG.index2.mwtoken_creators["".join(t)], key=lambda m: m.monster_id) + creators = sorted(DGCOG.index.mwtoken_creators["".join(t)], key=lambda m: m.monster_id) ret += f"{' '.join(t).title()}" ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + - str(find_monster.get_most_eligable_monster(DGCOG.index2.all_name_tokens[''.join(t)], + str(find_monster.get_most_eligable_monster(DGCOG.index.all_name_tokens[''.join(t)], DGCOG).monster_id) + ")\n") @@ -1067,7 +1067,7 @@ def additmods(ms, om): '/' + k[1].name.replace("Nil", "None") + additmods(v, token) for k, v in tms.DUAL_COLOR_MAP.items() if token in v], *["Series: " + series[k].name_en + additmods(v, token) - for k, v in DGCOG.index2.series_id_to_pantheon_nickname.items() if token in v], + for k, v in DGCOG.index.series_id_to_pantheon_nickname.items() if token in v], *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", token)], *["Base rarity: " + m for m in re.findall(r"^(\d+)\*b$", token)], @@ -1099,7 +1099,7 @@ async def idtraceback(self, ctx, *, query): query = rmdiacritics(query).strip().lower().replace(",", "") tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) bestmatch, matches, _ = max( await find_monster_search(tokenized_query, dgcog), @@ -1134,7 +1134,7 @@ async def idtraceback(self, ctx, *, query): f" ({round(calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" for t in sorted(mtokens)) ntokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" - f" ({round(calc_ratio_name(t[0], t[1], dgcog.index2), 2) if t[0] != t[1] else 'exact'})" + f" ({round(calc_ratio_name(t[0], t[1], dgcog.index), 2) if t[0] != t[1] else 'exact'})" f" {t[2]}" for t in sorted(ntokens)) @@ -1154,7 +1154,7 @@ async def idsearch(self, ctx, *, query): query = rmdiacritics(query).strip().lower().replace(",", "") tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index2.multi_word_tokens) + mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) best_match, matches, mgen = max( await find_monster_search(tokenized_query, dgcog), From ad6dd26ec22009e65d4a17da55e3e114e07c73f5 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Feb 2021 05:21:50 -0700 Subject: [PATCH 0641/1220] Remove lock in Dadguide cog setup (#843) --- dadguide/dadguide.py | 1 - padglobal/padglobal.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 1f6f4ff3..8c2d05d8 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -93,7 +93,6 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" - await self.wait_until_ready() self.index = await MonsterIndex(self.database.get_all_monsters(False), self.database) def get_monster(self, monster_id: int) -> MonsterModel: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index f8581672..7de79000 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -214,8 +214,9 @@ async def forceindexreload3(self, ctx): async with ctx.typing(), self.fir3_lock: start = time.perf_counter() - dadguide_cog = self.bot.get_cog('Dadguide') - dadguide_cog.index = await dadguide_cog.create_index() + dgcog = self.bot.get_cog('Dadguide') + await dgcog.wait_until_ready() + await dgcog.create_index() await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) @commands.group(aliases=['pdg']) From 9344e1faa8656dcedde9d5b8c58169b706a524c9 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 18 Feb 2021 17:37:21 -0600 Subject: [PATCH 0642/1220] Move child menu logic to MonsterListMenu class (#844) * start refactor * Move most of the child logic to monster id class * Determine if pane type is needed in IdMenu refresh * Fix requests * Bye MenuEmojiConfig * Fix formatting in a random file --- padinfo/menu/closable_embed.py | 6 +- padinfo/menu/common.py | 16 ++++++ padinfo/menu/id.py | 7 +-- padinfo/menu/leader_skill.py | 5 +- padinfo/menu/leader_skill_single.py | 10 +--- padinfo/menu/monster_list.py | 65 ++++++++++++---------- padinfo/menu/simple_text.py | 9 +-- padinfo/menu/transforminfo.py | 7 +-- padinfo/padinfo.py | 85 ++++++++++++----------------- padinfo/view/simple_text.py | 7 +++ 10 files changed, 102 insertions(+), 115 deletions(-) diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index df6fb82d..7409eaf9 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -1,13 +1,11 @@ from discordmenu.embed.control import EmbedControl -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu from padinfo.menu.common import MenuPanes -from padinfo.view.id_traceback import IdTracebackView from padinfo.view.closable_embed import ClosableEmbedViewState +from padinfo.view.id_traceback import IdTracebackView emoji_button_names = ['\N{CROSS MARK}'] -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') view_types = { IdTracebackView.VIEW_TYPE: IdTracebackView @@ -21,7 +19,7 @@ class ClosableEmbedMenu: @staticmethod def menu(): - embed = EmbedMenu({}, ClosableEmbedMenu.message_control, menu_emoji_config) + embed = EmbedMenu({}, ClosableEmbedMenu.message_control) return embed @staticmethod diff --git a/padinfo/menu/common.py b/padinfo/menu/common.py index 6ab83862..3d3d096e 100644 --- a/padinfo/menu/common.py +++ b/padinfo/menu/common.py @@ -40,3 +40,19 @@ def emoji_name_to_function(cls, name: str): if data_pair[1] == name: return data_pair[1] return None + + @classmethod + def respond_to_emoji_with_parent(cls, emoji: str): + """Only defined for menus that support having children""" + for _, data_pair in cls.DATA.items(): + if data_pair[0] == emoji: + return data_pair[2] + return None + + @classmethod + def respond_to_emoji_with_child(cls, emoji: str): + """Only defined for menus that support having children""" + for _, data_pair in cls.DATA.items(): + if data_pair[0] == emoji: + return data_pair[3] + return None diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 46985423..dccf46e7 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING, Optional from discord import Message -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji @@ -19,8 +18,6 @@ from dadguide.models.monster_model import MonsterModel from dadguide.database_context import DbContext -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') - class IdMenu: MENU_TYPE = 'IdMenu' @@ -30,7 +27,7 @@ def menu(initial_control=None): if initial_control is None: initial_control = IdMenu.id_control - embed = EmbedMenu(IdMenuPanes.transitions(), initial_control, menu_emoji_config, + embed = EmbedMenu(IdMenuPanes.transitions(), initial_control, delete_func=IdMenu.respond_with_delete) return embed @@ -96,7 +93,7 @@ def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_ev async def respond_with_refresh(message: Optional[Message], ims, **data): # This is used by disambig screen & other multi-message embeds, where we need to deserialize & then # re-serialize the ims, with the same information in place - pane_type = ims.get('pane_type') + pane_type = ims.get('pane_type') or IdView.VIEW_TYPE pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] return await response_func(message, ims, **data) diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 07a8787c..5143f815 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -1,7 +1,6 @@ from typing import Optional from discord import Message -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji @@ -10,15 +9,13 @@ from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillViewState -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') - class LeaderSkillMenu: MENU_TYPE = 'LeaderSkill' @staticmethod def menu(): - return EmbedMenu(LeaderSkillMenuPanes.transitions(), LeaderSkillMenu.ls_control, menu_emoji_config) + return EmbedMenu(LeaderSkillMenuPanes.transitions(), LeaderSkillMenu.ls_control) @staticmethod async def respond_to_r(message: Optional[Message], ims, **data): diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index 322b6a27..17729260 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -1,7 +1,6 @@ -from typing import TYPE_CHECKING, Optional +from typing import Optional from discord import Message -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache @@ -9,11 +8,6 @@ from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill_single import LeaderSkillSingleView, LeaderSkillSingleViewState -if TYPE_CHECKING: - pass - -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') - class LeaderSkillSingleMenu: MENU_TYPE = 'LeaderSkillSingle' @@ -21,7 +15,7 @@ class LeaderSkillSingleMenu: @staticmethod def menu(): return EmbedMenu(LeaderSkillSingleMenuPanes.transitions(), - LeaderSkillSingleMenu.ls_control, menu_emoji_config) + LeaderSkillSingleMenu.ls_control) @staticmethod async def respond_with_ls(message: Optional[Message], ims, *, dgcog, user_config, **data): diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 1869179a..bbc8fa70 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -1,17 +1,14 @@ from typing import Optional from discord import Message -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji from padinfo.menu.common import emoji_buttons, MenuPanes -from padinfo.menu.id import IdMenu, IdMenuPanes +from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuPaneNames from padinfo.view.id import IdView from padinfo.view.monster_list import MonsterListView, MonsterListViewState -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') - class MonsterListPaneNames: home = 'home' @@ -27,7 +24,7 @@ class MonsterListMenu: def menu(initial_control=None): if initial_control is None: initial_control = MonsterListMenu.monster_list_control - embed = EmbedMenu(MonsterListMenuPanes.transitions(), initial_control, menu_emoji_config) + embed = EmbedMenu(MonsterListMenuPanes.transitions(), initial_control) return embed @staticmethod @@ -51,17 +48,8 @@ async def respond_with_monster_list(message: Optional[Message], ims, **data): return control @staticmethod - async def respond_with_n(message: Optional[Message], ims, n, **data): - ims['resolved_monster_id'] = int(ims['monster_list'][n]) - reaction_list = IdMenuPanes.emoji_names() - if not data.get('child_message_ims') or data['child_message_ims']['menu_type'] != IdMenu.MENU_TYPE: - # default to the overview screen if we weren't already on a screen - ims['reaction_list'] = reaction_list - ims['is_child'] = True - return await IdMenu.respond_with_current_id(message, ims, **data) - data['child_message_ims']['reaction_list'] = reaction_list - data['child_message_ims']['resolved_monster_id'] = int(ims['resolved_monster_id']) - return await IdMenu.respond_with_refresh(message, data['child_message_ims'], **data) + async def respond_with_n(message: Optional[Message], ims, _n, **data): + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod async def respond_with_0(message: Optional[Message], ims, **data): @@ -117,25 +105,42 @@ def monster_list_control(state: MonsterListViewState): reaction_list ) + @staticmethod + def get_child_data(ims, emoji_clicked): + emoji_response = IdMenuPanes.emoji_name_to_emoji(IdMenuPaneNames.refresh) \ + if MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None + if emoji_response is None: + return None, {} + n = MonsterListMenuPanes.emoji_names().index(emoji_clicked) + extra_ims = { + 'is_child': True, + 'resolved_monster_id': int(ims['monster_list'][n]), + 'reaction_list': IdMenuPanes.emoji_names(), + 'menu_type': IdMenu.MENU_TYPE, + } + return emoji_response, extra_ims + class MonsterListMenuPanes(MenuPanes): INITIAL_EMOJI = '\N{HOUSE BUILDING}' DATA = { - MonsterListMenu.respond_with_monster_list: (emoji_buttons[MonsterListPaneNames.home], MonsterListPaneNames.home), - MonsterListMenu.respond_with_0: (char_to_emoji('0'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_1: (char_to_emoji('1'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_2: (char_to_emoji('2'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_3: (char_to_emoji('3'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_4: (char_to_emoji('4'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_5: (char_to_emoji('5'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_6: (char_to_emoji('6'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_7: (char_to_emoji('7'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_8: (char_to_emoji('8'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_9: (char_to_emoji('9'), IdView.VIEW_TYPE), - MonsterListMenu.respond_with_10: ('\N{KEYCAP TEN}', IdView.VIEW_TYPE), + # tuple parts: emoji, pane_type, respond_with_parent, respond_with_child + MonsterListMenu.respond_with_monster_list: ( + emoji_buttons[MonsterListPaneNames.home], MonsterListPaneNames.home, True, False), + MonsterListMenu.respond_with_0: (char_to_emoji('0'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_1: (char_to_emoji('1'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_2: (char_to_emoji('2'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_3: (char_to_emoji('3'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_4: (char_to_emoji('4'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_5: (char_to_emoji('5'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_6: (char_to_emoji('6'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_7: (char_to_emoji('7'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_8: (char_to_emoji('8'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_9: (char_to_emoji('9'), IdView.VIEW_TYPE, False, True), + MonsterListMenu.respond_with_10: (char_to_emoji('10'), IdView.VIEW_TYPE, False, True), MonsterListMenu.respond_with_refresh: ( - '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', MonsterListPaneNames.refresh), - MonsterListMenu.respond_with_reset: (emoji_buttons['reset'], MonsterListPaneNames.reset) + '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', MonsterListPaneNames.refresh, True, False), + MonsterListMenu.respond_with_reset: (emoji_buttons['reset'], MonsterListPaneNames.reset, True, False) } HIDDEN_EMOJIS = [ MonsterListPaneNames.home, diff --git a/padinfo/menu/simple_text.py b/padinfo/menu/simple_text.py index aca73d46..ae681b90 100644 --- a/padinfo/menu/simple_text.py +++ b/padinfo/menu/simple_text.py @@ -1,18 +1,12 @@ -from typing import TYPE_CHECKING, Optional +from typing import Optional from discord import Message from discordmenu.embed.control import EmbedControl -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu from padinfo.menu.common import MenuPanes from padinfo.view.simple_text import SimpleTextView, SimpleTextViewState -if TYPE_CHECKING: - pass - -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') - class SimpleTextNames: home = 'home' @@ -25,7 +19,6 @@ class SimpleTextMenu: @staticmethod def menu(): embed = EmbedMenu(SimpleTextMenuPanes.transitions(), SimpleTextMenu.message_control, - menu_emoji_config, delete_func=SimpleTextMenu.respond_with_delete) return embed diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index fa85ebcf..a893a039 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -1,25 +1,20 @@ from typing import Optional from discord import Message -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from padinfo.menu.closable_embed import emoji_button_names from padinfo.menu.common import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.transforminfo import TransformInfoView, TransformInfoViewState -menu_emoji_config = EmbedMenuEmojiConfig(delete_message='\N{CROSS MARK}') - class TransformInfoMenu: MENU_TYPE = 'TransformInfo' @staticmethod def menu(): - return EmbedMenu(TransformInfoMenuPanes.transitions(), TransformInfoMenu.tf_control, - menu_emoji_config) + return EmbedMenu(TransformInfoMenuPanes.transitions(), TransformInfoMenu.tf_control) @staticmethod async def respond_with_base(message: Optional[Message], ims, **data): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index dd3fcf66..c3ce6d6a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -182,37 +182,23 @@ async def test_reaction_add(self, reaction, member): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] menu_map = { - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu.menu, - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu.menu, - IdMenu.MENU_TYPE: IdMenu.menu, - TransformInfoMenu.MENU_TYPE: TransformInfoMenu.menu, - MonsterListMenu.MENU_TYPE: MonsterListMenu.menu, - SimpleTextMenu.MENU_TYPE: SimpleTextMenu.menu, - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu.menu, + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu, + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu, + IdMenu.MENU_TYPE: IdMenu, + TransformInfoMenu.MENU_TYPE: TransformInfoMenu, + MonsterListMenu.MENU_TYPE: MonsterListMenu, + SimpleTextMenu.MENU_TYPE: SimpleTextMenu, + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, } - # If true then the top menu will also respond on reaction - respond_with_child = { - (MonsterListMenu.MENU_TYPE, char_to_emoji(0)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(1)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(2)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(3)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(4)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(5)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(6)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(7)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(8)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(9)): False, - (MonsterListMenu.MENU_TYPE, char_to_emoji(10)): False, - } - - menu_func = menu_map.get(menu_type) + menu_1_class = menu_map.get(menu_type) + menu_func = menu_1_class.menu if not menu_func: return - embed_menu = menu_func() - if not (await embed_menu.should_respond( + menu = menu_func() + if not (await menu.should_respond( message, reaction, await self.get_reaction_filters( original_author_id, menu_to_emoji_list_map[menu_type]), member)): return @@ -223,31 +209,30 @@ async def test_reaction_add(self, reaction, member): 'dgcog': dgcog, 'user_config': user_config } - if ims.get('child_message_id') and (ims['menu_type'], emoji_clicked) in respond_with_child.keys(): - try: - await message.remove_reaction(emoji_clicked, member) - except discord.Forbidden: - pass - fctx = await self.bot.get_context(message) - child_message = await fctx.fetch_message(int(ims['child_message_id'])) - previous_child_ims = child_message.embeds and IntraMessageState.extract_data(child_message.embeds[0]) - if previous_child_ims: - data['child_message_ims'] = previous_child_ims - next_child_ims = ims.copy() - next_child_ims['menu_type'] = IdMenu.MENU_TYPE - next_child_ims['is_child'] = True - - # The order here is really important!! The set of emojis attached to the ims is going to be changed in - # the second transition, so it's VITAL that we reset prior to showing the child menu. - # It's also better from a perceived performance standpoint becuase the emojis are so rate-limited and - # the reset wouldn't happen until all emojis showed up in the child, so this way it feels like everything - # happens faster, but regardless the reset must happen first. - if respond_with_child[(ims['menu_type'], emoji_clicked)]: - await embed_menu.transition(message, ims, emoji_clicked, member, **data) - await embed_menu.transition(child_message, next_child_ims, emoji_clicked, member, **data) - return - - await embed_menu.transition(message, ims, emoji_clicked, member, **data) + menu_1_ims = ims + message_1 = message + failsafe = 0 + while menu_1_ims.get('child_message_id'): + if failsafe == 10: + break + failsafe += 1 + menu_2 = IdMenu.menu() + menu_2_ims = None + message_2 = None + emoji_simulated_clicked_2, extra_ims = menu_1_class.get_child_data(menu_1_ims, emoji_clicked) + if emoji_simulated_clicked_2: + fctx = await self.bot.get_context(message_1) + try: + message_2 = await fctx.fetch_message(int(menu_1_ims['child_message_id'])) + menu_2_ims = message_2.embeds and IntraMessageState.extract_data(message_2.embeds[0]) + menu_2_ims.update(extra_ims) + await menu_2.transition(message_2, menu_2_ims, emoji_simulated_clicked_2, member, **data) + except discord.errors.NotFound: + break + menu_1_ims = menu_2_ims + message_1 = message_2 + + await menu.transition(message, ims, emoji_clicked, member, **data) async def get_reaction_filters(self, original_author_id, valid_emoji_names): friend_cog = self.bot.get_cog("Friend") diff --git a/padinfo/view/simple_text.py b/padinfo/view/simple_text.py index cee08bd7..7eca5196 100644 --- a/padinfo/view/simple_text.py +++ b/padinfo/view/simple_text.py @@ -16,6 +16,13 @@ def __init__(self, original_author_id, menu_type, raw_query, self.message = message self.color = color + def serialize(self): + ret = super().serialize() + ret.update({ + 'message': self.message, + }) + return ret + @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] From 5adda9061d0e8d7b5abbec63bc74e0ebcd7b6dfe Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 18 Feb 2021 18:51:16 -0600 Subject: [PATCH 0643/1220] Clean up & add TODO about full child-menu support (#845) --- padinfo/menu/monster_list.py | 2 +- padinfo/padinfo.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index bbc8fa70..be060636 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -126,7 +126,7 @@ class MonsterListMenuPanes(MenuPanes): DATA = { # tuple parts: emoji, pane_type, respond_with_parent, respond_with_child MonsterListMenu.respond_with_monster_list: ( - emoji_buttons[MonsterListPaneNames.home], MonsterListPaneNames.home, True, False), + emoji_buttons[MonsterListPaneNames.home], MonsterListPaneNames.home, True, False), MonsterListMenu.respond_with_0: (char_to_emoji('0'), IdView.VIEW_TYPE, False, True), MonsterListMenu.respond_with_1: (char_to_emoji('1'), IdView.VIEW_TYPE, False, True), MonsterListMenu.respond_with_2: (char_to_emoji('2'), IdView.VIEW_TYPE, False, True), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c3ce6d6a..03a04230 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -213,12 +213,13 @@ async def test_reaction_add(self, reaction, member): message_1 = message failsafe = 0 while menu_1_ims.get('child_message_id'): + # before this loop can actually work as a loop, the type of menu_2 can't be hard-coded as IdMenu anymore, + # and we have to update menu_1_class to be menu_2_class during the loop. + # TODO: Add support for triply-linked menus or children other than IdMenus if failsafe == 10: break failsafe += 1 menu_2 = IdMenu.menu() - menu_2_ims = None - message_2 = None emoji_simulated_clicked_2, extra_ims = menu_1_class.get_child_data(menu_1_ims, emoji_clicked) if emoji_simulated_clicked_2: fctx = await self.bot.get_context(message_1) From 47242945a31ebd0537f05ac48b7598bdaa4ee904 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 19 Feb 2021 01:04:43 -0700 Subject: [PATCH 0644/1220] Make numerical name tokens require exact matching (#850) --- padinfo/core/find_monster.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 528e6448..efd015de 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -31,8 +31,11 @@ def calc_ratio_name(token, full_word, index2, factor=.05): mw = index2.mwt_to_len[full_word] != 1 jw = jaro_winkler(token, full_word, factor) + if token.isdigit() and full_word.isdigit() and token != full_word: + return 0.0 + if full_word == token: - score = 1 + score = 1.0 elif len(token) >= 3 and full_word.startswith(token): score = .995 if mw and jw < score: From 812362d6258022b641660014e0187cbaa033fc82 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 19 Feb 2021 01:38:16 -0700 Subject: [PATCH 0645/1220] Remove case-insensitivity aliases from Id (#851) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 03a04230..07607617 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -257,7 +257,7 @@ async def jpname(self, ctx, *, query: str): else: await self.send_id_failure_message(ctx, query) - @commands.command(name="id", aliases=["iD", "Id", "ID"]) + @commands.command(name="id") @checks.bot_has_permissions(embed_links=True) async def _id(self, ctx, *, query: str): """Monster info (main tab)""" From 493cc9c5782e5fb068062c2c20dc8d7770f2b185 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 19 Feb 2021 02:12:39 -0700 Subject: [PATCH 0646/1220] Add PEP-8 variable name compliance (#852) * Remove aliases * Add PEP-8 variable name compliance --- profile/profile.py | 74 +++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/profile/profile.py b/profile/profile.py index 424b46e0..a9281ecb 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -12,7 +12,7 @@ SUPPORTED_SERVERS = ["NA", "KR", "JP", "EU"] -def validateAndCleanId(id): +def validate_and_clean_id(id): id = id.replace('-', '').replace(' ', '').replace(',', '').replace('.', '').strip() if re.match(r'^\d{9}$', id): return id @@ -20,16 +20,16 @@ def validateAndCleanId(id): return None -def formatNameLine(server, pad_name, pad_id): - group = computeOldGroup(pad_id) - return "[{}]: '{}' : {} (Group {})".format(server, pad_name, formatId(pad_id), group) +def format_name_line(server, pad_name, pad_id): + group = compute_old_group(pad_id) + return "[{}]: '{}' : {} (Group {})".format(server, pad_name, format_id(pad_id), group) -def formatId(id): +def format_id(id): return id[0:3] + "," + id[3:6] + "," + id[6:9] -def computeOldGroup(str_id): +def compute_old_group(str_id): old_id_digit = str(str_id)[2] return chr(ord('A') + (int(old_id_digit) % 5)) @@ -64,25 +64,25 @@ async def red_delete_data_for_user(self, *, requester, user_id): else: self.settings.clearUserDataFull(user_id) - @commands.command(name="idme") - async def idMe(self, ctx, server=None): + @commands.command() + async def idme(self, ctx, server=None): """Displays your profile to the current channel If you do not provide a server, your default is used """ - profile_msg = await self.getIdMsg(ctx, ctx.author, server, False) + profile_msg = await self.get_id_msg(ctx, ctx.author, server, False) if profile_msg is None: return await ctx.send(profile_msg) - @commands.command(name="idto") - async def idTo(self, ctx, user: discord.Member, server=None): + @commands.command() + async def idto(self, ctx, user: discord.Member, server=None): """Whispers your profile to specified user If you do not provide a server, your default is used """ - profile_msg = await self.getIdMsg(ctx, ctx.author, server) + profile_msg = await self.get_id_msg(ctx, ctx.author, server) if profile_msg is None: return @@ -92,19 +92,19 @@ async def idTo(self, ctx, user: discord.Member, server=None): await ctx.send(user, msg) await ctx.author.send(inline("Sent your profile to " + user.name)) - @commands.command(name="idfor") - async def idFor(self, ctx, user: discord.Member, server=None): + @commands.command() + async def idfor(self, ctx, user: discord.Member, server=None): """Displays the profile of the specified user If you do not provide a server, your default is used. """ - profile_msg = await self.getIdMsg(ctx, user, server) + profile_msg = await self.get_id_msg(ctx, user, server) if profile_msg is None: return await ctx.author.send(profile_msg) - async def getServer(self, ctx, user_id, server=None): + async def get_server(self, ctx, user_id, server=None): if server is None: server = self.settings.getDefaultServer(user_id) server = normalize_server_name(server) @@ -113,8 +113,8 @@ async def getServer(self, ctx, user_id, server=None): return return server - async def getIdMsg(self, ctx, user, server=None, for_other=True): - server = await self.getServer(ctx, user.id, server) + async def get_id_msg(self, ctx, user, server=None, for_other=True): + server = await self.get_server(ctx, user.id, server) if server is None: return @@ -123,7 +123,7 @@ async def getIdMsg(self, ctx, user, server=None, for_other=True): profile_text = self.settings.getProfileText(user.id, server) line1 = "Info for " + user.name - line2 = formatNameLine(server, pad_name, pad_id) + line2 = format_name_line(server, pad_name, pad_id) line3 = profile_text msg = inline(line1) + "\n" + box(line2 + "\n" + line3) @@ -133,8 +133,8 @@ async def getIdMsg(self, ctx, user, server=None, for_other=True): async def profile(self, ctx): """Manage profile storage""" - @profile.command(name="server") - async def setServer(self, ctx, server): + @profile.command() + async def server(self, ctx, server): """Set your default server to one of [NA, EU, JP, KR] This server is used to default the idme command if you don't provide a server. @@ -148,28 +148,28 @@ async def setServer(self, ctx, server): await ctx.send(inline('Set your default server to: ' + server)) @profile.command(name="id") - async def setId(self, ctx, server, *id): + async def _id(self, ctx, server, *id): """Sets your ID for a server ID must be 9 digits, can be space/comma/dash delimited. """ - server = await self.getServer(ctx, ctx.author.id, server) + server = await self.get_server(ctx, ctx.author.id, server) if server is None: return None id = " ".join(id) - clean_id = validateAndCleanId(id) + clean_id = validate_and_clean_id(id) if clean_id is None: await ctx.send(inline('Your ID looks invalid, expected a 9 digit code, got: {}'.format(id))) return self.settings.setId(ctx.author.id, server, clean_id) - await ctx.send(inline('Set your id for {} to: {}'.format(server, formatId(clean_id)))) + await ctx.send(inline('Set your id for {} to: {}'.format(server, format_id(clean_id)))) - @profile.command(name="name") - async def setName(self, ctx, server, *name): + @profile.command() + async def name(self, ctx, server, *name): """Sets your in game name for a server""" - server = await self.getServer(ctx, ctx, server) + server = await self.get_server(ctx, ctx, server) if server is None: return None @@ -177,13 +177,13 @@ async def setName(self, ctx, server, *name): self.settings.setName(ctx.author.id, server, name) await ctx.send(inline('Set your name for {} to: {}'.format(server, name))) - @profile.command(name="text") - async def setText(self, ctx, server, *text): + @profile.command() + async def text(self, ctx, server, *text): """Sets your profile text for the server. This info is used by the idme command and search. """ - server = await self.getServer(ctx, ctx.author.id, server) + server = await self.get_server(ctx, ctx.author.id, server) if server is None: return None @@ -196,7 +196,7 @@ async def setText(self, ctx, server, *text): self.settings.setProfileText(ctx.author.id, server, text) await ctx.send(inline('Set your profile for ' + server + ' to:\n' + text)) - @profile.command(name="clear") + @profile.command() async def clear(self, ctx, server=None): """Deletes your saved profile for a server @@ -211,13 +211,13 @@ async def clear(self, ctx, server=None): self.settings.clearProfile(user_id, server) await ctx.send(inline('Cleared your profile for ' + server)) - @profile.command(name="search") + @profile.command() async def search(self, ctx, server, *search_text): """profile search Scans all profiles for the search text and PMs the results. """ - server = await self.getServer(ctx, ctx.author.id, server) + server = await self.get_server(ctx, ctx.author.id, server) if server is None: return None @@ -245,7 +245,7 @@ async def search(self, ctx, server, *search_text): msg = 'Displaying {} matches for server {}:\n'.format(len(matching_profiles), server) for p in matching_profiles: - pad_id = formatId(p['id']) + pad_id = format_id(p['id']) pad_name = p.get('name', 'unknown') profile_text = p['text'].replace('`', '') @@ -253,9 +253,9 @@ async def search(self, ctx, server, *search_text): line2 = profile_text msg = msg + line1 + "\n" + line2 + "\n\n" - await self.pageOutput(ctx, msg) + await self.page_output(ctx, msg) - async def pageOutput(self, ctx, msg): + async def page_output(self, ctx, msg): msg = msg.strip() msg = pagify(msg, ["\n"], shorten_by=20) for page in msg: From 57284f32e207ffb10925134a6b8c65e9b5a37a11 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 19 Feb 2021 10:03:41 -0700 Subject: [PATCH 0647/1220] Refactor find_monster (#854) * Refactor find_monster * Fix find_monster_debug signature --- padinfo/core/find_monster.py | 141 +++++++++++++++++++--------------- padinfo/core/leader_skills.py | 8 +- padinfo/core/transforminfo.py | 4 +- padinfo/idtest_mixin.py | 6 +- padinfo/padinfo.py | 72 +++++++---------- padinfo/view/common.py | 4 +- 6 files changed, 114 insertions(+), 121 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index efd015de..41494054 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -49,11 +49,24 @@ def calc_ratio_name(token, full_word, index2, factor=.05): return score +class MonsterMatch: + def __init__(self, score=0, name=None, mod=None): + self.score = score + if name is None: + self.name = set() + if mod is None: + self.mod = set() + + def __repr__(self): + return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) + + class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 - def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): + @staticmethod + def merge_multi_word_tokens(tokens, valid_multi_word_tokens): result = [] skip = 0 multi_word_tokens_sorted = sorted(valid_multi_word_tokens, @@ -68,7 +81,7 @@ def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): continue for c2, token2 in enumerate(mwt): if (tokens[c1 + c2] != token2 and len(token2) < 5) \ - or calc_ratio_modifier(tokens[c1 + c2], token2) < self.TOKEN_JW_DISTANCE: + or calc_ratio_modifier(tokens[c1 + c2], token2) < FindMonster.TOKEN_JW_DISTANCE: break else: skip = len(mwt) - 1 @@ -78,7 +91,8 @@ def merge_multi_word_tokens(self, tokens, valid_multi_word_tokens): result.append(token1) return result - def _monster_has_modifier(self, monster, token, matches, monster_mods): + @staticmethod + def _monster_has_modifier(monster, token, matches, monster_mods): if len(token) < 6: if token in monster_mods: matches[monster].mod.add((token, token)) @@ -87,13 +101,14 @@ def _monster_has_modifier(self, monster, token, matches, monster_mods): else: closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) ratio = calc_ratio_modifier(closest, token) - if ratio > self.MODIFIER_JW_DISTANCE: + if ratio > FindMonster.MODIFIER_JW_DISTANCE: matches[monster].mod.add((token, closest)) matches[monster].score += ratio return True return False - def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: + @staticmethod + def interpret_query(tokenized_query: List[str], index2) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: modifiers = [] negative_modifiers = set() name = set() @@ -104,7 +119,7 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if any(calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in index2.suffixes): + if any(calc_ratio_modifier(m, token, .1) > FindMonster.MODIFIER_JW_DISTANCE for m in index2.suffixes): if negated: negative_modifiers.add(token) else: @@ -118,7 +133,7 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], negated = token.startswith("-") token = token.lstrip('-') if token in index2.all_modifiers or ( - any(calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) + any(calc_ratio_modifier(m, token, .1) > FindMonster.MODIFIER_JW_DISTANCE for m in longmods) and token not in index2.all_name_tokens and len(token) >= 8): if negated: @@ -148,18 +163,19 @@ def interpret_query(self, tokenized_query: List[str], index2) -> Tuple[Set[str], return set(modifiers), negative_modifiers, name, negative_name - def process_name_tokens(self, name_query_tokens, neg_name_tokens, dgcog, matches): + @staticmethod + def process_name_tokens(name_query_tokens, neg_name_tokens, dgcog, matches): matched_mons = None for pos_name in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(pos_name, dgcog.index, matches) + valid = FindMonster.get_valid_monsters_from_name_token(pos_name, dgcog.index, matches) if matched_mons is not None: matched_mons.intersection_update(valid) else: matched_mons = valid for neg_name in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(neg_name, dgcog.index, matches, mult=-10) + invalid = FindMonster.get_valid_monsters_from_name_token(neg_name, dgcog.index, matches, mult=-10) if matched_mons is not None: matched_mons.difference_update(invalid) else: @@ -167,11 +183,12 @@ def process_name_tokens(self, name_query_tokens, neg_name_tokens, dgcog, matches return matched_mons - def get_valid_monsters_from_name_token(self, token, index2, matches, mult=1): + @staticmethod + def get_valid_monsters_from_name_token(token, index2, matches, mult=1): valid_monsters = set() all_monsters_name_tokens_scores = {nt: calc_ratio_name(token, nt, index2) for nt in index2.all_name_tokens} matched_tokens = sorted((nt for nt in all_monsters_name_tokens_scores - if all_monsters_name_tokens_scores[nt] > self.TOKEN_JW_DISTANCE), + if all_monsters_name_tokens_scores[nt] > FindMonster.TOKEN_JW_DISTANCE), key=lambda nt: all_monsters_name_tokens_scores[nt], reverse=True) matched_tokens += [t for t in index2.all_name_tokens if t.startswith(token)] for match in matched_tokens: @@ -194,26 +211,28 @@ def get_valid_monsters_from_name_token(self, token, index2, matches, mult=1): return valid_monsters - def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches, monster_mods): + @staticmethod + def process_modifiers(mod_tokens, neg_mod_tokens, potential_evos, matches, monster_mods): for pos_mod_token in mod_tokens: potential_evos = {m for m in potential_evos if - self._monster_has_modifier(m, pos_mod_token, matches, monster_mods[m])} + FindMonster._monster_has_modifier(m, pos_mod_token, matches, monster_mods[m])} if not potential_evos: return None for neg_mod_token in neg_mod_tokens: potential_evos = {m for m in potential_evos if - not self._monster_has_modifier(m, neg_mod_token, matches, monster_mods[m])} + not FindMonster._monster_has_modifier(m, neg_mod_token, matches, monster_mods[m])} if not potential_evos: return None return potential_evos - def get_priority_tuple(self, monster, dgcog, tokenized_query=None, matches=None): + @staticmethod + def get_priority_tuple(monster, dgcog, tokenized_query=None, matches=None): if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] - + return (matches[monster].score, not monster.is_equip, # Match na on id overlap @@ -227,15 +246,17 @@ def get_priority_tuple(self, monster, dgcog, tokenized_query=None, matches=None) monster.rarity, monster.monster_no_na) - def get_most_eligable_monster(self, monsters, dgcog, tokenized_query=None, matches=None): + @staticmethod + def get_most_eligable_monster(monsters, dgcog, tokenized_query=None, matches=None): if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] - return max(monsters, key=lambda m: self.get_priority_tuple(m, dgcog, tokenized_query, matches)) + return max(monsters, key=lambda m: FindMonster.get_priority_tuple(m, dgcog, tokenized_query, matches)) - def get_monster_evos(self, database, matched_mons, matches): + @staticmethod + def get_monster_evos(database, matched_mons, matches): monster_evos = set() for monster in sorted(matched_mons, key=lambda m: matches[m].score, reverse=True): for evo in database.graph.get_alt_monsters(monster): @@ -248,47 +269,10 @@ def get_monster_evos(self, database, matched_mons, matches): return monster_evos -async def findMonster3(dgcog, query) -> Optional["MonsterModel"]: - matched_monster = await _findMonster3(dgcog, query) - - monster_no = matched_monster.monster_id if matched_monster else -1 - historic_lookups_id3[query] = monster_no - json.dump(historic_lookups_id3, open(historic_lookups_file_path_id3, "w+")) - - return matched_monster - - -async def _findMonster3(dgcog, query) -> Optional["MonsterModel"]: - await dgcog.wait_until_ready() - - query = rmdiacritics(query).lower().replace(",", "") - tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) - - return max( - await find_monster_search(tokenized_query, dgcog), - await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, {}), - key=lambda t: t[1].get(t[0], MonsterMatch()).score - )[0] - - -class MonsterMatch: - def __init__(self, score=0, name=None, mod=None): - self.score = score - if name is None: - self.name = set() - if mod is None: - self.mod = set() - - def __repr__(self): - return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) - - async def find_monster_search(tokenized_query, dgcog) -> \ Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"]]: mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ - find_monster.interpret_query(tokenized_query, dgcog.index) + FindMonster.interpret_query(tokenized_query, dgcog.index) name_query_tokens.difference_update({'|'}) @@ -300,22 +284,22 @@ async def find_monster_search(tokenized_query, dgcog) -> \ matches = defaultdict(MonsterMatch) if name_query_tokens or neg_name_tokens: - matched_mons = find_monster.process_name_tokens(name_query_tokens, + matched_mons = FindMonster.process_name_tokens(name_query_tokens, neg_name_tokens, dgcog, matches) if not matched_mons: # No monsters match the given name tokens return None, {}, set() - matched_mons = find_monster.get_monster_evos(dgcog.database, matched_mons, matches) + matched_mons = FindMonster.get_monster_evos(dgcog.database, matched_mons, matches) else: # There are no name tokens in the query matched_mons = {*dgcog.database.get_all_monsters()} monster_score = defaultdict(int) # Expand search to the evo tree - matched_mons = find_monster.process_modifiers(mod_tokens, neg_mod_tokens, matched_mons, matches, - dgcog.index.modifiers) + matched_mons = FindMonster.process_modifiers(mod_tokens, neg_mod_tokens, matched_mons, matches, + dgcog.index.modifiers) if not matched_mons: # no modifiers match any monster in the evo tree return None, {}, set() @@ -324,9 +308,38 @@ async def find_monster_search(tokenized_query, dgcog) -> \ # if k in matched_mons}) # Return most likely candidate based on query. - mon = find_monster.get_most_eligable_monster(matched_mons, dgcog, tokenized_query, matches) + mon = FindMonster.get_most_eligable_monster(matched_mons, dgcog, tokenized_query, matches) return mon, matches, matched_mons -find_monster = FindMonster() +async def find_monster_debug(dgcog, query) -> \ + Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"], int]: + await dgcog.wait_until_ready() + + query = rmdiacritics(query).lower().replace(",", "") + tokenized_query = query.split() + mw_tokenized_query = FindMonster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) + + best_monster, matches_dict, valid_monsters = max( + await find_monster_search(tokenized_query, dgcog), + await find_monster_search(mw_tokenized_query, dgcog) + if tokenized_query != mw_tokenized_query else (None, {}, set()), + + key=lambda t: t[1].get(t[0], MonsterMatch()).score + ) + + historic_lookups_id3[query] = best_monster.monster_id if best_monster else -1 + json.dump(historic_lookups_id3, open(historic_lookups_file_path_id3, "w+")) + + return best_monster, matches_dict, valid_monsters, 0 + + +async def find_monster(dgcog, query) -> Optional["MonsterModel"]: + matched_monster, _, _, _ = await find_monster_debug(dgcog, query) + return matched_monster + + +async def find_monsters(dgcog, query) -> List["MonsterModel"]: + _, matches, monsters, _ = await find_monster_debug(dgcog, query) + return sorted(monsters, key=lambda m: FindMonster.get_priority_tuple(m)) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index fecca625..07797b61 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,6 +1,6 @@ import re -from padinfo.core.find_monster import findMonster3 +from padinfo.core.find_monster import find_monster def humanize_number(number, sigfigs=2): @@ -82,7 +82,7 @@ async def perform_leaderskill_query(dgcog, raw_query): right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" - m = await findMonster3(dgcog, query) + m = await find_monster(dgcog, query) if m and left_query in dgcog.index.modifiers[m]: left_query = query right_query = None @@ -91,9 +91,9 @@ async def perform_leaderskill_query(dgcog, raw_query): else: # no separators left_query, right_query = query, None - left_m = await findMonster3(dgcog, left_query) + left_m = await find_monster(dgcog, left_query) if right_query: - right_m = await findMonster3(dgcog, right_query) + right_m = await find_monster(dgcog, right_query) else: right_m = left_m return left_m, left_query, right_m, right_query diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index bb9b1203..24389778 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -1,10 +1,10 @@ -from padinfo.core.find_monster import findMonster3 +from padinfo.core.find_monster import find_monster async def perform_transforminfo_query(dgcog, raw_query): db_context = dgcog.database mgraph = dgcog.database.graph - found_monster = await findMonster3(dgcog, raw_query) + found_monster = await find_monster(dgcog, raw_query) if not found_monster: return None, None diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index 3410a7e5..22f7c7a0 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -5,7 +5,7 @@ from redbot.core import commands, Config, checks from redbot.core.utils.chat_formatting import box, pagify -from padinfo.core.find_monster import findMonster3 +from padinfo.core.find_monster import find_monster class IdTest: @@ -365,7 +365,7 @@ async def idt_run(self, ctx): for i, qr in enumerate(sorted(suite.items())): q, r = qr try: - m = await findMonster3(dgcog, q) or -1 + m = await find_monster(dgcog, q) or -1 except Exception: m = -2 mid = getattr(m, "monster_id", m) @@ -431,7 +431,7 @@ async def idt_runall(self, ctx): async with ctx.typing(): for c, q in enumerate(sorted(qsuite)): try: - m = await findMonster3(dgcog, q) or -1 + m = await find_monster(dgcog, q) or -1 except Exception: m = -2 mid = getattr(m, "monster_id", m) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 07607617..7e6d8f5c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -18,15 +18,14 @@ from redbot.core import checks, commands, data_manager, Config from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate -from tsutils import char_to_emoji, is_donor, rmdiacritics +from tsutils import char_to_emoji, is_donor from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ get_attribute_emoji_by_monster from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info -from padinfo.core.find_monster import find_monster, findMonster3, \ - calc_ratio_name, calc_ratio_modifier, find_monster_search, findMonster3 +from padinfo.core.find_monster import FindMonster, calc_ratio_name, calc_ratio_modifier, find_monster, find_monster_debug from padinfo.core.historic_lookups import historic_lookups from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings @@ -102,7 +101,7 @@ def __init__(self, bot, *args, **kwargs): self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False, test_suite={}, fluff_suite=[]) - self.fm3 = lambda q: fm.findMonster3(bot.get_cog("Dadguide"), q) + self.fm3 = lambda q: fm.find_monster(bot.get_cog("Dadguide"), q) self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster self.settings = settings @@ -250,7 +249,7 @@ async def get_reaction_filters(self, original_author_id, valid_emoji_names): async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" dgcog = await self.get_dgcog() - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) if monster is not None: await ctx.send(MonsterHeader.short(monster)) await ctx.send(box(monster.name_ja)) @@ -313,7 +312,7 @@ async def send_message(): async with self.config.bad_queries() as bad_queries: bad_queries.append((raw_query, ctx.author.id)) - monster = await findMonster3(dgcog, raw_query) + monster = await find_monster(dgcog, raw_query) if not monster: await self.send_id_failure_message(ctx, query) @@ -404,7 +403,7 @@ async def evos(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await findMonster3(dgcog, raw_query) + monster = await find_monster(dgcog, raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -435,7 +434,7 @@ async def evomats(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await findMonster3(dgcog, raw_query) + monster = await find_monster(dgcog, raw_query) if not monster: await self.send_id_failure_message(ctx, query) @@ -467,7 +466,7 @@ async def pantheon(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await findMonster3(dgcog, raw_query) + monster = await find_monster(dgcog, raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -497,7 +496,7 @@ async def pic(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await findMonster3(dgcog, raw_query) + monster = await find_monster(dgcog, raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -522,7 +521,7 @@ async def otherinfo(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await findMonster3(dgcog, raw_query) + monster = await find_monster(dgcog, raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -543,7 +542,7 @@ async def otherinfo(self, ctx, *, query: str): async def links(self, ctx, *, query: str): """Monster links""" dgcog = await self.get_dgcog() - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -556,7 +555,7 @@ async def links(self, ctx, *, query: str): async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" dgcog = await self.get_dgcog() - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -569,7 +568,7 @@ async def lookup(self, ctx, *, query: str): async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" dgcog = await self.get_dgcog() - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -583,7 +582,7 @@ async def buttoninfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): dgcog = await self.get_dgcog() - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -684,7 +683,7 @@ async def get_user_friends(self, original_author_id): @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): dgcog = await self.get_dgcog() - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) if not monster: await self.send_id_failure_message(ctx, query) return @@ -742,7 +741,7 @@ async def padsay(self, ctx, server, *, query: str = None): query = query.strip().lower() dgcog = await self.get_dgcog() - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) if monster is not None: voice_id = monster.voice_id_jp if server == 'jp' else monster.voice_id_na if voice_id is None: @@ -884,7 +883,7 @@ async def send_id_failure_message(ctx, query: str): async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" dgcog = self.bot.get_cog("Dadguide") - mon = await findMonster3(dgcog, query) + mon = await find_monster(dgcog, query) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) return @@ -925,10 +924,10 @@ async def debugiddist(self, ctx, s1, s2): await ctx.send(f"Printing info for {inline(s1)}, {inline(s2)}\n" + box(f"Jaro-Winkler Distance: {round(dist, 4)}\n" f"Name Matching Distance: {round(dist2, 4)}\n" - f"Modifier token threshold: {find_monster.MODIFIER_JW_DISTANCE} " - f"{yes if dist >= find_monster.MODIFIER_JW_DISTANCE else no}\n" - f"Name token threshold: {find_monster.TOKEN_JW_DISTANCE} " - f"{yes if dist2 >= find_monster.TOKEN_JW_DISTANCE else no}")) + f"Modifier token threshold: {FindMonster.MODIFIER_JW_DISTANCE} " + f"{yes if dist >= FindMonster.MODIFIER_JW_DISTANCE else no}\n" + f"Name token threshold: {FindMonster.TOKEN_JW_DISTANCE} " + f"{yes if dist2 >= FindMonster.TOKEN_JW_DISTANCE else no}")) @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query=""): @@ -1025,8 +1024,8 @@ def f(m, s): ret += f"{' '.join(t).title()}" ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + - str(find_monster.get_most_eligable_monster(DGCOG.index.all_name_tokens[''.join(t)], - DGCOG).monster_id) + str(FindMonster.get_most_eligable_monster(DGCOG.index.all_name_tokens[''.join(t)], + DGCOG).monster_id) + ")\n") def additmods(ms, om): @@ -1081,18 +1080,8 @@ async def idtraceback(self, ctx, *, query): selected_monster_id = int(selected_monster_id.strip()) dgcog = self.bot.get_cog("Dadguide") - await dgcog.wait_until_ready() - - query = rmdiacritics(query).strip().lower().replace(",", "") - tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) - bestmatch, matches, _ = max( - await find_monster_search(tokenized_query, dgcog), - await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, {}), - key=lambda t: t[1][t[0]].score if t[0] else 0 - ) + bestmatch, matches, _, _ = await find_monster_debug(dgcog, query) if bestmatch is None: await ctx.send("No monster matched.") @@ -1136,25 +1125,16 @@ async def idtraceback(self, ctx, *, query): @commands.command(aliases=["ids"]) async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") - await dgcog.wait_until_ready() - query = rmdiacritics(query).strip().lower().replace(",", "") - tokenized_query = query.split() - mw_tokenized_query = find_monster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) + best_match, matches, mgen, _ = await find_monster_debug(dgcog, query) - best_match, matches, mgen = max( - await find_monster_search(tokenized_query, dgcog), - await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, {}), - key=lambda t: t[1][t[0]].score if t[0] else 0 - ) if not best_match: await ctx.send("No monster matched.") return used = set() monster_list = [] - for mon in sorted(mgen, key=lambda m: find_monster.get_priority_tuple(m, dgcog, matches=matches), reverse=True): + for mon in sorted(mgen, key=lambda m: FindMonster.get_priority_tuple(m, dgcog, matches=matches), reverse=True): base_id = dgcog.database.graph.get_base_id(mon) if base_id not in used: used.add(base_id) diff --git a/padinfo/view/common.py b/padinfo/view/common.py index c4ec83cd..058957dc 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -1,4 +1,4 @@ -from padinfo.core.find_monster import findMonster3 +from padinfo.core.find_monster import find_monster async def get_monster_from_ims(dgcog, ims: dict): @@ -8,5 +8,5 @@ async def get_monster_from_ims(dgcog, ims: dict): resolved_monster_id = int(resolved_monster_id_str) if resolved_monster_id_str else None if resolved_monster_id: return dgcog.database.graph.get_monster(resolved_monster_id) - monster = await findMonster3(dgcog, query) + monster = await find_monster(dgcog, query) return monster From 18083df26636697a7866fd7c9a1c38b84ae7e62a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 19 Feb 2021 10:07:39 -0700 Subject: [PATCH 0648/1220] Use find_monsters in idsearch (#855) --- padinfo/core/find_monster.py | 2 +- padinfo/padinfo.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index 41494054..c9456f48 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -342,4 +342,4 @@ async def find_monster(dgcog, query) -> Optional["MonsterModel"]: async def find_monsters(dgcog, query) -> List["MonsterModel"]: _, matches, monsters, _ = await find_monster_debug(dgcog, query) - return sorted(monsters, key=lambda m: FindMonster.get_priority_tuple(m)) + return sorted(monsters, key=lambda m: FindMonster.get_priority_tuple(m, dgcog, matches=matches), reverse=True) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7e6d8f5c..f18b878a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -25,7 +25,8 @@ get_attribute_emoji_by_monster from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info -from padinfo.core.find_monster import FindMonster, calc_ratio_name, calc_ratio_modifier, find_monster, find_monster_debug +from padinfo.core.find_monster import FindMonster, calc_ratio_name, calc_ratio_modifier, find_monster, \ + find_monster_debug, find_monsters from padinfo.core.historic_lookups import historic_lookups from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings @@ -1126,15 +1127,15 @@ async def idtraceback(self, ctx, *, query): async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") - best_match, matches, mgen, _ = await find_monster_debug(dgcog, query) + matched_monsters = await find_monsters(dgcog, query) - if not best_match: + if not matched_monsters: await ctx.send("No monster matched.") return used = set() monster_list = [] - for mon in sorted(mgen, key=lambda m: FindMonster.get_priority_tuple(m, dgcog, matches=matches), reverse=True): + for mon in matched_monsters: base_id = dgcog.database.graph.get_base_id(mon) if base_id not in used: used.add(base_id) From c21810f6e15e83bc95394e9d5411f69ef945ee96 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 19 Feb 2021 23:21:07 -0700 Subject: [PATCH 0649/1220] Remove print statements in find_monster_search (#856) --- padinfo/core/find_monster.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index c9456f48..d7e65c2c 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -280,8 +280,6 @@ async def find_monster_search(tokenized_query, dgcog) -> \ if mod_token not in dgcog.index.all_modifiers: settings.add_typo_mod(mod_token) - # print(mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens) - matches = defaultdict(MonsterMatch) if name_query_tokens or neg_name_tokens: matched_mons = FindMonster.process_name_tokens(name_query_tokens, @@ -304,9 +302,6 @@ async def find_monster_search(tokenized_query, dgcog) -> \ # no modifiers match any monster in the evo tree return None, {}, set() - # print({k: v for k, v in sorted(matches.items(), key=lambda kv: kv[1].score, reverse=True) - # if k in matched_mons}) - # Return most likely candidate based on query. mon = FindMonster.get_most_eligable_monster(matched_mons, dgcog, tokenized_query, matches) From 3cf3a9a48d6ecdeda86f2f34649926e1ebfa1d48 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 20 Feb 2021 00:21:45 -0600 Subject: [PATCH 0650/1220] Rewrite collab/seriesscroll with menu2 (#853) * initial commit in which i did basically everything * oops didnt commit everything * Fix imports/formatting * comment print statement * Change to vertical scroll by entire page * Skip rarities without any cards * Add bolded indicator of current rarity in a field * Change emojis & comment print statement --- padinfo/menu/closable_embed.py | 5 +- padinfo/menu/common.py | 4 - padinfo/menu/monster_list.py | 4 + padinfo/menu/series_scroll.py | 205 +++++++++++++++++++++++++++++++++ padinfo/padinfo.py | 79 +++++++++++-- padinfo/view/series_scroll.py | 149 ++++++++++++++++++++++++ 6 files changed, 430 insertions(+), 16 deletions(-) create mode 100644 padinfo/menu/series_scroll.py create mode 100644 padinfo/view/series_scroll.py diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index 7409eaf9..ebc8efab 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -5,8 +5,6 @@ from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.id_traceback import IdTracebackView -emoji_button_names = ['\N{CROSS MARK}'] - view_types = { IdTracebackView.VIEW_TYPE: IdTracebackView } @@ -14,7 +12,6 @@ class ClosableEmbedMenu: MENU_TYPE = 'ClosableEmbedMenu' - EMOJI_BUTTON_NAMES = emoji_button_names message = None @staticmethod @@ -27,7 +24,7 @@ def message_control(state: ClosableEmbedViewState): view = view_types[state.view_type] return EmbedControl( [view.embed(state, state.props)], - ClosableEmbedMenu.EMOJI_BUTTON_NAMES + [] ) diff --git a/padinfo/menu/common.py b/padinfo/menu/common.py index 3d3d096e..e57e513e 100644 --- a/padinfo/menu/common.py +++ b/padinfo/menu/common.py @@ -23,10 +23,6 @@ def transitions(cls): def pane_types(cls): return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} - @classmethod - def get_initial_reaction_list(cls, number_of_evos: int): - return cls.emoji_names()[:number_of_evos] - @classmethod def emoji_name_to_emoji(cls, name: str): for _, data_pair in cls.DATA.items(): diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index be060636..f9a07c69 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -147,3 +147,7 @@ class MonsterListMenuPanes(MenuPanes): MonsterListPaneNames.refresh, MonsterListPaneNames.reset, ] + + @classmethod + def get_initial_reaction_list(cls, number_of_evos: int): + return cls.emoji_names()[:number_of_evos] diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py new file mode 100644 index 00000000..6fc4065f --- /dev/null +++ b/padinfo/menu/series_scroll.py @@ -0,0 +1,205 @@ +from typing import Optional, List + +from discord import Message +from discordmenu.embed.menu import EmbedMenu, EmbedControl +from tsutils import char_to_emoji + +from padinfo.menu.common import emoji_buttons, MenuPanes +from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuPaneNames +from padinfo.view.id import IdView +from padinfo.view.series_scroll import SeriesScrollView, SeriesScrollViewState + + +class SeriesScrollPaneNames: + home = 'home' + refresh = 'refresh' + reset = 'reset' + + +def _get_min_index(ims): + return ims['current_min_index'].get(str(ims['rarity'])) or 0 + + +def _set_min_index(ims, val): + ims['current_min_index'][str(ims['rarity'])] = val + + +class SeriesScrollMenu: + MENU_TYPE = 'SeriesScrollMenu' + CHILD_MENU_TYPE = 'IdMenu' + SCROLL_INTERVAL = SeriesScrollViewState.MAX_ITEMS_PER_PANE + RARITY_INITIAL_TRY_ORDER = [6, 7, 8, 9, 10, 5, 4, 3, 11, 12, 2, 1, 13, 14] + SCROLL_INITIAL_POSITION = 0 + + @staticmethod + def menu(initial_control=None): + if initial_control is None: + initial_control = SeriesScrollMenu.monster_list_control + embed = EmbedMenu(SeriesScrollMenuPanes.transitions(), initial_control) + return embed + + @staticmethod + async def respond_with_refresh(message: Optional[Message], ims, **data): + # this is only called once on message load + if data.get('child_message_id'): + ims['child_message_id'] = data['child_message_id'] + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_reset(message: Optional[Message], ims, **data): + # replace with the overview list after the child menu changes + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_left(message: Optional[Message], ims, **data): + current_rarity_index = ims['all_rarities'].index(ims['rarity']) + ims['rarity'] = ims['all_rarities'][current_rarity_index - 1] + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_right(message: Optional[Message], ims, **data): + current_rarity_index = ims['all_rarities'].index(ims['rarity']) + if current_rarity_index == len(ims['all_rarities']) - 1: + ims['rarity'] = ims['all_rarities'][0] + else: + ims['rarity'] = ims['all_rarities'][current_rarity_index + 1] + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_up(message: Optional[Message], ims, **data): + _set_min_index(ims, max(_get_min_index(ims) - SeriesScrollMenu.SCROLL_INTERVAL, 0)) + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_down(message: Optional[Message], ims, **data): + attempted_new_index = _get_min_index(ims) + SeriesScrollMenu.SCROLL_INTERVAL + if attempted_new_index < len(ims['full_monster_list']): + _set_min_index(ims, attempted_new_index) + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_monster_list(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + view_state = await SeriesScrollViewState.deserialize(dgcog, user_config, ims) + control = SeriesScrollMenu.monster_list_control(view_state) + return control + + @staticmethod + async def respond_with_delete(message: Message, ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_n(message: Optional[Message], ims, _n, **data): + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_0(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 0, **data) + + @staticmethod + async def respond_with_1(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 1, **data) + + @staticmethod + async def respond_with_2(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 2, **data) + + @staticmethod + async def respond_with_3(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 3, **data) + + @staticmethod + async def respond_with_4(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 4, **data) + + @staticmethod + async def respond_with_5(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 5, **data) + + @staticmethod + async def respond_with_6(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 6, **data) + + @staticmethod + async def respond_with_7(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 7, **data) + + @staticmethod + async def respond_with_8(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 8, **data) + + @staticmethod + async def respond_with_9(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 9, **data) + + @staticmethod + async def respond_with_10(message: Optional[Message], ims, **data): + return await SeriesScrollMenu.respond_with_n(message, ims, 10, **data) + + @staticmethod + def monster_list_control(state: SeriesScrollViewState): + return EmbedControl( + [SeriesScrollView.embed(state)], + SeriesScrollMenuPanes.get_initial_reaction_list(state.max_len_so_far) + ) + + @staticmethod + def get_child_data(ims, emoji_clicked): + emoji_response = IdMenuPanes.emoji_name_to_emoji(IdMenuPaneNames.refresh) \ + if SeriesScrollMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None + if emoji_response is None: + return None, {} + n = SeriesScrollMenuPanes.emoji_names().index(emoji_clicked) + extra_ims = { + 'is_child': True, + 'resolved_monster_id': int( + ims['full_monster_list'][n + _get_min_index(ims) - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT]), + 'reaction_list': IdMenuPanes.emoji_names(), + 'menu_type': IdMenu.MENU_TYPE, + } + return emoji_response, extra_ims + + +class SeriesScrollMenuPanes(MenuPanes): + INITIAL_EMOJI = '\N{HOUSE BUILDING}' + NON_MONSTER_EMOJI_COUNT = 5 + DATA = { + # tuple parts: emoji, pane_type, respond_with_parent, respond_with_child + SeriesScrollMenu.respond_with_delete: ('\N{CROSS MARK}', None, True, False), + SeriesScrollMenu.respond_with_monster_list: ( + emoji_buttons[SeriesScrollPaneNames.home], SeriesScrollPaneNames.home, True, False), + SeriesScrollMenu.respond_with_left: ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), + SeriesScrollMenu.respond_with_right: ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), + SeriesScrollMenu.respond_with_up: ( + '\N{BLACK UP-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), + SeriesScrollMenu.respond_with_down: ( + '\N{BLACK DOWN-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), + SeriesScrollMenu.respond_with_0: (char_to_emoji('0'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_1: (char_to_emoji('1'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_2: (char_to_emoji('2'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_3: (char_to_emoji('3'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_4: (char_to_emoji('4'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_5: (char_to_emoji('5'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_6: (char_to_emoji('6'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_7: (char_to_emoji('7'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_8: (char_to_emoji('8'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_9: (char_to_emoji('9'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_10: (char_to_emoji('10'), IdView.VIEW_TYPE, False, True), + SeriesScrollMenu.respond_with_refresh: ( + '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', SeriesScrollPaneNames.refresh, True, False), + SeriesScrollMenu.respond_with_reset: (emoji_buttons['reset'], SeriesScrollPaneNames.reset, True, False) + } + HIDDEN_EMOJIS = [ + SeriesScrollPaneNames.home, + SeriesScrollPaneNames.refresh, + SeriesScrollPaneNames.reset, + ] + + @classmethod + def get_initial_reaction_list(cls, number_of_evos: int): + return cls.emoji_names()[:number_of_evos + cls.NON_MONSTER_EMOJI_COUNT] + + @classmethod + def get_previous_reaction_list_num_monsters(cls, reaction_list: List): + return len(reaction_list) - cls.NON_MONSTER_EMOJI_COUNT diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f18b878a..3e265bbf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -37,6 +37,7 @@ from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes +from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list @@ -54,11 +55,13 @@ from padinfo.view.otherinfo import OtherInfoViewState from padinfo.view.pantheon import PantheonViewState from padinfo.view.pic import PicViewState +from padinfo.view.series_scroll import SeriesScrollViewState from padinfo.view.simple_text import SimpleTextViewState from padinfo.view.transforminfo import TransformInfoViewState if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel + from dadguide.models.series_model import SeriesModel logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -163,6 +166,7 @@ async def test_reaction_add(self, reaction, member): LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes.emoji_names(), LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenuPanes.emoji_names(), MonsterListMenu.MENU_TYPE: MonsterListMenuPanes.emoji_names(), + SeriesScrollMenu.MENU_TYPE: SeriesScrollMenuPanes.emoji_names(), SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes.emoji_names(), TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes.emoji_names(), } @@ -182,25 +186,26 @@ async def test_reaction_add(self, reaction, member): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] menu_map = { + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, + IdMenu.MENU_TYPE: IdMenu, LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu, LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu, - IdMenu.MENU_TYPE: IdMenu, - TransformInfoMenu.MENU_TYPE: TransformInfoMenu, MonsterListMenu.MENU_TYPE: MonsterListMenu, + SeriesScrollMenu.MENU_TYPE: SeriesScrollMenu, SimpleTextMenu.MENU_TYPE: SimpleTextMenu, - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, + TransformInfoMenu.MENU_TYPE: TransformInfoMenu, } menu_1_class = menu_map.get(menu_type) - menu_func = menu_1_class.menu - - if not menu_func: + if not menu_1_class: return + menu_func = menu_1_class.menu + menu = menu_func() if not (await menu.should_respond( - message, reaction, await self.get_reaction_filters( - original_author_id, menu_to_emoji_list_map[menu_type]), member)): + message, reaction, await self.get_reaction_filters( + original_author_id, menu_to_emoji_list_map[menu_type]), member)): return dgcog = await self.get_dgcog() @@ -632,6 +637,63 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM # The user could delete the menu before we can do this pass + @commands.command(aliases=['seriesscroll', 'ss']) + @checks.bot_has_permissions(embed_links=True) + async def collabscroll(self, ctx, *, query): + dgcog = await self.get_dgcog() + monster: "MonsterModel" = await find_monster(dgcog, query) + + if monster is None: + await self.send_id_failure_message(ctx, query) + return + series_id = monster.series_id + series_object: "SeriesModel" = monster.series + title = series_object.name_en + monster_list = None + rarity = None + for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: + monster_list = SeriesScrollViewState.query(dgcog, monster.series_id, rarity) + if monster_list: + break + all_rarities = SeriesScrollViewState.query_all_rarities(dgcog, series_id) + + raw_query = query + original_author_id = ctx.message.author.id + color = await self.get_user_embed_color(ctx) + initial_reaction_list = SeriesScrollMenuPanes.get_initial_reaction_list(len(monster_list)) + initial_min_indices = {rarity: SeriesScrollMenu.SCROLL_INITIAL_POSITION} + instruction_message = 'Click a reaction to see monster details!' + + state = SeriesScrollViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, query, color, + series_id, initial_min_indices, monster_list, monster_list, int(rarity), + all_rarities, + title, instruction_message, + reaction_list=initial_reaction_list) + parent_menu = SeriesScrollMenu.menu() + message = await ctx.send('Setting up!') + + ims = state.serialize() + user_config = await BotConfig.get_user(self.config, ctx.author.id) + data = { + 'dgcog': dgcog, + 'user_config': user_config, + } + child_state = SimpleTextViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, color, + instruction_message, + reaction_list=[] + ) + child_menu = SimpleTextMenu.menu() + child_message = await child_menu.create(ctx, child_state) + + data['child_message_id'] = child_message.id + try: + await parent_menu.transition(message, ims, SeriesScrollMenuPanes.emoji_name_to_emoji('refresh'), ctx.author, + **data) + await message.edit(content=None) + except discord.errors.NotFound: + # The user could delete the menu before we can do this + pass + @commands.command(aliases=['leaders', 'leaderskills', 'ls'], usage=" [card_2]") @checks.bot_has_permissions(embed_links=True) async def leaderskill(self, ctx, *, raw_query): @@ -1124,6 +1186,7 @@ async def idtraceback(self, ctx, *, query): await menu.create(ctx, state) @commands.command(aliases=["ids"]) + @checks.bot_has_permissions(embed_links=True) async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py new file mode 100644 index 00000000..4f146c3a --- /dev/null +++ b/padinfo/view/series_scroll.py @@ -0,0 +1,149 @@ +from typing import TYPE_CHECKING, List + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView +from tsutils import char_to_emoji + +from padinfo.common.config import UserConfig +from padinfo.view.components.base import pad_info_footer_with_state +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.view_state_base import ViewStateBase + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + from dadguide.database_context import DbContext + + +class SeriesScrollViewState(ViewStateBase): + MAX_ITEMS_PER_PANE = 11 + + def __init__(self, original_author_id, menu_type, raw_query, query, color, series_id, current_min_index, + monster_list: List["MonsterModel"], full_monster_list: List["MonsterModel"], rarity: int, + all_rarities: List[int], + title, message, + max_len_so_far: int = None, + reaction_list=None, extra_state=None, + child_message_id=None): + super().__init__(original_author_id, menu_type, raw_query, + extra_state=extra_state) + self.all_rarities = all_rarities + self.current_min_index = current_min_index + self.series_id = series_id + self.rarity = rarity + self.message = message + self.child_message_id = child_message_id + self.title = title + self.monster_list = monster_list + self.full_monster_list = full_monster_list + self.reaction_list = reaction_list + self.color = color + self.query = query + self.max_len_so_far = max(max_len_so_far or len(monster_list), len(self.monster_list)) + + def serialize(self): + ret = super().serialize() + ret.update({ + 'pane_type': SeriesScrollView.VIEW_TYPE, + 'series_id': self.series_id, + 'current_min_index': self.current_min_index, + 'title': self.title, + 'full_monster_list': [str(m.monster_no) for m in self.full_monster_list], + 'rarity': self.rarity, + 'all_rarities': self.all_rarities, + 'reaction_list': self.reaction_list, + 'child_message_id': self.child_message_id, + 'message': self.message, + 'max_len_so_far': self.max_len_so_far + }) + return ret + + @staticmethod + async def deserialize(dgcog, user_config: UserConfig, ims: dict): + # print(ims) + if ims.get('unsupported_transition'): + return None + series_id = ims['series_id'] + rarity = ims['rarity'] + all_rarities = ims['all_rarities'] + full_monster_list = SeriesScrollViewState.query(dgcog, series_id, rarity) + current_min_index = ims.get('current_min_index') or {str(rarity): 0} + current_min_index_num: int = current_min_index.get(str(rarity)) or 0 + current_max_index_num: int = current_min_index_num + SeriesScrollViewState.MAX_ITEMS_PER_PANE + monster_list = full_monster_list[current_min_index_num:current_max_index_num] + title = ims['title'] + + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + reaction_list = ims.get('reaction_list') + child_message_id = ims.get('child_message_id') + message = ims.get('message') + max_len_so_far = max(ims['max_len_so_far'] or len(monster_list), len(monster_list)) + + return SeriesScrollViewState(original_author_id, menu_type, raw_query, query, user_config.color, series_id, + current_min_index, monster_list, full_monster_list, rarity, + all_rarities, + title, message, + max_len_so_far=max_len_so_far, + reaction_list=reaction_list, + extra_state=ims, + child_message_id=child_message_id) + + @staticmethod + def query(dgcog, series_id, rarity): + db_context: "DbContext" = dgcog.database + all_series_monsters = db_context.get_monsters_by_series(series_id) + base_monsters_of_rarity = list(filter( + lambda m: db_context.graph.monster_is_base(m) and m.rarity == rarity, all_series_monsters)) + return base_monsters_of_rarity + + @staticmethod + def query_all_rarities(dgcog, series_id): + db_context: "DbContext" = dgcog.database + return sorted({m.rarity for m in db_context.get_all_monsters() if + m.series_id == series_id and db_context.graph.monster_is_base(m)}) + + +class SeriesScrollView: + VIEW_TYPE = 'SeriesScroll' + + @staticmethod + def embed(state: SeriesScrollViewState): + fields = [ + EmbedField(SeriesScrollView._rarity_text(state.rarity), + Box(*SeriesScrollView._monster_list( + state.monster_list)) if state.monster_list else Box( + 'No monsters of this rarity to display')), + EmbedField('**All rarities**', + Box(SeriesScrollView._all_rarity_text(state.all_rarities, state.rarity)) + ), + ] + + return EmbedView( + EmbedMain( + title=state.title, + color=state.color, + ), + embed_footer=pad_info_footer_with_state(state), + embed_fields=fields) + + @staticmethod + def _rarity_text(this_rarity): + return 'Current rarity: {}'.format( + str(this_rarity) + ) + + @staticmethod + def _all_rarity_text(all_rarities, this_rarity): + return ', '.join([str(r) if r != this_rarity else '**{}**'.format(str(r)) for r in all_rarities]) + + @staticmethod + def _monster_list(monsters): + if not len(monsters): + return [] + return [ + MonsterHeader.short_with_emoji(mon, link=True, prefix=char_to_emoji(i)) + for i, mon in enumerate(monsters) + ] From 6c8f13db07e68b0b2647f844468a67750fe58d21 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 20 Feb 2021 20:24:39 -0700 Subject: [PATCH 0651/1220] Add modifier of "new" for the newest card per tree (#858) --- dadguide/monster_graph.py | 4 ++++ dadguide/monster_index.py | 4 ++++ dadguide/token_mappings.py | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 4da36810..6a99bfc6 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -563,6 +563,10 @@ def monster_is_rem_evo_by_id(self, monster_id): def monster_is_rem_evo(self, monster: MonsterModel): return self.monster_is_rem_evo_by_id(monster.monster_no) + + def monster_is_new(self, monster: MonsterModel): + latest_time = max(am.reg_date for am in self.get_alt_monsters(monster)) + return monster.reg_date == latest_time def monster_acquisition(self, monster: MonsterModel): acquire_text = None diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a8b43ff1..bcef00e6 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -380,6 +380,10 @@ def is_story(m, do_transform=True): if is_story(m): modifiers.update(MISC_MAP[MiscModifiers.STORY]) + # New + if self.graph.monster_is_new(m): + modifiers.update(MISC_MAP[MiscModifiers.NEW]) + # Method of Obtaining if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): modifiers.update(MISC_MAP[MiscModifiers.FARMABLE]) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 3ed5af6d..4e2a8ee6 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -249,6 +249,7 @@ class MiscModifiers(Enum): EVENT = 'Metaseries: Event' SEASONAL = 'Metaseries: Seasonal' COLLAB = 'Metaseries: Collab' + NEW = 'Newest monster in series' MISC_MAP = { @@ -264,7 +265,8 @@ class MiscModifiers(Enum): MiscModifiers.REGULAR: ('regular',), MiscModifiers.EVENT: ('event',), MiscModifiers.SEASONAL: ('seasonal',), - MiscModifiers.COLLAB: ('collab',) + MiscModifiers.COLLAB: ('collab',), + MiscModifiers.NEW: ('new',) } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { From 72a4bdd360dc5e1ccd21579d5d344155e59c0678 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 20 Feb 2021 20:53:15 -0700 Subject: [PATCH 0652/1220] Improve processing of cards with JA names (#857) --- dadguide/monster_index.py | 16 +++++++++++----- dadguide/token_mappings.py | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index bcef00e6..b9234d21 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -7,7 +7,7 @@ import aiohttp from redbot.core.utils import AsyncIter -from tsutils import aobject +import tsutils from .token_mappings import * @@ -22,7 +22,7 @@ logger = logging.getLogger('red.pad-cogs.dadguide.monster_index') -class MonsterIndex(aobject): +class MonsterIndex(tsutils.aobject): async def __ainit__(self, monsters, db): self.graph = db.graph @@ -156,6 +156,8 @@ async def _build_monster_index(self, monsters): # Propagate name tokens throughout all evos for me in alt_monsters: + if tsutils.contains_ja(me.name_en): + continue if last_token != me.name_en.split(',')[-1].strip(): autotoken = False for t in self.monster_id_to_nametokens[me.monster_id]: @@ -175,8 +177,8 @@ async def _build_monster_index(self, monsters): treenames.add(match.group(1)) # Add important tokens - for t in self.monster_id_to_nametokens[m.monster_id]: - self.add_name_token(self.name_tokens, t, m) + for token in self.monster_id_to_nametokens[m.monster_id]: + self.add_name_token(self.name_tokens, token, m) if m.monster_id in self.treename_overrides: pass elif autotoken: @@ -208,7 +210,8 @@ async def _build_monster_index(self, monsters): for mevo in alt_monsters: if not mevo.is_equip: for token2 in self._get_important_tokens(mevo.name_en, treenames): - self.add_name_token(self.name_tokens, token2, m) + if token2 not in HAZARDOUS_IN_NAME_PREFIXES: + self.add_name_token(self.name_tokens, token2, m) # Fluff tokens for token in nametokens: @@ -253,6 +256,9 @@ def _get_important_tokens(cls, oname, treenames=None): if treenames is None: treenames = set() + if tsutils.contains_ja(oname): + return list(treenames) + name = oname.split(", ") if len(name) == 1: return cls._name_to_tokens(oname) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 4e2a8ee6..6ae7ce24 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -309,7 +309,8 @@ class MiscModifiers(Enum): } HAZARDOUS_IN_NAME_PREFIXES = { - "reincarnated" + "reincarnated", + "awoken", } ID1_SUPPORTED = {'hw', 'h', 'x', 'ny', 'gh', 'v', 'np', 'ma', 'a', 'r', 'rr', 'rg', 'rb', 'rl', 'rd', 'rx', 'b', 'br', From 22d9be4fe73e0361fe5593d0392ac79ce7986c29 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 20 Feb 2021 22:46:28 -0700 Subject: [PATCH 0653/1220] Add problematic series token set to fix sonia etc (#860) * Add hazardous series tokens * Change prefix -> mod * add Problematic Series Token set --- dadguide/monster_index.py | 19 +++++++++++-------- dadguide/token_mappings.py | 12 +++++++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index b9234d21..ade3e592 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -6,8 +6,8 @@ from collections import defaultdict import aiohttp -from redbot.core.utils import AsyncIter import tsutils +from redbot.core.utils import AsyncIter from .token_mappings import * @@ -22,6 +22,7 @@ logger = logging.getLogger('red.pad-cogs.dadguide.monster_index') + class MonsterIndex(tsutils.aobject): async def __ainit__(self, monsters, db): self.graph = db.graph @@ -29,9 +30,10 @@ async def __ainit__(self, monsters, db): self.monster_id_to_nickname = defaultdict(set) self.monster_id_to_nametokens = defaultdict(set) self.monster_id_to_treename = defaultdict(set) - self.series_id_to_pantheon_nickname = defaultdict(set, {m.series_id: {m.series.name_en.lower().replace(" ", "")} - for m - in db.get_all_monsters()}) + self.series_id_to_pantheon_nickname = \ + defaultdict(set, {m.series_id: {m.series.name_en.lower().replace(" ", "")} + for m in db.get_all_monsters() + if m.series.name_en.lower() not in PROBLEMATIC_SERIES_TOKENS}) self.mwtoken_creators = defaultdict(set) @@ -113,7 +115,6 @@ async def __ainit__(self, monsters, db): for emid in self.graph.get_alt_ids_by_id(mid): self.manual_prefixes[emid].update(aliases) - self._known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() for x in xs}.union(KNOWN_MODIFIERS) @@ -210,7 +211,7 @@ async def _build_monster_index(self, monsters): for mevo in alt_monsters: if not mevo.is_equip: for token2 in self._get_important_tokens(mevo.name_en, treenames): - if token2 not in HAZARDOUS_IN_NAME_PREFIXES: + if token2 not in HAZARDOUS_IN_NAME_MODS: self.add_name_token(self.name_tokens, token2, m) # Fluff tokens @@ -234,13 +235,13 @@ def add_name_token(self, token_dict, token, m, depth=5): return token_dict[token.lower()].add(m) - if token.lower() in self._known_mods and token.lower() not in HAZARDOUS_IN_NAME_PREFIXES: + if token.lower() in self._known_mods and token.lower() not in HAZARDOUS_IN_NAME_MODS: self.modifiers[m].add(token.lower()) # Replacements for ts in (k for k in self.replacement_tokens if token.lower() in k and all(m in token_dict[t] for t in k)): for t in self.replacement_tokens[ts]: - self.add_name_token(token_dict, t, m, depth-1) + self.add_name_token(token_dict, t, m, depth - 1) @staticmethod def _name_to_tokens(oname): @@ -434,6 +435,7 @@ def copydict(token_dict): copy[k] = v.copy() return copy + def combine_tokens_dicts(d1, *ds): combined = defaultdict(set, d1.copy()) for d2 in ds: @@ -446,6 +448,7 @@ def token_count(tstr): tstr = re.sub(r"\(.+\)", "", tstr) return len(re.split(r'\W+', tstr)) + def get_modifier_aliases(mod): output = {mod} for mods in ALL_TOKEN_DICTS: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 6ae7ce24..27a437bb 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -308,11 +308,21 @@ class MiscModifiers(Enum): "eq", } -HAZARDOUS_IN_NAME_PREFIXES = { +# These tokens have been found to be harmful and will only be added to monsters explicitly. +HAZARDOUS_IN_NAME_MODS = { "reincarnated", "awoken", } +PROBLEMATIC_SERIES_TOKENS = { + "sonia", + "odin", + "metatron", + "kali", + "fenrir", + "sherias", +} + ID1_SUPPORTED = {'hw', 'h', 'x', 'ny', 'gh', 'v', 'np', 'ma', 'a', 'r', 'rr', 'rg', 'rb', 'rl', 'rd', 'rx', 'b', 'br', 'bg', 'bb', 'bl', 'bd', 'bx', 'g', 'gr', 'gg', 'gb', 'gl', 'gd', 'gx', 'l', 'lr', 'lg', 'lb', 'll', 'ld', 'lx', 'd', 'dr', 'dg', 'db', 'dl', 'dd', 'dx', 'x', 'xr', 'xg', 'xb', 'xl', 'xd', 'xx'} From 9fa6e0eae29171bbc7414078689e37dc00dc9738 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 21 Feb 2021 00:55:05 -0500 Subject: [PATCH 0654/1220] Display more information in transforminfo (#862) * Add tfinfo view augmentations and change triangle emoji * Have tfinfo logic also find full list of ids for reactions * Reformat tfinfo non-transform error message --- padinfo/core/transforminfo.py | 16 ++++-- padinfo/menu/transforminfo.py | 2 +- padinfo/padinfo.py | 8 +-- padinfo/view/transforminfo.py | 98 ++++++++++++++++++++++++++++++----- 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index 24389778..ad18bf5e 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -7,7 +7,7 @@ async def perform_transforminfo_query(dgcog, raw_query): found_monster = await find_monster(dgcog, raw_query) if not found_monster: - return None, None + return None, None, None transformed_mon = None base_mon = None @@ -20,6 +20,16 @@ async def perform_transforminfo_query(dgcog, raw_query): break if not transformed_mon: - return found_monster, transformed_mon + return found_monster, None, None - return base_mon, transformed_mon + reaction_ids = [base_mon.monster_id, transformed_mon.monster_id] + current_id = transformed_mon.monster_id + while True: + next_transform_id = mgraph.get_next_transform_id_by_monster_id(current_id) + if next_transform_id is not None and next_transform_id not in reaction_ids: + reaction_ids.append(next_transform_id) + current_id = next_transform_id + else: + break + + return base_mon, transformed_mon, reaction_ids diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index a893a039..f31dd833 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -65,5 +65,5 @@ class TransformInfoMenuPanes(MenuPanes): DATA = { TransformInfoMenu.respond_with_overview: (emoji_buttons['home'], TransformInfoView.VIEW_TYPE), TransformInfoMenu.respond_with_base: ('\N{DOWN-POINTING RED TRIANGLE}', IdView.VIEW_TYPE), - TransformInfoMenu.respond_with_transform: ('\N{BLACK RIGHT-POINTING TRIANGLE}', IdView.VIEW_TYPE), + TransformInfoMenu.respond_with_transform: ('\N{UP-POINTING RED TRIANGLE}', IdView.VIEW_TYPE), } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3e265bbf..ce8c50ab 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -762,16 +762,16 @@ async def leaderskillsingle(self, ctx, *, query): async def transforminfo(self, ctx, *, query): """Show info about a transform card, including some helpful details about the base card.""" dgcog = await self.get_dgcog() - base_mon, transformed_mon = await perform_transforminfo_query(dgcog, query) + base_mon, transformed_mon, reaction_ids = await perform_transforminfo_query(dgcog, query) if not base_mon: await self.send_id_failure_message(ctx, query) return if not transformed_mon: - await ctx.send( - 'Your query `{}` found [{}] {}, '.format(query, base_mon.monster_id, - base_mon.name_en) + 'which has no evos that transform.') + await ctx.send('Your query `{}` '.format(query) + + 'found [{}] {}, '.format(base_mon.monster_id, base_mon.name_en) + + 'which has no evos that transform.') return color = await self.get_user_embed_color(ctx) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index addbdaa5..d1c8bdd9 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.text import Text +from discordmenu.embed.text import Text, BoldText, LabeledText from discordmenu.embed.view import EmbedView from padinfo.common.config import UserConfig @@ -10,13 +10,15 @@ from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.id import IdView from padinfo.view.components.view_state_base import ViewStateBase +from padinfo.view.leader_skill import createMultiplierText +from padinfo.view.id import IdView if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel BASE_EMOJI = '\N{DOWN-POINTING RED TRIANGLE}' +TRANSFORM_EMOJI = '\N{UP-POINTING RED TRIANGLE}' class TransformInfoViewState(ViewStateBase): @@ -65,6 +67,18 @@ async def query(dgcog, base_mon, transformed_mon): return acquire_raw, base_rarity, true_evo_type_raw +def _get_tf_stat_diff_text(stat, tf_stat): + return Box( + Text(str(stat)), + Text('-> {}'.format(tf_stat)), + delimiter=' ' + ) + + +def transformat(text: str): + return '__{}__'.format(text) + + def base_info(m: "MonsterModel"): return Box( Box( @@ -77,6 +91,52 @@ def base_info(m: "MonsterModel"): ) +def stats(base_mon: "MonsterModel", transformed_mon: "MonsterModel"): + base_hp, base_atk, base_rcv, base_weighted = base_mon.stats() + tf_hp, tf_atk, tf_rcv, tf_weighed = transformed_mon.stats() + return Box( + LabeledText('HP', _get_tf_stat_diff_text(base_hp, tf_hp)), + LabeledText('ATK', _get_tf_stat_diff_text(base_atk, tf_atk)), + LabeledText('RCV', _get_tf_stat_diff_text(base_rcv, tf_rcv)) + ) + + +def transform_active_header(m: "MonsterModel"): + active_skill = m.active_skill + active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' + return Box( + TRANSFORM_EMOJI, + BoldText('Transform Active Skill {}'.format(active_cd)), + delimiter=' ' + ) + + +def base_active_header(m: "MonsterModel"): + return Box( + BASE_EMOJI, + BoldText('Base'), + IdView.active_skill_header(m, m), + delimiter=' ' + ) + + +def leader_header(m: "MonsterModel", is_base: bool): + if is_base: + emoji = BASE_EMOJI + label = 'Base' + else: + emoji = TRANSFORM_EMOJI + label = 'Transform' + leader_skill = m.leader_skill + + return Box( + emoji, + BoldText(label), + IdView.leader_skill_header(m).to_markdown(), + delimiter=' ' + ) + + class TransformInfoView: VIEW_TYPE = 'TransformInfo' @@ -89,30 +149,44 @@ def embed(state: TransformInfoViewState): EmbedField( '/'.join(['{}'.format(t.name) for t in transformed_mon.types]), Box( - IdView.normal_awakenings_row(transformed_mon) - if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), + Box( + TRANSFORM_EMOJI, + IdView.normal_awakenings_row(transformed_mon) + if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), + delimiter=' ' + ), base_info(base_mon), - IdView.killers_row(transformed_mon, base_mon) + IdView.killers_row(base_mon, base_mon) ), ), EmbedField( - 'Card info', + BoldText(transformat('Card info')), IdView.misc_info(transformed_mon, state.true_evo_type_raw, state.acquire_raw, state.base_rarity), inline=True ), EmbedField( - IdView.stats_header(transformed_mon).to_markdown(), - IdView.stats(transformed_mon), + BoldText('Stats -> ' + transformat('Transform')), + stats(base_mon, transformed_mon), inline=True ), EmbedField( - IdView.active_skill_header(transformed_mon, base_mon).to_markdown(), - Text(transformed_mon.active_skill.desc if transformed_mon.active_skill else 'None') + transformat(transform_active_header(transformed_mon).to_markdown()), + Box( + Text(transformed_mon.active_skill.desc if transformed_mon.active_skill + else 'None'), + base_active_header(base_mon).to_markdown(), + Text(base_mon.active_skill.desc if base_mon.active_skill else 'None') + ) ), EmbedField( - IdView.leader_skill_header(transformed_mon).to_markdown(), - Text(transformed_mon.leader_skill.desc if transformed_mon.leader_skill else 'None') + transformat(leader_header(transformed_mon, False).to_markdown()), + Box( + Text(transformed_mon.leader_skill.desc if transformed_mon.leader_skill + else 'None'), + leader_header(base_mon, True).to_markdown(), + Text(base_mon.leader_skill.desc if base_mon.leader_skill else 'None') + ) ) ] From 32a34fec014314d991790df35ef506a91d2d852e Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 21 Feb 2021 09:57:02 -0700 Subject: [PATCH 0655/1220] Don't deprioritize equips for "new" modifier (#866) --- padinfo/core/find_monster.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index d7e65c2c..de493e0f 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -234,7 +234,8 @@ def get_priority_tuple(monster, dgcog, tokenized_query=None, matches=None): tokenized_query = [] return (matches[monster].score, - not monster.is_equip, + # Don't deprio evos with new modifier + not monster.is_equip if 'new' not in {m[0] for m in matches[monster].mod} else True, # Match na on id overlap bool(monster.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), SERIES_TYPE_PRIORITY.get(monster.series.series_type), From f7bd99bcba0b04a4a5053e7ae755bbf48844687f Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 21 Feb 2021 10:32:17 -0700 Subject: [PATCH 0656/1220] Strip whitespace from sheet (#868) * Strip whitespace from sheet * strip somewhere else --- dadguide/monster_index.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index ade3e592..a1c70f7c 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -40,7 +40,7 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens = {tuple(m.series.name_en.lower().split()) for m in db.get_all_monsters() - if " " in m.series.name_en}.union(MULTI_WORD_TOKENS) + if " " in m.series.name_en.strip()}.union(MULTI_WORD_TOKENS) self.replacement_tokens = defaultdict(set) self.treename_overrides = set() @@ -56,6 +56,7 @@ async def __ainit__(self, monsters, db): for m_id, name, lp, ov, i in nickname_data: if m_id.isdigit() and not i: + name = name.strip().lower() mid = int(m_id) if ov: self.treename_overrides.add(mid) @@ -69,6 +70,7 @@ async def __ainit__(self, monsters, db): for m_id, name, mp, ov, i in treenames_data: if m_id.isdigit() and not i: + name = name.strip().lower() mid = int(m_id) if ov: for emid in self.graph.get_alt_ids_by_id(mid): @@ -84,6 +86,7 @@ async def __ainit__(self, monsters, db): for sid, name in pantheon_data: if sid.isdigit(): + name = name.strip().lower() if " " in name: self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) @@ -97,7 +100,7 @@ async def __ainit__(self, monsters, db): if mid.isdigit(): mid = int(mid) for mod in mods.split(","): - mod = mod.strip() + mod = mod.strip().lower() if " " in mod: self.multi_word_tokens.add(tuple(mod.lower().split(" "))) mod = mod.lower().replace(" ", "") From bb829a3c7d7d549c54be6e1788b948ca4e4b86e7 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 21 Feb 2021 11:20:03 -0700 Subject: [PATCH 0657/1220] Add NA prioritization within trees (#869) This behavior may be changed still depending on user feedback --- padinfo/core/find_monster.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index de493e0f..a710a691 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -239,11 +239,12 @@ def get_priority_tuple(monster, dgcog, tokenized_query=None, matches=None): # Match na on id overlap bool(monster.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), SERIES_TYPE_PRIORITY.get(monster.series.series_type), - monster.on_na if monster.series.series_type == "collab" else 0, + monster.on_na if monster.series.series_type == "collab" else True, dgcog.database.graph.monster_is_rem_evo(monster), not all(t.value in [0, 12, 14, 15] for t in monster.types), not any(t.value in [0, 12, 14, 15] for t in monster.types), -dgcog.database.graph.get_base_id(monster), + monster.on_na, monster.rarity, monster.monster_no_na) From acb6bf803da5245cfd79899262745608ea6854c4 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 21 Feb 2021 13:30:58 -0700 Subject: [PATCH 0658/1220] Show equips for base (#872) * Add in-tree rarity check * Show equips for base --- padinfo/core/find_monster.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/core/find_monster.py b/padinfo/core/find_monster.py index a710a691..4e300b0c 100644 --- a/padinfo/core/find_monster.py +++ b/padinfo/core/find_monster.py @@ -235,7 +235,7 @@ def get_priority_tuple(monster, dgcog, tokenized_query=None, matches=None): return (matches[monster].score, # Don't deprio evos with new modifier - not monster.is_equip if 'new' not in {m[0] for m in matches[monster].mod} else True, + not monster.is_equip if not {m[0] for m in matches[monster].mod}.intersection({'new', 'base'}) else True, # Match na on id overlap bool(monster.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), SERIES_TYPE_PRIORITY.get(monster.series.series_type), @@ -245,6 +245,7 @@ def get_priority_tuple(monster, dgcog, tokenized_query=None, matches=None): not any(t.value in [0, 12, 14, 15] for t in monster.types), -dgcog.database.graph.get_base_id(monster), monster.on_na, + not monster.is_equip, monster.rarity, monster.monster_no_na) From 339809f971c61f7bfbd70122906301883926873a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 23 Feb 2021 16:18:18 -0600 Subject: [PATCH 0659/1220] add cross attack awakening (#885) --- dadguide/token_mappings.py | 1 + padinfo/common/emoji_map.py | 1 + 2 files changed, 2 insertions(+) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 27a437bb..3ab1eecc 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -152,6 +152,7 @@ class Awakenings(Enum): GREENCOMBOCOUNT = 75 LIGHTCOMBOCOUNT = 76 DARKCOMBOCOUNT = 77 + CROSSATTACK = 78 AWOKEN_MAP = { diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 0714f9ab..5e18d50b 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -81,6 +81,7 @@ 75: 'cc_wood', 76: 'cc_light', 77: 'cc_dark', + 78: 'cross_attack', } AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { From f8bb9e5a071950cf9646115ecf4245546a04ccd0 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 23 Feb 2021 16:22:29 -0600 Subject: [PATCH 0660/1220] Cross attack modifiers (#886) --- dadguide/token_mappings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 3ab1eecc..ff701584 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -233,6 +233,7 @@ class Awakenings(Enum): Awakenings.GREENCOMBOCOUNT: ('ccg', 'cc'), Awakenings.LIGHTCOMBOCOUNT: ('ccl', 'cc'), Awakenings.DARKCOMBOCOUNT: ('ccd', 'cc'), + Awakenings.CROSSATTACK: ('crossattack', 'crossblind'), } From 36df601f68845ab24394dce32018322d02e40ab9 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 23 Feb 2021 17:45:55 -0700 Subject: [PATCH 0661/1220] Remove confusing REM Card vs REM Evo (#890) --- dadguide/monster_graph.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 6a99bfc6..05593778 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -569,25 +569,14 @@ def monster_is_new(self, monster: MonsterModel): return monster.reg_date == latest_time def monster_acquisition(self, monster: MonsterModel): - acquire_text = None - if self.monster_is_farmable(monster) and not self.monster_is_mp_evo(monster): - # Some MP shop monsters 'drop' in PADR - acquire_text = 'Farmable' - elif self.monster_is_farmable_evo(monster) and not self.monster_is_mp_evo(monster): - acquire_text = 'Farmable Evo' - elif monster.in_pem: - acquire_text = 'In PEM' + if self.monster_is_mp_evo(monster): + return 'MP Shop Card' + elif self.monster_is_farmable_evo(monster): + return 'Farmable Card' elif self.monster_is_pem_evo(monster): - acquire_text = 'PEM Evo' - elif monster.in_rem: - acquire_text = 'In REM' + return 'PEM Card' elif self.monster_is_rem_evo(monster): - acquire_text = 'REM Evo' - elif monster.in_mpshop: - acquire_text = 'MP Shop' - elif self.monster_is_mp_evo(monster): - acquire_text = 'MP Shop Evo' - return acquire_text + return 'REM Card' def numeric_next_monster_id_by_id(self, monster_id: int) -> Optional[int]: next_monster = None From 7e94f292aadcf92fea32cca67c4794979996bf72 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 23 Feb 2021 17:48:00 -0700 Subject: [PATCH 0662/1220] [ForceIndexReload] Don't crash on new awakening (#889) --- dadguide/dadguide.py | 41 ++++++++++++++++++++++++++++++++++++--- dadguide/monster_index.py | 5 ++++- padglobal/padglobal.py | 33 ------------------------------- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 8c2d05d8..d170a910 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -9,12 +9,13 @@ import asyncio import os import shutil +import time from io import BytesIO from typing import Optional import tsutils -from redbot.core import checks, data_manager -from redbot.core import commands +from redbot.core import checks, data_manager, commands +from tsutils import auth_check from . import token_mappings from .database_loader import load_database @@ -66,11 +67,18 @@ def __init__(self, bot, *args, **kwargs): self.database = None self.index = None # type: Optional[MonsterIndex] + self.fir_lock = asyncio.Lock() + self.fir3_lock = asyncio.Lock() + self.monster_stats = monster_stats self.MonsterStatModifierInput = MonsterStatModifierInput self.token_maps = token_mappings + GADMIN_COG = self.bot.get_cog("GlobalAdmin") + if GADMIN_COG: + GADMIN_COG.register_perm("contentadmin") + async def wait_until_ready(self): """Wait until the Dadguide cog is ready. @@ -91,9 +99,36 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return + @commands.command(aliases=['fir']) + @auth_check('contentadmin') + async def forceindexreload(self, ctx): + if self.fir_lock.locked(): + await ctx.send("Index is already being reloaded.") + return + + async with ctx.typing(), self.fir_lock: + start = time.perf_counter() + await ctx.send('Starting reload...') + await self.wait_until_ready() + await self.download_and_refresh_nicknames() + await ctx.send('Reload finished in {} seconds.'.format(round(time.perf_counter() - start, 2))) + + @commands.command(aliases=['fir3']) + @auth_check('contentadmin') + async def forceindexreload3(self, ctx): + if self.fir3_lock.locked(): + await ctx.send("Index is already being reloaded.") + return + + async with ctx.typing(), self.fir3_lock: + start = time.perf_counter() + await self.wait_until_ready() + await self.create_index() + await ctx.send('Reload finished in {} seconds.'.format(round(time.perf_counter() - start, 2))) + async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" - self.index = await MonsterIndex(self.database.get_all_monsters(False), self.database) + self.index = await MonsterIndex(self.database.get_all_monsters(), self.database) def get_monster(self, monster_id: int) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a1c70f7c..bd91e8e8 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -357,7 +357,10 @@ async def get_modifiers(self, m): # Awakenings for aw in m.awakenings: - modifiers.update(AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]) + try: + modifiers.update(AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]) + except ValueError: + logger.warning(f"Invalid awoken skill ID: {aw.awoken_skill_id}") # Chibi if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or \ diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 7de79000..8982767a 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -5,7 +5,6 @@ import logging import os import re -import time from collections import defaultdict from io import BytesIO @@ -107,9 +106,6 @@ def __init__(self, bot, *args, **kwargs): self.c_commands = safe_read_json(self.file_path) self.settings = PadGlobalSettings("padglobal") - self.fir_lock = asyncio.Lock() - self.fir3_lock = asyncio.Lock() - self._export_data() async def red_get_data_for_user(self, *, user_id): @@ -190,35 +186,6 @@ async def togglepadglobal(self, ctx): status = 'disabled' if self.settings.checkDisabled(ctx.message) else 'enabled' await ctx.send(inline('PAD Global commands {} on this server').format(status)) - @commands.command(aliases=['fir']) - @auth_check('contentadmin') - async def forceindexreload(self, ctx): - if self.fir_lock.locked(): - await ctx.send("Index is already being reloaded.") - return - - async with ctx.typing(), self.fir_lock: - start = time.perf_counter() - await ctx.send('Starting reload...') - dadguide_cog = self.bot.get_cog('Dadguide') - await dadguide_cog.download_and_refresh_nicknames() - await dadguide_cog.wait_until_ready() - await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) - - @commands.command(aliases=['fir3']) - @auth_check('contentadmin') - async def forceindexreload3(self, ctx): - if self.fir3_lock.locked(): - await ctx.send("Index2 is already being reloaded.") - return - - async with ctx.typing(), self.fir3_lock: - start = time.perf_counter() - dgcog = self.bot.get_cog('Dadguide') - await dgcog.wait_until_ready() - await dgcog.create_index() - await ctx.send('Reload finished in {} seconds.'.format(time.perf_counter() - start)) - @commands.group(aliases=['pdg']) @auth_check('contentadmin') async def padglobal(self, ctx): From 1c891b1e2471e5e9404730ea58219337fc146b44 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 23 Feb 2021 18:22:37 -0700 Subject: [PATCH 0663/1220] Add lv 120 monsters to Tsubaki (#891) --- dadguide/models/monster_stats.py | 7 +++++-- padinfo/view/otherinfo.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 3ae3af2c..dd9615ec 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -39,6 +39,7 @@ def get_awakening_addition(self, key: StatType): class MonsterStats: PLUS_DICT = {'hp': 10, 'atk': 5, 'rcv': 3} + LV_120_MULT_DICT = {'hp': 10, 'atk': 5, 'rcv': 5} def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus_297=True, stat_latents: MonsterStatModifierInput = None): @@ -67,8 +68,10 @@ def base_stat(self, key, lv, monster_model): s_val = s_min + (s_max - s_min) * ((min(lv, monster_model.level) - 1) / (monster_model.level - 1)) ** scale else: s_val = s_min - if lv > 99: - s_val *= 1 + (monster_model.limit_mult / 11 * (lv - 99)) / 100 + if 99 < lv: + s_val *= 1 + (monster_model.limit_mult / 11 * (min(lv, 110) - 99)) / 100 + if 110 < lv: + s_val *= 1 + (self.LV_120_MULT_DICT[key] / 10 * (lv - 110)) / 100 return s_val def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatModifierInput = None): diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index c19794a1..7cb71332 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -32,7 +32,7 @@ def statsbox(m): tbl.hrules = prettytable.NONE tbl.vrules = prettytable.NONE tbl.align = "l" - levels = (m.level, 110) if m.limit_mult > 0 else (m.level,) + levels = (m.level, 110, 120) if m.limit_mult > 0 else (m.level,) for lv in levels: for inh in (False, True): hp, atk, rcv, _ = m.stats(lv, plus=297, inherit=inh) From 91673a3b6a3c64f174305a1ecb541a4a2f9f2b1c Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Tue, 23 Feb 2021 22:49:16 -0500 Subject: [PATCH 0664/1220] Adjust card info in tfinfo to show rarity and cost changes (#892) --- padinfo/padinfo.py | 6 ++---- padinfo/view/transforminfo.py | 29 +++++++++++++++++------------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ce8c50ab..2ee52989 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -776,11 +776,9 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - acquire_raw, base_rarity, true_evo_type_raw = \ - await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, - color, base_mon, transformed_mon, base_rarity, acquire_raw, - true_evo_type_raw) + color, base_mon, transformed_mon, acquire_raw) menu = TransformInfoMenu.menu() await menu.create(ctx, state) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index d1c8bdd9..fe2c8cec 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -23,14 +23,12 @@ class TransformInfoViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, - base_rarity, acquire_raw, true_evo_type_raw): + acquire_raw): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color self.base_mon = base_mon self.transformed_mon = transformed_mon - self.base_rarity = base_rarity self.acquire_raw = acquire_raw - self.true_evo_type_raw = true_evo_type_raw def serialize(self): ret = super().serialize() @@ -51,20 +49,16 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): base_mon = dgcog.get_monster(base_mon_id) transformed_mon = dgcog.get_monster(transformed_mon_id) - acquire_raw, base_rarity, true_evo_type_raw = \ - await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, - base_mon, transformed_mon, base_rarity, acquire_raw, - true_evo_type_raw) + base_mon, transformed_mon, acquire_raw) @staticmethod async def query(dgcog, base_mon, transformed_mon): db_context = dgcog.database acquire_raw = db_context.graph.monster_acquisition(transformed_mon) - base_rarity = db_context.graph.get_base_monster_by_id(transformed_mon.monster_no).rarity - true_evo_type_raw = db_context.graph.true_evo_type_by_monster(transformed_mon).value - return acquire_raw, base_rarity, true_evo_type_raw + return acquire_raw def _get_tf_stat_diff_text(stat, tf_stat): @@ -91,6 +85,18 @@ def base_info(m: "MonsterModel"): ) +def card_info(base_mon: "MonsterModel", transformed_mon: "MonsterModel", acquire_raw): + rarity = Box( + LabeledText('Rarity', '{} -> {}'.format(base_mon.rarity, transformed_mon.rarity)), + Text("" if base_mon.orb_skin_id is None else "(Orb Skin)"), + delimiter=' ' + ) + cost = LabeledText('Cost', '{} -> {}'.format(base_mon.cost, transformed_mon.cost)) + acquire = BoldText(acquire_raw) if acquire_raw else None + + return Box(rarity, cost, acquire) + + def stats(base_mon: "MonsterModel", transformed_mon: "MonsterModel"): base_hp, base_atk, base_rcv, base_weighted = base_mon.stats() tf_hp, tf_atk, tf_rcv, tf_weighed = transformed_mon.stats() @@ -161,8 +167,7 @@ def embed(state: TransformInfoViewState): ), EmbedField( BoldText(transformat('Card info')), - IdView.misc_info(transformed_mon, state.true_evo_type_raw, state.acquire_raw, - state.base_rarity), + card_info(base_mon, transformed_mon, state.acquire_raw), inline=True ), EmbedField( From 6866e74c3c4736c8a461925d92f4e4e93b80c427 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 25 Feb 2021 10:15:35 -0700 Subject: [PATCH 0665/1220] Fix auto-treetoken for trees with equips (#894) * Fix trees with two evos where one is an equip --- dadguide/monster_index.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index bd91e8e8..3a6543ff 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -153,20 +153,20 @@ async def _build_monster_index(self, monsters): nametokens = self._name_to_tokens(m.name_en) last_token = m.name_en.split(',')[-1].strip() alt_monsters = self.graph.get_alt_monsters(m) - autotoken = len(alt_monsters) > 1 + autotoken = len(me for me in alt_monsters if not me.is_equip) > 1 for jpt in m.name_ja.split(" "): self.name_tokens[jpt].add(m) # Propagate name tokens throughout all evos for me in alt_monsters: - if tsutils.contains_ja(me.name_en): - continue - if last_token != me.name_en.split(',')[-1].strip(): - autotoken = False for t in self.monster_id_to_nametokens[me.monster_id]: if t in nametokens: self.add_name_token(self.name_tokens, t, m) + if me.is_equip or tsutils.contains_ja(me.name_en): + continue + if last_token != me.name_en.split(',')[-1].strip(): + autotoken = False # Find likely treenames treenames = set() From 48a8a5842a4c2df0e3f9d2d0f86d78d0f7e060f4 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 25 Feb 2021 10:21:27 -0700 Subject: [PATCH 0666/1220] Fix monster index syntax that was crashing load (#895) --- dadguide/monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 3a6543ff..613b64cf 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -153,7 +153,7 @@ async def _build_monster_index(self, monsters): nametokens = self._name_to_tokens(m.name_en) last_token = m.name_en.split(',')[-1].strip() alt_monsters = self.graph.get_alt_monsters(m) - autotoken = len(me for me in alt_monsters if not me.is_equip) > 1 + autotoken = len([me for me in alt_monsters if not me.is_equip]) > 1 for jpt in m.name_ja.split(" "): self.name_tokens[jpt].add(m) From 408d4b9736016ffc616e96f53a72928bf014d543 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 26 Feb 2021 17:26:30 -0600 Subject: [PATCH 0667/1220] Add prev/next monster buttons to SeriesScroll (#897) * Fix stash pop merge conflict * Create get_current_indices method * Refactor panes so I can have multiple child responses * Refactored series scroll a bit & fixed monster list * Rewrite without up/down, lazy buttons don't work yet * Make lazy buttons work * comment print * Fix view text * Current rarity * the name is bold in the header * Refactor ui * BoldText * rename functions to prev/next monster --- padinfo/menu/common.py | 41 +++--- padinfo/menu/id.py | 34 +++-- padinfo/menu/leader_skill.py | 22 +-- padinfo/menu/leader_skill_single.py | 11 +- padinfo/menu/monster_list.py | 70 ++++++--- padinfo/menu/series_scroll.py | 216 +++++++++++++++++++++------- padinfo/menu/simple_text.py | 10 +- padinfo/menu/transforminfo.py | 12 +- padinfo/padinfo.py | 63 ++++---- padinfo/view/series_scroll.py | 89 ++++++++---- 10 files changed, 385 insertions(+), 183 deletions(-) diff --git a/padinfo/menu/common.py b/padinfo/menu/common.py index e57e513e..b808b24d 100644 --- a/padinfo/menu/common.py +++ b/padinfo/menu/common.py @@ -13,42 +13,33 @@ class MenuPanes: @classmethod def emoji_names(cls): - return [v[0] for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] + return [k for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] @classmethod def transitions(cls): - return {v[0]: k for k, v in cls.DATA.items()} + return {k: v[0] for k, v in cls.DATA.items() if v[0] is not None} @classmethod def pane_types(cls): - return {v[1]: k for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} - - @classmethod - def emoji_name_to_emoji(cls, name: str): - for _, data_pair in cls.DATA.items(): - if data_pair[1] == name: - return data_pair[0] - return None - - @classmethod - def emoji_name_to_function(cls, name: str): - for _, data_pair in cls.DATA.items(): - if data_pair[1] == name: - return data_pair[1] - return None + return {v[1]: v[0] for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} @classmethod def respond_to_emoji_with_parent(cls, emoji: str): """Only defined for menus that support having children""" - for _, data_pair in cls.DATA.items(): - if data_pair[0] == emoji: - return data_pair[2] - return None + if cls.DATA.get(emoji) is None: + return None + return cls.DATA[emoji][0] is not None @classmethod def respond_to_emoji_with_child(cls, emoji: str): """Only defined for menus that support having children""" - for _, data_pair in cls.DATA.items(): - if data_pair[0] == emoji: - return data_pair[3] - return None + if cls.DATA.get(emoji) is None: + return None + return cls.DATA[emoji][2] is not None + + @classmethod + def get_child_data_func(cls, emoji: str): + """Only defined for menus that support having children""" + if cls.DATA.get(emoji) is None: + return None + return cls.DATA[emoji][2] diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index dccf46e7..a664cffe 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -227,19 +227,31 @@ class IdMenuPaneNames: delete = 'delete' +class IdMenuEmoji: + left = '\N{BLACK LEFT-POINTING TRIANGLE}' + right = '\N{BLACK RIGHT-POINTING TRIANGLE}' + home = '\N{HOUSE BUILDING}' + evos = char_to_emoji('E') + mats = '\N{MEAT ON BONE}' + pic = '\N{FRAME WITH PICTURE}' + pantheon = '\N{CLASSICAL BUILDING}' + otherinfo = '\N{SCROLL}' + refresh = "\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}" + delete = '\N{CROSS MARK}' + + class IdMenuPanes(MenuPanes): DATA = { - IdMenu.respond_with_left: ('\N{BLACK LEFT-POINTING TRIANGLE}', None), - IdMenu.respond_with_right: ('\N{BLACK RIGHT-POINTING TRIANGLE}', None), - IdMenu.respond_with_current_id: ('\N{HOUSE BUILDING}', IdView.VIEW_TYPE), - IdMenu.respond_with_evos: (char_to_emoji('E'), EvosView.VIEW_TYPE), - IdMenu.respond_with_mats: ('\N{MEAT ON BONE}', MaterialsView.VIEW_TYPE), - IdMenu.respond_with_picture: ('\N{FRAME WITH PICTURE}', PicView.VIEW_TYPE), - IdMenu.respond_with_pantheon: ('\N{CLASSICAL BUILDING}', PantheonView.VIEW_TYPE), - IdMenu.respond_with_otherinfo: ('\N{SCROLL}', OtherInfoView.VIEW_TYPE), - IdMenu.respond_with_refresh: ( - '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', IdMenuPaneNames.refresh), - IdMenu.respond_with_delete: ('\N{CROSS MARK}', IdMenuPaneNames.delete), + IdMenuEmoji.left: (IdMenu.respond_with_left, None), + IdMenuEmoji.right: (IdMenu.respond_with_right, None), + IdMenuEmoji.home: (IdMenu.respond_with_current_id, IdView.VIEW_TYPE), + IdMenuEmoji.evos: (IdMenu.respond_with_evos, EvosView.VIEW_TYPE), + IdMenuEmoji.mats: (IdMenu.respond_with_mats, MaterialsView.VIEW_TYPE), + IdMenuEmoji.pic: (IdMenu.respond_with_picture, PicView.VIEW_TYPE), + IdMenuEmoji.pantheon: (IdMenu.respond_with_pantheon, PantheonView.VIEW_TYPE), + IdMenuEmoji.otherinfo: (IdMenu.respond_with_otherinfo, OtherInfoView.VIEW_TYPE), + IdMenuEmoji.refresh: (IdMenu.respond_with_refresh, IdMenuPaneNames.refresh), + IdMenuEmoji.delete: (IdMenu.respond_with_delete, IdMenuPaneNames.delete), } HIDDEN_EMOJIS = [ IdMenuPaneNames.refresh, diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 5143f815..8d0e680a 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -5,7 +5,7 @@ from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from padinfo.menu.common import emoji_buttons, MenuPanes +from padinfo.menu.common import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillViewState @@ -18,7 +18,7 @@ def menu(): return EmbedMenu(LeaderSkillMenuPanes.transitions(), LeaderSkillMenu.ls_control) @staticmethod - async def respond_to_r(message: Optional[Message], ims, **data): + async def respond_with_right(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] @@ -30,7 +30,7 @@ async def respond_to_r(message: Optional[Message], ims, **data): return id_control @staticmethod - async def respond_to_l(message: Optional[Message], ims, **data): + async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] @@ -42,7 +42,7 @@ async def respond_to_l(message: Optional[Message], ims, **data): return id_control @staticmethod - async def respond_to_house(message: Optional[Message], ims, **data): + async def respond_with_house(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] ls_view_state = await LeaderSkillViewState.deserialize(dgcog, user_config, ims) @@ -64,10 +64,16 @@ def id_control(state: IdViewState): ) +class LeaderSkillEmoji: + home = emoji_buttons['home'] + left = char_to_emoji('l') + right = char_to_emoji('r') + + class LeaderSkillMenuPanes(MenuPanes): - INITIAL_EMOJI = emoji_buttons['home'] + INITIAL_EMOJI = LeaderSkillEmoji.home DATA = { - LeaderSkillMenu.respond_to_house: (emoji_buttons['home'], None), - LeaderSkillMenu.respond_to_l: (char_to_emoji('l'), IdView.VIEW_TYPE), - LeaderSkillMenu.respond_to_r: (char_to_emoji('r'), IdView.VIEW_TYPE), + LeaderSkillEmoji.home: (LeaderSkillMenu.respond_with_house, None), + LeaderSkillEmoji.left: (LeaderSkillMenu.respond_with_left, IdView.VIEW_TYPE), + LeaderSkillEmoji.right: (LeaderSkillMenu.respond_with_right, IdView.VIEW_TYPE), } diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index 17729260..8a80745f 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -44,9 +44,14 @@ def id_control(state: IdViewState): ) +class LeaderSkillSingleEmoji: + home = emoji_buttons["home"] + id = '\N{SQUARED ID}' + + class LeaderSkillSingleMenuPanes(MenuPanes): - INITIAL_EMOJI = emoji_buttons['home'] + INITIAL_EMOJI = LeaderSkillSingleEmoji.home DATA = { - LeaderSkillSingleMenu.respond_with_ls: (emoji_buttons['home'], None), - LeaderSkillSingleMenu.respond_with_id: ('\N{SQUARED ID}', IdView.VIEW_TYPE), + LeaderSkillSingleEmoji.home: (LeaderSkillSingleMenu.respond_with_ls, None), + LeaderSkillSingleEmoji.id: (LeaderSkillSingleMenu.respond_with_id, IdView.VIEW_TYPE), } diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index f9a07c69..e99aad5d 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -5,7 +5,7 @@ from tsutils import char_to_emoji from padinfo.menu.common import emoji_buttons, MenuPanes -from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuPaneNames +from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuPaneNames, IdMenuEmoji from padinfo.view.id import IdView from padinfo.view.monster_list import MonsterListView, MonsterListViewState @@ -16,6 +16,23 @@ class MonsterListPaneNames: reset = 'reset' +class MonsterListEmoji: + home = emoji_buttons['home'] + zero = char_to_emoji('0') + one = char_to_emoji('1') + two = char_to_emoji('2') + three = char_to_emoji('3') + four = char_to_emoji('4') + five = char_to_emoji('5') + six = char_to_emoji('6') + seven = char_to_emoji('7') + eight = char_to_emoji('8') + nine = char_to_emoji('9') + ten = char_to_emoji('10') + refresh = '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}' + reset = emoji_buttons['reset'] + + class MonsterListMenu: MENU_TYPE = 'MonsterListMenu' CHILD_MENU_TYPE = 'IdMenu' @@ -106,8 +123,8 @@ def monster_list_control(state: MonsterListViewState): ) @staticmethod - def get_child_data(ims, emoji_clicked): - emoji_response = IdMenuPanes.emoji_name_to_emoji(IdMenuPaneNames.refresh) \ + def click_child_number(ims, emoji_clicked, **data): + emoji_response = IdMenuEmoji.refresh \ if MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} @@ -122,25 +139,36 @@ def get_child_data(ims, emoji_clicked): class MonsterListMenuPanes(MenuPanes): - INITIAL_EMOJI = '\N{HOUSE BUILDING}' + INITIAL_EMOJI = MonsterListEmoji.home DATA = { - # tuple parts: emoji, pane_type, respond_with_parent, respond_with_child - MonsterListMenu.respond_with_monster_list: ( - emoji_buttons[MonsterListPaneNames.home], MonsterListPaneNames.home, True, False), - MonsterListMenu.respond_with_0: (char_to_emoji('0'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_1: (char_to_emoji('1'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_2: (char_to_emoji('2'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_3: (char_to_emoji('3'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_4: (char_to_emoji('4'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_5: (char_to_emoji('5'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_6: (char_to_emoji('6'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_7: (char_to_emoji('7'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_8: (char_to_emoji('8'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_9: (char_to_emoji('9'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_10: (char_to_emoji('10'), IdView.VIEW_TYPE, False, True), - MonsterListMenu.respond_with_refresh: ( - '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', MonsterListPaneNames.refresh, True, False), - MonsterListMenu.respond_with_reset: (emoji_buttons['reset'], MonsterListPaneNames.reset, True, False) + # tuple parts: parent_response, pane_type, respond_with_child + MonsterListEmoji.home: (MonsterListMenu.respond_with_monster_list, MonsterListPaneNames.home, None), + MonsterListEmoji.zero: ( + MonsterListMenu.respond_with_0, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.one: ( + MonsterListMenu.respond_with_1, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.two: ( + MonsterListMenu.respond_with_2, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.three: ( + MonsterListMenu.respond_with_3, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.four: ( + MonsterListMenu.respond_with_4, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.five: ( + MonsterListMenu.respond_with_5, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.six: ( + MonsterListMenu.respond_with_6, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.seven: ( + MonsterListMenu.respond_with_7, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.eight: ( + MonsterListMenu.respond_with_8, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.nine: ( + MonsterListMenu.respond_with_9, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.ten: ( + MonsterListMenu.respond_with_10, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), + MonsterListEmoji.refresh: ( + MonsterListMenu.respond_with_refresh, MonsterListPaneNames.refresh, None), + MonsterListEmoji.reset: ( + MonsterListMenu.respond_with_reset, MonsterListPaneNames.reset, None) } HIDDEN_EMOJIS = [ MonsterListPaneNames.home, diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 6fc4065f..c0c78436 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -1,11 +1,12 @@ +from copy import deepcopy from typing import Optional, List from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji -from padinfo.menu.common import emoji_buttons, MenuPanes -from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuPaneNames +from padinfo.menu.common import MenuPanes, emoji_buttons +from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView from padinfo.view.series_scroll import SeriesScrollView, SeriesScrollViewState @@ -16,12 +17,26 @@ class SeriesScrollPaneNames: reset = 'reset' -def _get_min_index(ims): - return ims['current_min_index'].get(str(ims['rarity'])) or 0 - - -def _set_min_index(ims, val): - ims['current_min_index'][str(ims['rarity'])] = val +class SeriesScrollEmoji: + delete = '\N{CROSS MARK}' + home = emoji_buttons['home'] + prev_page = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + prev_mon = '\N{BLACK LEFT-POINTING TRIANGLE}' + next_mon = '\N{BLACK RIGHT-POINTING TRIANGLE}' + next_page = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + zero = char_to_emoji('0') + one = char_to_emoji('1') + two = char_to_emoji('2') + three = char_to_emoji('3') + four = char_to_emoji('4') + five = char_to_emoji('5') + six = char_to_emoji('6') + seven = char_to_emoji('7') + eight = char_to_emoji('8') + nine = char_to_emoji('9') + ten = char_to_emoji('10') + refresh = '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}' + reset = emoji_buttons['reset'] class SeriesScrollMenu: @@ -52,29 +67,33 @@ async def respond_with_reset(message: Optional[Message], ims, **data): @staticmethod async def respond_with_left(message: Optional[Message], ims, **data): + current_page = ims['current_page'] + if current_page > 0: + ims['current_page'] = current_page - 1 + ims['current_index'] = None + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) current_rarity_index = ims['all_rarities'].index(ims['rarity']) ims['rarity'] = ims['all_rarities'][current_rarity_index - 1] + paginated_monsters = SeriesScrollViewState.query_from_ims(data['dgcog'], ims) + ims['current_page'] = len(paginated_monsters) - 1 + ims['current_index'] = None return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): + paginated_monsters = SeriesScrollViewState.query_from_ims(data['dgcog'], ims) + current_page = ims['current_page'] + if current_page < len(paginated_monsters) - 1: + ims['current_page'] = current_page + 1 + ims['current_index'] = None + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) current_rarity_index = ims['all_rarities'].index(ims['rarity']) if current_rarity_index == len(ims['all_rarities']) - 1: ims['rarity'] = ims['all_rarities'][0] else: ims['rarity'] = ims['all_rarities'][current_rarity_index + 1] - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) - - @staticmethod - async def respond_with_up(message: Optional[Message], ims, **data): - _set_min_index(ims, max(_get_min_index(ims) - SeriesScrollMenu.SCROLL_INTERVAL, 0)) - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) - - @staticmethod - async def respond_with_down(message: Optional[Message], ims, **data): - attempted_new_index = _get_min_index(ims) + SeriesScrollMenu.SCROLL_INTERVAL - if attempted_new_index < len(ims['full_monster_list']): - _set_min_index(ims, attempted_new_index) + ims['current_page'] = 0 + ims['current_index'] = None return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) @staticmethod @@ -85,12 +104,75 @@ async def respond_with_monster_list(message: Optional[Message], ims, **data): control = SeriesScrollMenu.monster_list_control(view_state) return control + @staticmethod + async def respond_with_previous_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + SeriesScrollMenu._update_ims_prev(dgcog, ims) + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + def _update_ims_prev(dgcog, ims): + current_index = ims['current_index'] + page = ims['current_page'] + paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, ims) + max_index = len(paginated_monsters[page]) - 1 + if current_index is None: + ims['current_index'] = max_index + return + if current_index > 0: + ims['current_index'] = current_index - 1 + return + if current_index == 0 and page > 0: + ims['current_page'] = page - 1 + ims['current_index'] = SeriesScrollViewState.MAX_ITEMS_PER_PANE + return + if current_index == 0: + # respond with left but then set the current index to the max thing possible + current_rarity_index = ims['all_rarities'].index(ims['rarity']) + ims['rarity'] = ims['all_rarities'][current_rarity_index - 1] + paginated_monsters_new = SeriesScrollViewState.query_from_ims(dgcog, ims) + ims['current_page'] = len(paginated_monsters_new) - 1 + ims['current_index'] = len(paginated_monsters_new[-1]) - 1 + + @staticmethod + async def respond_with_next_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + SeriesScrollMenu._update_ims_next(dgcog, ims) + return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + def _update_ims_next(dgcog, ims): + current_index = ims['current_index'] + page = ims['current_page'] + paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, ims) + max_index = len(paginated_monsters[page]) - 1 + if current_index is None: + ims['current_index'] = 0 + return + if current_index < max_index: + ims['current_index'] = current_index + 1 + return + if current_index == max_index and page < len(paginated_monsters) - 1: + ims['current_page'] = page + 1 + ims['current_index'] = 0 + return + if current_index == max_index: + # copied from respond with right + current_rarity_index = ims['all_rarities'].index(ims['rarity']) + if current_rarity_index == len(ims['all_rarities']) - 1: + ims['rarity'] = ims['all_rarities'][0] + else: + ims['rarity'] = ims['all_rarities'][current_rarity_index + 1] + ims['current_page'] = 0 + ims['current_index'] = 0 + @staticmethod async def respond_with_delete(message: Message, ims, **data): return await message.delete() @staticmethod - async def respond_with_n(message: Optional[Message], ims, _n, **data): + async def respond_with_n(message: Optional[Message], ims, n, **data): + ims['current_index'] = n return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) @staticmethod @@ -145,50 +227,88 @@ def monster_list_control(state: SeriesScrollViewState): ) @staticmethod - def get_child_data(ims, emoji_clicked): - emoji_response = IdMenuPanes.emoji_name_to_emoji(IdMenuPaneNames.refresh) \ + def click_child_number(ims, emoji_clicked, **data): + dgcog = data['dgcog'] + emoji_response = IdMenuEmoji.refresh \ if SeriesScrollMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} n = SeriesScrollMenuPanes.emoji_names().index(emoji_clicked) + paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, ims) + page = ims.get('page') or 0 + monster_list = paginated_monsters[page] extra_ims = { 'is_child': True, - 'resolved_monster_id': int( - ims['full_monster_list'][n + _get_min_index(ims) - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT]), 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, + 'resolved_monster_id': + monster_list[n - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, } return emoji_response, extra_ims + @staticmethod + def auto_scroll_child_left(ims, _emoji_clicked, **data): + return SeriesScrollMenu._scroll_child(ims, SeriesScrollMenu._update_ims_prev, **data) + + @staticmethod + def auto_scroll_child_right(ims, _emoji_clicked, **data): + return SeriesScrollMenu._scroll_child(ims, SeriesScrollMenu._update_ims_next, **data) + + @staticmethod + def _scroll_child(ims, update_fn, **data): + dgcog = data['dgcog'] + copy_ims = deepcopy(ims) + update_fn(dgcog, copy_ims) + paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, copy_ims) + page = copy_ims.get('current_page') or 0 + monster_list = paginated_monsters[page] + # after we figure out new rarity + extra_ims = { + 'is_child': True, + 'reaction_list': IdMenuPanes.emoji_names(), + 'menu_type': IdMenu.MENU_TYPE, + 'resolved_monster_id': monster_list[copy_ims['current_index']].monster_id, + } + return IdMenuEmoji.refresh, extra_ims + class SeriesScrollMenuPanes(MenuPanes): INITIAL_EMOJI = '\N{HOUSE BUILDING}' NON_MONSTER_EMOJI_COUNT = 5 DATA = { # tuple parts: emoji, pane_type, respond_with_parent, respond_with_child - SeriesScrollMenu.respond_with_delete: ('\N{CROSS MARK}', None, True, False), - SeriesScrollMenu.respond_with_monster_list: ( - emoji_buttons[SeriesScrollPaneNames.home], SeriesScrollPaneNames.home, True, False), - SeriesScrollMenu.respond_with_left: ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), - SeriesScrollMenu.respond_with_right: ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), - SeriesScrollMenu.respond_with_up: ( - '\N{BLACK UP-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), - SeriesScrollMenu.respond_with_down: ( - '\N{BLACK DOWN-POINTING DOUBLE TRIANGLE}', SeriesScrollView.VIEW_TYPE, True, False), - SeriesScrollMenu.respond_with_0: (char_to_emoji('0'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_1: (char_to_emoji('1'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_2: (char_to_emoji('2'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_3: (char_to_emoji('3'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_4: (char_to_emoji('4'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_5: (char_to_emoji('5'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_6: (char_to_emoji('6'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_7: (char_to_emoji('7'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_8: (char_to_emoji('8'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_9: (char_to_emoji('9'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_10: (char_to_emoji('10'), IdView.VIEW_TYPE, False, True), - SeriesScrollMenu.respond_with_refresh: ( - '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}', SeriesScrollPaneNames.refresh, True, False), - SeriesScrollMenu.respond_with_reset: (emoji_buttons['reset'], SeriesScrollPaneNames.reset, True, False) + SeriesScrollEmoji.delete: (SeriesScrollMenu.respond_with_delete, None, None), + SeriesScrollEmoji.home: (SeriesScrollMenu.respond_with_monster_list, SeriesScrollPaneNames.home, None), + SeriesScrollEmoji.prev_page: (SeriesScrollMenu.respond_with_left, SeriesScrollView.VIEW_TYPE, None), + SeriesScrollEmoji.prev_mon: ( + SeriesScrollMenu.respond_with_previous_monster, None, SeriesScrollMenu.auto_scroll_child_left), + SeriesScrollEmoji.next_mon: ( + SeriesScrollMenu.respond_with_next_monster, None, SeriesScrollMenu.auto_scroll_child_right), + SeriesScrollEmoji.next_page: (SeriesScrollMenu.respond_with_right, SeriesScrollView.VIEW_TYPE, None), + SeriesScrollEmoji.zero: ( + SeriesScrollMenu.respond_with_0, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.one: ( + SeriesScrollMenu.respond_with_1, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.two: ( + SeriesScrollMenu.respond_with_2, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.three: ( + SeriesScrollMenu.respond_with_3, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.four: ( + SeriesScrollMenu.respond_with_4, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.five: ( + SeriesScrollMenu.respond_with_5, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.six: ( + SeriesScrollMenu.respond_with_6, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.seven: ( + SeriesScrollMenu.respond_with_7, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.eight: ( + SeriesScrollMenu.respond_with_8, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.nine: ( + SeriesScrollMenu.respond_with_9, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.ten: ( + SeriesScrollMenu.respond_with_10, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), + SeriesScrollEmoji.refresh: (SeriesScrollMenu.respond_with_refresh, SeriesScrollPaneNames.refresh, None), + SeriesScrollEmoji.reset: (SeriesScrollMenu.respond_with_reset, SeriesScrollPaneNames.reset, None) } HIDDEN_EMOJIS = [ SeriesScrollPaneNames.home, diff --git a/padinfo/menu/simple_text.py b/padinfo/menu/simple_text.py index ae681b90..40eae654 100644 --- a/padinfo/menu/simple_text.py +++ b/padinfo/menu/simple_text.py @@ -4,7 +4,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from padinfo.menu.common import MenuPanes +from padinfo.menu.common import MenuPanes, emoji_buttons from padinfo.view.simple_text import SimpleTextView, SimpleTextViewState @@ -45,10 +45,14 @@ def message_control(state: SimpleTextViewState): ) +class SimpleTextEmoji: + home = emoji_buttons['home'] + + class SimpleTextMenuPanes(MenuPanes): - INITIAL_EMOJI = '\N{HOUSE BUILDING}' + INITIAL_EMOJI = SimpleTextEmoji.home DATA = { - SimpleTextMenu.respond_with_message: ('\N{HOUSE BUILDING}', SimpleTextNames.home), + SimpleTextEmoji.home: (SimpleTextMenu.respond_with_message, SimpleTextNames.home), } HIDDEN_EMOJIS = [ diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index f31dd833..b6f8d904 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -61,9 +61,15 @@ def id_control(state: IdViewState): ) +class TransformInfoEmoji: + home = emoji_buttons['home'] + down = '\N{DOWN-POINTING RED TRIANGLE}' + up = '\N{UP-POINTING RED TRIANGLE}' + + class TransformInfoMenuPanes(MenuPanes): DATA = { - TransformInfoMenu.respond_with_overview: (emoji_buttons['home'], TransformInfoView.VIEW_TYPE), - TransformInfoMenu.respond_with_base: ('\N{DOWN-POINTING RED TRIANGLE}', IdView.VIEW_TYPE), - TransformInfoMenu.respond_with_transform: ('\N{UP-POINTING RED TRIANGLE}', IdView.VIEW_TYPE), + TransformInfoEmoji.home: (TransformInfoMenu.respond_with_overview, TransformInfoView.VIEW_TYPE), + TransformInfoEmoji.down: (TransformInfoMenu.respond_with_base, IdView.VIEW_TYPE), + TransformInfoEmoji.up: (TransformInfoMenu.respond_with_transform, IdView.VIEW_TYPE), } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2ee52989..9911fd8d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -4,6 +4,7 @@ import random import re import urllib.parse +from copy import deepcopy from io import BytesIO from typing import TYPE_CHECKING, List @@ -36,8 +37,8 @@ from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes -from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes -from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu +from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji +from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list @@ -160,20 +161,20 @@ async def test_reaction_add(self, reaction, member): else: emoji_clicked = emoji_obj.name - menu_to_emoji_list_map = { - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenuPanes.emoji_names(), - IdMenu.MENU_TYPE: IdMenuPanes.emoji_names(), - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes.emoji_names(), - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenuPanes.emoji_names(), - MonsterListMenu.MENU_TYPE: MonsterListMenuPanes.emoji_names(), - SeriesScrollMenu.MENU_TYPE: SeriesScrollMenuPanes.emoji_names(), - SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes.emoji_names(), - TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes.emoji_names(), + menu_to_panes_map = { + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenuPanes, + IdMenu.MENU_TYPE: IdMenuPanes, + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes, + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenuPanes, + MonsterListMenu.MENU_TYPE: MonsterListMenuPanes, + SeriesScrollMenu.MENU_TYPE: SeriesScrollMenuPanes, + SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes, + TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes, } emoji_recognized = emoji_clicked in EmbedMenuEmojiConfig().to_list() - for menu_type, emoji_list in menu_to_emoji_list_map.items(): - if emoji_clicked in emoji_list: + for menu_type, panes_type in menu_to_panes_map.items(): + if emoji_clicked in panes_type.emoji_names(): emoji_recognized = True if not emoji_recognized: return @@ -183,7 +184,6 @@ async def test_reaction_add(self, reaction, member): if not ims: return - original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] menu_map = { ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, @@ -203,30 +203,34 @@ async def test_reaction_add(self, reaction, member): menu_func = menu_1_class.menu menu = menu_func() + if not (await menu.should_respond( message, reaction, await self.get_reaction_filters( - original_author_id, menu_to_emoji_list_map[menu_type]), member)): + ims['original_author_id'], menu_to_panes_map[menu_type].emoji_names()), member)): return dgcog = await self.get_dgcog() - user_config = await BotConfig.get_user(self.config, original_author_id) + user_config = await BotConfig.get_user(self.config, ims['original_author_id']) data = { 'dgcog': dgcog, 'user_config': user_config } - menu_1_ims = ims + menu_1_ims = deepcopy(ims) message_1 = message failsafe = 0 + await menu.transition(message, ims, emoji_clicked, member, **data) while menu_1_ims.get('child_message_id'): # before this loop can actually work as a loop, the type of menu_2 can't be hard-coded as IdMenu anymore, # and we have to update menu_1_class to be menu_2_class during the loop. - # TODO: Add support for triply-linked menus or children other than IdMenus if failsafe == 10: break failsafe += 1 menu_2 = IdMenu.menu() - emoji_simulated_clicked_2, extra_ims = menu_1_class.get_child_data(menu_1_ims, emoji_clicked) - if emoji_simulated_clicked_2: + child_data_func = menu_to_panes_map[menu_type].get_child_data_func(emoji_clicked) + emoji_simulated_clicked_2, extra_ims = None, {} + if child_data_func is not None: + emoji_simulated_clicked_2, extra_ims = child_data_func(menu_1_ims, emoji_clicked, **data) + if emoji_simulated_clicked_2 is not None: fctx = await self.bot.get_context(message_1) try: message_2 = await fctx.fetch_message(int(menu_1_ims['child_message_id'])) @@ -238,8 +242,6 @@ async def test_reaction_add(self, reaction, member): menu_1_ims = menu_2_ims message_1 = message_2 - await menu.transition(message, ims, emoji_clicked, member, **data) - async def get_reaction_filters(self, original_author_id, valid_emoji_names): friend_cog = self.bot.get_cog("Friend") friend_ids = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] @@ -630,8 +632,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM data['child_message_id'] = child_message.id try: - await parent_menu.transition(message, ims, MonsterListMenuPanes.emoji_name_to_emoji('refresh'), ctx.author, - **data) + await parent_menu.transition(message, ims, MonsterListEmoji.refresh, ctx.author, **data) await message.edit(content=None) except discord.errors.NotFound: # The user could delete the menu before we can do this @@ -649,23 +650,22 @@ async def collabscroll(self, ctx, *, query): series_id = monster.series_id series_object: "SeriesModel" = monster.series title = series_object.name_en - monster_list = None + paginated_monsters = None rarity = None for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: - monster_list = SeriesScrollViewState.query(dgcog, monster.series_id, rarity) - if monster_list: + paginated_monsters = SeriesScrollViewState.query(dgcog, monster.series_id, rarity) + if paginated_monsters: break all_rarities = SeriesScrollViewState.query_all_rarities(dgcog, series_id) raw_query = query original_author_id = ctx.message.author.id color = await self.get_user_embed_color(ctx) - initial_reaction_list = SeriesScrollMenuPanes.get_initial_reaction_list(len(monster_list)) - initial_min_indices = {rarity: SeriesScrollMenu.SCROLL_INITIAL_POSITION} + initial_reaction_list = SeriesScrollMenuPanes.get_initial_reaction_list(len(paginated_monsters)) instruction_message = 'Click a reaction to see monster details!' state = SeriesScrollViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, query, color, - series_id, initial_min_indices, monster_list, monster_list, int(rarity), + series_id, 0, paginated_monsters[0], int(rarity), len(paginated_monsters), all_rarities, title, instruction_message, reaction_list=initial_reaction_list) @@ -687,8 +687,7 @@ async def collabscroll(self, ctx, *, query): data['child_message_id'] = child_message.id try: - await parent_menu.transition(message, ims, SeriesScrollMenuPanes.emoji_name_to_emoji('refresh'), ctx.author, - **data) + await parent_menu.transition(message, ims, SeriesScrollEmoji.refresh, ctx.author, **data) await message.edit(content=None) except discord.errors.NotFound: # The user could delete the menu before we can do this diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 4f146c3a..74eeab53 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -2,6 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils import char_to_emoji @@ -18,24 +19,26 @@ class SeriesScrollViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 11 - def __init__(self, original_author_id, menu_type, raw_query, query, color, series_id, current_min_index, - monster_list: List["MonsterModel"], full_monster_list: List["MonsterModel"], rarity: int, + def __init__(self, original_author_id, menu_type, raw_query, query, color, series_id, current_page, + monster_list: List["MonsterModel"], rarity: int, pages_in_rarity: int, all_rarities: List[int], title, message, + current_index: int = None, max_len_so_far: int = None, reaction_list=None, extra_state=None, child_message_id=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.pages_in_rarity = pages_in_rarity + self.current_index = current_index self.all_rarities = all_rarities - self.current_min_index = current_min_index + self.current_page = current_page or 0 self.series_id = series_id self.rarity = rarity self.message = message self.child_message_id = child_message_id self.title = title self.monster_list = monster_list - self.full_monster_list = full_monster_list self.reaction_list = reaction_list self.color = color self.query = query @@ -46,15 +49,16 @@ def serialize(self): ret.update({ 'pane_type': SeriesScrollView.VIEW_TYPE, 'series_id': self.series_id, - 'current_min_index': self.current_min_index, + 'current_page': self.current_page, + 'pages_in_rarity': self.pages_in_rarity, 'title': self.title, - 'full_monster_list': [str(m.monster_no) for m in self.full_monster_list], 'rarity': self.rarity, 'all_rarities': self.all_rarities, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, 'message': self.message, - 'max_len_so_far': self.max_len_so_far + 'max_len_so_far': self.max_len_so_far, + 'current_index': self.current_index, }) return ret @@ -66,13 +70,12 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): series_id = ims['series_id'] rarity = ims['rarity'] all_rarities = ims['all_rarities'] - full_monster_list = SeriesScrollViewState.query(dgcog, series_id, rarity) - current_min_index = ims.get('current_min_index') or {str(rarity): 0} - current_min_index_num: int = current_min_index.get(str(rarity)) or 0 - current_max_index_num: int = current_min_index_num + SeriesScrollViewState.MAX_ITEMS_PER_PANE - monster_list = full_monster_list[current_min_index_num:current_max_index_num] + paginated_monsters = SeriesScrollViewState.query(dgcog, series_id, rarity) + current_page = ims['current_page'] + monster_list = paginated_monsters[current_page] title = ims['title'] + pages_in_rarity = len(paginated_monsters) raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -81,11 +84,13 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): child_message_id = ims.get('child_message_id') message = ims.get('message') max_len_so_far = max(ims['max_len_so_far'] or len(monster_list), len(monster_list)) + current_index = ims.get('current_index') return SeriesScrollViewState(original_author_id, menu_type, raw_query, query, user_config.color, series_id, - current_min_index, monster_list, full_monster_list, rarity, + current_page, monster_list, rarity, pages_in_rarity, all_rarities, title, message, + current_index=current_index, max_len_so_far=max_len_so_far, reaction_list=reaction_list, extra_state=ims, @@ -97,7 +102,10 @@ def query(dgcog, series_id, rarity): all_series_monsters = db_context.get_monsters_by_series(series_id) base_monsters_of_rarity = list(filter( lambda m: db_context.graph.monster_is_base(m) and m.rarity == rarity, all_series_monsters)) - return base_monsters_of_rarity + paginated_monsters = [base_monsters_of_rarity[i:i + SeriesScrollViewState.MAX_ITEMS_PER_PANE] + for i in range( + 0, len(base_monsters_of_rarity), SeriesScrollViewState.MAX_ITEMS_PER_PANE)] + return paginated_monsters @staticmethod def query_all_rarities(dgcog, series_id): @@ -105,6 +113,13 @@ def query_all_rarities(dgcog, series_id): return sorted({m.rarity for m in db_context.get_all_monsters() if m.series_id == series_id and db_context.graph.monster_is_base(m)}) + @staticmethod + def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: + series_id = ims['series_id'] + rarity = ims['rarity'] + paginated_monsters = SeriesScrollViewState.query(dgcog, series_id, rarity) + return paginated_monsters + class SeriesScrollView: VIEW_TYPE = 'SeriesScroll' @@ -112,13 +127,19 @@ class SeriesScrollView: @staticmethod def embed(state: SeriesScrollViewState): fields = [ - EmbedField(SeriesScrollView._rarity_text(state.rarity), + EmbedField(BoldText('Current rarity: {}'.format(state.rarity)), Box(*SeriesScrollView._monster_list( - state.monster_list)) if state.monster_list else Box( - 'No monsters of this rarity to display')), - EmbedField('**All rarities**', - Box(SeriesScrollView._all_rarity_text(state.all_rarities, state.rarity)) + state.monster_list, + state.current_index))), + EmbedField(BoldText('Rarities'), + Box( + SeriesScrollView._all_rarity_text(state), + ), inline=True ), + EmbedField(BoldText('Page'), + Box('{} of {}'.format(state.current_page + 1, state.pages_in_rarity)), + inline=True + ) ] return EmbedView( @@ -130,20 +151,30 @@ def embed(state: SeriesScrollViewState): embed_fields=fields) @staticmethod - def _rarity_text(this_rarity): - return 'Current rarity: {}'.format( - str(this_rarity) - ) + def _all_rarity_text(state): + lines = [] + for r in state.all_rarities: + if r != state.rarity: + lines.append(str(r)) + else: + lines.append('**{}**'.format(state.rarity)) + return ', '.join(lines) @staticmethod - def _all_rarity_text(all_rarities, this_rarity): - return ', '.join([str(r) if r != this_rarity else '**{}**'.format(str(r)) for r in all_rarities]) - - @staticmethod - def _monster_list(monsters): + def _monster_list(monsters, current_index): if not len(monsters): return [] return [ - MonsterHeader.short_with_emoji(mon, link=True, prefix=char_to_emoji(i)) + MonsterHeader.short_with_emoji( + mon, + link=SeriesScrollView._is_linked(i, current_index), + prefix=char_to_emoji(i) + ) for i, mon in enumerate(monsters) ] + + @staticmethod + def _is_linked(i, current_index): + if current_index is None: + return True + return i != current_index From ca2bedc3acfecad5aa9528fea3f69581aa1e4779 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 26 Feb 2021 17:41:45 -0600 Subject: [PATCH 0668/1220] Fix reaction list filter for incomplete menus (#898) --- padinfo/reaction_list.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index a7a39c24..31854cf9 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -3,7 +3,7 @@ from discordmenu.emoji.emoji_cache import emoji_cache from padinfo.core.padinfo_settings import settings -from padinfo.menu.id import IdMenuPanes, IdMenu +from padinfo.menu.id import IdMenuPanes, IdMenuEmoji from padinfo.view.evos import EvosViewState from padinfo.view.materials import MaterialsViewState from padinfo.view.pantheon import PantheonViewState @@ -21,14 +21,14 @@ async def get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", return full_reaction_list alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) if alt_versions is None: - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_left][0])] = None - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_right][0])] = None - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_evos][0])] = None + full_reaction_list[full_reaction_list.index(IdMenuEmoji.left)] = None + full_reaction_list[full_reaction_list.index(IdMenuEmoji.right)] = None + full_reaction_list[full_reaction_list.index(IdMenuEmoji.evos)] = None pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_pantheon][0])] = None + full_reaction_list[full_reaction_list.index(IdMenuEmoji.pantheon)] = None mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ await MaterialsViewState.query(dgcog, monster) if mats is None: - full_reaction_list[full_reaction_list.index(IdMenuPanes.DATA[IdMenu.respond_with_mats][0])] = None + full_reaction_list[full_reaction_list.index(IdMenuEmoji.mats)] = None return list(filter(None, full_reaction_list)) From 026fb21a814fab278554329981257773961626a4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 26 Feb 2021 17:49:55 -0600 Subject: [PATCH 0669/1220] More reaction list fixes/cleanup (#899) --- padinfo/reaction_list.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index 31854cf9..33151305 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -20,15 +20,14 @@ async def get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", if not force_evoscroll and not settings.checkEvoID(ctx.author.id): return full_reaction_list alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) - if alt_versions is None: - full_reaction_list[full_reaction_list.index(IdMenuEmoji.left)] = None - full_reaction_list[full_reaction_list.index(IdMenuEmoji.right)] = None - full_reaction_list[full_reaction_list.index(IdMenuEmoji.evos)] = None - pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + if alt_versions is None and gem_versions is None: + full_reaction_list.remove(IdMenuEmoji.left) + full_reaction_list.remove(IdMenuEmoji.right) + full_reaction_list.remove(IdMenuEmoji.evos) + pantheon_list, _ = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: - full_reaction_list[full_reaction_list.index(IdMenuEmoji.pantheon)] = None - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ - await MaterialsViewState.query(dgcog, monster) - if mats is None: - full_reaction_list[full_reaction_list.index(IdMenuEmoji.mats)] = None - return list(filter(None, full_reaction_list)) + full_reaction_list.remove(IdMenuEmoji.pantheon) + mats, usedin, gemid, _, skillups, _, _, _ = await MaterialsViewState.query(dgcog, monster) + if mats is None and usedin is None and gemid is None and skillups is None: + full_reaction_list.remove(IdMenuEmoji.mats) + return full_reaction_list From 253d4fa51131ad981c4e5c506344ef3fc59d5ca5 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 27 Feb 2021 00:10:42 -0700 Subject: [PATCH 0670/1220] Pagify whichto so it can't hit char limit (#901) --- padglobal/padglobal.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 8982767a..5f759d8c 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -603,8 +603,8 @@ async def which(self, ctx, *, term: str = None): if not success: await ctx.send('Which {}\n{}'.format(name, definition)) return - await ctx.send('Which {} - Last Updated {}'.format(name, timestamp)) - await ctx.send(self.emojify(definition)) + for page in pagify('Which {} - Last Updated {}\n{}'.format(name, timestamp, self.emojify(definition))): + await ctx.send(page) async def _resolve_which(self, ctx, term): db_context = self.bot.get_cog('Dadguide').database @@ -684,7 +684,8 @@ async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, t result = "{} asked me to send you this:\n{}".format( ctx.author.name, result_output) - await to_user.send(result) + for page in pagify(result): + await to_user.send(page) msg = "Sent info on {} to {}".format(name, to_user.name) await ctx.send(inline(msg)) From f059efaf89b54ae47a33905fabc248c089a806d7 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 27 Feb 2021 00:21:25 -0700 Subject: [PATCH 0671/1220] Use find_monster to lookup bosses (#902) * Use find_monster to lookup bosses Before you merge this, yell at Aradia to convert current bosses * Talk when invalid monster * Return after error msg * don't inline custom emotes --- padglobal/padglobal.py | 71 +++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 5f759d8c..5cb51e0a 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -500,10 +500,8 @@ async def rmglossary(self, ctx, *, term): async def boss(self, ctx, *, term: str = None): """Shows boss skill entries""" if term: - term_new, definition = self.lookup_boss(term) + name, definition = await self.lookup_boss(term) if definition: - if term_new != term.lower(): - await ctx.send('No entry for {} found, corrected to {}'.format(term, term_new)) await ctx.send(self.emojify(definition)) else: await ctx.send(inline('No mechanics found')) @@ -520,22 +518,24 @@ async def bosslist(self, ctx): for page in pagify(msg): await ctx.author.send(page) - def lookup_boss(self, term): - bosses = self.settings.boss() - term = term.lower() - definition = bosses.get(term, None) - if definition: - return term, definition - matches = self._get_corrected_cmds(term, bosses.keys()) + async def lookup_boss(self, term): + dgcog = self.bot.get_cog('Dadguide') + pdicog = self.bot.get_cog("PadInfo") + + term = term.lower().replace('?', '') + m = await lookup_monster_model(term) + if m is None: + return None, None + + m = dgcog.database.graph.get_base_monster(m) + name = pdicog.get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() + monster_id = m.monster_id + definition = self.settings.boss().get(monster_id, None) + + return name, definition + - if not matches: - matches = difflib.get_close_matches(term, bosses.keys(), n=1, cutoff=.8) - if not matches: - return term, None - else: - term = matches[0] - return term, bosses[term] def boss_to_text(self, ctx): bosses = self.settings.boss() @@ -557,30 +557,52 @@ def boss_to_text_index(self, ctx): async def addboss(self, ctx, term, *, definition): """Adds a set of boss mechanics. If you want to use a multiple word boss name, enclose it in quotes.""" + dgcog = self.bot.get_cog('Dadguide') + pdicog = self.bot.get_cog("PadInfo") + term = term.lower() - op = 'EDITED' if term in self.settings.boss() else 'ADDED' + m = await lookup_monster_model(term) + if m is None: + await ctx.send(f"No monster found for `{term}`") + return + + base = dgcog.database.graph.get_base_monster(m) + + + op = 'EDITED' if base.monster_id in self.settings.boss() else 'ADDED' if op == 'EDITED' and not await confirm_message(ctx, "Are you sure you want to edit the boss info for {}?".format( - term)): + base.name_en)): return definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') definition = replace_emoji_names_with_code(self._get_emojis(), definition) - self.settings.addBoss(term, definition) + self.settings.addBoss(base.monster_id, definition) await ctx.send("PAD boss mechanics successfully {}.".format(op)) @padglobal.command() async def rmboss(self, ctx, *, term): """Adds a set of boss mechanics.""" + dgcog = self.bot.get_cog('Dadguide') + pdicog = self.bot.get_cog("PadInfo") + term = term.lower() - if term not in self.settings.boss(): + m = await lookup_monster_model(term) + if m is None: + await ctx.send(f"No monster found for `{term}`. Make sure you didn't use quotes.") + return + + base = dgcog.database.graph.get_base_monster(m) + + if base.monster_id not in self.settings.boss(): await ctx.send("Boss mechanics item doesn't exist.") return if not await confirm_message(ctx, - "Are you sure you want to globally remove the boss data for {}?".format(term)): + "Are you sure you want to globally remove the boss data for {}?".format( + base.name_en)): return - self.settings.rmBoss(term) + self.settings.rmBoss(base.monster_id) await ctx.tick() @commands.command() @@ -686,8 +708,7 @@ async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, t ctx.author.name, result_output) for page in pagify(result): await to_user.send(page) - msg = "Sent info on {} to {}".format(name, to_user.name) - await ctx.send(inline(msg)) + await ctx.send("Sent info on {} to {}".format(name, to_user.name)) @padglobal.command() async def addwhich(self, ctx, monster_id: int, *, definition): From 21c356f440099f07ba9ae101c38046ee92c938df Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 27 Feb 2021 06:30:06 -0600 Subject: [PATCH 0672/1220] Refactor new menu test_reaction_add listener (#900) * pull out child menu * Refactor * optimize imports * fix formatting --- padinfo/menu/menu_maps.py | 30 ++++++++++++ padinfo/padinfo.py | 96 ++++++++++++++++----------------------- 2 files changed, 69 insertions(+), 57 deletions(-) create mode 100644 padinfo/menu/menu_maps.py diff --git a/padinfo/menu/menu_maps.py b/padinfo/menu/menu_maps.py new file mode 100644 index 00000000..23ef4cbc --- /dev/null +++ b/padinfo/menu/menu_maps.py @@ -0,0 +1,30 @@ +from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes +from padinfo.menu.id import IdMenu, IdMenuPanes +from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes +from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes +from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes +from padinfo.menu.series_scroll import SeriesScrollMenu, SeriesScrollMenuPanes +from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes +from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes + +menu_to_panes_map = { + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenuPanes, + IdMenu.MENU_TYPE: IdMenuPanes, + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes, + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenuPanes, + MonsterListMenu.MENU_TYPE: MonsterListMenuPanes, + SeriesScrollMenu.MENU_TYPE: SeriesScrollMenuPanes, + SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes, + TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes, +} + +menu_map = { + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, + IdMenu.MENU_TYPE: IdMenu, + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu, + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu, + MonsterListMenu.MENU_TYPE: MonsterListMenu, + SeriesScrollMenu.MENU_TYPE: SeriesScrollMenu, + SimpleTextMenu.MENU_TYPE: SimpleTextMenu, + TransformInfoMenu.MENU_TYPE: TransformInfoMenu, +} diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9911fd8d..70627ec9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -33,14 +33,15 @@ from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.idtest_mixin import IdTest -from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes +from padinfo.menu.closable_embed import ClosableEmbedMenu from padinfo.menu.id import IdMenu, IdMenuPanes -from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes -from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes +from padinfo.menu.leader_skill import LeaderSkillMenu +from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu +from padinfo.menu.menu_maps import menu_map, menu_to_panes_map from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji -from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes -from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes +from padinfo.menu.simple_text import SimpleTextMenu +from padinfo.menu.transforminfo import TransformInfoMenu from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.components.monster.header import MonsterHeader @@ -155,28 +156,8 @@ def get_monster(self, monster_id: int): @commands.Cog.listener('on_reaction_add') async def test_reaction_add(self, reaction, member): - emoji_obj = reaction.emoji - if isinstance(emoji_obj, str): - emoji_clicked = emoji_obj - else: - emoji_clicked = emoji_obj.name - - menu_to_panes_map = { - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenuPanes, - IdMenu.MENU_TYPE: IdMenuPanes, - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes, - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenuPanes, - MonsterListMenu.MENU_TYPE: MonsterListMenuPanes, - SeriesScrollMenu.MENU_TYPE: SeriesScrollMenuPanes, - SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes, - TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes, - } - - emoji_recognized = emoji_clicked in EmbedMenuEmojiConfig().to_list() - for menu_type, panes_type in menu_to_panes_map.items(): - if emoji_clicked in panes_type.emoji_names(): - emoji_recognized = True - if not emoji_recognized: + emoji_clicked = self.get_emoji_clicked(reaction) + if not emoji_clicked: return message = reaction.message @@ -185,40 +166,40 @@ async def test_reaction_add(self, reaction, member): return menu_type = ims['menu_type'] - menu_map = { - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, - IdMenu.MENU_TYPE: IdMenu, - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu, - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu, - MonsterListMenu.MENU_TYPE: MonsterListMenu, - SeriesScrollMenu.MENU_TYPE: SeriesScrollMenu, - SimpleTextMenu.MENU_TYPE: SimpleTextMenu, - TransformInfoMenu.MENU_TYPE: TransformInfoMenu, - } - - menu_1_class = menu_map.get(menu_type) - if not menu_1_class: + menu_class = menu_map.get(menu_type) + if not menu_class: return + menu = menu_class.menu() - menu_func = menu_1_class.menu - - menu = menu_func() - - if not (await menu.should_respond( - message, reaction, await self.get_reaction_filters( - ims['original_author_id'], menu_to_panes_map[menu_type].emoji_names()), member)): + if not (await menu.should_respond(message, reaction, + await self.get_reaction_filters(ims['original_author_id'], menu_type), + member)): return - dgcog = await self.get_dgcog() - user_config = await BotConfig.get_user(self.config, ims['original_author_id']) data = { - 'dgcog': dgcog, - 'user_config': user_config + 'dgcog': await self.get_dgcog(), + 'user_config': await BotConfig.get_user(self.config, ims['original_author_id']) } - menu_1_ims = deepcopy(ims) - message_1 = message + await menu.transition(message, deepcopy(ims), emoji_clicked, member, **data) + await self.listener_respond_with_child(deepcopy(ims), message, emoji_clicked, member, data) + + @staticmethod + def get_emoji_clicked(reaction): + emoji_obj = reaction.emoji + if isinstance(emoji_obj, str): + emoji_clicked = emoji_obj + else: + emoji_clicked = emoji_obj.name + + emoji_recognized = emoji_clicked in EmbedMenuEmojiConfig().to_list() + for menu_type, panes_type in menu_to_panes_map.items(): + if emoji_clicked in panes_type.emoji_names(): + return emoji_clicked + if not emoji_recognized: + return False + + async def listener_respond_with_child(self, menu_1_ims, message_1, emoji_clicked, member, data): failsafe = 0 - await menu.transition(message, ims, emoji_clicked, member, **data) while menu_1_ims.get('child_message_id'): # before this loop can actually work as a loop, the type of menu_2 can't be hard-coded as IdMenu anymore, # and we have to update menu_1_class to be menu_2_class during the loop. @@ -226,7 +207,8 @@ async def test_reaction_add(self, reaction, member): break failsafe += 1 menu_2 = IdMenu.menu() - child_data_func = menu_to_panes_map[menu_type].get_child_data_func(emoji_clicked) + panes_class = menu_to_panes_map[menu_1_ims['menu_type']] + child_data_func = panes_class.get_child_data_func(emoji_clicked) emoji_simulated_clicked_2, extra_ims = None, {} if child_data_func is not None: emoji_simulated_clicked_2, extra_ims = child_data_func(menu_1_ims, emoji_clicked, **data) @@ -242,11 +224,11 @@ async def test_reaction_add(self, reaction, member): menu_1_ims = menu_2_ims message_1 = message_2 - async def get_reaction_filters(self, original_author_id, valid_emoji_names): + async def get_reaction_filters(self, original_author_id, menu_type): friend_cog = self.bot.get_cog("Friend") friend_ids = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] reaction_filters = [ - ValidEmojiReactionFilter(valid_emoji_names), + ValidEmojiReactionFilter(menu_to_panes_map[menu_type].emoji_names()), NotPosterEmojiReactionFilter(), BotAuthoredMessageReactionFilter(self.bot.user.id), MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) From 5cb51cead05c1d3ea3432c19b5b7cb9f6cddd156 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 27 Feb 2021 14:57:30 -0600 Subject: [PATCH 0673/1220] Add ims exceptions, refactor listener more (#903) * Add errors, refactor listener more * Return None instead of False on invalid emoji --- padinfo/menu/errors.py | 6 +++++ padinfo/menu/menu_maps.py | 42 +++++++++++++++++++++++--------- padinfo/padinfo.py | 51 +++++++++++++++++++-------------------- 3 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 padinfo/menu/errors.py diff --git a/padinfo/menu/errors.py b/padinfo/menu/errors.py new file mode 100644 index 00000000..60333a8e --- /dev/null +++ b/padinfo/menu/errors.py @@ -0,0 +1,6 @@ +class MissingImsMenuType(Exception): + pass + + +class InvalidImsMenuType(Exception): + pass diff --git a/padinfo/menu/menu_maps.py b/padinfo/menu/menu_maps.py index 23ef4cbc..7c214f59 100644 --- a/padinfo/menu/menu_maps.py +++ b/padinfo/menu/menu_maps.py @@ -1,4 +1,5 @@ from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes +from padinfo.menu.errors import InvalidImsMenuType, MissingImsMenuType from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes @@ -7,7 +8,17 @@ from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes -menu_to_panes_map = { +type_to_menu = { + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, + IdMenu.MENU_TYPE: IdMenu, + LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu, + LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu, + MonsterListMenu.MENU_TYPE: MonsterListMenu, + SeriesScrollMenu.MENU_TYPE: SeriesScrollMenu, + SimpleTextMenu.MENU_TYPE: SimpleTextMenu, + TransformInfoMenu.MENU_TYPE: TransformInfoMenu, +} +type_to_panes = { ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenuPanes, IdMenu.MENU_TYPE: IdMenuPanes, LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes, @@ -18,13 +29,22 @@ TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes, } -menu_map = { - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, - IdMenu.MENU_TYPE: IdMenu, - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu, - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu, - MonsterListMenu.MENU_TYPE: MonsterListMenu, - SeriesScrollMenu.MENU_TYPE: SeriesScrollMenu, - SimpleTextMenu.MENU_TYPE: SimpleTextMenu, - TransformInfoMenu.MENU_TYPE: TransformInfoMenu, -} + +def get_menu(ims): + menu_type = ims.get('menu_type') + if menu_type is None: + raise MissingImsMenuType + menu = type_to_menu.get(menu_type) + if menu is None: + raise InvalidImsMenuType + return menu.menu() + + +def get_panes_class(ims): + menu_type = ims.get('menu_type') + if menu_type is None: + raise MissingImsMenuType + panes = type_to_panes.get(menu_type) + if panes is None: + raise InvalidImsMenuType + return panes diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 70627ec9..961f086b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -37,7 +37,7 @@ from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu -from padinfo.menu.menu_maps import menu_map, menu_to_panes_map +from padinfo.menu.menu_maps import type_to_panes, get_panes_class, get_menu from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji from padinfo.menu.simple_text import SimpleTextMenu @@ -157,31 +157,19 @@ def get_monster(self, monster_id: int): @commands.Cog.listener('on_reaction_add') async def test_reaction_add(self, reaction, member): emoji_clicked = self.get_emoji_clicked(reaction) - if not emoji_clicked: + if emoji_clicked is None: return message = reaction.message ims = message.embeds and IntraMessageState.extract_data(message.embeds[0]) if not ims: return - - menu_type = ims['menu_type'] - menu_class = menu_map.get(menu_type) - if not menu_class: - return - menu = menu_class.menu() - - if not (await menu.should_respond(message, reaction, - await self.get_reaction_filters(ims['original_author_id'], menu_type), - member)): + menu = get_menu(ims) + if not (await menu.should_respond(message, reaction, await self.get_reaction_filters(ims), member)): return - data = { - 'dgcog': await self.get_dgcog(), - 'user_config': await BotConfig.get_user(self.config, ims['original_author_id']) - } - await menu.transition(message, deepcopy(ims), emoji_clicked, member, **data) - await self.listener_respond_with_child(deepcopy(ims), message, emoji_clicked, member, data) + await menu.transition(message, deepcopy(ims), emoji_clicked, member, **(await self.get_menu_default_data(ims))) + await self.listener_respond_with_child(deepcopy(ims), message, emoji_clicked, member) @staticmethod def get_emoji_clicked(reaction): @@ -191,14 +179,16 @@ def get_emoji_clicked(reaction): else: emoji_clicked = emoji_obj.name - emoji_recognized = emoji_clicked in EmbedMenuEmojiConfig().to_list() - for menu_type, panes_type in menu_to_panes_map.items(): + # determine if this is potentially a valid reaction prior to doing any network call: + # this is true if it's a default emoji or in any of our global panes emoji lists + if emoji_clicked in EmbedMenuEmojiConfig().to_list(): + return emoji_clicked + for menu_type, panes_type in type_to_panes.items(): if emoji_clicked in panes_type.emoji_names(): return emoji_clicked - if not emoji_recognized: - return False + return None - async def listener_respond_with_child(self, menu_1_ims, message_1, emoji_clicked, member, data): + async def listener_respond_with_child(self, menu_1_ims, message_1, emoji_clicked, member): failsafe = 0 while menu_1_ims.get('child_message_id'): # before this loop can actually work as a loop, the type of menu_2 can't be hard-coded as IdMenu anymore, @@ -207,8 +197,9 @@ async def listener_respond_with_child(self, menu_1_ims, message_1, emoji_clicked break failsafe += 1 menu_2 = IdMenu.menu() - panes_class = menu_to_panes_map[menu_1_ims['menu_type']] + panes_class = type_to_panes[menu_1_ims['menu_type']] child_data_func = panes_class.get_child_data_func(emoji_clicked) + data = await self.get_menu_default_data(menu_1_ims) emoji_simulated_clicked_2, extra_ims = None, {} if child_data_func is not None: emoji_simulated_clicked_2, extra_ims = child_data_func(menu_1_ims, emoji_clicked, **data) @@ -224,17 +215,25 @@ async def listener_respond_with_child(self, menu_1_ims, message_1, emoji_clicked menu_1_ims = menu_2_ims message_1 = message_2 - async def get_reaction_filters(self, original_author_id, menu_type): + async def get_reaction_filters(self, ims): + original_author_id = ims['original_author_id'] friend_cog = self.bot.get_cog("Friend") friend_ids = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] reaction_filters = [ - ValidEmojiReactionFilter(menu_to_panes_map[menu_type].emoji_names()), + ValidEmojiReactionFilter(get_panes_class(ims).emoji_names()), NotPosterEmojiReactionFilter(), BotAuthoredMessageReactionFilter(self.bot.user.id), MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) ] return reaction_filters + async def get_menu_default_data(self, ims): + data = { + 'dgcog': await self.get_dgcog(), + 'user_config': await BotConfig.get_user(self.config, ims['original_author_id']) + } + return data + @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" From 482e3a0d2d35dd299bffdb61baaa65e391ce9674 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 28 Feb 2021 11:59:10 -0600 Subject: [PATCH 0674/1220] Add list of evos to every tab of id (#904) --- padinfo/padinfo.py | 25 ++++---- padinfo/view/components/view_state_base_id.py | 15 ++++- padinfo/view/evos.py | 10 +++- padinfo/view/id.py | 57 ++++++++++--------- padinfo/view/materials.py | 9 ++- padinfo/view/otherinfo.py | 6 +- padinfo/view/pantheon.py | 17 +++--- padinfo/view/pic.py | 5 +- 8 files changed, 87 insertions(+), 57 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 961f086b..220c7bc3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -317,15 +317,15 @@ async def send_message(): if await self.config.do_survey(): asyncio.create_task(self.send_survey_after(ctx, query, monster)) - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + alt_monsters = IdViewState.get_alt_monsters(dgcog, monster) + transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ await IdViewState.query(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, - use_evo_scroll=settings.checkEvoID(ctx.author.id), - reaction_list=initial_reaction_list) + state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, + use_evo_scroll=settings.checkEvoID(ctx.author.id), reaction_list=initial_reaction_list) menu = IdMenu.menu() await menu.create(ctx, state) @@ -409,7 +409,7 @@ async def evos(self, ctx, *, query: str): initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, alt_versions, gem_versions, + monster, alt_versions, alt_versions, gem_versions, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.evos_control) @@ -436,10 +436,11 @@ async def evomats(self, ctx, *, query: str): await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) return + alt_monsters = MaterialsViewState.get_alt_monsters(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) @@ -465,12 +466,12 @@ async def pantheon(self, ctx, *, query: str): if pantheon_list is None: await ctx.send(inline('Too many monsters in this series to display')) return - + alt_monsters = PantheonViewState.get_alt_monsters(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, pantheon_list, series_name, + monster, alt_monsters, pantheon_list, series_name, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.pantheon_control) @@ -491,11 +492,12 @@ async def pic(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return + alt_monsters = PicViewState.get_alt_monsters(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, + monster, alt_monsters, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.pic_control) @@ -516,11 +518,12 @@ async def otherinfo(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return + alt_monsters = PicViewState.get_alt_monsters(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, + monster, alt_monsters, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.otherinfo_control) diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 8edecac2..7686f68a 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -9,9 +9,11 @@ class ViewStateBaseId: def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + alt_monsters: List["MonsterModel"], use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): + self.alt_monsters = alt_monsters self.extra_state = extra_state or {} self.menu_type = menu_type self.original_author_id = original_author_id @@ -30,7 +32,7 @@ def serialize(self): 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), - 'reaction_list': self.reaction_list + 'reaction_list': self.reaction_list, } ret.update(self.extra_state) return ret @@ -41,6 +43,8 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): return None monster = await get_monster_from_ims(dgcog, ims) + alt_monsters = cls.get_alt_monsters(dgcog, monster) + raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -48,6 +52,13 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) + + @classmethod + def get_alt_monsters(cls, dgcog, monster): + db_context = dgcog.database + alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + key=lambda x: x.monster_id) + return alt_monsters diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index c46c8ed7..720353b7 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -6,11 +6,11 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx +from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId -from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -19,11 +19,14 @@ class EvosViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + + # this param is needed to placate the superclass but we won't duplicate evos in this view + _alt_monsters, alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], reaction_list: List[str] = None, use_evo_scroll: bool = True, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_versions, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -46,7 +49,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if alt_versions is None: return None - + alt_monsters = alt_versions raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -56,6 +59,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + alt_monsters, alt_versions, gem_versions, reaction_list=reaction_list, use_evo_scroll=use_evo_scroll, diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 53783353..42e9cac6 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -9,11 +9,11 @@ from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createMultiplierText +from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId -from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -22,20 +22,17 @@ class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters: List["MonsterModel"], - fallback_message: str = None, - use_evo_scroll: bool = True, - reaction_list: List[str] = None, - is_child: bool = False, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + alt_monsters: List["MonsterModel"], + transform_base, true_evo_type_raw, acquire_raw, base_rarity, + fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, + is_child: bool = False, extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) self.fallback_message = fallback_message self.is_child = is_child self.acquire_raw = acquire_raw - self.alt_monsters = alt_monsters self.base_rarity = base_rarity self.transform_base = transform_base self.true_evo_type_raw = true_evo_type_raw @@ -55,7 +52,8 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None monster = await get_monster_from_ims(dgcog, ims) - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters = \ + alt_monsters = cls.get_alt_monsters(dgcog, monster) + transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ await IdViewState.query(dgcog, monster) raw_query = ims['raw_query'] @@ -68,31 +66,29 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): fallback_message = ims.get('message') is_child = ims.get('is_child') - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, fallback_message=fallback_message, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, is_child=is_child, extra_state=ims) - @staticmethod - async def query(dgcog, monster): + @classmethod + async def query(cls, dgcog, monster): db_context = dgcog.database - acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw = \ + acquire_raw, base_rarity, transform_base, true_evo_type_raw = \ await IdViewState._get_monster_misc_info(db_context, monster) - return transform_base, true_evo_type_raw, acquire_raw, base_rarity, alt_monsters + return transform_base, true_evo_type_raw, acquire_raw, base_rarity - @staticmethod - async def _get_monster_misc_info(db_context, monster): + @classmethod + async def _get_monster_misc_info(cls, db_context, monster): transform_base = db_context.graph.get_transform_base(monster) true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value acquire_raw = db_context.graph.monster_acquisition(monster) base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity - alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, - key=lambda x: x.monster_id) - return acquire_raw, alt_monsters, base_rarity, transform_base, true_evo_type_raw + return acquire_raw, base_rarity, transform_base, true_evo_type_raw def _get_awakening_text(awakening: "AwakeningModel"): @@ -120,6 +116,17 @@ def _monster_is_enhance(m: "MonsterModel"): return any(x if x.name == 'Enhance' else None for x in m.types) +def evos_embed_field(state: ViewStateBaseId): + m = state.monster + return EmbedField( + "Alternate Evos", + HighlightableLinks( + links=[LinkedText(str(m.monster_no_na), puzzledragonx(m)) for m in state.alt_monsters], + highlighted=next(i for i, mon in enumerate(state.alt_monsters) if m.monster_id == mon.monster_id) + ) + ) + + class IdView: VIEW_TYPE = 'Id' @@ -266,13 +273,7 @@ def embed(state: IdViewState): IdView.leader_skill_header(m).to_markdown(), Text(m.leader_skill.desc if m.leader_skill else 'None') ), - EmbedField( - "Alternate Evos", - HighlightableLinks( - links=[LinkedText(str(m.monster_no_na), puzzledragonx(m)) for m in state.alt_monsters], - highlighted=next(i for i, mon in enumerate(state.alt_monsters) if m.monster_id == mon.monster_id) - ) - ) + evos_embed_field(state) ] return EmbedView( diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index a852b388..77452f3d 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -13,6 +13,7 @@ from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId from padinfo.view.common import get_monster_from_ims +from padinfo.view.id import evos_embed_field if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -22,13 +23,14 @@ class MaterialsViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + alt_monsters: List["MonsterModel"], mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, gem_override: bool, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, reaction_list=reaction_list, use_evo_scroll=use_evo_scroll, extra_state=extra_state) @@ -60,6 +62,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if mats is None: return None + alt_monsters = cls.get_alt_monsters(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query menu_type = ims['menu_type'] @@ -67,7 +70,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = ims.get('reaction_list') - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, @@ -165,5 +168,5 @@ def embed(state: MaterialsViewState): if state.gemusedin else None, skillup_field(state.skillups, state.skillup_evo_count, state.link) if not (state.monster.is_stackable or state.gem_override) else None - ] if f is not None] + ] if f is not None] + [evos_embed_field(state)] ) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 7cb71332..35e0fb60 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -10,8 +10,9 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.links import LinksView from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.id import evos_embed_field +from padinfo.view.links import LinksView if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -79,4 +80,5 @@ def embed(state: OtherInfoViewState): Box( LabeledText("Rarity", str(m.rarity)), LabeledText("Cost", str(m.cost)), - delimiter=' ')))]) + delimiter=' '))), + evos_embed_field(state)]) diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index d8be6c5d..0e2614fb 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -6,23 +6,24 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx +from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId -from padinfo.view.common import get_monster_from_ims +from padinfo.view.id import evos_embed_field if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel class PantheonViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters, pantheon_list: List["MonsterModel"], series_name: str, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -46,6 +47,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if pantheon_list is None: return None + alt_monsters = cls.get_alt_monsters(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -53,14 +55,14 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, pantheon_list, series_name, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) - @staticmethod - async def query(dgcog, monster): + @classmethod + async def query(cls, dgcog, monster): db_context = dgcog.database full_pantheon = db_context.get_monsters_by_series(monster.series_id) if not full_pantheon: @@ -85,7 +87,8 @@ def embed(state: PantheonViewState): *[MonsterHeader.short_with_emoji(m) for m in sorted(state.pantheon_list, key=lambda x: x.monster_no_na)] ) - )] + ), + evos_embed_field(state)] return EmbedView( EmbedMain( diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index cd401b5e..8a741819 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -8,6 +8,7 @@ from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.id import evos_embed_field class PicViewState(ViewStateBaseId): @@ -45,7 +46,9 @@ def embed(state: PicViewState): delimiter=' ' ) if state.monster.orb_skin_id else None, ) - )] + ), + evos_embed_field(state) + ] return EmbedView( EmbedMain( From 6ea536a6beed96ed9d50a997d7571a27aef0b95b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 1 Mar 2021 11:37:55 -0600 Subject: [PATCH 0675/1220] Use Emoji class instead of PaneNames (#906) There was a class "PaneNames" defined for some menus that was now doing basically the same thing as the new Emoji class, I refactored slightly to use Emoji instead for the purpose of seeing if emoji were to be hidden. Even though several menus were touched, the impact is very minimal and no one should really notice anything different. --- padinfo/menu/common.py | 2 +- padinfo/menu/id.py | 13 ++++--------- padinfo/menu/monster_list.py | 20 +++++++------------- padinfo/menu/series_scroll.py | 18 ++++++------------ 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/padinfo/menu/common.py b/padinfo/menu/common.py index b808b24d..38aaed2c 100644 --- a/padinfo/menu/common.py +++ b/padinfo/menu/common.py @@ -13,7 +13,7 @@ class MenuPanes: @classmethod def emoji_names(cls): - return [k for k, v in cls.DATA.items() if v[1] not in cls.HIDDEN_EMOJIS] + return [k for k, v in cls.DATA.items() if k not in cls.HIDDEN_EMOJIS] @classmethod def transitions(cls): diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index a664cffe..0c16644a 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -222,11 +222,6 @@ def otherinfo_control(state: OtherInfoViewState): ) -class IdMenuPaneNames: - refresh = 'refresh' - delete = 'delete' - - class IdMenuEmoji: left = '\N{BLACK LEFT-POINTING TRIANGLE}' right = '\N{BLACK RIGHT-POINTING TRIANGLE}' @@ -250,10 +245,10 @@ class IdMenuPanes(MenuPanes): IdMenuEmoji.pic: (IdMenu.respond_with_picture, PicView.VIEW_TYPE), IdMenuEmoji.pantheon: (IdMenu.respond_with_pantheon, PantheonView.VIEW_TYPE), IdMenuEmoji.otherinfo: (IdMenu.respond_with_otherinfo, OtherInfoView.VIEW_TYPE), - IdMenuEmoji.refresh: (IdMenu.respond_with_refresh, IdMenuPaneNames.refresh), - IdMenuEmoji.delete: (IdMenu.respond_with_delete, IdMenuPaneNames.delete), + IdMenuEmoji.refresh: (IdMenu.respond_with_refresh, None), + IdMenuEmoji.delete: (IdMenu.respond_with_delete, None), } HIDDEN_EMOJIS = [ - IdMenuPaneNames.refresh, - IdMenuPaneNames.delete, + IdMenuEmoji.refresh, + IdMenuEmoji.delete, ] diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index e99aad5d..4db55f60 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -5,17 +5,11 @@ from tsutils import char_to_emoji from padinfo.menu.common import emoji_buttons, MenuPanes -from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuPaneNames, IdMenuEmoji +from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView from padinfo.view.monster_list import MonsterListView, MonsterListViewState -class MonsterListPaneNames: - home = 'home' - refresh = 'refresh' - reset = 'reset' - - class MonsterListEmoji: home = emoji_buttons['home'] zero = char_to_emoji('0') @@ -142,7 +136,7 @@ class MonsterListMenuPanes(MenuPanes): INITIAL_EMOJI = MonsterListEmoji.home DATA = { # tuple parts: parent_response, pane_type, respond_with_child - MonsterListEmoji.home: (MonsterListMenu.respond_with_monster_list, MonsterListPaneNames.home, None), + MonsterListEmoji.home: (MonsterListMenu.respond_with_monster_list, MonsterListEmoji.home, None), MonsterListEmoji.zero: ( MonsterListMenu.respond_with_0, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), MonsterListEmoji.one: ( @@ -166,14 +160,14 @@ class MonsterListMenuPanes(MenuPanes): MonsterListEmoji.ten: ( MonsterListMenu.respond_with_10, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), MonsterListEmoji.refresh: ( - MonsterListMenu.respond_with_refresh, MonsterListPaneNames.refresh, None), + MonsterListMenu.respond_with_refresh, None, None), MonsterListEmoji.reset: ( - MonsterListMenu.respond_with_reset, MonsterListPaneNames.reset, None) + MonsterListMenu.respond_with_reset, None, None) } HIDDEN_EMOJIS = [ - MonsterListPaneNames.home, - MonsterListPaneNames.refresh, - MonsterListPaneNames.reset, + MonsterListEmoji.home, + MonsterListEmoji.refresh, + MonsterListEmoji.reset, ] @classmethod diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index c0c78436..26d46d32 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -11,12 +11,6 @@ from padinfo.view.series_scroll import SeriesScrollView, SeriesScrollViewState -class SeriesScrollPaneNames: - home = 'home' - refresh = 'refresh' - reset = 'reset' - - class SeriesScrollEmoji: delete = '\N{CROSS MARK}' home = emoji_buttons['home'] @@ -278,7 +272,7 @@ class SeriesScrollMenuPanes(MenuPanes): DATA = { # tuple parts: emoji, pane_type, respond_with_parent, respond_with_child SeriesScrollEmoji.delete: (SeriesScrollMenu.respond_with_delete, None, None), - SeriesScrollEmoji.home: (SeriesScrollMenu.respond_with_monster_list, SeriesScrollPaneNames.home, None), + SeriesScrollEmoji.home: (SeriesScrollMenu.respond_with_monster_list, None, None), SeriesScrollEmoji.prev_page: (SeriesScrollMenu.respond_with_left, SeriesScrollView.VIEW_TYPE, None), SeriesScrollEmoji.prev_mon: ( SeriesScrollMenu.respond_with_previous_monster, None, SeriesScrollMenu.auto_scroll_child_left), @@ -307,13 +301,13 @@ class SeriesScrollMenuPanes(MenuPanes): SeriesScrollMenu.respond_with_9, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), SeriesScrollEmoji.ten: ( SeriesScrollMenu.respond_with_10, IdView.VIEW_TYPE, SeriesScrollMenu.click_child_number), - SeriesScrollEmoji.refresh: (SeriesScrollMenu.respond_with_refresh, SeriesScrollPaneNames.refresh, None), - SeriesScrollEmoji.reset: (SeriesScrollMenu.respond_with_reset, SeriesScrollPaneNames.reset, None) + SeriesScrollEmoji.refresh: (SeriesScrollMenu.respond_with_refresh, None, None), + SeriesScrollEmoji.reset: (SeriesScrollMenu.respond_with_reset, None, None) } HIDDEN_EMOJIS = [ - SeriesScrollPaneNames.home, - SeriesScrollPaneNames.refresh, - SeriesScrollPaneNames.reset, + SeriesScrollEmoji.home, + SeriesScrollEmoji.refresh, + SeriesScrollEmoji.reset, ] @classmethod From 21dcd4addb280e1f56e6fd1a42829ea4540fd3d5 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Mar 2021 13:49:10 -0700 Subject: [PATCH 0676/1220] Add warning logging in discord to index (#907) --- dadguide/dadguide.py | 49 +++++++++++++++++++++------------------ dadguide/monster_index.py | 3 +++ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index d170a910..350d3a4e 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -9,12 +9,15 @@ import asyncio import os import shutil + +import discord import time from io import BytesIO from typing import Optional import tsutils -from redbot.core import checks, data_manager, commands +from redbot.core import checks, data_manager, commands, Config +from redbot.core.utils.chat_formatting import pagify, box from tsutils import auth_check from . import token_mappings @@ -62,14 +65,15 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self._is_ready = asyncio.Event() - self.settings = DadguideSettings("dadguide") - self.database = None self.index = None # type: Optional[MonsterIndex] self.fir_lock = asyncio.Lock() self.fir3_lock = asyncio.Lock() + self.config = Config.get_conf(self, identifier=64667103) + self.config.register_global(datafile='', indexlog=0) + self.monster_stats = monster_stats self.MonsterStatModifierInput = MonsterStatModifierInput @@ -128,7 +132,17 @@ async def forceindexreload3(self, ctx): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" - self.index = await MonsterIndex(self.database.get_all_monsters(), self.database) + self.index = await MonsterIndex(self.database.get_all_monsters(), self.database) # noqa + await self.check_index() + + async def check_index(self): + if not await self.config.indexlog(): + return + + channel = self.bot.get_channel(await self.config.indexlog()) + if self.index.issues: + for page in pagify("\n".join(self.index.issues[:100])): + await channel.send(box(page)) def get_monster(self, monster_id: int) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" @@ -168,9 +182,9 @@ async def reload_data_task(self): raise ex async def download_and_refresh_nicknames(self): - if self.settings.data_file(): + if await self.config.datafile(): logger.info('Copying dg data file') - shutil.copy2(self.settings.data_file(), DB_DUMP_FILE) + shutil.copy2(await self.config.datafile(), DB_DUMP_FILE) else: logger.info('Downloading dg data files') await self._download_files() @@ -193,22 +207,13 @@ async def dadguide(self, ctx): @dadguide.command() @checks.is_owner() - async def setdatafile(self, ctx, *, data_file): + async def setdatafile(self, ctx, data_file): """Set a local path to dadguide data instead of downloading it.""" - self.settings.set_data_file(data_file) + await self.config.datafile.set(data_file) await ctx.tick() - -class DadguideSettings(tsutils.CogSettings): - def make_default_settings(self): - config = { - 'data_file': '', - } - return config - - def data_file(self): - return self.bot_settings['data_file'] - - def set_data_file(self, data_file): - self.bot_settings['data_file'] = data_file - self.save_settings() + @dadguide.command() + @checks.is_owner() + async def setindexlog(self, ctx, channel: discord.TextChannel): + await self.config.indexlog.set(channel.id) + await ctx.tick() diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 613b64cf..15d2c2cf 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -27,6 +27,8 @@ class MonsterIndex(tsutils.aobject): async def __ainit__(self, monsters, db): self.graph = db.graph + self.issues = [] + self.monster_id_to_nickname = defaultdict(set) self.monster_id_to_nametokens = defaultdict(set) self.monster_id_to_treename = defaultdict(set) @@ -361,6 +363,7 @@ async def get_modifiers(self, m): modifiers.update(AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]) except ValueError: logger.warning(f"Invalid awoken skill ID: {aw.awoken_skill_id}") + self.issues.append(f"Invalid awoken skill ID: {aw.awoken_skill_id}") # Chibi if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or \ From 5b882b0fb193fa2d56e327c2cd83782206abf185 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 1 Mar 2021 14:38:16 -0700 Subject: [PATCH 0677/1220] Run idtest on index reload & inform of failures (#909) --- dadguide/dadguide.py | 7 ++- padinfo/idtest_mixin.py | 95 ++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 350d3a4e..003307a4 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -133,15 +133,18 @@ async def forceindexreload3(self, ctx): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" self.index = await MonsterIndex(self.database.get_all_monsters(), self.database) # noqa - await self.check_index() + asyncio.create_task(self.check_index()) async def check_index(self): if not await self.config.indexlog(): return + if self.bot.get_cog("PadInfo") and not await self.bot.get_cog("PadInfo").run_tests(): + self.index.issues.insert(0, "Test cases failed.") + channel = self.bot.get_channel(await self.config.indexlog()) if self.index.issues: - for page in pagify("\n".join(self.index.issues[:100])): + for page in pagify("Index Load Warnings:\n" + "\n".join(self.index.issues[:100])): await channel.send(box(page)) def get_monster(self, monster_id: int) -> MonsterModel: diff --git a/padinfo/idtest_mixin.py b/padinfo/idtest_mixin.py index 22f7c7a0..773d95e7 100644 --- a/padinfo/idtest_mixin.py +++ b/padinfo/idtest_mixin.py @@ -21,7 +21,7 @@ async def idtest(self, ctx): @idtest.command(name="add") @checks.is_owner() - async def idt_add(self, ctx, id: int, *, query): + async def idt_add(self, ctx, mid: int, *, query): """Add a test for the id3 test suite (Append `| reason` to add a reason)""" query, *reason = query.split("|") query = query.strip() @@ -32,16 +32,16 @@ async def idt_add(self, ctx, id: int, *, query): async with self.config.test_suite() as suite: oldd = suite.get(query, {}) - if oldd.get('result') == id: + if oldd.get('result') == mid: await ctx.send(f"This test case already exists with id `{sorted(suite).index(query)}`.") return suite[query] = { - 'result': id, + 'result': mid, 'ts': datetime.now().timestamp(), 'reason': reason[0].strip() if reason else '' } - if await tsutils.get_reaction(ctx, f"Added test case `{id} - {query}`" + if await tsutils.get_reaction(ctx, f"Added test case `{mid} - {query}`" f" with ref `{sorted(suite).index(query)}`", "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): if oldd: @@ -50,7 +50,7 @@ async def idt_add(self, ctx, id: int, *, query): del suite[query] await ctx.react_quietly("\N{CROSS MARK}") else: - await ctx.send(f"Successfully added test case `{id} - {query}` with ref `{sorted(suite).index(query)}`") + await ctx.send(f"Successfully added test case `{mid} - {query}` with ref `{sorted(suite).index(query)}`") await ctx.tick() @idtest.group(name="name") @@ -63,17 +63,17 @@ async def idt_fluff(self, ctx): @idt_name.command(name="add") @checks.is_owner() - async def idtn_add(self, ctx, id: int, token, *, reason=""): + async def idtn_add(self, ctx, mid: int, token, *, reason=""): """Add a name token test to the id3 test suite""" - await self.norf_add(ctx, id, token, reason, False) + await self.norf_add(ctx, mid, token, reason, False) @idt_fluff.command(name="add") @checks.is_owner() - async def idtf_add(self, ctx, id: int, token, *, reason=""): + async def idtf_add(self, ctx, mid: int, token, *, reason=""): """Add a fluff token test to the id3 test suite""" - await self.norf_add(ctx, id, token, reason, True) + await self.norf_add(ctx, mid, token, reason, True) - async def norf_add(self, ctx, id: int, token, reason, fluffy): + async def norf_add(self, ctx, mid: int, token, reason, fluffy): reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.confirm_message(ctx, @@ -82,16 +82,16 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): await self.config.user(ctx.author).lastaction.set('name') async with self.config.fluff_suite() as suite: - if any(t['id'] == id and t['token'] == token and t['fluff'] == fluffy for t in suite): + if any(t['id'] == mid and t['token'] == token and t['fluff'] == fluffy for t in suite): await ctx.send("This test already exists.") return old = None - if any(t['id'] == id and t['token'] == token for t in suite): - old = [t for t in suite if t['id'] == id and t['token'] == token][0] + if any(t['id'] == mid and t['token'] == token for t in suite): + old = [t for t in suite if t['id'] == mid and t['token'] == token][0] if not await tsutils.confirm_message(ctx, f"Are you sure you want to change" f" the type of test case #{suite.index(old)}" - f" `{id} - {token}` from " + f" `{mid} - {token}` from " f" **{'fluff' if fluffy else 'name'}** to" f" **{'name' if fluffy else 'fluff'}**?"): await ctx.react_quietly("\N{CROSS MARK}") @@ -99,7 +99,7 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): suite.remove(old) case = { - 'id': id, + 'id': mid, 'token': token, 'fluff': fluffy, 'reason': reason, @@ -110,7 +110,7 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) if await tsutils.get_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " - f"case `{id} - {token}` with ref `{suite.index(case)}`", + f"case `{mid} - {token}` with ref `{suite.index(case)}`", "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): suite.pop() if old: @@ -118,7 +118,7 @@ async def norf_add(self, ctx, id: int, token, reason, fluffy): await ctx.react_quietly("\N{CROSS MARK}") else: m = await ctx.send(f"Successfully added {'fluff' if fluffy else 'name'} " - f"case `{id} - {token}` with ref `{suite.index(case)}`") + f"case `{mid} - {token}` with ref `{suite.index(case)}`") await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") @idtest.command(name="import") @@ -287,10 +287,10 @@ async def norf_list(self, ctx, fluff, inclusive): suite = await self.config.fluff_suite() o = "" - for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - if inclusive or v['fluff'] == fluff: - o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ - f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" + for c, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + if inclusive or case['fluff'] == fluff: + o += f"{str(c).rjust(3)}. {case['token'].ljust(10)} - {str(case['id']).ljust(4)}" \ + f"\t{'fluff' if case['fluff'] else 'name '}\t{case.get('reason', '')}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): @@ -327,10 +327,10 @@ async def norf_listnoreason(self, ctx, fluff, inclusive): suite = await self.config.fluff_suite() o = "" - for c, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - if inclusive or v['fluff'] == fluff and not v['reason']: - o += f"{str(c).rjust(3)}. {v['token'].ljust(10)} - {str(v['id']).ljust(4)}" \ - f"\t{'fluff' if v['fluff'] else 'name '}\t{v.get('reason', '')}\n" + for c, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + if inclusive or case['fluff'] == fluff and not case['reason']: + o += f"{str(c).rjust(3)}. {case['token'].ljust(10)} - {str(case['id']).ljust(4)}" \ + f"\t{'fluff' if case['fluff'] else 'name '}\t{case.get('reason', '')}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): @@ -344,8 +344,9 @@ async def idt_listrecent(self, ctx, count: int = 0): count = len(suite) o = "" ml = len(max(suite, key=len)) - for c, kv in enumerate(sorted(suite.items(), key=lambda kv: kv[1].get('ts', 0), reverse=True)[:count]): - o += f"{kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + for c, kv_tuple in enumerate(sorted(suite.items(), key=lambda kv: kv[1].get('ts', 0), reverse=True)[:count]): + key, val = kv_tuple + o += f"{key.ljust(ml)} - {str(val['result']).ljust(4)}\t{val.get('reason') or ''}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): @@ -401,13 +402,13 @@ async def norf_run(self, ctx): o = "" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' async with ctx.typing(): - for i, v in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.name_tokens[v['token']]] + for i, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + fluff = case['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.fluff_tokens[case['token']]] + name = case['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.name_tokens[case['token']]] - if (v['fluff'] and not fluff) or (not v['fluff'] and not name): - q = '"{}"'.format(v['token']) - o += f"{i}. {str(v['id']).ljust(4)} {q.ljust(10)} - " \ + if (case['fluff'] and not fluff) or (not case['fluff'] and not name): + q = '"{}"'.format(case['token']) + o += f"{i}. {str(case['id']).ljust(4)} {q.ljust(10)} - " \ f"{ycircle if name or fluff else rcircle} " \ f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: @@ -449,8 +450,8 @@ async def idt_runall(self, ctx): fc = 0 async with ctx.typing(): for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in dgcog.index.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in dgcog.index.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) @@ -472,3 +473,27 @@ async def idt_runall(self, ctx): o += "\n\n\N{LARGE GREEN CIRCLE} \N{LARGE GREEN CIRCLE} All tests succeeded!!" for page in pagify(o): await ctx.send(box(page)) + + async def run_tests(self): + dgcog = await self.get_dgcog() + + qsuite = await self.config.test_suite() + for q in qsuite: + try: + m = await find_monster(dgcog, q) or -1 + except Exception: + m = -2 + mid = getattr(m, "monster_id", m) + + if mid != qsuite[q]['result']: + return False + + fsuite = await self.config.fluff_suite() + for v in fsuite: + fluff = v['id'] in [m.monster_id for m in dgcog.index.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in dgcog.index.name_tokens[v['token']]] + + if (v['fluff'] and not fluff) or (not v['fluff'] and not name): + return False + + return True From 2dd9883f84d7aca6e15d4b3a8db6ed187a6f9c40 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Wed, 3 Mar 2021 09:46:55 -0500 Subject: [PATCH 0678/1220] Enable further transform emojis in transforminfo (#911) * Actually use the list of monster ids returned from tfinfo query * Fix acquire display bug introduced in last commit * Add respond with n method and rest of emojis to tf menu * Implement extra emojis and respond_with_n for tfinfo * Dynamically choose which transform emoji to display --- padinfo/menu/transforminfo.py | 75 ++++++++++++++++++++++++++++++----- padinfo/padinfo.py | 16 ++++++-- padinfo/view/transforminfo.py | 17 +++++--- 3 files changed, 89 insertions(+), 19 deletions(-) diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index b6f8d904..8dcb082b 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -2,7 +2,7 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from discordmenu.emoji.emoji_cache import emoji_cache +from tsutils import char_to_emoji from padinfo.menu.common import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState @@ -21,10 +21,12 @@ async def respond_with_base(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] - ims['query'] = str(ims['b_resolved_monster_id']) + # base is always first + ims['query'] = str(ims['resolved_monster_ids'][0]) ims['resolved_monster_id'] = None id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) - id_control = TransformInfoMenu.id_control(id_view_state) + reaction_list = ims['reaction_list'] + id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) return id_control @staticmethod @@ -32,10 +34,28 @@ async def respond_with_transform(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] - ims['query'] = str(ims['t_resolved_monster_id']) + # transform is always second + ims['query'] = str(ims['resolved_monster_ids'][1]) ims['resolved_monster_id'] = None id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) - id_control = TransformInfoMenu.id_control(id_view_state) + reaction_list = ims['reaction_list'] + id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) + return id_control + + @staticmethod + async def respond_with_n(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + n = TransformInfoMenuPanes.get_n_from_reaction(data['reaction']) + # does this work? is this check necessary? is reaction guaranteed to resolve? + if n is None: + return None + + ims['query'] = str(ims['resolved_monster_ids'][n]) + ims['resolved_monster_id'] = None + id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + reaction_list = ims['reaction_list'] + id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) return id_control @staticmethod @@ -48,16 +68,17 @@ async def respond_with_overview(message: Optional[Message], ims, **data): @staticmethod def tf_control(state: TransformInfoViewState): + reaction_list = state.reaction_list return EmbedControl( [TransformInfoView.embed(state)], - [emoji_cache.get_by_name(e) for e in TransformInfoMenuPanes.emoji_names()] + reaction_list ) @staticmethod - def id_control(state: IdViewState): + def id_control(state: IdViewState, reaction_list): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in TransformInfoMenuPanes.emoji_names()] + reaction_list ) @@ -65,11 +86,47 @@ class TransformInfoEmoji: home = emoji_buttons['home'] down = '\N{DOWN-POINTING RED TRIANGLE}' up = '\N{UP-POINTING RED TRIANGLE}' + one = char_to_emoji('1') + two = char_to_emoji('2') + three = char_to_emoji('3') + four = char_to_emoji('4') + five = char_to_emoji('5') + six = char_to_emoji('6') + seven = char_to_emoji('7') + eight = char_to_emoji('8') + nine = char_to_emoji('9') + ten = char_to_emoji('10') class TransformInfoMenuPanes(MenuPanes): DATA = { - TransformInfoEmoji.home: (TransformInfoMenu.respond_with_overview, TransformInfoView.VIEW_TYPE), + TransformInfoEmoji.home: (TransformInfoMenu.respond_with_overview, + TransformInfoView.VIEW_TYPE), TransformInfoEmoji.down: (TransformInfoMenu.respond_with_base, IdView.VIEW_TYPE), TransformInfoEmoji.up: (TransformInfoMenu.respond_with_transform, IdView.VIEW_TYPE), + TransformInfoEmoji.one: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.two: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.three: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.four: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.five: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.six: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.seven: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.eight: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.nine: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), + TransformInfoEmoji.ten: (TransformInfoMenu.respond_with_n, IdView.VIEW_TYPE), } + + @classmethod + def get_reaction_list(cls, number_of_monsters: int): + if number_of_monsters > 2: + cls.HIDDEN_EMOJIS = TransformInfoEmoji.up + else: + cls.HIDDEN_EMOJIS = TransformInfoEmoji.one + + # add 1 for the home emoji + return cls.emoji_names()[:number_of_monsters + 1] + + @classmethod + def get_n_from_reaction(cls, reaction): + # offset by 1 because of the home emoji + return cls.emoji_names().index(reaction) - 1 if reaction in cls.emoji_names() else None diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 220c7bc3..1c28eca6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -41,7 +41,7 @@ from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji from padinfo.menu.simple_text import SimpleTextMenu -from padinfo.menu.transforminfo import TransformInfoMenu +from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.components.monster.header import MonsterHeader @@ -168,7 +168,12 @@ async def test_reaction_add(self, reaction, member): if not (await menu.should_respond(message, reaction, await self.get_reaction_filters(ims), member)): return - await menu.transition(message, deepcopy(ims), emoji_clicked, member, **(await self.get_menu_default_data(ims))) + data = await self.get_menu_default_data(ims) + data.update({ + 'reaction': emoji_clicked + }) + + await menu.transition(message, deepcopy(ims), emoji_clicked, member, **data) await self.listener_respond_with_child(deepcopy(ims), message, emoji_clicked, member) @staticmethod @@ -745,7 +750,7 @@ async def leaderskillsingle(self, ctx, *, query): async def transforminfo(self, ctx, *, query): """Show info about a transform card, including some helpful details about the base card.""" dgcog = await self.get_dgcog() - base_mon, transformed_mon, reaction_ids = await perform_transforminfo_query(dgcog, query) + base_mon, transformed_mon, monster_ids = await perform_transforminfo_query(dgcog, query) if not base_mon: await self.send_id_failure_message(ctx, query) @@ -760,8 +765,11 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + reaction_list = TransformInfoMenuPanes.get_reaction_list(len(monster_ids)) + state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, - color, base_mon, transformed_mon, acquire_raw) + color, base_mon, transformed_mon, acquire_raw, monster_ids, + reaction_list=reaction_list) menu = TransformInfoMenu.menu() await menu.create(ctx, state) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index fe2c8cec..609d47b6 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -23,18 +23,20 @@ class TransformInfoViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, - acquire_raw): + acquire_raw, monster_ids, reaction_list): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color self.base_mon = base_mon self.transformed_mon = transformed_mon self.acquire_raw = acquire_raw + self.monster_ids = monster_ids + self.reaction_list = reaction_list def serialize(self): ret = super().serialize() ret.update({ - 'b_resolved_monster_id': self.base_mon.monster_id, - 't_resolved_monster_id': self.transformed_mon.monster_id + 'resolved_monster_ids': self.monster_ids, + 'reaction_list': self.reaction_list }) return ret @@ -43,16 +45,19 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - base_mon_id = ims['b_resolved_monster_id'] - transformed_mon_id = ims['t_resolved_monster_id'] + monster_ids = ims['resolved_monster_ids'] + base_mon_id = monster_ids[0] + transformed_mon_id = monster_ids[1] base_mon = dgcog.get_monster(base_mon_id) transformed_mon = dgcog.get_monster(transformed_mon_id) acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + reaction_list = ims['reaction_list'] return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, - base_mon, transformed_mon, acquire_raw) + base_mon, transformed_mon, acquire_raw, monster_ids, + reaction_list=reaction_list) @staticmethod async def query(dgcog, base_mon, transformed_mon): From 4d498c232ad54a076fd241515125893f9633ccd8 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Mar 2021 12:32:04 -0700 Subject: [PATCH 0679/1220] Remove unused glob matching types from padsearch (#913) --- padsearch/padsearch.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index cd7349dd..c140e2b7 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -222,9 +222,7 @@ class PadSearchLexer(object): 'RCV', 'WEIGHTED', 'REACTIVE', - 'GACTIVE', 'RELEADER', - 'GLEADER', ] def t_ACTIVE(self, t): @@ -239,12 +237,6 @@ def t_REACTIVE(self, t): t.value = replace_colors_in_text(t.value) return t - def t_GACTIVE(self, t): - r"gactive\(.+?\)" - t.value = clean_name(t.value, 'gactive') - t.value = replace_colors_in_text(t.value) - return t - def t_ALL(self, t): r'all(\(\))?' return t @@ -305,12 +297,6 @@ def t_RELEADER(self, t): t.value = replace_colors_in_text(t.value) return t - def t_GLEADER(self, t): - r"gleader\(.+?\)" - t.value = clean_name(t.value, 'gleader') - t.value = replace_colors_in_text(t.value) - return t - def t_NAME(self, t): r'name\([a-zA-Z0-9 ]+\)' t.value = clean_name(t.value, 'name') @@ -438,14 +424,12 @@ def __init__(self, lexer, db_context: "DbContext"): self.active = [] self.reactive = [] - self.gactive = [] self.board = [] self.column = [] self.color = [] self.hascolor = [] self.leader = [] self.releader = [] - self.gleader = [] self.name = [] self.row = [] self.types = [] @@ -476,8 +460,6 @@ def __init__(self, lexer, db_context: "DbContext"): self.active.append(value) if type == 'REACTIVE': self.reactive.append(value) - if type == 'GACTIVE': - self.gactive.append(value) if type == 'BOARD': self.board.append(split_csv_orbcolors(value)) if type == 'COLOR': @@ -490,8 +472,6 @@ def __init__(self, lexer, db_context: "DbContext"): self.leader.append(value) if type == 'RELEADER': self.releader.append(value) - if type == 'GLEADER': - self.gleader.append(value) if type == 'NAME': self.name.append(value) if type == 'ROW': From dcf9fb0ea837a44187d4229e5ba9fce2e1e7e50b Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Mar 2021 12:49:37 -0700 Subject: [PATCH 0680/1220] Add crud commands for awoken skills (#914) --- crud/crud.py | 222 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 186 insertions(+), 36 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 071e9352..c9abbb63 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -20,6 +20,16 @@ "series_type": None, } +AWOKEN_SKILL_KEYS = { + "awoken_skill_id": -1, + "name_en": 'Untranslated', + "name_ja": 'Untranslated', + "name_ko": 'Untranslated', + "desc_en": 'Untranslated', + "desc_ja": 'Untranslated', + "desc_ko": 'Untranslated', +} + SERIES_TYPES = [ "regular", "event", @@ -91,12 +101,56 @@ async def execute_read(self, ctx, sql, replacements): for page in pagify(msg): await ctx.send(box(page)) - @commands.group(aliases=['hera-ur']) + @commands.group() @auth_check('crud') @commands.check(check_crud_channel) async def crud(self, ctx): """PadGuide database CRUD.""" + @crud.command() + async def editmonsname(self, ctx, monster_id: int, *, name): + """Change a monster's name_en_override""" + if name.lower() in ["none", "null"]: + name = None + with await self.get_cursor() as cursor: + cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) + old_val = cursor.fetchall() + + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['name_en_override'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " english override from `{}` to `{}`?" + "").format(monster_id, old_val, name)): + return + + sql = ('UPDATE monsters' + ' SET name_en_override = %s' + ' WHERE monster_id = %s') + await self.execute_write(ctx, sql, (name, monster_id)) + + @crud.command() + async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): + """Change a monster's series_id""" + with await self.get_cursor() as cursor: + cursor.execute("SELECT series_id FROM monsters WHERE monster_id = %s", (monster_id,)) + old_val = cursor.fetchall() + + if not old_val: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + old_val = old_val[0]['series_id'] + if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + " series id from `{}` to `{}`?" + "").format(monster_id, old_val, series_id)): + return + + sql = ('UPDATE monsters' + ' SET series_id = %s' + ' WHERE monster_id = %s') + await self.execute_write(ctx, sql, (series_id, monster_id)) + @crud.command() async def searchdungeon(self, ctx, *, search_text): """Search for a dungeon via its jp or na name""" @@ -171,7 +225,7 @@ async def series_add(self, ctx, *elements): @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *elements): - """Edit an existing series series. + """Edit an existing series. Valid element keys are: `name_en`, `name_ko`, `name_ja`, `series_type` @@ -225,49 +279,145 @@ async def series_delete(self, ctx, series_id: int): async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - @crud.command() - async def editmonsname(self, ctx, monster_id: int, *, name): - """Change a monster's name_en_override""" - if name.lower() in ["none", "null"]: - name = None - with await self.get_cursor() as cursor: - cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) - old_val = cursor.fetchall() + @crud.group(aliases=["awos"]) + async def awokenskill(self, ctx): + """Awoken skill related commands""" - if not old_val: - await ctx.send("There is no monster with id: {}".format(monster_id)) + @awokenskill.command(name="search") + async def awokenskill_search(self, ctx, *, search_text): + """Search for a awoken skill via its jp or na name""" + search_text = '%{}%'.format(search_text).lower() + sql = ('SELECT awoken_skill_id, name_en, name_ja, name_ko, desc_en,' + ' desc_ja, desc_ko FROM awoken_skills' + ' WHERE lower(name_en) LIKE %s OR lower(name_ja) LIKE %s' + ' ORDER BY awoken_skill_id DESC LIMIT 20') + await self.execute_read(ctx, sql, [search_text, search_text]) + + @awokenskill.command(name="add") + @checks.is_owner() + async def awokenskill_add(self, ctx, *elements): + """Add a new awoken skill. + + Valid element keys are: `name_en`, `name_ko`, `name_ja`, `desc_en`, `desc_ko`, `desc_ja` + + Example Usage: + [p]crud awokenskill add key1 "Value1" key2 "Value2" + """ + if len(elements) % 2 != 0: + await ctx.send_help() return - old_val = old_val[0]['name_en_override'] - if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" - " english override from `{}` to `{}`?" - "").format(monster_id, old_val, name)): + elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} + + if not (elements and all(x in AWOKEN_SKILL_KEYS for x in elements)): + await ctx.send_help() return - sql = ('UPDATE monsters' - ' SET name_en_override = %s' - ' WHERE monster_id = %s') - await self.execute_write(ctx, sql, (name, monster_id)) + if "awoken_skill_id" not in elements or not elements["awoken_skill_id"].isdigit(): + await ctx.send("You must supply an numeric `awoken_skill_id` when adding a new awoken skill.") + return + elements["awoken_skill_id"] = int(elements["awoken_skill_id"]) - @crud.command() - async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): - """Change a monster's series_id""" - with await self.get_cursor() as cursor: - cursor.execute("SELECT series_id FROM monsters WHERE monster_id = %s", (monster_id,)) - old_val = cursor.fetchall() + EXTRAS = { + 'adj_hp': 0, + 'adj_atk': 0, + 'adj_rcv': 0, + 'tstamp': int(datetime.now().timestamp()) + } + elements = {**AWOKEN_SKILL_KEYS, **EXTRAS, **elements} - if not old_val: - await ctx.send("There is no monster with id: {}".format(monster_id)) + key_infix = ", ".join(elements.keys()) + value_infix = ", ".join("%s" for v in elements.values()) + sql = ('INSERT INTO awoken_skills ({})' + ' VALUES ({})').format(key_infix, value_infix) + + await self.execute_write(ctx, sql, (*elements.values(),)) + + fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/awoken_skill.json') + async with aiofiles.open(fn, 'r') as f: + j = json.loads(await f.read()) + j.append({ + "adj_atk": 0, + "adj_hp": 0, + "adj_rcv": 0, + "desc_ja": elements['desc_ja'], + "desc_ja_official": "Unknown Official Text", + "desc_ko": elements['desc_ko'], + "desc_ko_official": "Unknown Official Text", + "desc_en": elements['desc_en'], + "desc_en_official": "Unknown Official Text", + 'name_ja': elements['name_ja'], + "name_ja_official": "Unknown Official Name", + 'name_en': elements['name_en'], + "name_ko_official": "Unknown Official Name", + 'name_ko': elements['name_ko'], + "name_en_official": "Unknown Official Name", + "pad_awakening_id": elements['awoken_skill_id'], + }) + async with aiofiles.open(fn, 'w') as f: + await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + + @awokenskill.command(name="edit") + async def awokenskill_edit(self, ctx, awoken_skill_id, *elements): + """Edit an existing awoken skill. + + Valid element keys are: `name_en`, `name_ko`, `name_ja`, `desc_en`, `desc_ko`, `desc_ja` + + Example Usage: + [p]crud awokenskill edit 100 key1 "Value1" key2 "Value2" + """ + if not awoken_skill_id.isdigit(): + await ctx.send("String awoken skill lookup is not supported yet.") return - old_val = old_val[0]['series_id'] - if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" - " series id from `{}` to `{}`?" - "").format(monster_id, old_val, series_id)): + + awoken_skill_id = int(awoken_skill_id) + + if len(elements) % 2 != 0: + await ctx.send_help() return + elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} - sql = ('UPDATE monsters' - ' SET series_id = %s' - ' WHERE monster_id = %s') - await self.execute_write(ctx, sql, (series_id, monster_id)) + if not (elements and all(x in AWOKEN_SKILL_KEYS for x in elements)): + await ctx.send_help() + return + + if 'awoken_skill_id' in elements: + await ctx.send("`awoken_skill_id` is not a supported key for editing.") + return + + replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) + sql = ('UPDATE awoken_skills' + ' SET {}' + ' WHERE awoken_skill_id = %s').format(replacement_infix) + + await self.execute_write(ctx, sql, (*elements.values(), awoken_skill_id)) + + fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/awoken_skill.json') + async with aiofiles.open(fn, 'r') as f: + j = json.loads(await f.read()) + + for e in j: + if e['pad_awakening_id'] == awoken_skill_id: + e.update(elements) + async with aiofiles.open(fn, 'w') as f: + await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + + @awokenskill.command(name="delete") + @checks.is_owner() + async def awokenskill_delete(self, ctx, awoken_skill_id: int): + """Delete an existing awoken skill""" + sql = ('DELETE FROM awoken_skills' + ' WHERE awoken_skill_id = %s') + + await self.execute_write(ctx, sql, awoken_skill_id) + + fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/awoken_skill.json') + async with aiofiles.open(fn, 'r') as f: + j = json.loads(await f.read()) + for e in j[:]: + if e['pad_awakening_id'] == awoken_skill_id: + j.remove(e) + async with aiofiles.open(fn, 'w') as f: + await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) @crud.command() @checks.is_owner() From 57de3be7abe5081033d07bd2c66bb26bc216ec2b Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Thu, 4 Mar 2021 14:53:32 -0500 Subject: [PATCH 0681/1220] Add awakeninghelp command (#915) * Add basic awakeninghelp command * Use small header for awakeninghelp * Fix awakeninghelp breaking when there are no super awakenings * Inform the user if a monster has no awakenings * Add awakening description and clean up code a little * Describe duplicate emojis by referring to the previous occurrence * Clean up awakeninghelp a little more --- padinfo/menu/closable_embed.py | 4 +- padinfo/padinfo.py | 19 +++++++ padinfo/view/awakening_help.py | 94 ++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 padinfo/view/awakening_help.py diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index ebc8efab..ff632eaf 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -2,11 +2,13 @@ from discordmenu.embed.menu import EmbedMenu from padinfo.menu.common import MenuPanes +from padinfo.view.awakening_help import AwakeningHelpView from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.id_traceback import IdTracebackView view_types = { - IdTracebackView.VIEW_TYPE: IdTracebackView + AwakeningHelpView.VIEW_TYPE: AwakeningHelpView, + IdTracebackView.VIEW_TYPE: IdTracebackView, } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 1c28eca6..6a13193e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -43,6 +43,7 @@ from padinfo.menu.simple_text import SimpleTextMenu from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list +from padinfo.view.awakening_help import AwakeningHelpView, AwakeningHelpViewProps from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.evos import EvosViewState @@ -773,6 +774,24 @@ async def transforminfo(self, ctx, *, query): menu = TransformInfoMenu.menu() await menu.create(ctx, state) + @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp']) + async def awakeninghelp(self, ctx, *, query): + """Describe a monster's regular and super awakenings in detail.""" + dgcog = await self.get_dgcog() + monster = await find_monster(dgcog, query) + + if not monster: + await self.send_id_failure_message(ctx, query) + return + + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + menu = ClosableEmbedMenu.menu() + props = AwakeningHelpViewProps(monster=monster) + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, + color, AwakeningHelpView.VIEW_TYPE, props) + await menu.create(ctx, state) + @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py new file mode 100644 index 00000000..e6e1b615 --- /dev/null +++ b/padinfo/view/awakening_help.py @@ -0,0 +1,94 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField, EmbedAuthor +from discordmenu.embed.text import Text +from discordmenu.embed.view import EmbedView + +from padinfo.common.emoji_map import get_awakening_emoji +from padinfo.common.external_links import puzzledragonx +from padinfo.view.components.base import pad_info_footer_with_state +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + from dadguide.models.awakening_model import AwakeningModel + +ORDINAL_WORDS = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth'] + + +class AwakeningHelpViewProps: + def __init__(self, monster: "MonsterModel"): + self.monster = monster + + +def _get_long_desc(awakening: "AwakeningModel"): + emoji_text = get_awakening_emoji(awakening.awoken_skill_id, awakening.name) + desc = awakening.awoken_skill.desc_en + return Box( + Text(emoji_text), + Text(desc), + delimiter=' ' + ) + + +def _get_short_desc(prev_index: int, awakening: "AwakeningModel"): + emoji_text = get_awakening_emoji(awakening.awoken_skill_id, awakening.name) + return Box( + Text(emoji_text), + Text('[Same as {} awakening]'.format(ORDINAL_WORDS[prev_index])), + delimiter=' ' + ) + + +def _get_all_awakening_descs(awakening_list): + appearances = {} + awakening_descs = [] + for index, awakening in enumerate(awakening_list): + if awakening.name not in appearances: + appearances[awakening.name] = index + awakening_descs.append(_get_long_desc(awakening)) + else: + awakening_descs.append(_get_short_desc(appearances[awakening.name], awakening)) + + return Box(*awakening_descs) + + +def normal_awakenings(monster: "MonsterModel"): + normal_awakening_count = len(monster.awakenings) - monster.superawakening_count + normal_awakenings = monster.awakenings[:normal_awakening_count] + return _get_all_awakening_descs(normal_awakenings) + + +def super_awakenings(monster: "MonsterModel"): + normal_awakening_count = len(monster.awakenings) - monster.superawakening_count + super_awakenings = monster.awakenings[normal_awakening_count:] + return _get_all_awakening_descs(super_awakenings) + + +class AwakeningHelpView: + VIEW_TYPE = 'AwakeningHelp' + + @staticmethod + def embed(state, props: AwakeningHelpViewProps): + monster = props.monster + + fields = [ + EmbedField('Normal Awakenings', normal_awakenings(monster)), + EmbedField('Super Awakenings', super_awakenings(monster)) + ] + + return EmbedView( + EmbedMain( + color=state.color, + description='This monster has no awakenings.' if not monster.awakenings else '' + ), + embed_author=EmbedAuthor( + MonsterHeader.long_v2(monster).to_markdown(), + puzzledragonx(monster), + MonsterImage.icon(monster) + ), + embed_footer=pad_info_footer_with_state(state), + embed_fields=fields + ) From 0658b09cb07e891d87d238cd4386914c1deea5eb Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 4 Mar 2021 13:16:38 -0700 Subject: [PATCH 0682/1220] Add emoji selection to crud awos edit (#917) --- crud/crud.py | 14 +++++++++++--- padinfo/padinfo.py | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index c9abbb63..5e1b78a2 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -357,16 +357,24 @@ async def awokenskill_add(self, ctx, *elements): await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) @awokenskill.command(name="edit") - async def awokenskill_edit(self, ctx, awoken_skill_id, *elements): + async def awokenskill_edit(self, ctx, awoken_skill, *elements): """Edit an existing awoken skill. Valid element keys are: `name_en`, `name_ko`, `name_ja`, `desc_en`, `desc_ko`, `desc_ja` Example Usage: [p]crud awokenskill edit 100 key1 "Value1" key2 "Value2" + [p]crud awokenskill edit misc_comboboost key1 "Value1" key2 "Value2" """ - if not awoken_skill_id.isdigit(): - await ctx.send("String awoken skill lookup is not supported yet.") + pdicog = self.bot.get_cog("PadInfo") + emoji_to_awid = getattr(pdicog, 'awoken_emoji_names', {}) + + if awoken_skill in emoji_to_awid: + awoken_skill_id = emoji_to_awid[awoken_skill] + elif awoken_skill.isdigit(): + awoken_skill_id = int(awoken_skill) + else: + await ctx.send("Invalid awoken skill.") return awoken_skill_id = int(awoken_skill_id) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6a13193e..ee328d59 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -23,7 +23,7 @@ from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ - get_attribute_emoji_by_monster + get_attribute_emoji_by_monster, AWAKENING_ID_TO_EMOJI_NAME_MAP from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info from padinfo.core.find_monster import FindMonster, calc_ratio_name, calc_ratio_modifier, find_monster, \ @@ -110,6 +110,7 @@ def __init__(self, bot, *args, **kwargs): self.fm3 = lambda q: fm.find_monster(bot.get_cog("Dadguide"), q) + self.awoken_emoji_names = {v: k for k, v in AWAKENING_ID_TO_EMOJI_NAME_MAP.items()} self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster self.settings = settings From 1de2f44df993b065a5196e6f2af6c393a5e16e18 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 5 Mar 2021 13:06:47 -0700 Subject: [PATCH 0683/1220] Move FindMonster to Dadguide Cog (#918) --- dadguide/dadguide.py | 25 ++- {padinfo/core => dadguide}/find_monster.py | 245 ++++++++++----------- {padinfo => dadguide}/idtest_mixin.py | 40 ++-- padbuildimg/padbuildimg.py | 10 +- padglobal/padglobal.py | 6 +- padinfo/core/historic_lookups.py | 15 -- padinfo/core/leader_skills.py | 8 +- padinfo/core/padinfo_settings.py | 6 - padinfo/core/transforminfo.py | 5 +- padinfo/padinfo.py | 77 +++---- padinfo/view/common.py | 5 +- pricecheck/pricecheck.py | 20 +- 12 files changed, 210 insertions(+), 252 deletions(-) rename {padinfo/core => dadguide}/find_monster.py (50%) rename {padinfo => dadguide}/idtest_mixin.py (93%) delete mode 100644 padinfo/core/historic_lookups.py diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 003307a4..779efc15 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -7,22 +7,23 @@ entire database could be leaked when the module is reloaded. """ import asyncio +import logging import os import shutil - -import discord -import time from io import BytesIO from typing import Optional +import discord +import time import tsutils from redbot.core import checks, data_manager, commands, Config from redbot.core.utils.chat_formatting import pagify, box -from tsutils import auth_check +from tsutils import auth_check, safe_read_json from . import token_mappings +from .find_monster import FindMonster from .database_loader import load_database -from .database_manager import * +from .idtest_mixin import IdTest from .models.monster_model import MonsterModel from .models.monster_stats import monster_stats, MonsterStatModifierInput from .monster_index import MonsterIndex @@ -57,7 +58,7 @@ def _data_file(file_name: str) -> str: pass -class Dadguide(commands.Cog): +class Dadguide(commands.Cog, IdTest): """Dadguide database manager""" def __init__(self, bot, *args, **kwargs): @@ -72,12 +73,18 @@ def __init__(self, bot, *args, **kwargs): self.fir3_lock = asyncio.Lock() self.config = Config.get_conf(self, identifier=64667103) - self.config.register_global(datafile='', indexlog=0) + self.config.register_global(datafile='', indexlog=0, test_suite={}, fluff_suite=[], typo_mods=[]) + self.config.register_user(lastaction=None) + self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') + self.historic_lookups = safe_read_json(self.historic_lookups_file_path) self.monster_stats = monster_stats self.MonsterStatModifierInput = MonsterStatModifierInput self.token_maps = token_mappings + self.mon_finder = FindMonster(self) + self.find_monster = self.mon_finder.find_monster + self.find_monsters = self.mon_finder.find_monsters GADMIN_COG = self.bot.get_cog("GlobalAdmin") if GADMIN_COG: @@ -132,14 +139,14 @@ async def forceindexreload3(self, ctx): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" - self.index = await MonsterIndex(self.database.get_all_monsters(), self.database) # noqa + self.index = await MonsterIndex(self.database.get_all_monsters(), self.database) # noqa asyncio.create_task(self.check_index()) async def check_index(self): if not await self.config.indexlog(): return - if self.bot.get_cog("PadInfo") and not await self.bot.get_cog("PadInfo").run_tests(): + if not await self.run_tests(): self.index.issues.insert(0, "Test cases failed.") channel = self.bot.get_channel(await self.config.indexlog()) diff --git a/padinfo/core/find_monster.py b/dadguide/find_monster.py similarity index 50% rename from padinfo/core/find_monster.py rename to dadguide/find_monster.py index 4e300b0c..7db3c4ff 100644 --- a/padinfo/core/find_monster.py +++ b/dadguide/find_monster.py @@ -6,9 +6,6 @@ from Levenshtein import jaro_winkler from tsutils import rmdiacritics -from padinfo.core.historic_lookups import historic_lookups_id3, historic_lookups_file_path_id3 -from padinfo.core.padinfo_settings import settings - if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -23,32 +20,6 @@ } -def calc_ratio_modifier(s1, s2, dist=.05): - return jaro_winkler(s1, s2, .05) - - -def calc_ratio_name(token, full_word, index2, factor=.05): - mw = index2.mwt_to_len[full_word] != 1 - jw = jaro_winkler(token, full_word, factor) - - if token.isdigit() and full_word.isdigit() and token != full_word: - return 0.0 - - if full_word == token: - score = 1.0 - elif len(token) >= 3 and full_word.startswith(token): - score = .995 - if mw and jw < score: - return score - else: - score = jw - - if mw: - score = score ** 10 * index2.mwt_to_len[full_word] - - return score - - class MonsterMatch: def __init__(self, score=0, name=None, mod=None): self.score = score @@ -65,11 +36,13 @@ class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 - @staticmethod - def merge_multi_word_tokens(tokens, valid_multi_word_tokens): + def __init__(self, dgcog): + self.dgcog = dgcog + + def merge_multi_word_tokens(self, tokens): result = [] skip = 0 - multi_word_tokens_sorted = sorted(valid_multi_word_tokens, + multi_word_tokens_sorted = sorted(self.dgcog.index.multi_word_tokens, key=lambda x: (len(x), len(''.join(x))), reverse=True) for c1, token1 in enumerate(tokens): @@ -81,7 +54,7 @@ def merge_multi_word_tokens(tokens, valid_multi_word_tokens): continue for c2, token2 in enumerate(mwt): if (tokens[c1 + c2] != token2 and len(token2) < 5) \ - or calc_ratio_modifier(tokens[c1 + c2], token2) < FindMonster.TOKEN_JW_DISTANCE: + or self.calc_ratio_modifier(tokens[c1 + c2], token2) < self.TOKEN_JW_DISTANCE: break else: skip = len(mwt) - 1 @@ -91,35 +64,34 @@ def merge_multi_word_tokens(tokens, valid_multi_word_tokens): result.append(token1) return result - @staticmethod - def _monster_has_modifier(monster, token, matches, monster_mods): + def _monster_has_modifier(self, monster, token, matches): if len(token) < 6: - if token in monster_mods: + if token in self.dgcog.index.modifiers[monster]: matches[monster].mod.add((token, token)) matches[monster].score += 1 return True else: - closest = max(monster_mods, key=lambda m: calc_ratio_modifier(m, token)) - ratio = calc_ratio_modifier(closest, token) - if ratio > FindMonster.MODIFIER_JW_DISTANCE: + closest = max(self.dgcog.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(m, token)) + ratio = self.calc_ratio_modifier(closest, token) + if ratio > self.MODIFIER_JW_DISTANCE: matches[monster].mod.add((token, closest)) matches[monster].score += ratio return True return False - @staticmethod - def interpret_query(tokenized_query: List[str], index2) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: + def interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: modifiers = [] negative_modifiers = set() name = set() negative_name = set() - longmods = [p for p in index2.all_modifiers if len(p) > 8] + longmods = [p for p in self.dgcog.index.all_modifiers if len(p) > 8] lastmodpos = False for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if any(calc_ratio_modifier(m, token, .1) > FindMonster.MODIFIER_JW_DISTANCE for m in index2.suffixes): + if any(self.calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in + self.dgcog.index.suffixes): if negated: negative_modifiers.add(token) else: @@ -132,9 +104,9 @@ def interpret_query(tokenized_query: List[str], index2) -> Tuple[Set[str], Set[s for i, token in enumerate(tokenized_query): negated = token.startswith("-") token = token.lstrip('-') - if token in index2.all_modifiers or ( - any(calc_ratio_modifier(m, token, .1) > FindMonster.MODIFIER_JW_DISTANCE for m in longmods) - and token not in index2.all_name_tokens + if token in self.dgcog.index.all_modifiers or ( + any(self.calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) + and token not in self.dgcog.index.all_name_tokens and len(token) >= 8): if negated: lastmodpos = False @@ -157,53 +129,52 @@ def interpret_query(tokenized_query: List[str], index2) -> Tuple[Set[str], Set[s name.add(token) if not (name or negative_name) and modifiers and lastmodpos: - if index2.manual[modifiers[-1]]: + if self.dgcog.index.manual[modifiers[-1]]: name.add(modifiers[-1]) modifiers = modifiers[:-1] return set(modifiers), negative_modifiers, name, negative_name - @staticmethod - def process_name_tokens(name_query_tokens, neg_name_tokens, dgcog, matches): + def process_name_tokens(self, name_query_tokens, neg_name_tokens, matches): matched_mons = None for pos_name in name_query_tokens: - valid = FindMonster.get_valid_monsters_from_name_token(pos_name, dgcog.index, matches) + valid = self.get_valid_monsters_from_name_token(pos_name, matches) if matched_mons is not None: matched_mons.intersection_update(valid) else: matched_mons = valid for neg_name in neg_name_tokens: - invalid = FindMonster.get_valid_monsters_from_name_token(neg_name, dgcog.index, matches, mult=-10) + invalid = self.get_valid_monsters_from_name_token(neg_name, matches, mult=-10) if matched_mons is not None: matched_mons.difference_update(invalid) else: - matched_mons = set(dgcog.database.get_all_monsters()).difference(invalid) + matched_mons = set(self.dgcog.database.get_all_monsters()).difference(invalid) return matched_mons - @staticmethod - def get_valid_monsters_from_name_token(token, index2, matches, mult=1): + def get_valid_monsters_from_name_token(self, token, matches, mult=1): valid_monsters = set() - all_monsters_name_tokens_scores = {nt: calc_ratio_name(token, nt, index2) for nt in index2.all_name_tokens} + all_monsters_name_tokens_scores = {nt: self.calc_ratio_name(token, nt) for nt in + self.dgcog.index.all_name_tokens} matched_tokens = sorted((nt for nt in all_monsters_name_tokens_scores - if all_monsters_name_tokens_scores[nt] > FindMonster.TOKEN_JW_DISTANCE), + if all_monsters_name_tokens_scores[nt] > self.TOKEN_JW_DISTANCE), key=lambda nt: all_monsters_name_tokens_scores[nt], reverse=True) - matched_tokens += [t for t in index2.all_name_tokens if t.startswith(token)] + matched_tokens += [t for t in self.dgcog.index.all_name_tokens if t.startswith(token)] for match in matched_tokens: score = all_monsters_name_tokens_scores[match] - for matched_monster in index2.manual[match]: + for matched_monster in self.dgcog.index.manual[match]: if matched_monster not in valid_monsters: matches[matched_monster].name.add((token, match, '(manual)')) matches[matched_monster].score += (score + .001) * mult valid_monsters.add(matched_monster) - for matched_monster in index2.name_tokens[match]: + for matched_monster in self.dgcog.index.name_tokens[match]: if matched_monster not in valid_monsters: matches[matched_monster].name.add((token, match, '(name)')) matches[matched_monster].score += score * mult valid_monsters.add(matched_monster) - for matched_monster in index2.fluff_tokens[match]: + for matched_monster in self.dgcog.index.fluff_tokens[match]: if matched_monster not in valid_monsters: matches[matched_monster].name.add((token, match, '(fluff)')) matches[matched_monster].score += score * mult / 2 @@ -211,58 +182,55 @@ def get_valid_monsters_from_name_token(token, index2, matches, mult=1): return valid_monsters - @staticmethod - def process_modifiers(mod_tokens, neg_mod_tokens, potential_evos, matches, monster_mods): + def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches): for pos_mod_token in mod_tokens: potential_evos = {m for m in potential_evos if - FindMonster._monster_has_modifier(m, pos_mod_token, matches, monster_mods[m])} + self._monster_has_modifier(m, pos_mod_token, matches)} if not potential_evos: return None for neg_mod_token in neg_mod_tokens: potential_evos = {m for m in potential_evos if - not FindMonster._monster_has_modifier(m, neg_mod_token, matches, monster_mods[m])} + not self._monster_has_modifier(m, neg_mod_token, matches)} if not potential_evos: return None return potential_evos - @staticmethod - def get_priority_tuple(monster, dgcog, tokenized_query=None, matches=None): + def get_priority_tuple(self, monster, tokenized_query=None, matches=None): if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] - + return (matches[monster].score, # Don't deprio evos with new modifier - not monster.is_equip if not {m[0] for m in matches[monster].mod}.intersection({'new', 'base'}) else True, + not monster.is_equip if not {m[0] for m in matches[monster].mod}.intersection( + {'new', 'base'}) else True, # Match na on id overlap bool(monster.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), SERIES_TYPE_PRIORITY.get(monster.series.series_type), monster.on_na if monster.series.series_type == "collab" else True, - dgcog.database.graph.monster_is_rem_evo(monster), + self.dgcog.database.graph.monster_is_rem_evo(monster), not all(t.value in [0, 12, 14, 15] for t in monster.types), not any(t.value in [0, 12, 14, 15] for t in monster.types), - -dgcog.database.graph.get_base_id(monster), + -self.dgcog.database.graph.get_base_id(monster), monster.on_na, not monster.is_equip, monster.rarity, monster.monster_no_na) - @staticmethod - def get_most_eligable_monster(monsters, dgcog, tokenized_query=None, matches=None): + def get_most_eligable_monster(self, monsters, tokenized_query=None, matches=None): if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: tokenized_query = [] - return max(monsters, key=lambda m: FindMonster.get_priority_tuple(m, dgcog, tokenized_query, matches)) + return max(monsters, key=lambda m: self.get_priority_tuple(m, tokenized_query, matches)) - @staticmethod - def get_monster_evos(database, matched_mons, matches): + def get_monster_evos(self, matched_mons, matches): monster_evos = set() for monster in sorted(matched_mons, key=lambda m: matches[m].score, reverse=True): - for evo in database.graph.get_alt_monsters(monster): + for evo in self.dgcog.database.graph.get_alt_monsters(monster): monster_evos.add(evo) if matches[evo].score < matches[monster].score: matches[evo].name = {(t[0], t[1], @@ -271,73 +239,92 @@ def get_monster_evos(database, matched_mons, matches): return monster_evos + async def find_monster_search(self, tokenized_query) -> \ + Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"]]: + mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ + self.interpret_query(tokenized_query) + + name_query_tokens.difference_update({'|'}) + + for mod_token in mod_tokens.union(neg_mod_tokens): + if mod_token not in self.dgcog.index.all_modifiers: + async with self.dgcog.config.typo_mods() as typo_mods: + typo_mods.append(mod_token) + + matches = defaultdict(MonsterMatch) + if name_query_tokens or neg_name_tokens: + matched_mons = self.process_name_tokens(name_query_tokens, + neg_name_tokens, + matches) + if not matched_mons: + # No monsters match the given name tokens + return None, {}, set() + matched_mons = self.get_monster_evos(matched_mons, matches) + else: + # There are no name tokens in the query + matched_mons = {*self.dgcog.database.get_all_monsters()} + monster_score = defaultdict(int) -async def find_monster_search(tokenized_query, dgcog) -> \ - Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"]]: - mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ - FindMonster.interpret_query(tokenized_query, dgcog.index) - - name_query_tokens.difference_update({'|'}) - - for mod_token in mod_tokens.union(neg_mod_tokens): - if mod_token not in dgcog.index.all_modifiers: - settings.add_typo_mod(mod_token) - - matches = defaultdict(MonsterMatch) - if name_query_tokens or neg_name_tokens: - matched_mons = FindMonster.process_name_tokens(name_query_tokens, - neg_name_tokens, - dgcog, - matches) + # Expand search to the evo tree + matched_mons = self.process_modifiers(mod_tokens, neg_mod_tokens, matched_mons, matches) if not matched_mons: - # No monsters match the given name tokens + # no modifiers match any monster in the evo tree return None, {}, set() - matched_mons = FindMonster.get_monster_evos(dgcog.database, matched_mons, matches) - else: - # There are no name tokens in the query - matched_mons = {*dgcog.database.get_all_monsters()} - monster_score = defaultdict(int) - # Expand search to the evo tree - matched_mons = FindMonster.process_modifiers(mod_tokens, neg_mod_tokens, matched_mons, matches, - dgcog.index.modifiers) - if not matched_mons: - # no modifiers match any monster in the evo tree - return None, {}, set() + # Return most likely candidate based on query. + mon = self.get_most_eligable_monster(matched_mons, tokenized_query, matches) + + return mon, matches, matched_mons - # Return most likely candidate based on query. - mon = FindMonster.get_most_eligable_monster(matched_mons, dgcog, tokenized_query, matches) + async def find_monster_debug(self, query) -> \ + Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"], int]: + await self.dgcog.wait_until_ready() - return mon, matches, matched_mons + query = rmdiacritics(query).lower().replace(",", "") + tokenized_query = query.split() + mw_tokenized_query = self.merge_multi_word_tokens(tokenized_query) + best_monster, matches_dict, valid_monsters = max( + await self.find_monster_search(tokenized_query), + await self.find_monster_search(mw_tokenized_query) + if tokenized_query != mw_tokenized_query else (None, {}, set()), -async def find_monster_debug(dgcog, query) -> \ - Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"], int]: - await dgcog.wait_until_ready() + key=lambda t: t[1].get(t[0], MonsterMatch()).score + ) - query = rmdiacritics(query).lower().replace(",", "") - tokenized_query = query.split() - mw_tokenized_query = FindMonster.merge_multi_word_tokens(tokenized_query, dgcog.index.multi_word_tokens) + self.dgcog.historic_lookups[query] = best_monster.monster_id if best_monster else -1 + json.dump(self.dgcog.historic_lookups, open(self.dgcog.historic_lookups_file_path, "w+")) - best_monster, matches_dict, valid_monsters = max( - await find_monster_search(tokenized_query, dgcog), - await find_monster_search(mw_tokenized_query, dgcog) - if tokenized_query != mw_tokenized_query else (None, {}, set()), + return best_monster, matches_dict, valid_monsters, 0 - key=lambda t: t[1].get(t[0], MonsterMatch()).score - ) + async def find_monster(self, query) -> Optional["MonsterModel"]: + matched_monster, _, _, _ = await self.find_monster_debug(query) + return matched_monster - historic_lookups_id3[query] = best_monster.monster_id if best_monster else -1 - json.dump(historic_lookups_id3, open(historic_lookups_file_path_id3, "w+")) + async def find_monsters(self, query) -> List["MonsterModel"]: + _, matches, monsters, _ = await self.find_monster_debug(query) + return sorted(monsters, key=lambda m: self.get_priority_tuple(m, matches=matches), reverse=True) - return best_monster, matches_dict, valid_monsters, 0 + def calc_ratio_modifier(self, s1, s2, dist=.05): + return jaro_winkler(s1, s2, .05) + def calc_ratio_name(self, token, full_word, factor=.05): + mw = self.dgcog.index.mwt_to_len[full_word] != 1 + jw = jaro_winkler(token, full_word, factor) -async def find_monster(dgcog, query) -> Optional["MonsterModel"]: - matched_monster, _, _, _ = await find_monster_debug(dgcog, query) - return matched_monster + if token.isdigit() and full_word.isdigit() and token != full_word: + return 0.0 + + if full_word == token: + score = 1.0 + elif len(token) >= 3 and full_word.startswith(token): + score = .995 + if mw and jw < score: + return score + else: + score = jw + if mw: + score = score ** 10 * self.dgcog.index.mwt_to_len[full_word] -async def find_monsters(dgcog, query) -> List["MonsterModel"]: - _, matches, monsters, _ = await find_monster_debug(dgcog, query) - return sorted(monsters, key=lambda m: FindMonster.get_priority_tuple(m, dgcog, matches=matches), reverse=True) + return score diff --git a/padinfo/idtest_mixin.py b/dadguide/idtest_mixin.py similarity index 93% rename from padinfo/idtest_mixin.py rename to dadguide/idtest_mixin.py index 773d95e7..309e153a 100644 --- a/padinfo/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -5,12 +5,11 @@ from redbot.core import commands, Config, checks from redbot.core.utils.chat_formatting import box, pagify -from padinfo.core.find_monster import find_monster - class IdTest: bot = None - get_dgcog = None + index = None + find_monster = None def __init__(self): self.config = Config.get_conf(self, identifier=-1) @@ -50,7 +49,8 @@ async def idt_add(self, ctx, mid: int, *, query): del suite[query] await ctx.react_quietly("\N{CROSS MARK}") else: - await ctx.send(f"Successfully added test case `{mid} - {query}` with ref `{sorted(suite).index(query)}`") + await ctx.send( + f"Successfully added test case `{mid} - {query}` with ref `{sorted(suite).index(query)}`") await ctx.tick() @idtest.group(name="name") @@ -356,7 +356,9 @@ async def idt_listrecent(self, ctx, count: int = 0): async def idt_run(self, ctx): """Run all id3 tests""" suite = await self.config.test_suite() - dgcog = await self.get_dgcog() + if not suite: + await ctx.send("No tests found.") + return await self.config.user(ctx.author).lastaction.set('id3') c = 0 o = "" @@ -366,7 +368,7 @@ async def idt_run(self, ctx): for i, qr in enumerate(sorted(suite.items())): q, r = qr try: - m = await find_monster(dgcog, q) or -1 + m = await self.find_monster(q) or -1 except Exception: m = -2 mid = getattr(m, "monster_id", m) @@ -397,14 +399,19 @@ async def idtf_run(self, ctx): async def norf_run(self, ctx): """Run all name/fluff tests""" suite = await self.config.fluff_suite() + if not suite: + await ctx.send("No tests found.") + return await self.config.user(ctx.author).lastaction.set('name') c = 0 o = "" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' async with ctx.typing(): for i, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): - fluff = case['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.fluff_tokens[case['token']]] - name = case['id'] in [m.monster_id for m in self.bot.get_cog("Dadguide").index.name_tokens[case['token']]] + fluff = case['id'] in [m.monster_id for m in + self.bot.get_cog("Dadguide").index.fluff_tokens[case['token']]] + name = case['id'] in [m.monster_id for m in + self.bot.get_cog("Dadguide").index.name_tokens[case['token']]] if (case['fluff'] and not fluff) or (not case['fluff'] and not name): q = '"{}"'.format(case['token']) @@ -424,15 +431,14 @@ async def norf_run(self, ctx): async def idt_runall(self, ctx): """Run all tests""" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' - dgcog = await self.get_dgcog() qsuite = await self.config.test_suite() qo = "" qc = 0 - ml = len(max(qsuite, key=len)) + 2 + ml = len(max(qsuite or [''], key=len)) + 2 async with ctx.typing(): for c, q in enumerate(sorted(qsuite)): try: - m = await find_monster(dgcog, q) or -1 + m = await self.find_monster(q) or -1 except Exception: m = -2 mid = getattr(m, "monster_id", m) @@ -450,8 +456,8 @@ async def idt_runall(self, ctx): fc = 0 async with ctx.typing(): for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in dgcog.index.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in dgcog.index.name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.index.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.index.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) @@ -475,12 +481,10 @@ async def idt_runall(self, ctx): await ctx.send(box(page)) async def run_tests(self): - dgcog = await self.get_dgcog() - qsuite = await self.config.test_suite() for q in qsuite: try: - m = await find_monster(dgcog, q) or -1 + m = await self.find_monster(q) or -1 except Exception: m = -2 mid = getattr(m, "monster_id", m) @@ -490,8 +494,8 @@ async def run_tests(self): fsuite = await self.config.fluff_suite() for v in fsuite: - fluff = v['id'] in [m.monster_id for m in dgcog.index.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in dgcog.index.name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.index.fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.index.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): return False diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 9f674fad..a4937702 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -438,9 +438,9 @@ def idx_to_xy(idx): class PadBuildImageGenerator(object): - def __init__(self, params, padinfo_cog, build_name='pad_build'): + def __init__(self, params, dgcog, build_name='pad_build'): self.params = params - self.padinfo_cog = padinfo_cog + self.dgcog = dgcog self.lexer = PaDTeamLexer().build() self.build = { 'NAME': build_name, @@ -511,7 +511,7 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card = await self.padinfo_cog.fm3(tok.value) + ass_card = await self.dgcog.find_monster(tok.value) if ass_card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') elif tok.type == 'REPEAT': @@ -521,7 +521,7 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card = await self.padinfo_cog.fm3(tok.value) + card = await self.dgcog.find_monster(tok.value) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') if not card.is_inheritable: @@ -797,7 +797,7 @@ async def padbuildimg(self, ctx, *, build_str: str): async with ctx.typing(): params = self.settings.buildImgParams() try: - pbg = PadBuildImageGenerator(params, self.bot.get_cog('PadInfo'), self.bot.get_cog('Dadguide')) + pbg = PadBuildImageGenerator(params, self.bot.get_cog('Dadguide')) await pbg.process_build(build_str) pbg.generate_build_image() except commands.UserFeedbackCheckFailure as ex: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 5cb51e0a..6a819428 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -62,10 +62,10 @@ def mod_help(self, ctx, help_type): async def lookup_monster_model(query: str): - padinfo_cog = PADGLOBAL_COG.bot.get_cog('PadInfo') - if padinfo_cog is None: + dgcog = PADGLOBAL_COG.bot.get_cog('Dadguide') + if dgcog is None: raise Exception("Cog not Loaded") - m = await padinfo_cog.fm3(str(query)) + m = await dgcog.find_monster(str(query)) return m diff --git a/padinfo/core/historic_lookups.py b/padinfo/core/historic_lookups.py deleted file mode 100644 index 8a6b880f..00000000 --- a/padinfo/core/historic_lookups.py +++ /dev/null @@ -1,15 +0,0 @@ -import os - -from redbot.core import data_manager -from tsutils import safe_read_json - - -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) - - -historic_lookups_file_path = _data_file('historic_lookups.json') -historic_lookups = safe_read_json(historic_lookups_file_path) - -historic_lookups_file_path_id3 = _data_file('historic_lookups_id3.json') -historic_lookups_id3 = safe_read_json(historic_lookups_file_path_id3) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 07797b61..6a2f7dd2 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,7 +1,5 @@ import re -from padinfo.core.find_monster import find_monster - def humanize_number(number, sigfigs=2): n = float("{0:.{1}g}".format(number, sigfigs)) @@ -82,7 +80,7 @@ async def perform_leaderskill_query(dgcog, raw_query): right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" - m = await find_monster(dgcog, query) + m = await dgcog.find_monster(query) if m and left_query in dgcog.index.modifiers[m]: left_query = query right_query = None @@ -91,9 +89,9 @@ async def perform_leaderskill_query(dgcog, raw_query): else: # no separators left_query, right_query = query, None - left_m = await find_monster(dgcog, left_query) + left_m = await dgcog.find_monster(left_query) if right_query: - right_m = await find_monster(dgcog, right_query) + right_m = await dgcog.find_monster(right_query) else: right_m = left_m return left_m, left_query, right_m, right_query diff --git a/padinfo/core/padinfo_settings.py b/padinfo/core/padinfo_settings.py index e66d843f..149535eb 100644 --- a/padinfo/core/padinfo_settings.py +++ b/padinfo/core/padinfo_settings.py @@ -8,7 +8,6 @@ def make_default_settings(self): 'alt_id_optout': [], 'voice_dir_path': '', 'emoji_use': {}, - 'typo_mods': [], } return config @@ -52,9 +51,4 @@ def log_emoji(self, emote): self.bot_settings['emoji_use'][emote] = self.bot_settings['emoji_use'].get(emote, 0) + 1 self.save_settings() - def add_typo_mod(self, typo): - self.bot_settings['typo_mods'].append(typo) - self.save_settings() - - settings = PadInfoSettings("padinfo") diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index ad18bf5e..21fe57de 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -1,10 +1,7 @@ -from padinfo.core.find_monster import find_monster - - async def perform_transforminfo_query(dgcog, raw_query): db_context = dgcog.database mgraph = dgcog.database.graph - found_monster = await find_monster(dgcog, raw_query) + found_monster = await dgcog.find_monster(raw_query) if not found_monster: return None, None, None diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ee328d59..c2d0f2c0 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -19,20 +19,15 @@ from redbot.core import checks, commands, data_manager, Config from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate -from tsutils import char_to_emoji, is_donor +from tsutils import char_to_emoji, is_donor, safe_read_json from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ get_attribute_emoji_by_monster, AWAKENING_ID_TO_EMOJI_NAME_MAP -from padinfo.core import find_monster as fm from padinfo.core.button_info import button_info -from padinfo.core.find_monster import FindMonster, calc_ratio_name, calc_ratio_modifier, find_monster, \ - find_monster_debug, find_monsters -from padinfo.core.historic_lookups import historic_lookups from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query -from padinfo.idtest_mixin import IdTest from padinfo.menu.closable_embed import ClosableEmbedMenu from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu @@ -90,7 +85,7 @@ def _data_file(file_name: str) -> str: } -class PadInfo(commands.Cog, IdTest): +class PadInfo(commands.Cog): """Info for PAD Cards""" def __init__(self, bot, *args, **kwargs): @@ -104,11 +99,10 @@ def __init__(self, bot, *args, **kwargs): self.remove_emoji = '\N{CROSS MARK}' self.config = Config.get_conf(self, identifier=9401770) - self.config.register_user(survey_mode=0, color=None, beta_id3=False, lastaction=None) - self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False, test_suite={}, - fluff_suite=[]) + self.config.register_user(survey_mode=0, color=None, beta_id3=False) + self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False) - self.fm3 = lambda q: fm.find_monster(bot.get_cog("Dadguide"), q) + self.historic_lookups = safe_read_json(_data_file('historic_lookups_id3.json')) self.awoken_emoji_names = {v: k for k, v in AWAKENING_ID_TO_EMOJI_NAME_MAP.items()} self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster @@ -116,7 +110,6 @@ def __init__(self, bot, *args, **kwargs): def cog_unload(self): """Manually nulling out database because the GC for cogs seems to be pretty shitty""" - # TODO.... manage historic_lookups??? probably not. async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -174,7 +167,7 @@ async def test_reaction_add(self, reaction, member): data.update({ 'reaction': emoji_clicked }) - + await menu.transition(message, deepcopy(ims), emoji_clicked, member, **data) await self.listener_respond_with_child(deepcopy(ims), message, emoji_clicked, member) @@ -245,7 +238,7 @@ async def get_menu_default_data(self, ims): async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if monster is not None: await ctx.send(MonsterHeader.short(monster)) await ctx.send(box(monster.name_ja)) @@ -308,7 +301,7 @@ async def send_message(): async with self.config.bad_queries() as bad_queries: bad_queries.append((raw_query, ctx.author.id)) - monster = await find_monster(dgcog, raw_query) + monster = await dgcog.find_monster(raw_query) if not monster: await self.send_id_failure_message(ctx, query) @@ -341,7 +334,7 @@ async def send_survey_after(self, ctx, query, result_monster): survey_mode = await self.config.user(ctx.author).survey_mode() survey_chance = (1, await self.config.sometimes_perc() / 100, 0)[survey_mode] if random.random() < survey_chance: - mid1 = historic_lookups.get(query) + mid1 = self.historic_lookups.get(query) m1 = mid1 and dgcog.get_monster(mid1) id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" @@ -399,7 +392,7 @@ async def evos(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await find_monster(dgcog, raw_query) + monster = await dgcog.find_monster(raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -430,7 +423,7 @@ async def evomats(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await find_monster(dgcog, raw_query) + monster = await dgcog.find_monster(raw_query) if not monster: await self.send_id_failure_message(ctx, query) @@ -463,7 +456,7 @@ async def pantheon(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await find_monster(dgcog, raw_query) + monster = await dgcog.find_monster(raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -493,7 +486,7 @@ async def pic(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await find_monster(dgcog, raw_query) + monster = await dgcog.find_monster(raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -519,7 +512,7 @@ async def otherinfo(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await find_monster(dgcog, raw_query) + monster = await dgcog.find_monster(raw_query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -541,7 +534,7 @@ async def otherinfo(self, ctx, *, query: str): async def links(self, ctx, *, query: str): """Monster links""" dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -554,7 +547,7 @@ async def links(self, ctx, *, query: str): async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -567,7 +560,7 @@ async def lookup(self, ctx, *, query: str): async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -581,7 +574,7 @@ async def buttoninfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -633,7 +626,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM @checks.bot_has_permissions(embed_links=True) async def collabscroll(self, ctx, *, query): dgcog = await self.get_dgcog() - monster: "MonsterModel" = await find_monster(dgcog, query) + monster: "MonsterModel" = await dgcog.find_monster(query) if monster is None: await self.send_id_failure_message(ctx, query) @@ -736,7 +729,7 @@ async def get_user_friends(self, original_author_id): @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if not monster: await self.send_id_failure_message(ctx, query) return @@ -779,7 +772,7 @@ async def transforminfo(self, ctx, *, query): async def awakeninghelp(self, ctx, *, query): """Describe a monster's regular and super awakenings in detail.""" dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if not monster: await self.send_id_failure_message(ctx, query) @@ -813,7 +806,7 @@ async def padsay(self, ctx, server, *, query: str = None): query = query.strip().lower() dgcog = await self.get_dgcog() - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) if monster is not None: voice_id = monster.voice_id_jp if server == 'jp' else monster.voice_id_na if voice_id is None: @@ -955,7 +948,7 @@ async def send_id_failure_message(ctx, query: str): async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" dgcog = self.bot.get_cog("Dadguide") - mon = await find_monster(dgcog, query) + mon = await dgcog.find_monster(query) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) return @@ -989,17 +982,17 @@ async def debugiddist(self, ctx, s1, s2): dgcog = self.bot.get_cog("Dadguide") - dist = calc_ratio_modifier(s1, s2) - dist2 = calc_ratio_name(s1, s2, dgcog.index) + dist = dgcog.mon_finder.calc_ratio_modifier(s1, s2) + dist2 = dgcog.mon_finder.calc_ratio_name(s1, s2) yes = '\N{WHITE HEAVY CHECK MARK}' no = '\N{CROSS MARK}' await ctx.send(f"Printing info for {inline(s1)}, {inline(s2)}\n" + box(f"Jaro-Winkler Distance: {round(dist, 4)}\n" f"Name Matching Distance: {round(dist2, 4)}\n" - f"Modifier token threshold: {FindMonster.MODIFIER_JW_DISTANCE} " - f"{yes if dist >= FindMonster.MODIFIER_JW_DISTANCE else no}\n" - f"Name token threshold: {FindMonster.TOKEN_JW_DISTANCE} " - f"{yes if dist2 >= FindMonster.TOKEN_JW_DISTANCE else no}")) + f"Modifier token threshold: {dgcog.mon_finder.MODIFIER_JW_DISTANCE} " + f"{yes if dist >= dgcog.mon_finder.MODIFIER_JW_DISTANCE else no}\n" + f"Name token threshold: {dgcog.mon_finder.TOKEN_JW_DISTANCE} " + f"{yes if dist2 >= dgcog.mon_finder.TOKEN_JW_DISTANCE else no}")) @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query=""): @@ -1096,8 +1089,8 @@ def f(m, s): ret += f"{' '.join(t).title()}" ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + - str(FindMonster.get_most_eligable_monster(DGCOG.index.all_name_tokens[''.join(t)], - DGCOG).monster_id) + str(DGCOG.mon_finder.get_most_eligable_monster(DGCOG.index.all_name_tokens[''.join(t)], + DGCOG).monster_id) + ")\n") def additmods(ms, om): @@ -1153,7 +1146,7 @@ async def idtraceback(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") - bestmatch, matches, _, _ = await find_monster_debug(dgcog, query) + bestmatch, matches, _, _ = await dgcog.mon_finder.find_monster_debug(query) if bestmatch is None: await ctx.send("No monster matched.") @@ -1178,10 +1171,10 @@ async def idtraceback(self, ctx, *, query): lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) mtokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" - f" ({round(calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" + f" ({round(dgcog.mon_finder.calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" for t in sorted(mtokens)) ntokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" - f" ({round(calc_ratio_name(t[0], t[1], dgcog.index), 2) if t[0] != t[1] else 'exact'})" + f" ({round(dgcog.mon_finder.calc_ratio_name(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" f" {t[2]}" for t in sorted(ntokens)) @@ -1199,7 +1192,7 @@ async def idtraceback(self, ctx, *, query): async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") - matched_monsters = await find_monsters(dgcog, query) + matched_monsters = await dgcog.find_monsters(query) if not matched_monsters: await ctx.send("No monster matched.") diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 058957dc..3fdab154 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -1,6 +1,3 @@ -from padinfo.core.find_monster import find_monster - - async def get_monster_from_ims(dgcog, ims: dict): query = ims.get('query') or ims['raw_query'] @@ -8,5 +5,5 @@ async def get_monster_from_ims(dgcog, ims: dict): resolved_monster_id = int(resolved_monster_id_str) if resolved_monster_id_str else None if resolved_monster_id: return dgcog.database.graph.get_monster(resolved_monster_id) - monster = await find_monster(dgcog, query) + monster = await dgcog.find_monster(query) return monster diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 30f4e536..ed8d85ef 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -46,14 +46,13 @@ async def red_delete_data_for_user(self, *, requester, user_id): @commands.group(invoke_without_command=True) async def pricecheck(self, ctx, *, query): """Displays pricing data for a tradable non-collab gem.""" - padinfo_cog = self.bot.get_cog('PadInfo') dgcog = self.bot.get_cog("Dadguide") - if padinfo_cog is None or dgcog is None: + if dgcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await padinfo_cog.fm3(query) + m = await dgcog.find_monster(query) if not m: await ctx.send("Monster not found.") return @@ -88,14 +87,13 @@ async def pcadmin(self, ctx): @pcadmin.command(aliases=['add']) async def set(self, ctx, stam_cost: float, *, query): """Adds stamina cost data to a card.""" - padinfo_cog = self.bot.get_cog('PadInfo') dgcog = self.bot.get_cog("Dadguide") - if padinfo_cog is None: + if dgcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await padinfo_cog.fm3(query) + m = await dgcog.find_monster(query) if not m: await ctx.send("Monster not found.") return @@ -110,14 +108,13 @@ async def set(self, ctx, stam_cost: float, *, query): @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) async def setfooter(self, ctx, query, *, footer=""): """Adds notes regarding the stamina cost of a card.""" - padinfo_cog = self.bot.get_cog('PadInfo') dgcog = self.bot.get_cog('Dadguide') - if padinfo_cog is None or dgcog is None: + if dgcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await padinfo_cog.fm3(query) + m = await dgcog.find_monster(query) if not m: await ctx.send("Monster not found.") return @@ -132,14 +129,13 @@ async def setfooter(self, ctx, query, *, footer=""): @pcadmin.command(aliases=['delete', 'del', 'rm']) async def remove(self, ctx, *, query): """Removes stamina cost data from a card.""" - padinfo_cog = self.bot.get_cog('PadInfo') dgcog = self.bot.get_cog('Dadguide') - if padinfo_cog is None or dgcog is None: + if dgcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await padinfo_cog.fm3(query) + m = await dgcog.find_monster(query) if not m: await ctx.send("Monster not found.") return From 7b2f7de1d06f4ff4b9d95d71f36aa86221028bb4 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 5 Mar 2021 18:36:08 -0700 Subject: [PATCH 0684/1220] Fix idtest import (#919) Before it didn't accept some entries including ones that had negative resolution ids --- dadguide/idtest_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index 309e153a..9375c3c4 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -130,7 +130,7 @@ async def idt_import(self, ctx, *, queries): return await self.config.user(ctx.author).lastaction.set('id3') - cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+) *(.*)', queries) + cases = re.findall(r'\s*(?:\d+. )?(.+?) +- (-?\d+) *(.*)', queries) async with self.config.test_suite() as suite: for query, result, reason in cases: suite[query] = {'result': int(result), 'reason': reason, 'ts': datetime.now().timestamp()} @@ -154,7 +154,7 @@ async def norf_import(self, ctx, queries): return await self.config.user(ctx.author).lastaction.set('name') - cases = re.findall(r'\s*(?:\d+. )?(.+?) + - (\d+)\s+(\w*) *(.*)', queries) + cases = re.findall(r'\s*(?:\d+. )?(.+?) +- (\d+)\s+(\w*) *(.*)', queries) async with self.config.fluff_suite() as suite: for query, result, fluff, reason in cases: # print(query, result, fluff, reason) From 954d572602a759ccd165caf88c26e14a70aaaf2b Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Thu, 11 Mar 2021 01:59:41 -0500 Subject: [PATCH 0685/1220] Guarantee a pantheon tab by filtering if needed (#921) * Stop using magic number in pantheon * Add filtering logic * Make pantheon filter on base monster's details * Filter pantheon by a monster's subatt if it has no main att * Refine material type filtering * Display pantheon filtering criteria * Clean up pantheon filtering code * Add base monster to pantheon view state and use it to show current position * Add filter labels to pantheon * Update pantheon failure message in padinfo * Use list comprehensions to filter pantheon * Fix bug filtering monsters with no main attribute * Add comment describing filtering logic to pantheon * Compare against enum name instead of value * Change no attribute to nil attribute * Rework filter display * Actually use correct max number of mons instead of one fewer * Condense list length comparison * Move pantheon lines method --- padinfo/padinfo.py | 7 +-- padinfo/reaction_list.py | 2 +- padinfo/view/pantheon.py | 105 +++++++++++++++++++++++++++++++++------ 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c2d0f2c0..24f46c48 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -462,16 +462,17 @@ async def pantheon(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return - pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + pantheon_list, series_name, base_monster = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: - await ctx.send(inline('Too many monsters in this series to display')) + await ctx.send('Unable to find a pantheon for the result of your query,' + + ' [{}] {}.'.format(monster.monster_id, monster.name_en)) return alt_monsters = PantheonViewState.get_alt_monsters(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, alt_monsters, pantheon_list, series_name, + monster, alt_monsters, pantheon_list, series_name, base_monster, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.pantheon_control) diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index 33151305..ae5af293 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -24,7 +24,7 @@ async def get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", full_reaction_list.remove(IdMenuEmoji.left) full_reaction_list.remove(IdMenuEmoji.right) full_reaction_list.remove(IdMenuEmoji.evos) - pantheon_list, _ = await PantheonViewState.query(dgcog, monster) + pantheon_list, _, _ = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: full_reaction_list.remove(IdMenuEmoji.pantheon) mats, usedin, gemid, _, skillups, _, _, _ = await MaterialsViewState.query(dgcog, monster) diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 0e2614fb..8075c83a 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -5,6 +5,7 @@ from discordmenu.embed.view import EmbedView from padinfo.common.config import UserConfig +from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum from padinfo.common.external_links import puzzledragonx from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state @@ -16,10 +17,14 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel +MAX_MONS_TO_SHOW = 20 +MAT_TYPES = ['Evolve', 'Awoken', 'Enhance', 'Vendor'] +NIL_ATT = 'Nil' + class PantheonViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters, - pantheon_list: List["MonsterModel"], series_name: str, + pantheon_list: List["MonsterModel"], series_name: str, base_monster, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): @@ -29,6 +34,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst extra_state=extra_state) self.series_name = series_name self.pantheon_list = pantheon_list + self.base_monster = base_monster def serialize(self): ret = super().serialize() @@ -42,7 +48,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None monster = await get_monster_from_ims(dgcog, ims) - pantheon_list, series_name = await PantheonViewState.query(dgcog, monster) + pantheon_list, series_name, base_monster = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: return None @@ -56,24 +62,96 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, - pantheon_list, series_name, + pantheon_list, series_name, base_monster, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) + @classmethod + def make_series_name(cls, series_name, filter_strings): + return '{} [Filters: {}]'.format(series_name, ' '.join(filter_strings)) + @classmethod async def query(cls, dgcog, monster): + # filtering rules: + # 1. don't show monsters that only have mat types (or show only mats if monster is a mat) + # 2. if still too many, show only monsters with the same base rarity + # 3. if still too many, show only monsters with the same base main attribute (any monster + # with nil main attribute is compared using its subattribute) + # 4. if still too many, show only monsters that match both base attributes + # 5. if still too many, truncate (and probably redefine pantheon) + db_context = dgcog.database + series_name = monster.series.name_en full_pantheon = db_context.get_monsters_by_series(monster.series_id) if not full_pantheon: - return None, None - pantheon_list = list(filter(lambda x: db_context.graph.monster_is_base(x), full_pantheon)) - if len(pantheon_list) == 0 or len(pantheon_list) > 20: - return None, None - - series_name = monster.series.name_en - - return pantheon_list, series_name + return None, None, None + + base_mon = db_context.graph.get_base_monster(monster) + + base_list = [m for m in full_pantheon if db_context.graph.monster_is_base(m)] + if 0 < len(base_list) <= MAX_MONS_TO_SHOW: + return base_list, series_name, base_mon + + # if monster has only mat types, show only mats, otherwise show everything else + type_list = [] + if all(t.name in MAT_TYPES for t in monster.types): + series_name += ' (Mat)' + type_list = [m for m in base_list if all(t.name in MAT_TYPES for t in m.types)] + else: + type_list = [m for m in base_list if any(t.name not in MAT_TYPES for t in m.types)] + if 0 < len(type_list) <= MAX_MONS_TO_SHOW: + return type_list, series_name, base_mon + + filters = [] + + rarity_list = [m for m in type_list if m.rarity == base_mon.rarity] + filters.append('{}*'.format(base_mon.rarity)) + if 0 < len(rarity_list) <= MAX_MONS_TO_SHOW: + return rarity_list, cls.make_series_name(series_name, filters), base_mon + + main_att = base_mon.attr1 + sub_att = base_mon.attr2 + + # if the monster has nil main attribute, do comparisons using its subattribute + # if any monster in the list has nil main attribute, also compare using its subattribute + main_att_list = [] + att_emoji = None + if main_att.name == NIL_ATT: + att_emoji = get_attribute_emoji_by_enum(sub_att) + main_att_list = [m for m in rarity_list if m.attr1.name == sub_att.name + or (m.attr1.name == NIL_ATT + and m.attr2.name == sub_att.name)] + else: + att_emoji = get_attribute_emoji_by_enum(main_att) + main_att_list = [m for m in rarity_list if m.attr1.name == main_att.name + or (m.attr1.name == NIL_ATT + and m.attr2.name == main_att.name)] + if 0 < len(main_att_list) <= MAX_MONS_TO_SHOW: + # append after check this time, because if we go to subatt we only want one emoji + filters.append(att_emoji) + return main_att_list, cls.make_series_name(series_name, filters), base_mon + + sub_att_list = [m for m in main_att_list if m.attr1.name == main_att.name + and m.attr2.name == sub_att.name] + filters.append(get_attribute_emoji_by_monster(base_mon)) + if 0 < len(sub_att_list) <= MAX_MONS_TO_SHOW: + return sub_att_list, cls.make_series_name(series_name, filters), base_mon + + # if we've managed to get here, just cut it off + pantheon_list = sub_att_list[:MAX_MONS_TO_SHOW] + filters.append('(still too many, truncated)') + + return pantheon_list, cls.make_series_name(series_name, filters), base_mon + + +def _pantheon_lines(monsters, base_monster): + if not len(monsters): + return [] + return [ + MonsterHeader.short_with_emoji(mon, link=mon.monster_id != base_monster.monster_id) + for mon in sorted(monsters, key=lambda x: int(x.monster_id)) + ] class PantheonView: @@ -83,10 +161,7 @@ class PantheonView: def embed(state: PantheonViewState): fields = [EmbedField( 'Pantheon: {}'.format(state.series_name), - Box( - *[MonsterHeader.short_with_emoji(m) - for m in sorted(state.pantheon_list, key=lambda x: x.monster_no_na)] - ) + Box(*_pantheon_lines(state.pantheon_list, state.base_monster)) ), evos_embed_field(state)] From cfb2b1287d23d60b3b021a13ffe13d748424aa2d Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 11 Mar 2021 16:42:31 -0700 Subject: [PATCH 0686/1220] Show unoverriden names in PDM (#923) --- dadguide/models/monster_model.py | 2 +- padmonitor/padmonitor.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index abc5262b..7a60cb17 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -33,7 +33,7 @@ def __init__(self, **m): self.series_id = m['series_id'] self.name_ja = m['name_ja'] self.name_ko = m['name_ko'] - self.name_en = m['name_en'] + self.name_en = self.unoverridden_name_en = m['name_en'] self.roma_subname = None if self.name_en == self.name_ja: self.roma_subname = self.make_roma_subname(self.name_ja) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 3a7d360e..ec6020e8 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -68,9 +68,8 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: - msg += '\n\t[{}] {}'.format(m.monster_no, m.name_en) - if tsutils.contains_ja( - m.name_en) and m.name_en_override != m.name_en and m.name_en_override is not None: + msg += '\n\t[{}] {}'.format(m.monster_no, m.unoverridden_name_en) + if tsutils.contains_ja(m.name_en): msg += ' ({})'.format(m.name_en_override) return msg else: From 90f53b395cbe569833a5b65ea454a508acbc5a6a Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 11 Mar 2021 16:43:23 -0700 Subject: [PATCH 0687/1220] Add more aliases to pdg delete (#926) --- padglobal/padglobal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 6a819428..dff90fb3 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -248,7 +248,7 @@ async def _add(self, ctx, command, text, confirm=True): json.dump(self.c_commands, open(self.file_path, 'w+')) await ctx.send("PAD command successfully {}.".format(op)) - @padglobal.command(aliases=['rmalias', 'delalias']) + @padglobal.command(aliases=['rmalias', 'delalias', 'remove', 'rm', 'del']) async def delete(self, ctx, command: str): """Deletes a PAD global command or alias @@ -568,7 +568,6 @@ async def addboss(self, ctx, term, *, definition): base = dgcog.database.graph.get_base_monster(m) - op = 'EDITED' if base.monster_id in self.settings.boss() else 'ADDED' if op == 'EDITED' and not await confirm_message(ctx, "Are you sure you want to edit the boss info for {}?".format( From 090c32fecf8ff119c48f09e2d012ac8fd7d61a56 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 11 Mar 2021 16:45:42 -0700 Subject: [PATCH 0688/1220] Show what was removed on idtest remove and idtest name/fluff remove (#924) --- dadguide/idtest_mixin.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index 9375c3c4..94fe8de6 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -178,13 +178,16 @@ async def idt_remove(self, ctx, *, item): async with self.config.test_suite() as suite: if item in suite: - del suite[item] + case = item elif item.isdigit() and int(item) < len(suite): - del suite[sorted(suite)[int(item)]] + case = sorted(suite)[int(item)] else: await ctx.react_quietly("\N{CROSS MARK}") return - await ctx.tick() + res = suite[case]['result'] + del suite[case] + await ctx.send( + f"Removed test case `{case} - {res}` with ref") @idt_name.command(name="remove") @checks.is_owner() @@ -208,8 +211,11 @@ async def norf_remove(self, ctx, item): if item >= len(suite): await ctx.send("There are not that many items.") return - suite.remove(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item]) - await ctx.tick() + case = sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))[item] + suite.remove(case) + # noinspection PyTypeChecker + await ctx.send(f"Successfully removed {'fluff' if case['fluff'] else 'name'} case" + f" `{case['id']} - {case['token']}`") @idtest.command(name="setreason", aliases=["addreason"]) @checks.is_owner() From a056534fff1c7119a1918514e351f04f50ee4856 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 12 Mar 2021 09:40:06 -0700 Subject: [PATCH 0689/1220] [PDM] Don't show name_en_override unless it exists --- padmonitor/padmonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index ec6020e8..f1278682 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -69,7 +69,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: msg += '\n\t[{}] {}'.format(m.monster_no, m.unoverridden_name_en) - if tsutils.contains_ja(m.name_en): + if tsutils.contains_ja(m.name_en) and m.name_en_override: msg += ' ({})'.format(m.name_en_override) return msg else: From 0df78b000262c77d7719d4b98853f5dcab4a1656 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 12 Mar 2021 11:40:50 -0700 Subject: [PATCH 0690/1220] Update padmonitor.py --- padmonitor/padmonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index f1278682..d9b91195 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -69,7 +69,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: msg += '\n\t[{}] {}'.format(m.monster_no, m.unoverridden_name_en) - if tsutils.contains_ja(m.name_en) and m.name_en_override: + if tsutils.contains_ja(m.name_en) and m.name_en_override is not None: msg += ' ({})'.format(m.name_en_override) return msg else: From 5e1391c0d11fcf0171fedc76f6d98f38fc44c21f Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 12 Mar 2021 18:02:06 -0700 Subject: [PATCH 0691/1220] Only show non-null name_en_override in PadMonitor (#927) * [PDM] Don't show name_en_override unless it exists * Update padmonitor.py --- padmonitor/padmonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index ec6020e8..d9b91195 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -69,7 +69,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: msg += '\n\t[{}] {}'.format(m.monster_no, m.unoverridden_name_en) - if tsutils.contains_ja(m.name_en): + if tsutils.contains_ja(m.name_en) and m.name_en_override is not None: msg += ' ({})'.format(m.name_en_override) return msg else: From 8834f3bc051647292d61fb77f2bf3787afd0524f Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 12 Mar 2021 18:02:16 -0700 Subject: [PATCH 0692/1220] Make jp/na priority a user pref in ^id (#925) Also adds a general-purpose framework for any per-user settings to be easily added and used in the find_monster function Note that ^idtraceback, specifically, does NOT honor this user preference. Merge this instead of #922 to allow user preference. They are mutually exclusive. --- dadguide/dadguide.py | 19 ++++++++--- dadguide/find_monster.py | 5 +-- padbuildimg/padbuildimg.py | 11 ++++--- padglobal/padglobal.py | 61 +++++++++++++---------------------- padinfo/core/leader_skills.py | 8 ++--- padinfo/core/transforminfo.py | 4 +-- padinfo/padinfo.py | 47 ++++++++++++++------------- padinfo/view/common.py | 2 +- padinfo/view/leader_skill.py | 2 +- pricecheck/pricecheck.py | 8 ++--- 10 files changed, 82 insertions(+), 85 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 779efc15..2aa0f5bb 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -11,7 +11,7 @@ import os import shutil from io import BytesIO -from typing import Optional +from typing import Optional, List import discord import time @@ -72,9 +72,10 @@ def __init__(self, bot, *args, **kwargs): self.fir_lock = asyncio.Lock() self.fir3_lock = asyncio.Lock() + self.fm_flags_default = {'na_prio': True} self.config = Config.get_conf(self, identifier=64667103) self.config.register_global(datafile='', indexlog=0, test_suite={}, fluff_suite=[], typo_mods=[]) - self.config.register_user(lastaction=None) + self.config.register_user(lastaction=None, fm_flags={}) self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') self.historic_lookups = safe_read_json(self.historic_lookups_file_path) @@ -82,9 +83,7 @@ def __init__(self, bot, *args, **kwargs): self.MonsterStatModifierInput = MonsterStatModifierInput self.token_maps = token_mappings - self.mon_finder = FindMonster(self) - self.find_monster = self.mon_finder.find_monster - self.find_monsters = self.mon_finder.find_monsters + self.mon_finder = FindMonster(self, self.fm_flags_default) GADMIN_COG = self.bot.get_cog("GlobalAdmin") if GADMIN_COG: @@ -227,3 +226,13 @@ async def setdatafile(self, ctx, data_file): async def setindexlog(self, ctx, channel: discord.TextChannel): await self.config.indexlog.set(channel.id) await ctx.tick() + + async def get_fm_flags(self, author_id): + # noinspection PyTypeChecker + return {**self.fm_flags_default, **(await self.config.user(discord.Object(author_id)).fm_flags())} + + async def find_monster(self, query: str, author_id: int = 0) -> MonsterModel: + return await FindMonster(self, await self.get_fm_flags(author_id)).find_monster(query) + + async def find_monsters(self, query: str, author_id: int = 0) -> List[MonsterModel]: + return await FindMonster(self, await self.get_fm_flags(author_id)).find_monsters(query) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 7db3c4ff..d42fe488 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -36,8 +36,9 @@ class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 - def __init__(self, dgcog): + def __init__(self, dgcog, flags): self.dgcog = dgcog + self.flags = flags def merge_multi_word_tokens(self, tokens): result = [] @@ -214,7 +215,7 @@ def get_priority_tuple(self, monster, tokenized_query=None, matches=None): not all(t.value in [0, 12, 14, 15] for t in monster.types), not any(t.value in [0, 12, 14, 15] for t in monster.types), -self.dgcog.database.graph.get_base_id(monster), - monster.on_na, + monster.on_na if self.flags['na_prio'] else True, not monster.is_equip, monster.rarity, monster.monster_no_na) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index a4937702..79ced647 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -438,9 +438,10 @@ def idx_to_xy(idx): class PadBuildImageGenerator(object): - def __init__(self, params, dgcog, build_name='pad_build'): + def __init__(self, params, ctx, build_name='pad_build'): self.params = params - self.dgcog = dgcog + self.ctx = ctx + self.dgcog = ctx.bot.get_cog("Dadguide") self.lexer = PaDTeamLexer().build() self.build = { 'NAME': build_name, @@ -511,7 +512,7 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card = await self.dgcog.find_monster(tok.value) + ass_card = await self.dgcog.find_monster(tok.value, self.ctx.author.id) if ass_card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') elif tok.type == 'REPEAT': @@ -521,7 +522,7 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card = await self.dgcog.find_monster(tok.value) + card = await self.dgcog.find_monster(tok.value, self.ctx.author.id) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') if not card.is_inheritable: @@ -797,7 +798,7 @@ async def padbuildimg(self, ctx, *, build_str: str): async with ctx.typing(): params = self.settings.buildImgParams() try: - pbg = PadBuildImageGenerator(params, self.bot.get_cog('Dadguide')) + pbg = PadBuildImageGenerator(params, ctx) await pbg.process_build(build_str) pbg.generate_build_image() except commands.UserFeedbackCheckFailure as ex: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index dff90fb3..fc1473c3 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -61,21 +61,6 @@ def mod_help(self, ctx, help_type): commands.Command.format_shortdoc_for_context = lambda s, c: mod_help(s, c, "short_doc") -async def lookup_monster_model(query: str): - dgcog = PADGLOBAL_COG.bot.get_cog('Dadguide') - if dgcog is None: - raise Exception("Cog not Loaded") - m = await dgcog.find_monster(str(query)) - return m - - -def monster_id_to_monster(monster_id): - dgcog = PADGLOBAL_COG.bot.get_cog('Dadguide') - if dgcog is None: - return None - return dgcog.get_monster(monster_id) - - async def check_enabled(ctx): """If the server is disabled, raise a warning and return False""" if PADGLOBAL_COG.settings.checkDisabled(ctx.message): @@ -500,7 +485,7 @@ async def rmglossary(self, ctx, *, term): async def boss(self, ctx, *, term: str = None): """Shows boss skill entries""" if term: - name, definition = await self.lookup_boss(term) + name, definition = await self.lookup_boss(term, ctx) if definition: await ctx.send(self.emojify(definition)) else: @@ -518,12 +503,12 @@ async def bosslist(self, ctx): for page in pagify(msg): await ctx.author.send(page) - async def lookup_boss(self, term): + async def lookup_boss(self, term, ctx): dgcog = self.bot.get_cog('Dadguide') pdicog = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') - m = await lookup_monster_model(term) + m = await dgcog.find_monster(term, ctx.author.id) if m is None: return None, None @@ -534,9 +519,6 @@ async def lookup_boss(self, term): return name, definition - - - def boss_to_text(self, ctx): bosses = self.settings.boss() msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( @@ -561,7 +543,7 @@ async def addboss(self, ctx, term, *, definition): pdicog = self.bot.get_cog("PadInfo") term = term.lower() - m = await lookup_monster_model(term) + m = await dgcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(f"No monster found for `{term}`") return @@ -586,7 +568,7 @@ async def rmboss(self, ctx, *, term): pdicog = self.bot.get_cog("PadInfo") term = term.lower() - m = await lookup_monster_model(term) + m = await dgcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(f"No monster found for `{term}`. Make sure you didn't use quotes.") return @@ -628,11 +610,12 @@ async def which(self, ctx, *, term: str = None): await ctx.send(page) async def _resolve_which(self, ctx, term): - db_context = self.bot.get_cog('Dadguide').database + dgcog = self.bot.get_cog('Dadguide') + db_context = dgcog.database padinfo = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') - m = await lookup_monster_model(term) + m = await dgcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(inline('No monster matched that query')) return None, None, None, None @@ -650,7 +633,7 @@ async def _resolve_which(self, ctx, term): if definition is not None: return name, definition, timestamp, True - monster = monster_id_to_monster(monster_id) + monster = dgcog.get_monster(monster_id) if db_context.graph.monster_is_mp_evo(monster): return name, MP_BUY_MSG.format(ctx.prefix), None, False @@ -682,7 +665,7 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): def which_to_text(self): monsters = defaultdict(list) for monster_id in self.settings.which(): - m = monster_id_to_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) if m is None: continue name = m.name_en.split(", ")[-1] @@ -716,7 +699,7 @@ async def addwhich(self, ctx, monster_id: int, *, definition): If you provide a monster ID, the term will be entered for that monster tree. e.x. [p]padglobal addwhich 3818 take the pixel one """ - m = monster_id_to_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster @@ -734,7 +717,7 @@ async def addwhich(self, ctx, monster_id: int, *, definition): @padglobal.command() async def rmwhich(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" - m = monster_id_to_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster @@ -759,7 +742,7 @@ async def getwhich(self, ctx): for w in self.settings.which(): w %= 10000 - m = monster_id_to_monster(w) + m = self.bot.get_cog("Dadguide").get_monster(w) name = m.name_en.split(', ')[-1] result = self.settings.which()[w] @@ -1009,22 +992,24 @@ async def guide(self, ctx, *, term: str = None): await self.send_guide(ctx) return - term, text, err = await self.get_guide_text(term) + term, text, err = await self.get_guide_text(term, ctx) if text is None: await ctx.send(inline(err)) return await ctx.send(self.emojify(text)) - async def get_guide_text(self, term: str): + async def get_guide_text(self, term: str, ctx): + dgcog = self.bot.get_cog("Dadguide") + term = term.lower() if term in self.settings.dungeonGuide(): return term, self.settings.dungeonGuide()[term], None - m = await lookup_monster_model(term) + m = await dgcog.find_monster(term, ctx.author.id) if m is None: return None, None, 'No dungeon or monster matched that query' - m = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + m = dgcog.database.graph.get_base_monster(m) name = m.name_en definition = self.settings.leaderGuide().get(m.monster_id, None) @@ -1047,7 +1032,7 @@ def guide_to_text(self): msg += '\n\n__**Leader Guides**__' for monster_id, definition in self.settings.leaderGuide().items(): - m = monster_id_to_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) if m is None: continue name = m.name_en.split(', ')[-1].title() @@ -1062,7 +1047,7 @@ async def guideto(self, ctx, to_user: discord.Member, *, term: str): [p]guideto @{0.author.name} osc10 """ - term, text, err = await self.get_guide_text(term) + term, text, err = await self.get_guide_text(term, ctx) if text is None: await ctx.send(inline(err)) return @@ -1103,7 +1088,7 @@ async def rmdungeonguide(self, ctx, term: str): @padglobal.command() async def addleaderguide(self, ctx, monster_id: int, *, definition: str): """Adds a leader guide to the [p]guide command""" - m = monster_id_to_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster @@ -1121,7 +1106,7 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): @padglobal.command() async def rmleaderguide(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" - m = monster_id_to_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 6a2f7dd2..3e17df1c 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -65,7 +65,7 @@ def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): return f"[{format_number(hp)}/{format_number(atk)}/{format_number(rcv)}{resist}] {extras}" -async def perform_leaderskill_query(dgcog, raw_query): +async def perform_leaderskill_query(dgcog, raw_query, author_id): # Remove unicode quotation marks query = re.sub("[\u201c\u201d]", '"', raw_query) @@ -80,7 +80,7 @@ async def perform_leaderskill_query(dgcog, raw_query): right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" - m = await dgcog.find_monster(query) + m = await dgcog.find_monster(query, author_id) if m and left_query in dgcog.index.modifiers[m]: left_query = query right_query = None @@ -89,9 +89,9 @@ async def perform_leaderskill_query(dgcog, raw_query): else: # no separators left_query, right_query = query, None - left_m = await dgcog.find_monster(left_query) + left_m = await dgcog.find_monster(left_query, author_id) if right_query: - right_m = await dgcog.find_monster(right_query) + right_m = await dgcog.find_monster(right_query, author_id) else: right_m = left_m return left_m, left_query, right_m, right_query diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index 21fe57de..945c7b80 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -1,7 +1,7 @@ -async def perform_transforminfo_query(dgcog, raw_query): +async def perform_transforminfo_query(dgcog, raw_query, author_id): db_context = dgcog.database mgraph = dgcog.database.graph - found_monster = await dgcog.find_monster(raw_query) + found_monster = await dgcog.find_monster(raw_query, author_id) if not found_monster: return None, None, None diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 24f46c48..125ef7d3 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -238,7 +238,7 @@ async def get_menu_default_data(self, ims): async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if monster is not None: await ctx.send(MonsterHeader.short(monster)) await ctx.send(box(monster.name_ja)) @@ -301,7 +301,7 @@ async def send_message(): async with self.config.bad_queries() as bad_queries: bad_queries.append((raw_query, ctx.author.id)) - monster = await dgcog.find_monster(raw_query) + monster = await dgcog.find_monster(raw_query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) @@ -392,7 +392,7 @@ async def evos(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query) + monster = await dgcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -423,7 +423,7 @@ async def evomats(self, ctx, *, query: str): raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query) + monster = await dgcog.find_monster(raw_query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) @@ -456,7 +456,7 @@ async def pantheon(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query) + monster = await dgcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -487,7 +487,7 @@ async def pic(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query) + monster = await dgcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -513,7 +513,7 @@ async def otherinfo(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query) + monster = await dgcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -535,7 +535,7 @@ async def otherinfo(self, ctx, *, query: str): async def links(self, ctx, *, query: str): """Monster links""" dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -548,7 +548,7 @@ async def links(self, ctx, *, query: str): async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -561,7 +561,7 @@ async def lookup(self, ctx, *, query: str): async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -575,7 +575,7 @@ async def buttoninfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -627,7 +627,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM @checks.bot_has_permissions(embed_links=True) async def collabscroll(self, ctx, *, query): dgcog = await self.get_dgcog() - monster: "MonsterModel" = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -690,7 +690,7 @@ async def leaderskill(self, ctx, *, raw_query): [p]ls sonia lubu """ dgcog = await self.get_dgcog() - l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) + l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, ctx.author.id) err_msg = ('{} query failed to match a monster: [ {} ]. If your query is multiple words,' ' try separating the queries with / or wrap with quotes.') @@ -730,7 +730,7 @@ async def get_user_friends(self, original_author_id): @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) return @@ -746,7 +746,7 @@ async def leaderskillsingle(self, ctx, *, query): async def transforminfo(self, ctx, *, query): """Show info about a transform card, including some helpful details about the base card.""" dgcog = await self.get_dgcog() - base_mon, transformed_mon, monster_ids = await perform_transforminfo_query(dgcog, query) + base_mon, transformed_mon, monster_ids = await perform_transforminfo_query(dgcog, query, ctx.author.id) if not base_mon: await self.send_id_failure_message(ctx, query) @@ -773,7 +773,7 @@ async def transforminfo(self, ctx, *, query): async def awakeninghelp(self, ctx, *, query): """Describe a monster's regular and super awakenings in detail.""" dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) @@ -807,7 +807,7 @@ async def padsay(self, ctx, server, *, query: str = None): query = query.strip().lower() dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ctx.author.id) if monster is not None: voice_id = monster.voice_id_jp if server == 'jp' else monster.voice_id_na if voice_id is None: @@ -883,10 +883,11 @@ async def embedcolor(self, ctx, *, color): await ctx.tick() @idset.command() - async def beta(self, ctx): - """Discontinued""" - await ctx.send(f"`id3 `is now enabled globally, see" - f" <{IDGUIDE}> for more information.") + async def naprio(self, ctx, value: bool): + """Change whether [p]id will default away from new evos of monsters that aren't in NA yet""" + async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + fm_flags['na_prio'] = value + await ctx.tick() @commands.group() @checks.is_owner() @@ -949,7 +950,7 @@ async def send_id_failure_message(ctx, query: str): async def debugid(self, ctx, *, query): """Get helpful id information about a monster""" dgcog = self.bot.get_cog("Dadguide") - mon = await dgcog.find_monster(query) + mon = await dgcog.find_monster(query, ctx.author.id) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) return @@ -1193,7 +1194,7 @@ async def idtraceback(self, ctx, *, query): async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") - matched_monsters = await dgcog.find_monsters(query) + matched_monsters = await dgcog.find_monsters(query, ctx.author.id) if not matched_monsters: await ctx.send("No monster matched.") diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 3fdab154..83d2a9e6 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -5,5 +5,5 @@ async def get_monster_from_ims(dgcog, ims: dict): resolved_monster_id = int(resolved_monster_id_str) if resolved_monster_id_str else None if resolved_monster_id: return dgcog.database.graph.get_monster(resolved_monster_id) - monster = await dgcog.find_monster(query) + monster = await dgcog.find_monster(query, ims['original_author_id']) return monster diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index ca58a9b1..ae1da345 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -39,7 +39,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] - l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query) + l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, ims['original_author_id']) return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, r_query) diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index ed8d85ef..d88cda08 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -52,7 +52,7 @@ async def pricecheck(self, ctx, *, query): return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query) + m = await dgcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return @@ -93,7 +93,7 @@ async def set(self, ctx, stam_cost: float, *, query): return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query) + m = await dgcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return @@ -114,7 +114,7 @@ async def setfooter(self, ctx, query, *, footer=""): return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query) + m = await dgcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return @@ -135,7 +135,7 @@ async def remove(self, ctx, *, query): return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query) + m = await dgcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return From 298d2d83f8e64d5fece37cc71494eae9c47c8d29 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 13 Mar 2021 18:50:00 -0700 Subject: [PATCH 0693/1220] Send more feedback on naprio (#930) --- padinfo/padinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 125ef7d3..88324640 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -882,12 +882,12 @@ async def embedcolor(self, ctx, *, color): return await ctx.tick() - @idset.command() + @idset.command(usage="") async def naprio(self, ctx, value: bool): """Change whether [p]id will default away from new evos of monsters that aren't in NA yet""" async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: fm_flags['na_prio'] = value - await ctx.tick() + await ctx.send(f"NA monster prioritization has been **{'en' if value else 'dis'}abled**.") @commands.group() @checks.is_owner() From 59c667ccadb7ac81a0b418d7c099f3c4e346b0d2 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sat, 13 Mar 2021 23:49:55 -0500 Subject: [PATCH 0694/1220] Add method to get rarity emoji (#933) --- padinfo/common/emoji_map.py | 5 +++++ padinfo/view/pantheon.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 5e18d50b..ed3b9b1c 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -112,6 +112,11 @@ def get_attribute_emoji_by_enum(type1: Union[Enum, Literal[False]], type2: Optio return get_emoji(emoji) +def get_rarity_emoji(rarity: int): + emoji = 'star_0{}'.format(rarity) if rarity < 10 else 'star_{}'.format(rarity) + return get_emoji(emoji) + + def get_awakening_emoji(awid: Union[Enum, int], default: str = None): return get_emoji(AWAKENING_ID_TO_EMOJI_NAME_MAP.get(awid if isinstance(awid, int) else awid.value, default)) diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 8075c83a..440d7c3f 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -5,7 +5,8 @@ from discordmenu.embed.view import EmbedView from padinfo.common.config import UserConfig -from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum +from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ + get_rarity_emoji from padinfo.common.external_links import puzzledragonx from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state @@ -106,7 +107,7 @@ async def query(cls, dgcog, monster): filters = [] rarity_list = [m for m in type_list if m.rarity == base_mon.rarity] - filters.append('{}*'.format(base_mon.rarity)) + filters.append(get_rarity_emoji(base_mon.rarity)) if 0 < len(rarity_list) <= MAX_MONS_TO_SHOW: return rarity_list, cls.make_series_name(series_name, filters), base_mon From 0f342fa9e8b2f471051ead4d6078c13d75b488a5 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 14 Mar 2021 18:17:48 -0400 Subject: [PATCH 0695/1220] Add idhistory command (#934) * Add idhistory command * Inform the user if id history is empty * Add embed permission check to awakeninghelp * Log links and lookup queries in idhistory as well * Update idhistory docstring * Have id history be saved per-user * Clean up text and function calls in idhistory --- padinfo/padinfo.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 88324640..d5ac7cbc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -67,6 +67,8 @@ IDGUIDE = "https://github.com/TsubakiBotPad/pad-cogs/wiki/id-user-guide" +HISTORY_DURATION = 11 + def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) @@ -99,7 +101,7 @@ def __init__(self, bot, *args, **kwargs): self.remove_emoji = '\N{CROSS MARK}' self.config = Config.get_conf(self, identifier=9401770) - self.config.register_user(survey_mode=0, color=None, beta_id3=False) + self.config.register_user(survey_mode=0, color=None, beta_id3=False, id_history=[]) self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False) self.historic_lookups = safe_read_json(_data_file('historic_lookups_id3.json')) @@ -307,6 +309,8 @@ async def send_message(): await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) + # id3 messaging stuff if monster and monster.monster_no_na != monster.monster_no_jp: await ctx.send("The NA ID and JP ID of this card differ! " @@ -398,6 +402,8 @@ async def evos(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) + alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) if alt_versions is None: @@ -429,6 +435,8 @@ async def evomats(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) + mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ await MaterialsViewState.query(dgcog, monster) @@ -462,6 +470,8 @@ async def pantheon(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) + pantheon_list, series_name, base_monster = await PantheonViewState.query(dgcog, monster) if pantheon_list is None: await ctx.send('Unable to find a pantheon for the result of your query,' @@ -493,6 +503,8 @@ async def pic(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) + alt_monsters = PicViewState.get_alt_monsters(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) @@ -519,6 +531,8 @@ async def otherinfo(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) + alt_monsters = PicViewState.get_alt_monsters(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) @@ -539,6 +553,7 @@ async def links(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) color = await self.get_user_embed_color(ctx) embed = LinksView.embed(monster, color).to_embed() await ctx.send(embed=embed) @@ -552,10 +567,20 @@ async def lookup(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) return + await self.log_id_result(ctx, monster.monster_id) color = await self.get_user_embed_color(ctx) embed = LookupView.embed(monster, color).to_embed() await ctx.send(embed=embed) + async def log_id_result(self, ctx, monster_id: int): + history = await self.config.user(ctx.author).id_history() + if monster_id in history: + history.remove(monster_id) + history.insert(0, monster_id) + if len(history) > HISTORY_DURATION: + history.pop() + await self.config.user(ctx.author).id_history.set(history) + @commands.command() @checks.bot_has_permissions(embed_links=True) async def buttoninfo(self, ctx, *, query: str): @@ -770,6 +795,7 @@ async def transforminfo(self, ctx, *, query): await menu.create(ctx, state) @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp']) + @checks.bot_has_permissions(embed_links=True) async def awakeninghelp(self, ctx, *, query): """Describe a monster's regular and super awakenings in detail.""" dgcog = await self.get_dgcog() @@ -787,6 +813,21 @@ async def awakeninghelp(self, ctx, *, query): color, AwakeningHelpView.VIEW_TYPE, props) await menu.create(ctx, state) + @commands.command(aliases=['idhist']) + @checks.bot_has_permissions(embed_links=True) + async def idhistory(self, ctx): + """Show a list of the 11 most recent monsters that the user looked up.""" + dgcog = await self.get_dgcog() + history = await self.config.user(ctx.author).id_history() + + monsters = [dgcog.get_monster(m) for m in history] + + if not monsters: + await ctx.send('Did not find any recent queries in history.') + return + + await self._do_monster_list(ctx, dgcog, '', monsters, 'Result History') + @commands.command() async def padsay(self, ctx, server, *, query: str = None): """Speak the voice line of a monster into your current chat""" From 26bcb909dee096909763f5c88829fa283ebf85d2 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Mar 2021 22:49:21 -0700 Subject: [PATCH 0696/1220] Add author as an auto-filled query parameter in ID feedback --- padinfo/padinfo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 24f46c48..0b4a6bf2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -339,7 +339,11 @@ async def send_survey_after(self, ctx, query, result_monster): id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" params = urllib.parse.urlencode( - {'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': id3res, 'entry.1016180044': id1res}) + {'usp': 'pp_url', + 'entry.154088017': query, + 'entry.173096863': id3res, + 'entry.1016180044': id1res, + 'entry.1787446565': str(ctx.author)}) url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", From a6ce285a3c5fa3c5d300e1977a314a09797311d9 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Mar 2021 23:10:39 -0700 Subject: [PATCH 0697/1220] Revert "Add author as an auto-filled query parameter in ID feedback" This reverts commit 26bcb909dee096909763f5c88829fa283ebf85d2. --- padinfo/padinfo.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0b4a6bf2..24f46c48 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -339,11 +339,7 @@ async def send_survey_after(self, ctx, query, result_monster): id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" params = urllib.parse.urlencode( - {'usp': 'pp_url', - 'entry.154088017': query, - 'entry.173096863': id3res, - 'entry.1016180044': id1res, - 'entry.1787446565': str(ctx.author)}) + {'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': id3res, 'entry.1016180044': id1res}) url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", From 1fe8cd120f6b8d4aa33585a4569f1f724bc0a8e3 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Mar 2021 23:10:42 -0700 Subject: [PATCH 0698/1220] Revert "Revert "Add author as an auto-filled query parameter in ID feedback"" This reverts commit a6ce285a3c5fa3c5d300e1977a314a09797311d9. --- padinfo/padinfo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 24f46c48..0b4a6bf2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -339,7 +339,11 @@ async def send_survey_after(self, ctx, query, result_monster): id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" params = urllib.parse.urlencode( - {'usp': 'pp_url', 'entry.154088017': query, 'entry.173096863': id3res, 'entry.1016180044': id1res}) + {'usp': 'pp_url', + 'entry.154088017': query, + 'entry.173096863': id3res, + 'entry.1016180044': id1res, + 'entry.1787446565': str(ctx.author)}) url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", From b8eff5b4824f6c247fd424835d58ec209ca4021f Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Mar 2021 23:10:44 -0700 Subject: [PATCH 0699/1220] Revert "Merge branch 'newbranch' of https://github.com/chasehult/padbot-cogs into newbranch" This reverts commit e23e3f804f45594911c8005899470cabbdb6ea0d, reversing changes made to 26bcb909dee096909763f5c88829fa283ebf85d2. --- padmonitor/padmonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index d9b91195..f1278682 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -69,7 +69,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: msg += '\n\t[{}] {}'.format(m.monster_no, m.unoverridden_name_en) - if tsutils.contains_ja(m.name_en) and m.name_en_override is not None: + if tsutils.contains_ja(m.name_en) and m.name_en_override: msg += ' ({})'.format(m.name_en_override) return msg else: From 12cda0df583c68407fba0c18b7d8a2e6bcfcdefd Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Mar 2021 18:51:52 -0700 Subject: [PATCH 0700/1220] Add awakenings as an alias for awakeninghelp --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 385a413d..38b3f3f6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -798,7 +798,7 @@ async def transforminfo(self, ctx, *, query): menu = TransformInfoMenu.menu() await menu.create(ctx, state) - @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp']) + @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp', 'awakenings']) @checks.bot_has_permissions(embed_links=True) async def awakeninghelp(self, ctx, *, query): """Describe a monster's regular and super awakenings in detail.""" From a88c4b4da721030318dec0de8e382e488b9208ca Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 18 Mar 2021 21:15:33 -0700 Subject: [PATCH 0701/1220] Remove some awakeninghelp aliases (#938) * Remove some awakeninghelp aliases * Remove awakehelp --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 38b3f3f6..65b9958e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -798,7 +798,7 @@ async def transforminfo(self, ctx, *, query): menu = TransformInfoMenu.menu() await menu.create(ctx, state) - @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp', 'awakenings']) + @commands.command(aliases=['awakeningshelp', 'awakenings']) @checks.bot_has_permissions(embed_links=True) async def awakeninghelp(self, ctx, *, query): """Describe a monster's regular and super awakenings in detail.""" From ba7e158e7708e665f7f0570162cf0538bc8295dd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 18 Mar 2021 23:31:08 -0500 Subject: [PATCH 0702/1220] Revert "Remove some awakeninghelp aliases (#938)" (#939) Now we know about aliases in documentation being toggleable, so we can readd them in the code! Yay! --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 65b9958e..38b3f3f6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -798,7 +798,7 @@ async def transforminfo(self, ctx, *, query): menu = TransformInfoMenu.menu() await menu.create(ctx, state) - @commands.command(aliases=['awakeningshelp', 'awakenings']) + @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp', 'awakenings']) @checks.bot_has_permissions(embed_links=True) async def awakeninghelp(self, ctx, *, query): """Describe a monster's regular and super awakenings in detail.""" From 5c05dda38044dcdc4b30acdfe33d1221adf96cbf Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 20 Mar 2021 19:07:40 -0700 Subject: [PATCH 0703/1220] Add panth as an alias to pantheon (#940) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 38b3f3f6..e2cd4ada 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -459,7 +459,7 @@ async def evomats(self, ctx, *, query: str): menu = IdMenu.menu(initial_control=IdMenu.mats_control) await menu.create(ctx, state) - @commands.command(aliases=["series"]) + @commands.command(aliases=["series", "panth"]) @checks.bot_has_permissions(embed_links=True) async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" From 4ec9888ed8b120d19c0c6031c6a0deaf1354cf20 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 26 Mar 2021 02:52:03 -0700 Subject: [PATCH 0704/1220] Add indexed evo modifiers (#947) --- dadguide/monster_index.py | 136 +++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 15d2c2cf..4a7e4a54 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -277,107 +277,112 @@ def _get_important_tokens(cls, oname, treenames=None): else: return cls._name_to_tokens(min(n1, n2, key=token_count)) - async def get_modifiers(self, m): - modifiers = self.manual_prefixes[m.monster_id].copy() + async def get_modifiers(self, monster): + modifiers = self.manual_prefixes[monster.monster_id].copy() - basemon = self.graph.get_base_monster(m) + basemon = self.graph.get_base_monster(monster) # Main Color - modifiers.update(COLOR_MAP[m.attr1]) + modifiers.update(COLOR_MAP[monster.attr1]) # Sub Color - modifiers.update(SUB_COLOR_MAP[m.attr2]) - if m.attr1.value == 6: - modifiers.update(COLOR_MAP[m.attr2]) + modifiers.update(SUB_COLOR_MAP[monster.attr2]) + if monster.attr1.value == 6: + modifiers.update(COLOR_MAP[monster.attr2]) # Both Colors - modifiers.update(DUAL_COLOR_MAP[(m.attr1, m.attr2)]) + modifiers.update(DUAL_COLOR_MAP[(monster.attr1, monster.attr2)]) # Type - for mt in m.types: + for mt in monster.types: modifiers.update(TYPE_MAP[mt]) # Series - if m.series_id in self.series_id_to_pantheon_nickname: - modifiers.update(self.series_id_to_pantheon_nickname[m.series_id]) + if monster.series_id in self.series_id_to_pantheon_nickname: + modifiers.update(self.series_id_to_pantheon_nickname[monster.series_id]) # Rarity - modifiers.add(str(m.rarity) + "*") + modifiers.add(str(monster.rarity) + "*") modifiers.add(str(basemon.rarity) + "*b") # Base - if self.graph.monster_is_base(m): + if self.graph.monster_is_base(monster): modifiers.update(EVO_MAP[EvoTypes.BASE]) - special_evo = ('覚醒' in m.name_ja or 'awoken' in m.name_en or '転生' in m.name_ja or - self.graph.true_evo_type_by_monster(m).value == "Reincarnated" or - 'reincarnated' in m.name_en or - self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated" or - m.is_equip or '極醒' in m.name_ja) + special_evo = ('覚醒' in monster.name_ja or 'awoken' in monster.name_en or '転生' in monster.name_ja or + self.graph.true_evo_type_by_monster(monster).value == "Reincarnated" or + 'reincarnated' in monster.name_en or + self.graph.true_evo_type_by_monster(monster).value == "Super Reincarnated" or + monster.is_equip or '極醒' in monster.name_ja) # Evo - if self.graph.monster_is_normal_evo(m) or self.graph.monster_is_first_evo(m): - modifiers.update(EVO_MAP[EvoTypes.EVO]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.EVO], + lambda m: self.graph.monster_is_normal_evo(m) + or self.graph.monster_is_first_evo(m)) # Uvo - if self.graph.monster_is_reversible_evo(m) and not special_evo: - modifiers.update(EVO_MAP[EvoTypes.UVO]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.UVO], + lambda m: self.graph.monster_is_reversible_evo(m) + and not special_evo) # UUvo - if self.graph.monster_is_second_ultimate(m): - modifiers.update(EVO_MAP[EvoTypes.UUVO]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.UUVO], + self.graph.monster_is_second_ultimate) # Transform - if not self.graph.monster_is_transform_base(m): - modifiers.update(EVO_MAP[EvoTypes.TRANS]) - elif self.graph.get_next_transform_by_monster(m): - modifiers.update(EVO_MAP[EvoTypes.BASETRANS]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.TRANS], + lambda m: not self.graph.monster_is_transform_base(m)) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.BASETRANS], + lambda m: self.graph.monster_is_transform_base(m) + and self.graph.get_next_transform_by_monster(m)) # Awoken - if '覚醒' in m.name_ja or 'awoken' in m.name_en.lower(): - modifiers.update(EVO_MAP[EvoTypes.AWOKEN]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.AWOKEN], + lambda m: '覚醒' in m.name_ja or 'awoken' in m.name_en.lower()) # Mega Awoken - if '極醒' in m.name_ja or 'mega awoken' in m.name_en.lower(): - modifiers.update(EVO_MAP[EvoTypes.MEGA]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.MEGA], + lambda m: '極醒' in m.name_ja or 'mega awoken' in m.name_en.lower()) # Reincarnated - if self.graph.true_evo_type_by_monster(m).value == "Reincarnated": - modifiers.update(EVO_MAP[EvoTypes.REVO]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.REVO], + lambda m: self.graph.true_evo_type_by_monster(m).value == "Reincarnated") # Super Reincarnated - if '超転生' in m.name_ja or self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated": - modifiers.update(EVO_MAP[EvoTypes.SREVO]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.SREVO], + lambda m: self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated") # Pixel - if (m.name_ja.startswith('ドット') or - m.name_en.startswith('pixel') or - self.graph.true_evo_type_by_monster(m).value == "Pixel"): - modifiers.update(EVO_MAP[EvoTypes.PIXEL]) - else: - modifiers.update(EVO_MAP[EvoTypes.NONPIXEL]) + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.PIXEL], + lambda m: m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') + or self.graph.true_evo_type_by_monster(m).value == "Pixel", + else_mods=EVO_MAP[EvoTypes.NONPIXEL]) # Awakenings - for aw in m.awakenings: + for aw in monster.awakenings: try: modifiers.update(AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]) except ValueError: logger.warning(f"Invalid awoken skill ID: {aw.awoken_skill_id}") self.issues.append(f"Invalid awoken skill ID: {aw.awoken_skill_id}") + # Numbered Equips + self.add_numbered_modifier(monster, modifiers, AWOKEN_MAP[Awakenings.EQUIP], + lambda m: m.is_equip) + # Chibi - if (m.name_en == m.name_en.lower() and m.name_en != m.name_ja) or \ - 'ミニ' in m.name_ja or '(chibi)' in m.name_en: - modifiers.update(MISC_MAP[MiscModifiers.CHIBI]) + self.add_numbered_modifier(monster, modifiers, MISC_MAP[MiscModifiers.CHIBI], + lambda m: m.name_en == m.name_en.lower() and m.name_en != m.name_ja + or 'ミニ' in m.name_ja or '(chibi)' in m.name_en) # Series Type - if m.series.series_type == 'regular': + if monster.series.series_type == 'regular': modifiers.update(MISC_MAP[MiscModifiers.REGULAR]) - if m.series.series_type == 'event': + if monster.series.series_type == 'event': modifiers.update(MISC_MAP[MiscModifiers.EVENT]) - if m.series.series_type == 'seasonal': + if monster.series.series_type == 'seasonal': modifiers.update(MISC_MAP[MiscModifiers.SEASONAL]) - if m.series.series_type == 'collab': + if monster.series.series_type == 'collab': modifiers.update(MISC_MAP[MiscModifiers.COLLAB]) # Story @@ -393,39 +398,48 @@ def is_story(m, do_transform=True): return True return False - if is_story(m): + if is_story(monster): modifiers.update(MISC_MAP[MiscModifiers.STORY]) # New - if self.graph.monster_is_new(m): + if self.graph.monster_is_new(monster): modifiers.update(MISC_MAP[MiscModifiers.NEW]) # Method of Obtaining - if self.graph.monster_is_farmable_evo(m) or self.graph.monster_is_mp_evo(m): + if self.graph.monster_is_farmable_evo(monster) or self.graph.monster_is_mp_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.FARMABLE]) - if self.graph.monster_is_mp_evo(m): + if self.graph.monster_is_mp_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.MP]) - if self.graph.monster_is_rem_evo(m): + if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) # Server - if m.on_jp: + if monster.on_jp: modifiers.update(MISC_MAP[MiscModifiers.INJP]) - if not m.on_na: + if not monster.on_na: modifiers.update(MISC_MAP[MiscModifiers.ONLYJP]) - if m.on_na: + if monster.on_na: modifiers.update(MISC_MAP[MiscModifiers.INNA]) - if not m.on_jp: + if not monster.on_jp: modifiers.update(MISC_MAP[MiscModifiers.ONLYNA]) - if m.monster_id + 10000 in self.graph.nodes: + if monster.monster_id + 10000 in self.graph.nodes: modifiers.add("idjp") - if m.monster_id > 10000: + if monster.monster_id > 10000: modifiers.add("idna") return modifiers + def add_numbered_modifier(self, monster, curr_mods, added_mods, condition, *, else_mods=None): + if condition(monster): + curr_mods.update(added_mods) + ms = [m for m in self.graph.get_alt_monsters(monster) if condition(m)] + if len(ms) > 1: + curr_mods.update(f'{mod}-{ms.index(monster) + 1}' for mod in added_mods) + elif else_mods: + curr_mods.update(else_mods) + # TODO: Move this to TSUtils async def sheet_to_reader(url, length=None): From 7a4c4f06ba76e2513c42f8fe1c07fc1654db8ed8 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 26 Mar 2021 02:52:48 -0700 Subject: [PATCH 0705/1220] Add Orb Skin and Animated modifiers (#948) --- dadguide/monster_index.py | 6 ++++++ dadguide/token_mappings.py | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 4a7e4a54..2d3794ba 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -415,6 +415,12 @@ def is_story(m, do_transform=True): if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) + # Art + if monster.orb_skin_id: + modifiers.update(MISC_MAP[MiscModifiers.ORBSKIN]) + if monster.has_animation: + modifiers.update(MISC_MAP[MiscModifiers.ANIMATED]) + # Server if monster.on_jp: modifiers.update(MISC_MAP[MiscModifiers.INJP]) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index ff701584..91423f17 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -252,6 +252,8 @@ class MiscModifiers(Enum): SEASONAL = 'Metaseries: Seasonal' COLLAB = 'Metaseries: Collab' NEW = 'Newest monster in series' + ORBSKIN = 'Grants an orb skin' + ANIMATED = 'Animated monster' MISC_MAP = { @@ -268,12 +270,15 @@ class MiscModifiers(Enum): MiscModifiers.EVENT: ('event',), MiscModifiers.SEASONAL: ('seasonal',), MiscModifiers.COLLAB: ('collab',), - MiscModifiers.NEW: ('new',) + MiscModifiers.NEW: ('new',), + MiscModifiers.ORBSKIN: ('orbskin',), + MiscModifiers.ANIMATED: ('animated',) } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { 'super reincarnated', - 'mega awoken' + 'mega awoken', + 'orb skin', }} ALL_TOKEN_DICTS = { From bacb42e139117c9d2399b3f79b4c271162cde4ac Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 26 Mar 2021 02:57:44 -0700 Subject: [PATCH 0706/1220] Update the README (#941) * Update the README * Use 'use' only once * Use ^ --- README.md | 56 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 2d92715d..d4330866 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Red v3 Cogs developed originally for Miru Bot, now for Tsubaki Bot. -Code should be pep8 formatted with a 100 character line limit. +Code should be PEP 8 formatted with a 100 character line limit. # Setting up your own Tsubaki instance for contributing code @@ -21,7 +21,7 @@ You will need a GitHub account to do this. Click the button that says "Fork" in I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) for development. Change into the `PycharmProjects` directory and clone your fork of the repo: -```shell script +```bash # Clone your repo, not this one!!!!!!!! You will want to load your own changes!!!! git clone https://github.com/TsubakiBotPad/padbot-cogs.git ``` @@ -40,49 +40,57 @@ If you are running on a cloud server for the first time and aren't sure what Lin ## Installing dependencies First download `requirements.txt` so that you can run it from your Red venv. In Linux this is done by running the following from the directory holding the folder `name-of-bot` that you created during the Red setup process: -```shell script +```bash wget https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/requirements.txt ``` Then run: -```shell script +```bash source name-of-bot/bin/activate pip install -r requirements.txt deactivate ``` -The rest of the guide takes place from inside Discord. +The rest of the guide takes place from inside Discord. Replace `^` with your prefix. ## Configuring development version Once the bot is launched, set it to use your repo directory as a cog path. Type this in Discord where the bot is: ``` -!addpath $pathToYourRepoThatYouClonedBefore +^addpath path/to/repo/root ``` ## Loading cogs -* Some cogs will have cross dependencies on each other. Check the command prompt that Tsubaki is running from or type `^traceback 1` if you encounter any errors. +* Some cogs will have cross dependencies on each other. Use `^traceback 1` if you encounter any errors to see your traceback. * Common dependencies include: * dadguide * padinfo -* Note that when you edit cogs with dependencies, you might need to do multiple reloads. For example, if updating dadguide, you will need to reload padinfo as well. -* After you have done all of this, restart the bot again. Hopefully by now `^id ` should work! ### Emoji * You probably don't actually need emoji to be working in order to do development for Tsubaki. -* There are three emoji servers: the main Tsubaki support server, `miru_extra_emoji`, and `miru_monsters_1`. River has access to these, talk to her if you think you need access to them. HOSTING YOUR OWN TSUBAKI/MIRU CLONE IS NOT REASON TO GET ACCESS TO THEM. In-depth development of the `^id` command may be reason to get access to them. -* You will use use `^padinfo setemojiservers` with the IDs she gives you. +* There are over seven emoji servers. River has access to these, talk to her if you think you need access to them. In-depth development of the `^id` command may be reason to get access to them. +* You will use `^padinfo emojiserver add` with the IDs she gives you. # Puzzle and Dragons -Most cogs here relate to the mobile game 'Puzzle and Dragons'. Data is sourced from the -DadGuide mobile app. - -| Cog | Purpose | -| --- | --- | -| damagecalc | Simple attack damage calculator | -| padboard | Converts board images to dawnglare board/solved board links | -| padglobal | Global PAD info commands | -| dadguide | Utility classes relating to DadGuide data | -| padinfo | Monster lookup and info display | -| padrem | Rare Egg Machine simulation | -| padvision | Utilities relating to PAD image scanning | -| profile | Global user PAD profile storage and lookup | +Most cogs here relate to the mobile game 'Puzzle & Dragons'. + +| Cog | Purpose | +| --- | --- | +| azurelane | **DISCONTINUED** Azur Lane card lookup | +| channelmirror | A better version of the built-in discord announcement channel | +| crud | Database editor. Not useful for bots other than prod Tsubaki | +| dadguide | Central cog to host all PAD data | +| damagecalc | Simple attack damage calculator | +| feedback | A specialized feedback cog for Tsubaki specifically | +| padboard | Converts board images to dawnglare board/solved board links | +| padbuildimg | Creates images of PAD teams via a special query language | +| padbuilds | See user-made builds for dungeons i think??? (nobody uses this) | +| padevents | A scheduler cog to help players see upcoming daily GH dungeons. | +| padguidedb | Database editor. Not offered to Tsubaki clones. | +| padglobal | Global PAD info commands | +| padinfo | Monster lookup and info display | +| padmonitor | Keeps an eye out for PAD updates | +| padsearch | Search for monsters with specific attributes | +| padrem | Rare Egg Machine simulation. I don't think this ever existed | +| pricecheck | Lumon's thing. Something about stamina trade equivalence | +| profile | Global user PAD profile storage and lookup | +| schoolidol | **DISCONTINUED** Love Live! School Idol Festival card lookup | From 1cf4404a8b0f556d988ffa2597aaba6c2542d6fd Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 26 Mar 2021 03:32:01 -0700 Subject: [PATCH 0707/1220] Fix number-indexed evo modifiers (#951) --- dadguide/monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 2d3794ba..28e58054 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -440,7 +440,7 @@ def is_story(m, do_transform=True): def add_numbered_modifier(self, monster, curr_mods, added_mods, condition, *, else_mods=None): if condition(monster): curr_mods.update(added_mods) - ms = [m for m in self.graph.get_alt_monsters(monster) if condition(m)] + ms = sorted((m for m in self.graph.get_alt_monsters(monster) if condition(m)), key=lambda m: m.monster_id) if len(ms) > 1: curr_mods.update(f'{mod}-{ms.index(monster) + 1}' for mod in added_mods) elif else_mods: From c53287881c851c4e22b34e41dc16f6b888f3978a Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 27 Mar 2021 04:31:42 -0700 Subject: [PATCH 0708/1220] Allow moddedfiers to be used as suffixes (#953) --- dadguide/find_monster.py | 2 +- dadguide/monster_graph.py | 2 +- padinfo/view/components/view_state_base_id.py | 2 +- padinfo/view/evos.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index d42fe488..4b3a2732 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -91,7 +91,7 @@ def interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[str], Set[str for i, token in enumerate(tokenized_query[::-1]): negated = token.startswith("-") token = token.lstrip('-') - if any(self.calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in + if any(self.calc_ratio_modifier(m, token.split('-')[0], .1) > self.MODIFIER_JW_DISTANCE for m in self.dgcog.index.suffixes): if negated: negative_modifiers.add(token) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 05593778..41f7cd66 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -317,7 +317,7 @@ def get_alt_ids_by_id(self, monster_id): def get_alt_monsters_by_id(self, monster_id): ids = self.get_alt_ids_by_id(monster_id) - return [self.get_monster(m_id) for m_id in ids] + return {self.get_monster(m_id) for m_id in ids} def get_alt_monsters(self, monster: MonsterModel): return self.get_alt_monsters_by_id(monster.monster_id) diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 7686f68a..c4268f72 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -59,6 +59,6 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): @classmethod def get_alt_monsters(cls, dgcog, monster): db_context = dgcog.database - alt_monsters = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + alt_monsters = sorted(db_context.graph.get_alt_monsters_by_id(monster.monster_no), key=lambda x: x.monster_id) return alt_monsters diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 720353b7..e5a5d2b7 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -68,7 +68,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): @staticmethod async def query(dgcog, monster): db_context = dgcog.database - alt_versions = sorted({*db_context.graph.get_alt_monsters_by_id(monster.monster_no)}, + alt_versions = sorted(db_context.graph.get_alt_monsters_by_id(monster.monster_no), key=lambda x: x.monster_id) gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) if len(alt_versions) == 1 and len(gem_versions) == 0: From 25d599bf8f67666f09ce6f71e96e6f0444f804cb Mon Sep 17 00:00:00 2001 From: TsubakiBotPad Date: Tue, 30 Mar 2021 10:41:35 -0700 Subject: [PATCH 0709/1220] Check for nulls on monster_model active skill --- dadguide/models/monster_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 7a60cb17..ff1a327b 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -174,8 +174,8 @@ def __init__(self, m: MonsterModel): leader_skill = m.leader_skill self.leader = leader_skill.desc.lower() if leader_skill else '' active_skill = m.active_skill - self.active_name = active_skill.name.lower() if active_skill else '' - self.active_desc = active_skill.desc.lower() if active_skill else '' + self.active_name = active_skill.name.lower() if active_skill and active_skill.name else '' + self.active_desc = active_skill.desc.lower() if active_skill and active_skill.desc else '' self.active = '{} {}'.format(self.active_name, self.active_desc) self.active_min = active_skill.turn_min if active_skill else None self.active_max = active_skill.turn_max if active_skill else None From fc5bca38bad3f43a9818485bccaa948ecbf69565 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 31 Mar 2021 16:04:50 -0700 Subject: [PATCH 0710/1220] Fix punctuation (#955) --- dadguide/models/active_skill_model.py | 5 +++- dadguide/models/leader_skill_model.py | 5 +++- dadguide/models/monster_model.py | 2 ++ padinfo/view/id.py | 42 +++++++++++++++++---------- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/dadguide/models/active_skill_model.py b/dadguide/models/active_skill_model.py index 665a790d..ee1d43aa 100644 --- a/dadguide/models/active_skill_model.py +++ b/dadguide/models/active_skill_model.py @@ -1,4 +1,5 @@ from .base_model import BaseModel +import random class ActiveSkillModel(BaseModel): @@ -9,7 +10,9 @@ def __init__(self, **kwargs): self.name_ko = kwargs['name_ko'] self.desc_ja = kwargs['desc_ja'] - self.desc_en = kwargs['desc_en'] + self.desc_en = kwargs['desc_en'] + '!' * random.randint(1, 10) + while ';' in self.desc_en: + self.desc_en = self.desc_en.replace(';', '!' * random.randint(1, 10), 1) self.desc_ko = kwargs['desc_ko'] self.turn_max = kwargs['turn_max'] diff --git a/dadguide/models/leader_skill_model.py b/dadguide/models/leader_skill_model.py index 535e411d..78d1583b 100644 --- a/dadguide/models/leader_skill_model.py +++ b/dadguide/models/leader_skill_model.py @@ -1,4 +1,5 @@ from .base_model import BaseModel +import random class LeaderSkillModel(BaseModel): @@ -15,7 +16,9 @@ def __init__(self, **kwargs): self.bonus_damage = kwargs['bonus_damage'] self.mult_bonus_damage = kwargs['mult_bonus_damage'] self.extra_time = kwargs['extra_time'] - self.desc_en = kwargs['desc_en'] + self.desc_en = kwargs['desc_en'] + '!' * random.randint(1, 10) + while ';' in self.desc_en: + self.desc_en = self.desc_en.replace(';', '!' * random.randint(1, 10), 1) self.desc_ja = kwargs['desc_ja'] self.desc_ko = kwargs['desc_ko'] diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index ff1a327b..6495dee4 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -1,3 +1,5 @@ +import random + from .base_model import BaseModel from .enum_types import Attribute from .enum_types import MonsterType diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 42e9cac6..7bb3bae9 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,3 +1,5 @@ +import random + from typing import TYPE_CHECKING, List from discordmenu.embed.base import Box @@ -20,6 +22,14 @@ from dadguide.models.awakening_model import AwakeningModel +def randappear(text, chance): + if random.random() < chance: + return text + return '' + +def randtimes(text, max): + return text * random.randint(1, max) + class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List["MonsterModel"], @@ -174,23 +184,23 @@ def killers_row(m: "MonsterModel", transform_base): Text('\N{DOWN-POINTING RED TRIANGLE}' if m != transform_base else ''), Text('[{} slots]'.format(m.latent_slots if m == transform_base \ else transform_base.latent_slots)), - Text(killers_text), + Text(killers_text+'!!!'), delimiter=' ' ) @staticmethod def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_rarity: str): rarity = Box( - LabeledText('Rarity', str(m.rarity)), - Text('({})'.format(LabeledText('Base', str(base_rarity)).to_markdown())), - Text("" if m.orb_skin_id is None else "(Orb Skin)"), + LabeledText('Rarity', str(m.rarity)+'!'), + Text('({})'.format(LabeledText('Base', str(base_rarity)+'!!').to_markdown())), + Text("" if m.orb_skin_id is None else "(Orb Skin!!!!)"), delimiter=' ' ) - cost = LabeledText('Cost', str(m.cost)) - acquire = BoldText(acquire_raw) if acquire_raw else None + cost = LabeledText('Cost', str(m.cost)+'!!!') + acquire = BoldText(acquire_raw+'!!!') if acquire_raw else None valid_true_evo_types = ("Reincarnated", "Assist", "Pixel", "Super Reincarnated") - true_evo_type = BoldText(true_evo_type_raw) if true_evo_type_raw in valid_true_evo_types else None + true_evo_type = BoldText(true_evo_type_raw+'!!!') if true_evo_type_raw in valid_true_evo_types else None return Box(rarity, cost, acquire, true_evo_type) @@ -202,7 +212,7 @@ def stats(m: "MonsterModel"): LabeledText('HP', _get_stat_text(hp, lb_hp, _get_awakening_emoji_for_stats(m, 1))), LabeledText('ATK', _get_stat_text(atk, lb_atk, _get_awakening_emoji_for_stats(m, 2))), LabeledText('RCV', _get_stat_text(rcv, lb_rcv, _get_awakening_emoji_for_stats(m, 3))), - LabeledText('Fodder EXP', "{:,}".format(m.fodder_exp)) if _monster_is_enhance(m) else None + LabeledText('Fodder EXP', "{:,}!!".format(m.fodder_exp)) if _monster_is_enhance(m) else None ) @staticmethod @@ -211,7 +221,7 @@ def stats_header(m: "MonsterModel"): header = Box( Text(voice), Text('Stats'), - Text('(LB, +{}%)'.format(m.limit_mult)) if m.limit_mult else None, + Text('(LB, +{}%!!!!!)'.format(m.limit_mult)) if m.limit_mult else None, delimiter=' ' ) return header @@ -226,12 +236,12 @@ def active_skill_header(m: "MonsterModel", transform_base): base_skill = transform_base.active_skill base_cd = ' (\N{DOWN-POINTING RED TRIANGLE} {} -> {})'.format(base_skill.turn_max, base_skill.turn_min) \ - if base_skill else 'None' + if base_skill else 'None!!!!' active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' active_cd += base_cd return Box( - BoldText('Active Skill'), + BoldText('Active Skill' + randtimes('!', 5) + randappear(' :D', .3)), BoldText(active_cd), delimiter=' ' ) @@ -239,7 +249,7 @@ def active_skill_header(m: "MonsterModel", transform_base): @staticmethod def leader_skill_header(m: "MonsterModel"): return Box( - BoldText('Leader Skill'), + BoldText('Leader Skill' + randtimes('?!', 3) + randappear(' :D', .3)), BoldText(createMultiplierText(m.leader_skill)), delimiter=' ' ) @@ -256,7 +266,7 @@ def embed(state: IdViewState): ) ), EmbedField( - 'Inheritable' if m.is_inheritable else 'Not inheritable', + 'Inheritable!!!' if m.is_inheritable else 'Not inheritable! :(', IdView.misc_info(m, state.true_evo_type_raw, state.acquire_raw, state.base_rarity), inline=True ), @@ -267,11 +277,11 @@ def embed(state: IdViewState): ), EmbedField( IdView.active_skill_header(m, state.transform_base).to_markdown(), - Text(m.active_skill.desc if m.active_skill else 'None') + Text(m.active_skill.desc if m.active_skill else 'None' + randtimes('!', 10)) ), EmbedField( IdView.leader_skill_header(m).to_markdown(), - Text(m.leader_skill.desc if m.leader_skill else 'None') + Text(m.leader_skill.desc if m.leader_skill else 'None' + randtimes('!', 10)) ), evos_embed_field(state) ] @@ -279,7 +289,7 @@ def embed(state: IdViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(m).to_markdown(), + title=MonsterHeader.long_v2(m).to_markdown()[:-1]+'!!!)', url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=pad_info_footer_with_state(state), From 34d7d03a6141c64f584604413fea95189c932a82 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 2 Apr 2021 00:51:18 -0500 Subject: [PATCH 0711/1220] Revert "Fix punctuation (#955)" :((((((( (#957) This reverts commit fc5bca38bad3f43a9818485bccaa948ecbf69565. --- dadguide/models/active_skill_model.py | 5 +--- dadguide/models/leader_skill_model.py | 5 +--- dadguide/models/monster_model.py | 2 -- padinfo/view/id.py | 42 ++++++++++----------------- 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/dadguide/models/active_skill_model.py b/dadguide/models/active_skill_model.py index ee1d43aa..665a790d 100644 --- a/dadguide/models/active_skill_model.py +++ b/dadguide/models/active_skill_model.py @@ -1,5 +1,4 @@ from .base_model import BaseModel -import random class ActiveSkillModel(BaseModel): @@ -10,9 +9,7 @@ def __init__(self, **kwargs): self.name_ko = kwargs['name_ko'] self.desc_ja = kwargs['desc_ja'] - self.desc_en = kwargs['desc_en'] + '!' * random.randint(1, 10) - while ';' in self.desc_en: - self.desc_en = self.desc_en.replace(';', '!' * random.randint(1, 10), 1) + self.desc_en = kwargs['desc_en'] self.desc_ko = kwargs['desc_ko'] self.turn_max = kwargs['turn_max'] diff --git a/dadguide/models/leader_skill_model.py b/dadguide/models/leader_skill_model.py index 78d1583b..535e411d 100644 --- a/dadguide/models/leader_skill_model.py +++ b/dadguide/models/leader_skill_model.py @@ -1,5 +1,4 @@ from .base_model import BaseModel -import random class LeaderSkillModel(BaseModel): @@ -16,9 +15,7 @@ def __init__(self, **kwargs): self.bonus_damage = kwargs['bonus_damage'] self.mult_bonus_damage = kwargs['mult_bonus_damage'] self.extra_time = kwargs['extra_time'] - self.desc_en = kwargs['desc_en'] + '!' * random.randint(1, 10) - while ';' in self.desc_en: - self.desc_en = self.desc_en.replace(';', '!' * random.randint(1, 10), 1) + self.desc_en = kwargs['desc_en'] self.desc_ja = kwargs['desc_ja'] self.desc_ko = kwargs['desc_ko'] diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 6495dee4..ff1a327b 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -1,5 +1,3 @@ -import random - from .base_model import BaseModel from .enum_types import Attribute from .enum_types import MonsterType diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 7bb3bae9..42e9cac6 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,5 +1,3 @@ -import random - from typing import TYPE_CHECKING, List from discordmenu.embed.base import Box @@ -22,14 +20,6 @@ from dadguide.models.awakening_model import AwakeningModel -def randappear(text, chance): - if random.random() < chance: - return text - return '' - -def randtimes(text, max): - return text * random.randint(1, max) - class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List["MonsterModel"], @@ -184,23 +174,23 @@ def killers_row(m: "MonsterModel", transform_base): Text('\N{DOWN-POINTING RED TRIANGLE}' if m != transform_base else ''), Text('[{} slots]'.format(m.latent_slots if m == transform_base \ else transform_base.latent_slots)), - Text(killers_text+'!!!'), + Text(killers_text), delimiter=' ' ) @staticmethod def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_rarity: str): rarity = Box( - LabeledText('Rarity', str(m.rarity)+'!'), - Text('({})'.format(LabeledText('Base', str(base_rarity)+'!!').to_markdown())), - Text("" if m.orb_skin_id is None else "(Orb Skin!!!!)"), + LabeledText('Rarity', str(m.rarity)), + Text('({})'.format(LabeledText('Base', str(base_rarity)).to_markdown())), + Text("" if m.orb_skin_id is None else "(Orb Skin)"), delimiter=' ' ) - cost = LabeledText('Cost', str(m.cost)+'!!!') - acquire = BoldText(acquire_raw+'!!!') if acquire_raw else None + cost = LabeledText('Cost', str(m.cost)) + acquire = BoldText(acquire_raw) if acquire_raw else None valid_true_evo_types = ("Reincarnated", "Assist", "Pixel", "Super Reincarnated") - true_evo_type = BoldText(true_evo_type_raw+'!!!') if true_evo_type_raw in valid_true_evo_types else None + true_evo_type = BoldText(true_evo_type_raw) if true_evo_type_raw in valid_true_evo_types else None return Box(rarity, cost, acquire, true_evo_type) @@ -212,7 +202,7 @@ def stats(m: "MonsterModel"): LabeledText('HP', _get_stat_text(hp, lb_hp, _get_awakening_emoji_for_stats(m, 1))), LabeledText('ATK', _get_stat_text(atk, lb_atk, _get_awakening_emoji_for_stats(m, 2))), LabeledText('RCV', _get_stat_text(rcv, lb_rcv, _get_awakening_emoji_for_stats(m, 3))), - LabeledText('Fodder EXP', "{:,}!!".format(m.fodder_exp)) if _monster_is_enhance(m) else None + LabeledText('Fodder EXP', "{:,}".format(m.fodder_exp)) if _monster_is_enhance(m) else None ) @staticmethod @@ -221,7 +211,7 @@ def stats_header(m: "MonsterModel"): header = Box( Text(voice), Text('Stats'), - Text('(LB, +{}%!!!!!)'.format(m.limit_mult)) if m.limit_mult else None, + Text('(LB, +{}%)'.format(m.limit_mult)) if m.limit_mult else None, delimiter=' ' ) return header @@ -236,12 +226,12 @@ def active_skill_header(m: "MonsterModel", transform_base): base_skill = transform_base.active_skill base_cd = ' (\N{DOWN-POINTING RED TRIANGLE} {} -> {})'.format(base_skill.turn_max, base_skill.turn_min) \ - if base_skill else 'None!!!!' + if base_skill else 'None' active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' active_cd += base_cd return Box( - BoldText('Active Skill' + randtimes('!', 5) + randappear(' :D', .3)), + BoldText('Active Skill'), BoldText(active_cd), delimiter=' ' ) @@ -249,7 +239,7 @@ def active_skill_header(m: "MonsterModel", transform_base): @staticmethod def leader_skill_header(m: "MonsterModel"): return Box( - BoldText('Leader Skill' + randtimes('?!', 3) + randappear(' :D', .3)), + BoldText('Leader Skill'), BoldText(createMultiplierText(m.leader_skill)), delimiter=' ' ) @@ -266,7 +256,7 @@ def embed(state: IdViewState): ) ), EmbedField( - 'Inheritable!!!' if m.is_inheritable else 'Not inheritable! :(', + 'Inheritable' if m.is_inheritable else 'Not inheritable', IdView.misc_info(m, state.true_evo_type_raw, state.acquire_raw, state.base_rarity), inline=True ), @@ -277,11 +267,11 @@ def embed(state: IdViewState): ), EmbedField( IdView.active_skill_header(m, state.transform_base).to_markdown(), - Text(m.active_skill.desc if m.active_skill else 'None' + randtimes('!', 10)) + Text(m.active_skill.desc if m.active_skill else 'None') ), EmbedField( IdView.leader_skill_header(m).to_markdown(), - Text(m.leader_skill.desc if m.leader_skill else 'None' + randtimes('!', 10)) + Text(m.leader_skill.desc if m.leader_skill else 'None') ), evos_embed_field(state) ] @@ -289,7 +279,7 @@ def embed(state: IdViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(m).to_markdown()[:-1]+'!!!)', + title=MonsterHeader.long_v2(m).to_markdown(), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=pad_info_footer_with_state(state), From 34c4a98f7fbdbba214019fcb0be1a5a9f6f5a8ff Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Mon, 5 Apr 2021 16:30:02 -0400 Subject: [PATCH 0712/1220] New MonIdListener cog (#958) * idlookup * cleaned * updated monidlistener * Delete settings.json * fixed suggestions * fixed suggestions * fixed suggestions --- .gitignore | 2 + monidlistener/__init__.py | 7 ++++ monidlistener/info.json | 21 ++++++++++ monidlistener/monidlistener.py | 73 ++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 monidlistener/__init__.py create mode 100644 monidlistener/info.json create mode 100644 monidlistener/monidlistener.py diff --git a/.gitignore b/.gitignore index 5a7bc711..ec76ca0a 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ bot_data/ .orig .DS_Store +.vscode +__pycache__ \ No newline at end of file diff --git a/monidlistener/__init__.py b/monidlistener/__init__.py new file mode 100644 index 00000000..b2de73a6 --- /dev/null +++ b/monidlistener/__init__.py @@ -0,0 +1,7 @@ +from .monidlistener import MonIdListener + +__red_end_user_data_statement__ = "No personal data is stored." + + +def setup(bot): + bot.add_cog(MonIdListener(bot)) diff --git a/monidlistener/info.json b/monidlistener/info.json new file mode 100644 index 00000000..02ff1fd2 --- /dev/null +++ b/monidlistener/info.json @@ -0,0 +1,21 @@ +{ + "author": "fluff#2368", + "description": "Gets monster names when monster ids are sent.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "", + "short": "Monster Id Lookup", + "min_bot_version": "3.4.0", + "max_bot_version": "0.0.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "required_cogs": {}, + "requirements": ["re","tsutils"], + "tags": [ + "pad" + ] + } \ No newline at end of file diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py new file mode 100644 index 00000000..65faeef3 --- /dev/null +++ b/monidlistener/monidlistener.py @@ -0,0 +1,73 @@ +import asyncio +import os +import re +import urllib.parse + +from redbot.core import checks, commands, data_manager, Config + +import discord +import tsutils + + +class MonIdListener(commands.Cog): + """Monster Name from ID""" + + def __init__(self, bot): + self.bot = bot + self.config = Config.get_conf(self, identifier=10100779) + self.config.register_channel(enabled=False) + + @commands.Cog.listener('on_message') + async def on_message(self, message): + channel = message.channel + content = message.content + if await self.config.channel(channel).enabled(): + if message.guild is None: # dms + return + if message.author == self.bot.user: # dont reply to self + return + if await self.is_command(message): # skip commands + return + dgcog = self.bot.get_cog("Dadguide") + if dgcog is None: + await channel.send("Error: Dadguide Cog not loaded. Please alert a bot owner.") + return + if re.search(r'\b\d\d\d[ -]?\d\d\d[ -]?\d\d\d\b', content): # friend code + return + if "+" in content or "plus" in content: + return + if re.search(r'\d{3,4}', content): + matches = re.findall(r'\d{3,4}', content) + ret = "" + for i in matches: + m = await dgcog.find_monster(i, message.author.id) + if not m: # monster not found + continue + ret += "[{}] {}\n".format(i, m.name_en) + if ret != "": + await channel.send(ret) + + @commands.group(aliases=['monidlistener']) + async def midlistener(self, ctx): + """Commands pertaining to monster id lookup""" + + @midlistener.command() + @checks.admin_or_permissions(manage_messages=True) + async def enable(self, ctx): + """Enable monster ID listener in this channel""" + await self.config.channel(ctx.channel).enabled.set(True) + await ctx.send("Enabled monster ID listener in this channel.") + + @midlistener.command() + @checks.admin_or_permissions(manage_messages=True) + async def disable(self, ctx): + """Disable monster ID listener in this channel""" + await self.config.channel(ctx.channel).enabled.set(False) + await ctx.send("Disabled monster ID listener in this channel.") + + async def is_command(self, msg): + prefixes = await self.bot.get_valid_prefixes() + for p in prefixes: + if msg.content.startswith(p): + return True + return False From 1f6f6fccb7d44eac0f771e7ab8292f8f6fff9ffa Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 5 Apr 2021 13:46:16 -0700 Subject: [PATCH 0713/1220] Remove re from requirements (#962) --- monidlistener/info.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monidlistener/info.json b/monidlistener/info.json index 02ff1fd2..4ec0ece4 100644 --- a/monidlistener/info.json +++ b/monidlistener/info.json @@ -14,8 +14,8 @@ "hidden": false, "disabled": false, "required_cogs": {}, - "requirements": ["re","tsutils"], + "requirements": ["tsutils"], "tags": [ "pad" ] - } \ No newline at end of file + } From 035db368f46ca24f5401a760b48742998055fe9e Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 5 Apr 2021 13:46:30 -0700 Subject: [PATCH 0714/1220] Add Red required GDPR cog methods (#961) --- monidlistener/monidlistener.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 65faeef3..2fdaacaf 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -2,8 +2,9 @@ import os import re import urllib.parse +from io import BytesIO -from redbot.core import checks, commands, data_manager, Config +from redbot.core import checks, commands, Config import discord import tsutils @@ -16,6 +17,18 @@ def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=10100779) self.config.register_channel(enabled=False) + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return @commands.Cog.listener('on_message') async def on_message(self, message): From 071818aebec0294e78094fc940c5214f5931cadb Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Tue, 6 Apr 2021 11:03:37 -0400 Subject: [PATCH 0715/1220] fixed id boundary (#963) --- monidlistener/monidlistener.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 2fdaacaf..e628e652 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -49,8 +49,8 @@ async def on_message(self, message): return if "+" in content or "plus" in content: return - if re.search(r'\d{3,4}', content): - matches = re.findall(r'\d{3,4}', content) + if re.search(r'\b\d{3,4}\b', content): + matches = re.findall(r'\b\d{3,4}\b', content) ret = "" for i in matches: m = await dgcog.find_monster(i, message.author.id) From 815b46ad02a25d433d696d2f39f493f59d5e5dc3 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Wed, 7 Apr 2021 06:55:48 -0400 Subject: [PATCH 0716/1220] Add comma as friend code sep, filter out "297" (#964) * fixed id boundary * add comma * remove 297 * filter 297 --- monidlistener/monidlistener.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index e628e652..7c87ec0e 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -34,6 +34,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def on_message(self, message): channel = message.channel content = message.content + content = re.sub(r'297','',content) if await self.config.channel(channel).enabled(): if message.guild is None: # dms return @@ -45,7 +46,7 @@ async def on_message(self, message): if dgcog is None: await channel.send("Error: Dadguide Cog not loaded. Please alert a bot owner.") return - if re.search(r'\b\d\d\d[ -]?\d\d\d[ -]?\d\d\d\b', content): # friend code + if re.search(r'\b\d\d\d[ -,]?\d\d\d[ -,]?\d\d\d\b', content): # friend code return if "+" in content or "plus" in content: return From 73eef61aa5a020d3c377604492549593b2377408 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 7 Apr 2021 10:02:28 -0700 Subject: [PATCH 0717/1220] Make author field a list in info.json (#965) --- monidlistener/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monidlistener/info.json b/monidlistener/info.json index 4ec0ece4..22ec6722 100644 --- a/monidlistener/info.json +++ b/monidlistener/info.json @@ -1,5 +1,5 @@ { - "author": "fluff#2368", + "author": ["fluff#2368"], "description": "Gets monster names when monster ids are sent.", "end_user_data_statement": "No personal data is stored.", "install_msg": "", From c8defd94fd6357ac33b42b17b241363aca61e064 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Tue, 13 Apr 2021 22:07:11 -0400 Subject: [PATCH 0718/1220] Improve ignoring friend code; also, ignore '100' (#969) Apparently, some people use commas AND spaces, sigh --- monidlistener/monidlistener.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 7c87ec0e..513c5465 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -46,7 +46,7 @@ async def on_message(self, message): if dgcog is None: await channel.send("Error: Dadguide Cog not loaded. Please alert a bot owner.") return - if re.search(r'\b\d\d\d[ -,]?\d\d\d[ -,]?\d\d\d\b', content): # friend code + if re.search(r'\b\d{3}[ -,]{0,2}\d{3}[ -,]{0,2}\d{3}\b', content): # friend code return if "+" in content or "plus" in content: return @@ -54,6 +54,8 @@ async def on_message(self, message): matches = re.findall(r'\b\d{3,4}\b', content) ret = "" for i in matches: + if(i=="100"): # skip when people say "is 100 mp or over" ~~and ryan's posts~~ + continue m = await dgcog.find_monster(i, message.author.id) if not m: # monster not found continue From f5bbaf6257ad073d4ab12a418065035e70258f92 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 14 Apr 2021 19:00:27 -0700 Subject: [PATCH 0719/1220] Only send one message for small mirrored messages (#970) --- channelmirror/channelmirror.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index 17651efd..0a886caa 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -27,10 +27,11 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.settings = ChannelMirrorSettings("channelmirror") - self.channel_last_spoke = {} self.config = Config.get_conf(self, identifier=3747737700) self.config.register_channel(multiedit=False, mirroredit_target=None, nodeletion=False) + self.config.init_custom("dest_message", 1) + self.config.register_custom("dest_message", small=False) GACOG = self.bot.get_cog("GlobalAdmin") if GACOG: @@ -195,8 +196,8 @@ async def countreactions(self, ctx, message: discord.Message): reacts[str(react)] += dest_reaction.count - 1 o = "" maxlen = len(str(max(reacts.values(), key=lambda x: len(str(x))))) - for r, c in reacts.items(): - o += "{{}}: {{:{}}}\n".format(maxlen).format(r, c) + for reaction, count in reacts.items(): + o += "{{}}: {{:{}}}\n".format(maxlen).format(reaction, count) await ctx.send(o) @channelmirror.command() @@ -251,6 +252,7 @@ async def mirror_msg(self, message): attribution_required = last_spoke != author.id attribution_required |= (now_time - last_spoke_time).total_seconds() > ATTRIBUTION_TIME_SECONDS attribution_required &= not await self.config.channel(message.channel).multiedit() + attribution_required = True self.settings.set_last_spoke(channel.id, author.id) @@ -289,11 +291,16 @@ async def mirror_msg(self, message): if not dest_channel: continue try: + fmessage = await self.mformat(message.content, message.channel, dest_channel) + + small = False if attribution_required: msg = self.makeheader(message, author) - await dest_channel.send(msg) - - fmessage = await self.mformat(message.content, message.channel, dest_channel) + if len(fmessage) > 500: + await dest_channel.send(msg) + else: + fmessage = msg + '\n' + fmessage + small = True if attachment_bytes: try: @@ -317,8 +324,8 @@ async def mirror_msg(self, message): logger.warning('Failed to mirror message from {} no action to take'.format(channel.id)) continue - self.settings.add_mirrored_message( - channel.id, message.id, dest_channel.id, dest_message.id) + self.settings.add_mirrored_message(channel.id, message.id, dest_channel.id, dest_message.id) + await self.config.custom("dest_message", dest_message.id).small.set(small) except discord.Forbidden: if dest_channel.guild.owner: try: @@ -401,6 +408,10 @@ async def mirror_msg_mod(self, message, if new_message_content: fcontent = await self.mformat(new_message_content, channel, dest_message.channel) + if await self.config.custom("dest_message", dest_message.id).small(): + fcontent = self.makeheader(message) + '\n' + fcontent + if len(fcontent) > 2000: + fcontent = fcontent[:1971] + "... *(Continued in original)*" await dest_message.edit(content=fcontent) elif new_message_reaction: try: From 5858618cccc36683ab52da9631a7b8d72d42a7cd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 14 Apr 2021 21:09:45 -0500 Subject: [PATCH 0720/1220] Remove attribution_required testing line (#971) --- channelmirror/channelmirror.py | 1 - 1 file changed, 1 deletion(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index 0a886caa..fc856033 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -252,7 +252,6 @@ async def mirror_msg(self, message): attribution_required = last_spoke != author.id attribution_required |= (now_time - last_spoke_time).total_seconds() > ATTRIBUTION_TIME_SECONDS attribution_required &= not await self.config.channel(message.channel).multiedit() - attribution_required = True self.settings.set_last_spoke(channel.id, author.id) From 034c6e3a07a752a18ebbe2abe5cde6c20f136ce0 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 16 Apr 2021 07:56:19 -0700 Subject: [PATCH 0721/1220] Allow token removal via sheet (#972) This also makes the MP modifier reliant on not having the REM modifier --- dadguide/monster_index.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 28e58054..5a73b303 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -9,6 +9,7 @@ import tsutils from redbot.core.utils import AsyncIter +from .models.monster_model import MonsterModel from .token_mappings import * SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY' \ @@ -45,14 +46,16 @@ async def __ainit__(self, monsters, db): if " " in m.series.name_en.strip()}.union(MULTI_WORD_TOKENS) self.replacement_tokens = defaultdict(set) + self.remove_mods = defaultdict(set) self.treename_overrides = set() + nickname_data, treenames_data, pantheon_data, nt_alias_data, mod_data, treemod_data = await asyncio.gather( sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 5), sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 5), sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, 2), sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2), - sheet_to_reader(MODIFIER_OVERRIDE_SHEET, 2), + sheet_to_reader(MODIFIER_OVERRIDE_SHEET, 3), sheet_to_reader(TREE_MODIFIER_OVERRIDE_SHEET, 2), ) @@ -98,7 +101,7 @@ async def __ainit__(self, monsters, db): self.replacement_tokens[frozenset(re.split(r'\W+', tokens))].add(alias) self.manual_prefixes = defaultdict(set) - for mid, mods in mod_data: + for mid, mods, rmv in mod_data: if mid.isdigit(): mid = int(mid) for mod in mods.split(","): @@ -106,7 +109,10 @@ async def __ainit__(self, monsters, db): if " " in mod: self.multi_word_tokens.add(tuple(mod.lower().split(" "))) mod = mod.lower().replace(" ", "") - self.manual_prefixes[mid].update(get_modifier_aliases(mod)) + if rmv: + self.remove_mods[mid].update(mod) + else: + self.manual_prefixes[mid].update(get_modifier_aliases(mod)) for mid, mods in treemod_data: if mid.isdigit(): @@ -277,7 +283,7 @@ def _get_important_tokens(cls, oname, treenames=None): else: return cls._name_to_tokens(min(n1, n2, key=token_count)) - async def get_modifiers(self, monster): + async def get_modifiers(self, monster: MonsterModel): modifiers = self.manual_prefixes[monster.monster_id].copy() basemon = self.graph.get_base_monster(monster) @@ -409,11 +415,11 @@ def is_story(m, do_transform=True): if self.graph.monster_is_farmable_evo(monster) or self.graph.monster_is_mp_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.FARMABLE]) - if self.graph.monster_is_mp_evo(monster): - modifiers.update(MISC_MAP[MiscModifiers.MP]) - if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) + elif self.graph.monster_is_mp_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.MP]) + # Art if monster.orb_skin_id: @@ -435,6 +441,8 @@ def is_story(m, do_transform=True): if monster.monster_id > 10000: modifiers.add("idna") + modifiers.difference_update(self.remove_mods[monster.monster_id]) + return modifiers def add_numbered_modifier(self, monster, curr_mods, added_mods, condition, *, else_mods=None): From 6460e1c7a8e49ffd8a81e34469e5b275f03380d4 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 16 Apr 2021 07:59:16 -0700 Subject: [PATCH 0722/1220] Add "medal" and "shop" tokens to exchange monsters (#973) Co-authored-by: River <18037011+RheingoldRiver@users.noreply.github.com> --- dadguide/monster_graph.py | 48 ++++++++++++++++++++++++++++++++++++++ dadguide/monster_index.py | 2 ++ dadguide/token_mappings.py | 4 +++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 41f7cd66..f739435d 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,4 +1,5 @@ import json +import re from collections import defaultdict from typing import Optional, List @@ -93,6 +94,12 @@ JOIN d_egg_machine_types ON d_egg_machine_types.egg_machine_type_id = egg_machines.egg_machine_type_id """ +EXCHANGE_QUERY = """SELECT + * +FROM + exchanges +""" + class MonsterGraph(object): def __init__(self, database: DadguideDatabase): @@ -111,6 +118,7 @@ def build_graph(self): es = self.database.query_many(EVOS_QUERY, ()) aws = self.database.query_many(AWAKENINGS_QUERY, ()) ems = self.database.query_many(EGG_QUERY, ()) + exs = self.database.query_many(EXCHANGE_QUERY, ()) mtoawo = defaultdict(list) for a in aws: @@ -241,6 +249,13 @@ def build_graph(self): mat, evo_model.to_id, type="material_of", model=evo_model) already_used_in_this_evo.append(mat) + for ex in exs: + for vendor_id in re.findall(r'\d+', ex.required_monster_ids): + self.graph.add_edge( + ex.target_monster_id, int(vendor_id), type='exchange_from') + self.graph.add_edge( + int(vendor_id), ex.target_monster_id, type='exchange_for') + # Caching for mid in self.graph.nodes: self.graph.nodes[mid]['alt_versions'] = self.process_alt_versions(mid) @@ -564,6 +579,39 @@ def monster_is_rem_evo_by_id(self, monster_id): def monster_is_rem_evo(self, monster: MonsterModel): return self.monster_is_rem_evo_by_id(monster.monster_no) + # redeemable + def monster_is_exchange_by_id(self, monster_id): + return bool(self._get_edges(self.graph[monster_id], 'exchange_from')) + + def monster_is_exchange(self, monster: MonsterModel): + return self.monster_is_exchange_by_id(monster.monster_no) + + def monster_is_exchange_evo_by_id(self, monster_id): + return any( + m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_exchange_by_id(m)) + + def monster_is_exchange_evo(self, monster: MonsterModel): + return self.monster_is_exchange_evo_by_id(monster.monster_no) + + def get_monster_exchange_mat_ids_by_id(self, monster_id): + return self._get_edges(self.graph[monster_id], 'exchange_from') + + def get_monster_exchange_mat_ids(self, monster: MonsterModel): + return self.get_monster_exchange_mat_ids_by_id(monster.monster_no) + + def get_monster_exchange_mats_by_id(self, monster_id): + return {self.get_monster(mid) for mid in self.get_monster_exchange_mat_ids_by_id(monster_id)} + + def get_monster_exchange_mats(self, monster: MonsterModel): + return self.get_monster_exchange_mats_by_id(monster.monster_no) + + def monster_is_vendor_exchange_by_id(self, monster_id): + ids = self.get_monster_exchange_mats_by_id(monster_id) + return bool(ids) and all(15 in [t.value for t in m.types] for m in ids) + + def monster_is_vendor_exchange(self, monster: MonsterModel): + return self.monster_is_vendor_exchange_by_id(monster.monster_no) + def monster_is_new(self, monster: MonsterModel): latest_time = max(am.reg_date for am in self.get_alt_monsters(monster)) return monster.reg_date == latest_time diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 5a73b303..134a7e61 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -417,6 +417,8 @@ def is_story(m, do_transform=True): if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) + elif self.graph.monster_is_vendor_exchange(monster): + modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) elif self.graph.monster_is_mp_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.MP]) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 91423f17..fcd4d101 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -254,6 +254,7 @@ class MiscModifiers(Enum): NEW = 'Newest monster in series' ORBSKIN = 'Grants an orb skin' ANIMATED = 'Animated monster' + MEDAL_EXC = 'Exchangable for vendor mats' MISC_MAP = { @@ -272,7 +273,8 @@ class MiscModifiers(Enum): MiscModifiers.COLLAB: ('collab',), MiscModifiers.NEW: ('new',), MiscModifiers.ORBSKIN: ('orbskin',), - MiscModifiers.ANIMATED: ('animated',) + MiscModifiers.ANIMATED: ('animated',), + MiscModifiers.MEDAL_EXC: ('medal', 'shop') } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { From b21391000ff3f9396001e359601b94f48c0c0719 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 16 Apr 2021 08:07:10 -0700 Subject: [PATCH 0723/1220] Make `MP` and `shop` modifiers not mutually exclusive (#974) --- dadguide/monster_index.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 134a7e61..7ee3f529 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -417,11 +417,11 @@ def is_story(m, do_transform=True): if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) - elif self.graph.monster_is_vendor_exchange(monster): - modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) - elif self.graph.monster_is_mp_evo(monster): - modifiers.update(MISC_MAP[MiscModifiers.MP]) - + else: + if self.graph.monster_is_vendor_exchange(monster): + modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) + if self.graph.monster_is_mp_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.MP]) # Art if monster.orb_skin_id: From 604f758ecc1def252a80d042d89ff5bc847bde54 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Sat, 17 Apr 2021 02:38:54 -0400 Subject: [PATCH 0724/1220] Set default date for ^which to 1969 (#980) --- padglobal/padglobal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index fc1473c3..761c16ef 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -625,7 +625,7 @@ async def _resolve_which(self, ctx, term): name = padinfo.get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() monster_id = m.monster_id definition = self.settings.which().get(monster_id, None) - timestamp = "2000-01-01" + timestamp = "1969-01-01" if isinstance(definition, list): definition, timestamp = definition @@ -749,7 +749,7 @@ async def getwhich(self, ctx): if isinstance(result, list): monsters.append([name, result[1]]) else: - monsters.append([name, "2000-01-01"]) + monsters.append([name, "1969-01-01"]) tbl = prettytable.PrettyTable(['Monster', 'Timestamp']) tbl.hrules = prettytable.HEADER From 091d779ff8a5b77b450ab24695f953bab1a319f7 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 18 Apr 2021 06:27:22 -0700 Subject: [PATCH 0725/1220] Add cc & cross awakening images to pdchu (#983) --- padbuildimg/assets/awk/73.png | Bin 0 -> 2335 bytes padbuildimg/assets/awk/74.png | Bin 0 -> 2357 bytes padbuildimg/assets/awk/75.png | Bin 0 -> 2346 bytes padbuildimg/assets/awk/76.png | Bin 0 -> 2337 bytes padbuildimg/assets/awk/77.png | Bin 0 -> 2358 bytes padbuildimg/assets/awk/78.png | Bin 0 -> 1087 bytes padbuildimg/padbuildimg.py | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 padbuildimg/assets/awk/73.png create mode 100644 padbuildimg/assets/awk/74.png create mode 100644 padbuildimg/assets/awk/75.png create mode 100644 padbuildimg/assets/awk/76.png create mode 100644 padbuildimg/assets/awk/77.png create mode 100644 padbuildimg/assets/awk/78.png diff --git a/padbuildimg/assets/awk/73.png b/padbuildimg/assets/awk/73.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec3806c92d9d8101528b9dc2e80c761b1c36d42 GIT binary patch literal 2335 zcmZuzc|4TcAD+cBCS#A1Wr$>(u{6bmi^iIfCK|~$!_Z(B`F`&9fYL+vhlI$%!uvmZ!Cx-yQKoJ0V3&ME;KurLMlM8c( zRy-NNwOt1QFq{Z5a;FhVa0?2-lY}D?+(S7fFcbKT%LM(z%QC?~bNCkbz1rdjj(Qi_ z);_=Y1`HJ&GfdU(8v79Bu=|qO&MtF zZOi|w_NzHPjyn2?ABhk^^RuPVd<_13cY4VGB7TtdkXv^D$Nu-T-L7&D*8s|4{&P_V z(Bt?p5deTE!2)AsYYt=K$yT;_v2UY3{4D@&CR4{%Sd4oPSL?O2>ba|%1ck-Ant0GO z5248#IFRMKrwWB-iL|wQv1JE}QR^Db;-Y(QRN>y@Zjdre#1G4BX?N?ab%)=XZUUaS zv1x9a=xGR@9$k91l)sc8Y-Ttem?fRvsSqT_pD~+WJh$q$?lEx>nq1{n8oDC*5kDsD z`LJObB-+G%>eWia-M|vqC56D8)l4}@{%AP-pfXQ7gD_zR5Vg0?THNT3BldoCH&Py0 zq)<*5q+PgH4FjGTE8Bfa^k(CXbhHH1)hCPIUbuEh-J-mhFtXc{%b1 z;)3jlKOGE z#@+c%P2MI4dOU7ezhn44aHZ`K6NYzPCxWdmzxs0RHmn3kG*wm*H#}jddTELQ)_W2o z6_jy|XY}UD#PQeUiRL>5ikx`Z7pp5pM^2>hN#VJTVy$ICiaX|A#u^25-}xtb-b_ZZvZn3-(v%7 z?DCb*sIeZUl_Wb@IZ2v5xB>-Dzd71{tp91ZS@MYEJQUYxt!UOQysu1^1pk)aSbL9t ztWfy4^X&P&Y~rO|rTJ`uiBh_2v$vhEm6zLSL+3}#$XiN z_SuJ-{-ZlrQ$%*4D+gXQYVX-0VIWwBOIeyuT3EQY_VlDT1C5y>{dOuul2<^c{m>fi z2<=nn-5mu3uA}R&55xV(-z*Qmq1TNQch_<$2C%)78t^?PBNe_a4iy-3GZo`&LIy+LVv@scxfC{~r@Y%Ir>T+f5qB_@8hdJZi6X4#s zE?4Rf1ymcBoF(PKRc#CZv*&ruEBioEva>oP5!$~oY0S3ZPJ$+esjT}7w`vsK-rX)P z=+iG)UW2P$h^lyG{#P@3jKa1Crvj$-e-zoBc_cB(ViJ?n$171`q#t!Q)MajCbW_Gi@Tgh$V4AFeorUhsZn zmnn2M^{+3$4+To1pQcXN5M(a2s9o4Iha=X~g0#97Q7eOU!~_`VnzLl`m9FF%B`jZu zQhjJ=Z&y1Dxj42lyDw8kG52lz+u5q?wX2;**-xYe37b~VvKHiqz`mE>XNhQ*9Y`XP zoT|D$K|3V*!Q61Z|eC4x0CpPiiB z5Jurx8l$B*V$s^(u$Td~S38Nu)N7zrMf+}vU$k`;tK|kMo+%GXSIHm$)TvwjDaL!Z zcN4XvLVEHHGsBMDT)^zvP&qZN@~l31xLXD-+u$TUn5y)wRdamVRsow9OYdJT=5Ja_ z(TZq{6!j>)4=Rf9(RZBweW2V$%jNm1&3n}#YRAYVJH(3wWy`c(&&LH-{q|UEF3eHO2COD;K2S zLs%N{uzPL7nB3E;EIF(E zPlf11EId7z;a;eN;%8_&==~ulOyEJqmJOMHUd-as`S|5{0}FUHOfa`XduTSI-M{pL zZA*5k$;iyu`JxnSWDILZ-D=y*OF}tJBa3eoh!?APtn0C|Dr_%`o(nElyTvT)N67BC z;7OZ&3-bzK2#}F&@Q7HbOnd^i0)K9NbAY(Kko)|J$+&P{cbetm>pLBw;X%Hbs)2YX zWd8%(aqC&*$Hj?lIoIRen*S6o_Pup`4Kh5~$}GsP)Wbb0x|if1Kks)EeubA4QY2wtgrqOsp~0#;(zS18WfBfdBvi literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/74.png b/padbuildimg/assets/awk/74.png new file mode 100644 index 0000000000000000000000000000000000000000..b1fecfc47361cc8f262e67acfb7f4c66927584a9 GIT binary patch literal 2357 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 A=l}o! literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/76.png b/padbuildimg/assets/awk/76.png new file mode 100644 index 0000000000000000000000000000000000000000..7b64ba9507ee77246986768eefd7739c3fa21627 GIT binary patch literal 2337 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 O8K#Do29+3(h`#}T3i)~f literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/78.png b/padbuildimg/assets/awk/78.png new file mode 100644 index 0000000000000000000000000000000000000000..fb9a5489920ebdd81fe085a3e3da65724f948ce5 GIT binary patch literal 1087 zcmV-F1i<@=P)Vl&|000CANkl-;4TipJEo5j>bP;FG66O11gzOSiSHWlz*EUnv;H1|FFh7U$fa z8S5KMcVIK9c5pCodVEkWFBYX-8GRvRZg=vw9xdw+7f)yc&EWpF+YIi@+Sa0sm5x0F zyuLCnS!Rj);A=-;mv7$eixRN}h=ud8eMjIbe0hCd!B>{fi*L6deI{9E@uqlo@HP48 z!*|t4!u%FT0=FMs+l5z~Hxl91=A2|1i?W?SE*yGQ+Lzvr;3VEiG0&q;gil!8~!9u^EZ$y_+_N-TlrfD7}p1LNVax=vxYV z{C-}+VFE*80%NB%hS-bcyi^)rn(}+P4N1y-ff<`Ff1q&AcJ=I#78y{~Vtl6XtOY(? zJiG(HN5N&of%Nz16huoOxG85*%!&;l#au1fg6LP-Zuav6N7SmU_k2i zx@>Q6YfJyu%B&{9HnQkYZlAE_Kb(0(fxy2n45*J|A%2##kYH7RqXI+ZL6AJ%yA^s{-Tz1v0(g9<{8XT_M2yRAj27Lq0BvV|g)uJ81&p7dH zrCm4tsOOyf_JvT!h>62*?!r;+gZb<$NA+*(byeT3+~)Cm0rw4`jnl$m+Yucp2b5&V zXP%GFyn7CJn1Y-vjmXdI^ZHIKuV0m0D^t4Vsc?#jvm|p`E2^|=PmA{x;*pBOPfDfn zg(;uc4uGVp9+~J%^ryw+PlJaAZSZ-vJ%m#noTXpYRC!vp6Q6Vo@F&B^cWzPwoUKl) z>i_HS1)tVOhHXz!9#-|QTGc2!RM@WyeLp*7%9H%P;Nc4w!RMsXn9>S8K9G;@F<@#S zt=f~%JRd#1mcVYoh1(v&r(`3TfB*frJqy%5KC^IAdB6WVBaRLq9=1z~@Z>Y!{XXwrJgW}}9{))4tEINu z{#-VKS Date: Sun, 18 Apr 2021 06:28:45 -0700 Subject: [PATCH 0726/1220] Support exact-matching id3 tokens inside "" (#982) --- dadguide/find_monster.py | 226 +++++++++++++++++++++------------------ dadguide/info.json | 2 +- 2 files changed, 125 insertions(+), 103 deletions(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 4b3a2732..1ae7ab21 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -1,13 +1,12 @@ import json import re from collections import defaultdict -from typing import Set, List, Tuple, Optional, TYPE_CHECKING, Mapping +from typing import Set, List, Tuple, Optional, Mapping, Union, Iterable, Any from Levenshtein import jaro_winkler from tsutils import rmdiacritics -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel +from dadguide.models.monster_model import MonsterModel SERIES_TYPE_PRIORITY = { "regular": 4, @@ -20,6 +19,25 @@ } +class Token: + def __init__(self, value, *, negated=False, exact=False): + self.value = value + self.negated = negated + self.exact = exact + + def __eq__(self, other): + if isinstance(other, Token): + return self.value == other.value + elif isinstance(other, str): + return self.value == other + + def __hash__(self): + return hash(self.value) ^ (self.negated << 5) ^ (self.exact << 6) + + def __repr__(self): + return ("-" if self.negated else "") + (repr(self.value) if self.exact else self.value) + + class MonsterMatch: def __init__(self, score=0, name=None, mod=None): self.score = score @@ -32,15 +50,18 @@ def __repr__(self): return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) +MatchMap = Mapping[MonsterModel, MonsterMatch] + + class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 - def __init__(self, dgcog, flags): + def __init__(self, dgcog, flags: Mapping[str, Any]): self.dgcog = dgcog self.flags = flags - def merge_multi_word_tokens(self, tokens): + def merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: result = [] skip = 0 multi_word_tokens_sorted = sorted(self.dgcog.index.multi_word_tokens, @@ -65,139 +86,132 @@ def merge_multi_word_tokens(self, tokens): result.append(token1) return result - def _monster_has_modifier(self, monster, token, matches): - if len(token) < 6: - if token in self.dgcog.index.modifiers[monster]: - matches[monster].mod.add((token, token)) + def _monster_has_modifier(self, monster: MonsterModel, token: Token, matches: MatchMap) -> bool: + if len(token.value) < 6: + if token.value in self.dgcog.index.modifiers[monster]: + matches[monster].mod.add((token.value, token.value)) matches[monster].score += 1 return True else: - closest = max(self.dgcog.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(m, token)) - ratio = self.calc_ratio_modifier(closest, token) + closest = max(self.dgcog.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(token, m)) + ratio = self.calc_ratio_modifier(closest, token.value) if ratio > self.MODIFIER_JW_DISTANCE: - matches[monster].mod.add((token, closest)) + matches[monster].mod.add((token.value, closest)) matches[monster].score += ratio return True return False - def interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: + def _string_to_token(self, value: str) -> Token: + if (negated := value.startswith('-')): + value = value.lstrip('-') + if (exact := bool(re.fullmatch(r'".+"', value))): + value = value[1:-1] + return Token(value, negated=negated, exact=exact) + + def interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: modifiers = [] - negative_modifiers = set() name = set() - negative_name = set() longmods = [p for p in self.dgcog.index.all_modifiers if len(p) > 8] lastmodpos = False - for i, token in enumerate(tokenized_query[::-1]): - negated = token.startswith("-") - token = token.lstrip('-') - if any(self.calc_ratio_modifier(m, token.split('-')[0], .1) > self.MODIFIER_JW_DISTANCE for m in - self.dgcog.index.suffixes): - if negated: - negative_modifiers.add(token) - else: - modifiers.append(token) + # Suffixes + for i, value in enumerate(tokenized_query[::-1]): + token = self._string_to_token(value) + + if any(self.calc_ratio_modifier(m, token.value.split('-')[0], .1) > self.MODIFIER_JW_DISTANCE + for m in self.dgcog.index.suffixes): + modifiers.append(token) else: if i != 0: tokenized_query = tokenized_query[:-i] break - for i, token in enumerate(tokenized_query): - negated = token.startswith("-") - token = token.lstrip('-') - if token in self.dgcog.index.all_modifiers or ( - any(self.calc_ratio_modifier(m, token, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) - and token not in self.dgcog.index.all_name_tokens - and len(token) >= 8): - if negated: - lastmodpos = False - negative_modifiers.add(token) - else: - lastmodpos = True - modifiers.append(token) + # Prefixes + for i, value in enumerate(tokenized_query): + token = self._string_to_token(value) + + if token.value in self.dgcog.index.all_modifiers or ( + any(self.calc_ratio_modifier(token, m, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) + and token.value not in self.dgcog.index.all_name_tokens + and len(token.value) >= 8): + lastmodpos = not token.negated + modifiers.append(token) else: tokenized_query = tokenized_query[i:] break else: tokenized_query = [] - for token in tokenized_query: - negated = token.startswith("-") - token = token.lstrip('-') - if negated: - negative_name.add(token) - else: - name.add(token) + # Name Tokens + for value in tokenized_query: + name.add(self._string_to_token(value)) - if not (name or negative_name) and modifiers and lastmodpos: + # Allow modifiers to match as name tokens if they're alone (Fix for hel and cloud) + if not name and modifiers and lastmodpos: if self.dgcog.index.manual[modifiers[-1]]: name.add(modifiers[-1]) modifiers = modifiers[:-1] - return set(modifiers), negative_modifiers, name, negative_name + return set(modifiers), name - def process_name_tokens(self, name_query_tokens, neg_name_tokens, matches): + def process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) -> Optional[Set[MonsterModel]]: matched_mons = None - for pos_name in name_query_tokens: - valid = self.get_valid_monsters_from_name_token(pos_name, matches) - if matched_mons is not None: - matched_mons.intersection_update(valid) - else: - matched_mons = valid - - for neg_name in neg_name_tokens: - invalid = self.get_valid_monsters_from_name_token(neg_name, matches, mult=-10) - if matched_mons is not None: - matched_mons.difference_update(invalid) + for name_token in name_query_tokens: + if name_token.negated: + invalid = self.get_valid_monsters_from_name_token(name_token, matches, mult=-10) + if matched_mons is not None: + matched_mons.difference_update(invalid) + else: + matched_mons = set(self.dgcog.database.get_all_monsters()).difference(invalid) else: - matched_mons = set(self.dgcog.database.get_all_monsters()).difference(invalid) + valid = self.get_valid_monsters_from_name_token(name_token, matches) + if matched_mons is not None: + matched_mons.intersection_update(valid) + else: + matched_mons = valid return matched_mons - def get_valid_monsters_from_name_token(self, token, matches, mult=1): + def get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, + mult: Union[int, float] = 1) -> Set[MonsterModel]: valid_monsters = set() all_monsters_name_tokens_scores = {nt: self.calc_ratio_name(token, nt) for nt in self.dgcog.index.all_name_tokens} - matched_tokens = sorted((nt for nt in all_monsters_name_tokens_scores - if all_monsters_name_tokens_scores[nt] > self.TOKEN_JW_DISTANCE), + matched_tokens = sorted((nt for nt, s in all_monsters_name_tokens_scores.items() if s > self.TOKEN_JW_DISTANCE), key=lambda nt: all_monsters_name_tokens_scores[nt], reverse=True) - matched_tokens += [t for t in self.dgcog.index.all_name_tokens if t.startswith(token)] + matched_tokens += [t for t in self.dgcog.index.all_name_tokens if t.startswith(token.value)] for match in matched_tokens: score = all_monsters_name_tokens_scores[match] for matched_monster in self.dgcog.index.manual[match]: if matched_monster not in valid_monsters: - matches[matched_monster].name.add((token, match, '(manual)')) + matches[matched_monster].name.add((token.value, match, '(manual)')) matches[matched_monster].score += (score + .001) * mult valid_monsters.add(matched_monster) for matched_monster in self.dgcog.index.name_tokens[match]: if matched_monster not in valid_monsters: - matches[matched_monster].name.add((token, match, '(name)')) + matches[matched_monster].name.add((token.value, match, '(name)')) matches[matched_monster].score += score * mult valid_monsters.add(matched_monster) for matched_monster in self.dgcog.index.fluff_tokens[match]: if matched_monster not in valid_monsters: - matches[matched_monster].name.add((token, match, '(fluff)')) + matches[matched_monster].name.add((token.value, match, '(fluff)')) matches[matched_monster].score += score * mult / 2 valid_monsters.add(matched_monster) return valid_monsters - def process_modifiers(self, mod_tokens, neg_mod_tokens, potential_evos, matches): - for pos_mod_token in mod_tokens: - potential_evos = {m for m in potential_evos if - self._monster_has_modifier(m, pos_mod_token, matches)} - if not potential_evos: - return None - for neg_mod_token in neg_mod_tokens: + def process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[MonsterModel], + matches: MatchMap) -> Set[MonsterModel]: + for mod_token in mod_tokens: potential_evos = {m for m in potential_evos if - not self._monster_has_modifier(m, neg_mod_token, matches)} + self._monster_has_modifier(m, mod_token, matches) ^ mod_token.negated} if not potential_evos: - return None - + return set() return potential_evos - def get_priority_tuple(self, monster, tokenized_query=None, matches=None): + def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = None, + matches: MatchMap = None) -> Tuple[Any, ...]: if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: @@ -220,7 +234,8 @@ def get_priority_tuple(self, monster, tokenized_query=None, matches=None): monster.rarity, monster.monster_no_na) - def get_most_eligable_monster(self, monsters, tokenized_query=None, matches=None): + def get_most_eligable_monster(self, monsters: Iterable[MonsterModel], tokenized_query: List[str] = None, + matches: MatchMap = None) -> MonsterModel: if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: @@ -228,7 +243,7 @@ def get_most_eligable_monster(self, monsters, tokenized_query=None, matches=None return max(monsters, key=lambda m: self.get_priority_tuple(m, tokenized_query, matches)) - def get_monster_evos(self, matched_mons, matches): + def get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: monster_evos = set() for monster in sorted(matched_mons, key=lambda m: matches[m].score, reverse=True): for evo in self.dgcog.database.graph.get_alt_monsters(monster): @@ -240,23 +255,20 @@ def get_monster_evos(self, matched_mons, matches): return monster_evos - async def find_monster_search(self, tokenized_query) -> \ - Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"]]: - mod_tokens, neg_mod_tokens, name_query_tokens, neg_name_tokens = \ - self.interpret_query(tokenized_query) + async def find_monster_search(self, tokenized_query: List[str]) -> \ + Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel]]: + mod_tokens, name_query_tokens = self.interpret_query(tokenized_query) name_query_tokens.difference_update({'|'}) - for mod_token in mod_tokens.union(neg_mod_tokens): - if mod_token not in self.dgcog.index.all_modifiers: + for mod_token in mod_tokens: + if mod_token.value not in self.dgcog.index.all_modifiers: async with self.dgcog.config.typo_mods() as typo_mods: - typo_mods.append(mod_token) + typo_mods.append(mod_token.value) matches = defaultdict(MonsterMatch) - if name_query_tokens or neg_name_tokens: - matched_mons = self.process_name_tokens(name_query_tokens, - neg_name_tokens, - matches) + if name_query_tokens: + matched_mons = self.process_name_tokens(name_query_tokens, matches) if not matched_mons: # No monsters match the given name tokens return None, {}, set() @@ -267,7 +279,7 @@ async def find_monster_search(self, tokenized_query) -> \ monster_score = defaultdict(int) # Expand search to the evo tree - matched_mons = self.process_modifiers(mod_tokens, neg_mod_tokens, matched_mons, matches) + matched_mons = self.process_modifiers(mod_tokens, matched_mons, matches) if not matched_mons: # no modifiers match any monster in the evo tree return None, {}, set() @@ -277,8 +289,8 @@ async def find_monster_search(self, tokenized_query) -> \ return mon, matches, matched_mons - async def find_monster_debug(self, query) -> \ - Tuple[Optional["MonsterModel"], Mapping["MonsterModel", MonsterMatch], Set["MonsterModel"], int]: + async def find_monster_debug(self, query: str) -> \ + Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel], int]: await self.dgcog.wait_until_ready() query = rmdiacritics(query).lower().replace(",", "") @@ -298,27 +310,37 @@ async def find_monster_debug(self, query) -> \ return best_monster, matches_dict, valid_monsters, 0 - async def find_monster(self, query) -> Optional["MonsterModel"]: + async def find_monster(self, query: str) -> Optional[MonsterModel]: matched_monster, _, _, _ = await self.find_monster_debug(query) return matched_monster - async def find_monsters(self, query) -> List["MonsterModel"]: + async def find_monsters(self, query: str) -> List[MonsterModel]: _, matches, monsters, _ = await self.find_monster_debug(query) return sorted(monsters, key=lambda m: self.get_priority_tuple(m, matches=matches), reverse=True) - def calc_ratio_modifier(self, s1, s2, dist=.05): - return jaro_winkler(s1, s2, .05) + def calc_ratio_modifier(self, s1: Union[Token, str], s2: str, dist: float = .05) -> float: + if isinstance(s1, Token): + if s1.exact: + return 1.0 if s1.value == s2 else 0.0 + s1 = s1.value + + return jaro_winkler(s1, s2, dist) + + def calc_ratio_name(self, token: Union[Token, str], full_word: str, factor: int = .05) -> float: + string = token.value if isinstance(token, Token) else token - def calc_ratio_name(self, token, full_word, factor=.05): mw = self.dgcog.index.mwt_to_len[full_word] != 1 - jw = jaro_winkler(token, full_word, factor) + jw = jaro_winkler(string, full_word, factor) + + if isinstance(token, Token) and token.exact and string != full_word: + return 0.0 - if token.isdigit() and full_word.isdigit() and token != full_word: + if string.isdigit() and full_word.isdigit() and string != full_word: return 0.0 - if full_word == token: + if full_word == string: score = 1.0 - elif len(token) >= 3 and full_word.startswith(token): + elif len(string) >= 3 and full_word.startswith(string): score = .995 if mw and jw < score: return score diff --git a/dadguide/info.json b/dadguide/info.json index 554084d7..e811d2b5 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils>=3.2.1", + "tsutils>=3.2.3", "pytz", "romkan", "networkx" From d5c1883ade23c0fd4e4ca21490ec2b7262ac047a Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Apr 2021 18:01:02 -0700 Subject: [PATCH 0727/1220] Show up to 25 test cases on index warning (#986) --- dadguide/dadguide.py | 3 +-- dadguide/idtest_mixin.py | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 2aa0f5bb..010f6dff 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -145,8 +145,7 @@ async def check_index(self): if not await self.config.indexlog(): return - if not await self.run_tests(): - self.index.issues.insert(0, "Test cases failed.") + self.index.issues.extend((await self.run_tests())[:25]) channel = self.bot.get_channel(await self.config.indexlog()) if self.index.issues: diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index 94fe8de6..6f69b241 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -1,5 +1,6 @@ import re from datetime import datetime +from typing import List import tsutils from redbot.core import commands, Config, checks @@ -486,9 +487,14 @@ async def idt_runall(self, ctx): for page in pagify(o): await ctx.send(box(page)) - async def run_tests(self): + async def run_tests(self) -> List[str]: + """Run all tests""" + rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' + failures = [] + qsuite = await self.config.test_suite() - for q in qsuite: + ml = len(max(qsuite or [''], key=len)) + 2 + for c, q in enumerate(sorted(qsuite)): try: m = await self.find_monster(q) or -1 except Exception: @@ -496,14 +502,19 @@ async def run_tests(self): mid = getattr(m, "monster_id", m) if mid != qsuite[q]['result']: - return False + reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' + qq = '"' + q + '"' + failures.append(f"{str(c).rjust(4)}. {qq.ljust(ml)} - {rcircle} " + f"Ex: {qsuite[q]['result']}, Ac: {mid}{reason}") fsuite = await self.config.fluff_suite() - for v in fsuite: + for c, v in enumerate(fsuite): fluff = v['id'] in [m.monster_id for m in self.index.fluff_tokens[v['token']]] name = v['id'] in [m.monster_id for m in self.index.name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): - return False - - return True + q = '"{}"'.format(v['token']) + failures.append(f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)} - " + f"{ycircle if name or fluff else rcircle} " + f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token") + return failures From b51274e5e6d6bc138aa38ca3c6aabe40d747bf7e Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 28 Apr 2021 16:35:00 -0700 Subject: [PATCH 0728/1220] Remove menu listener code (moved to misc-cogs) (#987) --- padinfo/__init__.py | 1 + padinfo/menu/errors.py | 6 --- padinfo/menu/menu_maps.py | 49 ++++-------------- padinfo/padinfo.py | 104 ++++++-------------------------------- 4 files changed, 26 insertions(+), 134 deletions(-) delete mode 100644 padinfo/menu/errors.py diff --git a/padinfo/__init__.py b/padinfo/__init__.py index 054e04e4..56291a27 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -6,4 +6,5 @@ def setup(bot): n = PadInfo(bot) bot.add_cog(n) + bot.loop.create_task(n.register_menu()) bot.loop.create_task(n.reload_nicknames()) diff --git a/padinfo/menu/errors.py b/padinfo/menu/errors.py deleted file mode 100644 index 60333a8e..00000000 --- a/padinfo/menu/errors.py +++ /dev/null @@ -1,6 +0,0 @@ -class MissingImsMenuType(Exception): - pass - - -class InvalidImsMenuType(Exception): - pass diff --git a/padinfo/menu/menu_maps.py b/padinfo/menu/menu_maps.py index 7c214f59..f50a675a 100644 --- a/padinfo/menu/menu_maps.py +++ b/padinfo/menu/menu_maps.py @@ -1,5 +1,4 @@ from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes -from padinfo.menu.errors import InvalidImsMenuType, MissingImsMenuType from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes @@ -8,43 +7,13 @@ from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes -type_to_menu = { - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu, - IdMenu.MENU_TYPE: IdMenu, - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenu, - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenu, - MonsterListMenu.MENU_TYPE: MonsterListMenu, - SeriesScrollMenu.MENU_TYPE: SeriesScrollMenu, - SimpleTextMenu.MENU_TYPE: SimpleTextMenu, - TransformInfoMenu.MENU_TYPE: TransformInfoMenu, +padinfo_menu_map = { + ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), + IdMenu.MENU_TYPE: (IdMenu, IdMenuPanes), + LeaderSkillMenu.MENU_TYPE: (LeaderSkillMenu, LeaderSkillMenuPanes), + LeaderSkillSingleMenu.MENU_TYPE: (LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes), + MonsterListMenu.MENU_TYPE: (MonsterListMenu, MonsterListMenuPanes), + SeriesScrollMenu.MENU_TYPE: (SeriesScrollMenu, SeriesScrollMenuPanes), + SimpleTextMenu.MENU_TYPE: (SimpleTextMenu, SimpleTextMenuPanes), + TransformInfoMenu.MENU_TYPE: (TransformInfoMenu, TransformInfoMenuPanes), } -type_to_panes = { - ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenuPanes, - IdMenu.MENU_TYPE: IdMenuPanes, - LeaderSkillMenu.MENU_TYPE: LeaderSkillMenuPanes, - LeaderSkillSingleMenu.MENU_TYPE: LeaderSkillSingleMenuPanes, - MonsterListMenu.MENU_TYPE: MonsterListMenuPanes, - SeriesScrollMenu.MENU_TYPE: SeriesScrollMenuPanes, - SimpleTextMenu.MENU_TYPE: SimpleTextMenuPanes, - TransformInfoMenu.MENU_TYPE: TransformInfoMenuPanes, -} - - -def get_menu(ims): - menu_type = ims.get('menu_type') - if menu_type is None: - raise MissingImsMenuType - menu = type_to_menu.get(menu_type) - if menu is None: - raise InvalidImsMenuType - return menu.menu() - - -def get_panes_class(ims): - menu_type = ims.get('menu_type') - if menu_type is None: - raise MissingImsMenuType - panes = type_to_panes.get(menu_type) - if panes is None: - raise InvalidImsMenuType - return panes diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e2cd4ada..602739b4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -4,18 +4,13 @@ import random import re import urllib.parse -from copy import deepcopy from io import BytesIO from typing import TYPE_CHECKING, List import discord import tsutils from discord import Color -from discordmenu.embed.emoji import EmbedMenuEmojiConfig from discordmenu.emoji.emoji_cache import emoji_cache -from discordmenu.intra_message_state import IntraMessageState -from discordmenu.reaction_filter import FriendReactionFilter, MessageOwnerReactionFilter, \ - BotAuthoredMessageReactionFilter, NotPosterEmojiReactionFilter, ValidEmojiReactionFilter from redbot.core import checks, commands, data_manager, Config from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate @@ -32,7 +27,7 @@ from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu -from padinfo.menu.menu_maps import type_to_panes, get_panes_class, get_menu +from padinfo.menu.menu_maps import padinfo_menu_map from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji from padinfo.menu.simple_text import SimpleTextMenu @@ -90,6 +85,8 @@ def _data_file(file_name: str) -> str: class PadInfo(commands.Cog): """Info for PAD Cards""" + menu_map = padinfo_menu_map + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -106,6 +103,7 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups = safe_read_json(_data_file('historic_lookups_id3.json')) + self.id_menu = IdMenu # TODO: Support multi-menus for real self.awoken_emoji_names = {v: k for k, v in AWAKENING_ID_TO_EMOJI_NAME_MAP.items()} self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster self.settings = settings @@ -125,6 +123,14 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return + async def register_menu(self): + await self.bot.wait_until_ready() + menucog = self.bot.get_cog("MenuCog") + if menucog is None: + logger.warning("MenuCog is not loaded.") + return + await menucog.register(self) + async def reload_nicknames(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadInfo'): @@ -147,88 +153,6 @@ async def get_dgcog(self): await dgcog.wait_until_ready() return dgcog - def get_monster(self, monster_id: int): - dgcog = self.bot.get_cog('Dadguide') - return dgcog.get_monster(monster_id) - - @commands.Cog.listener('on_reaction_add') - async def test_reaction_add(self, reaction, member): - emoji_clicked = self.get_emoji_clicked(reaction) - if emoji_clicked is None: - return - - message = reaction.message - ims = message.embeds and IntraMessageState.extract_data(message.embeds[0]) - if not ims: - return - menu = get_menu(ims) - if not (await menu.should_respond(message, reaction, await self.get_reaction_filters(ims), member)): - return - - data = await self.get_menu_default_data(ims) - data.update({ - 'reaction': emoji_clicked - }) - - await menu.transition(message, deepcopy(ims), emoji_clicked, member, **data) - await self.listener_respond_with_child(deepcopy(ims), message, emoji_clicked, member) - - @staticmethod - def get_emoji_clicked(reaction): - emoji_obj = reaction.emoji - if isinstance(emoji_obj, str): - emoji_clicked = emoji_obj - else: - emoji_clicked = emoji_obj.name - - # determine if this is potentially a valid reaction prior to doing any network call: - # this is true if it's a default emoji or in any of our global panes emoji lists - if emoji_clicked in EmbedMenuEmojiConfig().to_list(): - return emoji_clicked - for menu_type, panes_type in type_to_panes.items(): - if emoji_clicked in panes_type.emoji_names(): - return emoji_clicked - return None - - async def listener_respond_with_child(self, menu_1_ims, message_1, emoji_clicked, member): - failsafe = 0 - while menu_1_ims.get('child_message_id'): - # before this loop can actually work as a loop, the type of menu_2 can't be hard-coded as IdMenu anymore, - # and we have to update menu_1_class to be menu_2_class during the loop. - if failsafe == 10: - break - failsafe += 1 - menu_2 = IdMenu.menu() - panes_class = type_to_panes[menu_1_ims['menu_type']] - child_data_func = panes_class.get_child_data_func(emoji_clicked) - data = await self.get_menu_default_data(menu_1_ims) - emoji_simulated_clicked_2, extra_ims = None, {} - if child_data_func is not None: - emoji_simulated_clicked_2, extra_ims = child_data_func(menu_1_ims, emoji_clicked, **data) - if emoji_simulated_clicked_2 is not None: - fctx = await self.bot.get_context(message_1) - try: - message_2 = await fctx.fetch_message(int(menu_1_ims['child_message_id'])) - menu_2_ims = message_2.embeds and IntraMessageState.extract_data(message_2.embeds[0]) - menu_2_ims.update(extra_ims) - await menu_2.transition(message_2, menu_2_ims, emoji_simulated_clicked_2, member, **data) - except discord.errors.NotFound: - break - menu_1_ims = menu_2_ims - message_1 = message_2 - - async def get_reaction_filters(self, ims): - original_author_id = ims['original_author_id'] - friend_cog = self.bot.get_cog("Friend") - friend_ids = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - reaction_filters = [ - ValidEmojiReactionFilter(get_panes_class(ims).emoji_names()), - NotPosterEmojiReactionFilter(), - BotAuthoredMessageReactionFilter(self.bot.user.id), - MessageOwnerReactionFilter(original_author_id, FriendReactionFilter(original_author_id, friend_ids)) - ] - return reaction_filters - async def get_menu_default_data(self, ims): data = { 'dgcog': await self.get_dgcog(), @@ -236,6 +160,10 @@ async def get_menu_default_data(self, ims): } return data + def get_monster(self, monster_id: int): + dgcog = self.bot.get_cog('Dadguide') + return dgcog.get_monster(monster_id) + @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" From 36872d83c1fc9840521f3e7c8f16acab76e816b3 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 28 Apr 2021 16:47:05 -0700 Subject: [PATCH 0729/1220] Register on MenuListener instead of MenuCog (#988) --- padinfo/padinfo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 602739b4..76031731 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -125,11 +125,11 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def register_menu(self): await self.bot.wait_until_ready() - menucog = self.bot.get_cog("MenuCog") - if menucog is None: - logger.warning("MenuCog is not loaded.") + menulistener = self.bot.get_cog("MenuListener") + if menulistener is None: + logger.warning("MenuListener is not loaded.") return - await menucog.register(self) + await menulistener.register(self) async def reload_nicknames(self): await self.bot.wait_until_ready() From b3656fd2ba6cce0d910fa1d38dc63a104df094dd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 29 Apr 2021 22:32:02 -0500 Subject: [PATCH 0730/1220] Rename menu_maps.py to menu_map.py (#989) --- padinfo/menu/{menu_maps.py => menu_map.py} | 0 padinfo/padinfo.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename padinfo/menu/{menu_maps.py => menu_map.py} (100%) diff --git a/padinfo/menu/menu_maps.py b/padinfo/menu/menu_map.py similarity index 100% rename from padinfo/menu/menu_maps.py rename to padinfo/menu/menu_map.py diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 76031731..bdea4925 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -27,7 +27,7 @@ from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu -from padinfo.menu.menu_maps import padinfo_menu_map +from padinfo.menu.menu_map import padinfo_menu_map from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji from padinfo.menu.simple_text import SimpleTextMenu From 984072e2da5ca7afdae42ee3ee0a714fb5ddfcae Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 30 Apr 2021 04:03:04 -0500 Subject: [PATCH 0731/1220] Use colons not dashes in id3 testcase confirm msg (#991) --- dadguide/idtest_mixin.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index 6f69b241..e11cb7b8 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -41,7 +41,7 @@ async def idt_add(self, ctx, mid: int, *, query): 'reason': reason[0].strip() if reason else '' } - if await tsutils.get_reaction(ctx, f"Added test case `{mid} - {query}`" + if await tsutils.get_reaction(ctx, f"Added test case `{mid}: {query}`" f" with ref `{sorted(suite).index(query)}`", "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): if oldd: @@ -51,7 +51,7 @@ async def idt_add(self, ctx, mid: int, *, query): await ctx.react_quietly("\N{CROSS MARK}") else: await ctx.send( - f"Successfully added test case `{mid} - {query}` with ref `{sorted(suite).index(query)}`") + f"Successfully added test case `{mid}: {query}` with ref `{sorted(suite).index(query)}`") await ctx.tick() @idtest.group(name="name") @@ -92,7 +92,7 @@ async def norf_add(self, ctx, mid: int, token, reason, fluffy): old = [t for t in suite if t['id'] == mid and t['token'] == token][0] if not await tsutils.confirm_message(ctx, f"Are you sure you want to change" f" the type of test case #{suite.index(old)}" - f" `{mid} - {token}` from " + f" `{mid}: {token}` from " f" **{'fluff' if fluffy else 'name'}** to" f" **{'name' if fluffy else 'fluff'}**?"): await ctx.react_quietly("\N{CROSS MARK}") @@ -111,7 +111,7 @@ async def norf_add(self, ctx, mid: int, token, reason, fluffy): suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) if await tsutils.get_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " - f"case `{mid} - {token}` with ref `{suite.index(case)}`", + f"case `{mid}: {token}` with ref `{suite.index(case)}`", "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): suite.pop() if old: @@ -119,7 +119,7 @@ async def norf_add(self, ctx, mid: int, token, reason, fluffy): await ctx.react_quietly("\N{CROSS MARK}") else: m = await ctx.send(f"Successfully added {'fluff' if fluffy else 'name'} " - f"case `{mid} - {token}` with ref `{suite.index(case)}`") + f"case `{mid}: {token}` with ref `{suite.index(case)}`") await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") @idtest.command(name="import") @@ -188,7 +188,7 @@ async def idt_remove(self, ctx, *, item): res = suite[case]['result'] del suite[case] await ctx.send( - f"Removed test case `{case} - {res}` with ref") + f"Removed test case `{case}: {res}` with ref") @idt_name.command(name="remove") @checks.is_owner() @@ -216,7 +216,7 @@ async def norf_remove(self, ctx, item): suite.remove(case) # noinspection PyTypeChecker await ctx.send(f"Successfully removed {'fluff' if case['fluff'] else 'name'} case" - f" `{case['id']} - {case['token']}`") + f" `{case['id']}: {case['token']}`") @idtest.command(name="setreason", aliases=["addreason"]) @checks.is_owner() @@ -273,7 +273,7 @@ async def idt_list(self, ctx): o = "" ml = len(max(suite, key=len)) for c, kv in enumerate(sorted(suite.items())): - o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)}: {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): @@ -296,7 +296,7 @@ async def norf_list(self, ctx, fluff, inclusive): o = "" for c, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): if inclusive or case['fluff'] == fluff: - o += f"{str(c).rjust(3)}. {case['token'].ljust(10)} - {str(case['id']).ljust(4)}" \ + o += f"{str(c).rjust(3)}. {case['token'].ljust(10)}: {str(case['id']).ljust(4)}" \ f"\t{'fluff' if case['fluff'] else 'name '}\t{case.get('reason', '')}\n" if not o: await ctx.send("There are no test cases.") @@ -313,7 +313,7 @@ async def idt_listnoreason(self, ctx): ml = len(max(suite, key=len)) for c, kv in enumerate(sorted(suite.items())): if not kv[1].get('reason'): - o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)} - {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" + o += f"{str(c).rjust(3)}. {kv[0].ljust(ml)}: {str(kv[1]['result']).ljust(4)}\t{kv[1].get('reason') or ''}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): @@ -336,7 +336,7 @@ async def norf_listnoreason(self, ctx, fluff, inclusive): o = "" for c, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): if inclusive or case['fluff'] == fluff and not case['reason']: - o += f"{str(c).rjust(3)}. {case['token'].ljust(10)} - {str(case['id']).ljust(4)}" \ + o += f"{str(c).rjust(3)}. {case['token'].ljust(10)}: {str(case['id']).ljust(4)}" \ f"\t{'fluff' if case['fluff'] else 'name '}\t{case.get('reason', '')}\n" if not o: await ctx.send("There are no test cases.") @@ -353,7 +353,7 @@ async def idt_listrecent(self, ctx, count: int = 0): ml = len(max(suite, key=len)) for c, kv_tuple in enumerate(sorted(suite.items(), key=lambda kv: kv[1].get('ts', 0), reverse=True)[:count]): key, val = kv_tuple - o += f"{key.ljust(ml)} - {str(val['result']).ljust(4)}\t{val.get('reason') or ''}\n" + o += f"{key.ljust(ml)}: {str(val['result']).ljust(4)}\t{val.get('reason') or ''}\n" if not o: await ctx.send("There are no test cases.") for page in pagify(o): @@ -383,7 +383,7 @@ async def idt_run(self, ctx): if mid != r['result']: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' q = '"' + q + '"' - o += f"{i}. {q.ljust(ml)} - {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" + o += f"{i}. {q.ljust(ml)}: {rcircle} Ex: {r['result']}, Ac: {mid}{reason}\n" else: c += 1 if c != len(suite): @@ -422,7 +422,7 @@ async def norf_run(self, ctx): if (case['fluff'] and not fluff) or (not case['fluff'] and not name): q = '"{}"'.format(case['token']) - o += f"{i}. {str(case['id']).ljust(4)} {q.ljust(10)} - " \ + o += f"{i}. {str(case['id']).ljust(4)} {q.ljust(10)}: " \ f"{ycircle if name or fluff else rcircle} " \ f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: @@ -453,7 +453,7 @@ async def idt_runall(self, ctx): if mid != qsuite[q]['result']: reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' qq = '"' + q + '"' - qo += (f"{str(c).rjust(4)}. {qq.ljust(ml)} - {rcircle} " + qo += (f"{str(c).rjust(4)}. {qq.ljust(ml)}: {rcircle} " f"Ex: {qsuite[q]['result']}, Ac: {mid}{reason}\n") else: qc += 1 @@ -468,7 +468,7 @@ async def idt_runall(self, ctx): if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) - fo += f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)} - " \ + fo += f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)}: " \ f"{ycircle if name or fluff else rcircle} " \ f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token\n" else: @@ -504,7 +504,7 @@ async def run_tests(self) -> List[str]: if mid != qsuite[q]['result']: reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' qq = '"' + q + '"' - failures.append(f"{str(c).rjust(4)}. {qq.ljust(ml)} - {rcircle} " + failures.append(f"{str(c).rjust(4)}. {qq.ljust(ml)}: {rcircle} " f"Ex: {qsuite[q]['result']}, Ac: {mid}{reason}") fsuite = await self.config.fluff_suite() @@ -514,7 +514,7 @@ async def run_tests(self) -> List[str]: if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) - failures.append(f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)} - " + failures.append(f"{str(c).rjust(4)}. {str(v['id']).ljust(4)} {q.ljust(ml - 5)}: " f"{ycircle if name or fluff else rcircle} " f"Not {'Fluff' if name else 'Name' if fluff else 'A'} Token") return failures From bf31d9fc07c4ac0cd642bf742363acbf2daf5380 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Apr 2021 15:08:36 -0700 Subject: [PATCH 0732/1220] Add number-of-awakening moddedfiers (#992) --- dadguide/database_context.py | 4 +- dadguide/find_monster.py | 111 +++++++++++++------------- dadguide/find_monster_tokens.py | 80 +++++++++++++++++++ dadguide/models/awakening_model.py | 10 ++- dadguide/models/awoken_skill_model.py | 8 ++ dadguide/token_mappings.py | 28 ++++++- 6 files changed, 181 insertions(+), 60 deletions(-) create mode 100644 dadguide/find_monster_tokens.py diff --git a/dadguide/database_context.py b/dadguide/database_context.py index bb01897c..b508b45c 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -1,4 +1,4 @@ -from typing import Generator, List +from typing import Generator, List, Optional from .database_manager import DadguideDatabase from .models.awoken_skill_model import AwokenSkillModel @@ -22,6 +22,8 @@ def __init__(self, database: DadguideDatabase, graph: MonsterGraph): self.database = database self.graph = graph + self.awoken_skill_map = {awsk.awoken_skill_id: awsk for awsk in self.get_all_awoken_skills()} + def get_awoken_skill_ids(self): SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' return [r.awoken_skill_id for r in diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 1ae7ab21..83e367bd 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -6,6 +6,7 @@ from Levenshtein import jaro_winkler from tsutils import rmdiacritics +from dadguide.find_monster_tokens import Token, SPECIAL_TOKEN_TYPES from dadguide.models.monster_model import MonsterModel SERIES_TYPE_PRIORITY = { @@ -19,25 +20,6 @@ } -class Token: - def __init__(self, value, *, negated=False, exact=False): - self.value = value - self.negated = negated - self.exact = exact - - def __eq__(self, other): - if isinstance(other, Token): - return self.value == other.value - elif isinstance(other, str): - return self.value == other - - def __hash__(self): - return hash(self.value) ^ (self.negated << 5) ^ (self.exact << 6) - - def __repr__(self): - return ("-" if self.negated else "") + (repr(self.value) if self.exact else self.value) - - class MonsterMatch: def __init__(self, score=0, name=None, mod=None): self.score = score @@ -61,7 +43,7 @@ def __init__(self, dgcog, flags: Mapping[str, Any]): self.dgcog = dgcog self.flags = flags - def merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: + def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: result = [] skip = 0 multi_word_tokens_sorted = sorted(self.dgcog.index.multi_word_tokens, @@ -87,6 +69,9 @@ def merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: return result def _monster_has_modifier(self, monster: MonsterModel, token: Token, matches: MatchMap) -> bool: + if not token.matches(monster): + return False + if len(token.value) < 6: if token.value in self.dgcog.index.modifiers[monster]: matches[monster].mod.add((token.value, token.value)) @@ -106,9 +91,13 @@ def _string_to_token(self, value: str) -> Token: value = value.lstrip('-') if (exact := bool(re.fullmatch(r'".+"', value))): value = value[1:-1] + if re.fullmatch(r'.+-.+', value): + for special in SPECIAL_TOKEN_TYPES: + if re.fullmatch(special.RE_MATCH, value): + return special(value, negated=negated, exact=exact, database=self.dgcog.database) return Token(value, negated=negated, exact=exact) - def interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: + def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: modifiers = [] name = set() longmods = [p for p in self.dgcog.index.all_modifiers if len(p) > 8] @@ -120,6 +109,7 @@ def interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[T if any(self.calc_ratio_modifier(m, token.value.split('-')[0], .1) > self.MODIFIER_JW_DISTANCE for m in self.dgcog.index.suffixes): + # TODO: Store this as a list of regexes. Don't split on '-' anymore. modifiers.append(token) else: if i != 0: @@ -148,24 +138,24 @@ def interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[T # Allow modifiers to match as name tokens if they're alone (Fix for hel and cloud) if not name and modifiers and lastmodpos: - if self.dgcog.index.manual[modifiers[-1]]: + if self.dgcog.index.manual[modifiers[-1].value]: name.add(modifiers[-1]) modifiers = modifiers[:-1] return set(modifiers), name - def process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) -> Optional[Set[MonsterModel]]: + def _process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) -> Optional[Set[MonsterModel]]: matched_mons = None for name_token in name_query_tokens: if name_token.negated: - invalid = self.get_valid_monsters_from_name_token(name_token, matches, mult=-10) + invalid = self._get_valid_monsters_from_name_token(name_token, matches, mult=-10) if matched_mons is not None: matched_mons.difference_update(invalid) else: matched_mons = set(self.dgcog.database.get_all_monsters()).difference(invalid) else: - valid = self.get_valid_monsters_from_name_token(name_token, matches) + valid = self._get_valid_monsters_from_name_token(name_token, matches) if matched_mons is not None: matched_mons.intersection_update(valid) else: @@ -173,8 +163,8 @@ def process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) return matched_mons - def get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, - mult: Union[int, float] = 1) -> Set[MonsterModel]: + def _get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, + mult: Union[int, float] = 1) -> Set[MonsterModel]: valid_monsters = set() all_monsters_name_tokens_scores = {nt: self.calc_ratio_name(token, nt) for nt in self.dgcog.index.all_name_tokens} @@ -184,25 +174,25 @@ def get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, for match in matched_tokens: score = all_monsters_name_tokens_scores[match] for matched_monster in self.dgcog.index.manual[match]: - if matched_monster not in valid_monsters: + if matched_monster not in valid_monsters and token.matches(matched_monster): matches[matched_monster].name.add((token.value, match, '(manual)')) matches[matched_monster].score += (score + .001) * mult valid_monsters.add(matched_monster) for matched_monster in self.dgcog.index.name_tokens[match]: - if matched_monster not in valid_monsters: + if matched_monster not in valid_monsters and token.matches(matched_monster): matches[matched_monster].name.add((token.value, match, '(name)')) matches[matched_monster].score += score * mult valid_monsters.add(matched_monster) for matched_monster in self.dgcog.index.fluff_tokens[match]: - if matched_monster not in valid_monsters: + if matched_monster not in valid_monsters and token.matches(matched_monster): matches[matched_monster].name.add((token.value, match, '(fluff)')) matches[matched_monster].score += score * mult / 2 valid_monsters.add(matched_monster) return valid_monsters - def process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[MonsterModel], - matches: MatchMap) -> Set[MonsterModel]: + def _process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[MonsterModel], + matches: MatchMap) -> Set[MonsterModel]: for mod_token in mod_tokens: potential_evos = {m for m in potential_evos if self._monster_has_modifier(m, mod_token, matches) ^ mod_token.negated} @@ -212,6 +202,10 @@ def process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[MonsterM def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = None, matches: MatchMap = None) -> Tuple[Any, ...]: + """Get the priority tuple for a monster. + + This is a comparable tuple that's used to sort how well a monster matches a query. + """ if matches is None: matches = defaultdict(MonsterMatch) if tokenized_query is None: @@ -234,16 +228,7 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = monster.rarity, monster.monster_no_na) - def get_most_eligable_monster(self, monsters: Iterable[MonsterModel], tokenized_query: List[str] = None, - matches: MatchMap = None) -> MonsterModel: - if matches is None: - matches = defaultdict(MonsterMatch) - if tokenized_query is None: - tokenized_query = [] - - return max(monsters, key=lambda m: self.get_priority_tuple(m, tokenized_query, matches)) - - def get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: + def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: monster_evos = set() for monster in sorted(matched_mons, key=lambda m: matches[m].score, reverse=True): for evo in self.dgcog.database.graph.get_alt_monsters(monster): @@ -254,10 +239,20 @@ def get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) - matches[evo].score = matches[monster].score - .003 return monster_evos + + def get_most_eligable_monster(self, monsters: Iterable[MonsterModel], tokenized_query: List[str] = None, + matches: MatchMap = None) -> MonsterModel: + """Get the most eligable monster from a list of monsters and debug info from a query""" + if matches is None: + matches = defaultdict(MonsterMatch) + if tokenized_query is None: + tokenized_query = [] - async def find_monster_search(self, tokenized_query: List[str]) -> \ + return max(monsters, key=lambda m: self.get_priority_tuple(m, tokenized_query, matches)) + + async def _find_monster_search(self, tokenized_query: List[str]) -> \ Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel]]: - mod_tokens, name_query_tokens = self.interpret_query(tokenized_query) + mod_tokens, name_query_tokens = self._interpret_query(tokenized_query) name_query_tokens.difference_update({'|'}) @@ -268,18 +263,18 @@ async def find_monster_search(self, tokenized_query: List[str]) -> \ matches = defaultdict(MonsterMatch) if name_query_tokens: - matched_mons = self.process_name_tokens(name_query_tokens, matches) + matched_mons = self._process_name_tokens(name_query_tokens, matches) if not matched_mons: # No monsters match the given name tokens return None, {}, set() - matched_mons = self.get_monster_evos(matched_mons, matches) + matched_mons = self._get_monster_evos(matched_mons, matches) else: # There are no name tokens in the query matched_mons = {*self.dgcog.database.get_all_monsters()} monster_score = defaultdict(int) # Expand search to the evo tree - matched_mons = self.process_modifiers(mod_tokens, matched_mons, matches) + matched_mons = self._process_modifiers(mod_tokens, matched_mons, matches) if not matched_mons: # no modifiers match any monster in the evo tree return None, {}, set() @@ -291,15 +286,19 @@ async def find_monster_search(self, tokenized_query: List[str]) -> \ async def find_monster_debug(self, query: str) -> \ Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel], int]: + """Get debug info from a search. + + This gives info that isn't necessary for non-debug functions. Consider using + findmonster or findmonsters instead.""" await self.dgcog.wait_until_ready() query = rmdiacritics(query).lower().replace(",", "") tokenized_query = query.split() - mw_tokenized_query = self.merge_multi_word_tokens(tokenized_query) + mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) best_monster, matches_dict, valid_monsters = max( - await self.find_monster_search(tokenized_query), - await self.find_monster_search(mw_tokenized_query) + await self._find_monster_search(tokenized_query), + await self._find_monster_search(mw_tokenized_query) if tokenized_query != mw_tokenized_query else (None, {}, set()), key=lambda t: t[1].get(t[0], MonsterMatch()).score @@ -311,26 +310,30 @@ async def find_monster_debug(self, query: str) -> \ return best_monster, matches_dict, valid_monsters, 0 async def find_monster(self, query: str) -> Optional[MonsterModel]: + """Get the best matching monster for a query. Returns None if no eligable monsters exist.""" matched_monster, _, _, _ = await self.find_monster_debug(query) return matched_monster async def find_monsters(self, query: str) -> List[MonsterModel]: + """Get a list of monsters sorted by how well they match""" _, matches, monsters, _ = await self.find_monster_debug(query) return sorted(monsters, key=lambda m: self.get_priority_tuple(m, matches=matches), reverse=True) - def calc_ratio_modifier(self, s1: Union[Token, str], s2: str, dist: float = .05) -> float: + def calc_ratio_modifier(self, s1: Union[Token, str], s2: str, prefix_weight: float = .05) -> float: + """Calculate the modifier distance between two tokens""" if isinstance(s1, Token): if s1.exact: return 1.0 if s1.value == s2 else 0.0 s1 = s1.value - return jaro_winkler(s1, s2, dist) + return jaro_winkler(s1, s2, prefix_weight) - def calc_ratio_name(self, token: Union[Token, str], full_word: str, factor: int = .05) -> float: + def calc_ratio_name(self, token: Union[Token, str], full_word: str, prefix_weight: float = .05) -> float: + """Calculate the name distance between two tokens""" string = token.value if isinstance(token, Token) else token mw = self.dgcog.index.mwt_to_len[full_word] != 1 - jw = jaro_winkler(string, full_word, factor) + jw = jaro_winkler(string, full_word, prefix_weight) if isinstance(token, Token) and token.exact and string != full_word: return 0.0 diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py new file mode 100644 index 00000000..196798ba --- /dev/null +++ b/dadguide/find_monster_tokens.py @@ -0,0 +1,80 @@ +import re + +from dadguide.models.monster_model import MonsterModel +from dadguide.token_mappings import AWAKENING_TOKENS, inverse_map, AWOKEN_MAP, AWAKENING_EQUIVALENCES + + +def regexlist(tokens): + return '(?:' + '|'.join(re.escape(t) for t in tokens) + ")" + + +class Token: + def __init__(self, value, *, negated=False, exact=False): + self.value = value + self.negated = negated + self.exact = exact + + def matches(self, other: MonsterModel) -> bool: + return True + + def __eq__(self, other): + if isinstance(other, Token): + return self.__dict__ == other.__dict__ + elif isinstance(other, str): + return self.value == other + + def __hash__(self): + return hash(self.value) + + def __repr__(self): + return ("-" if self.negated else "") + (repr(self.value) if self.exact else self.value) + + +class SpecialToken(Token): + RE_MATCH = r"" + + def __init__(self, value, *, negated=False, exact=False, database): + self.database = database + super().__init__(value, negated=negated, exact=exact) + + def matches(self, other: MonsterModel) -> bool: + return False + + def __repr__(self): + return f"{self.__class__.__name__}(token={super().__repr__()})" + + +class MultipleAwakeningToken(SpecialToken): + RE_MATCH = rf"(\d+)-(sa-)?-?({regexlist(AWAKENING_TOKENS)})" + + def __init__(self, value, *, negated=False, exact=False, database): + count, sa, value = re.fullmatch(self.RE_MATCH, value).groups() + self.count = int(count) + self.sa = bool(sa) + super().__init__(value, negated=negated, exact=exact, database=database) + + def matches(self, other): + awlist = other.awakenings + if not self.sa: + awlist = awlist[:-other.superawakening_count] + + c = 0 + for aw in inverse_map(AWOKEN_MAP)[self.value]: + aw = self.database.awoken_skill_map[aw.value] + for maw in awlist: + if maw == aw: + c += 1 + elif (eq := AWAKENING_EQUIVALENCES.get(maw.awoken_skill_id)) and eq[1] == aw.awoken_skill_id: + c += eq[0] + if c >= self.count: + return True + return False + + def __repr__(self): + return super().__repr__() + f"" + + + +SPECIAL_TOKEN_TYPES = [ + MultipleAwakeningToken, +] diff --git a/dadguide/models/awakening_model.py b/dadguide/models/awakening_model.py index bed8ee48..d185fc15 100644 --- a/dadguide/models/awakening_model.py +++ b/dadguide/models/awakening_model.py @@ -7,7 +7,7 @@ class AwakeningModel(BaseModel): This class represents an awakening belonging to a monster, in contrast to AwokenSkillModel, which represents an "abstract" awoken skill. """ - def __init__(self, awoken_skill_model: AwokenSkillModel = None, **kwargs): + def __init__(self, awoken_skill_model: AwokenSkillModel, **kwargs): self.awakening_id = kwargs['awakening_id'] self.monster_id = kwargs['monster_id'] self.awoken_skill_id = kwargs['awoken_skill_id'] @@ -20,3 +20,11 @@ def to_dict(self): return { 'awakening_id': self.awakening_id, } + + def __eq__(self, other): + if isinstance(other, AwakeningModel): + return self.awakening_id == other.awakening_id + elif isinstance(other, AwokenSkillModel): + return self.awoken_skill == other + else: + return False diff --git a/dadguide/models/awoken_skill_model.py b/dadguide/models/awoken_skill_model.py index 30f6fa63..94557d8d 100644 --- a/dadguide/models/awoken_skill_model.py +++ b/dadguide/models/awoken_skill_model.py @@ -21,3 +21,11 @@ def to_dict(self): 'awoken_skill_id': self.awoken_skill_id, 'name_en': self.name_en, } + + def __eq__(self, other): + if hasattr(other, "awoken_skill"): + return self == other.awoken_skill + elif hasattr(other, "awoken_skill_id"): + return self.awoken_skill_id == other.awoken_skill_id + else: + return False diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index fcd4d101..efe6cbbe 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -1,7 +1,19 @@ +from collections import defaultdict from enum import Enum +from typing import Mapping, Tuple, TypeVar from .models.enum_types import Attribute, MonsterType +K = TypeVar('K') +V = TypeVar('V') + +def inverse_map(token_map: Mapping[K, Tuple[V]]) -> Mapping[V, Tuple[K]]: + ret = defaultdict(tuple) + for k, vs in token_map.items(): + for v in vs: + ret[v] += (k,) + return ret + COLOR_MAP = { Attribute.Fire: ('r', 'red', 'fire'), Attribute.Water: ('b', 'blue', 'water'), @@ -165,7 +177,7 @@ class Awakenings(Enum): Awakenings.REDUCELIGHT: ('elresl', 'elres'), Awakenings.REDUCEDARK: ('elresd', 'elres'), Awakenings.AUTOHEAL: ('autoheal',), - Awakenings.BINDRES: ('unbindable',), + Awakenings.BINDRES: ('unbindable', 'bindres'), Awakenings.BLINDRES: ('resb',), Awakenings.JAMMERRES: ('resj',), Awakenings.POISONRES: ('resp',), @@ -207,11 +219,12 @@ class Awakenings(Enum): Awakenings.EQUIP: ('equip', 'assist', 'eq'), Awakenings.SUPERFUA: ('sfua',), Awakenings.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), - Awakenings.UNBINDABLE: ('unbindable',), - Awakenings.EXTMOVEPLUS: ('te+', 'finger+'), + Awakenings.UNBINDABLE: ('unbindable', '' + ''), + Awakenings.EXTMOVEPLUS: ('te+', 'te', 'finger+', 'finger'), Awakenings.CLOUDRESIST: ('cloudres', 'cloud'), Awakenings.TAPERESIST: ('taperes', 'tape'), - Awakenings.SKILLBOOSTPLUS: ('sb+',), + Awakenings.SKILLBOOSTPLUS: ('sb+', 'sb'), Awakenings.HP80ORMORE: ('>80', 'highhp'), Awakenings.HP50ORLESS: ('<50', 'lowhp'), Awakenings.ELSHIELD: ('elshield', 'elh', 'hel'), @@ -335,3 +348,10 @@ class MiscModifiers(Enum): ID1_SUPPORTED = {'hw', 'h', 'x', 'ny', 'gh', 'v', 'np', 'ma', 'a', 'r', 'rr', 'rg', 'rb', 'rl', 'rd', 'rx', 'b', 'br', 'bg', 'bb', 'bl', 'bd', 'bx', 'g', 'gr', 'gg', 'gb', 'gl', 'gd', 'gx', 'l', 'lr', 'lg', 'lb', 'll', 'ld', 'lx', 'd', 'dr', 'dg', 'db', 'dl', 'dd', 'dx', 'x', 'xr', 'xg', 'xb', 'xl', 'xd', 'xx'} + +# This probably doesn't belong in here +AWAKENING_EQUIVALENCES = { + 52: (2, 10), # 1 unbindable = 2 bind resistance + 53: (2, 19), # 1 te+ = 2 te + 56: (2, 21), # 1 sb+ = 2 sb +} From dae43a30148c2d0adadfc35d67c374a0ce4d0ce7 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Fri, 30 Apr 2021 18:44:26 -0400 Subject: [PATCH 0733/1220] Add additional info to ^buttoninfo (#993) --- padinfo/core/button_info.py | 54 +++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 190b8726..63984f59 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -1,3 +1,5 @@ +from collections import namedtuple + LIMIT_BREAK_LEVEL = 110 INFO_STRING = """[{}] {} @@ -12,12 +14,34 @@ Base: {} Subattr: {} Total: {} + +-------------- +{} +{} """ +TEAM_BUTTON_FORMAT = "As {} base ({}x {}): Contributes {}" +CARD_BUTTON_FORMAT = "As {} base ({}x): Deals {}" + +MonsterStat = namedtuple('MonsterStat', 'name id mult att') + +TEAM_BUTTONS = [ + MonsterStat("Nergi Hunter", 4172, 40, [2, 4]), + MonsterStat("Oversoul", 5273, 50, [0, 1, 2, 3, 4]), + MonsterStat("Ryuno Ume", 5252, 80, [2, 4]) +] +CARD_BUTTONS = [ + MonsterStat("Satan", 4286, 300, []), + MonsterStat("Brachydios", 4134, 350, []), + MonsterStat("Rajang", 5527, 550, []), + MonsterStat("Balrog", 5108, 450, []) +] + +COLORS = ["R", "B", "G", "L", "D"] + class ButtonInfo: def get_info(self, dgcog, monster_model): - """ Usage: ^buttoninfo Vajrayasaka @@ -69,10 +93,36 @@ def _get_sub_attr_multiplier(self, monster_model): return 1 / 3 def to_string(self, monster, info): + card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, info, monster) + team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, info, monster) return INFO_STRING.format(monster.monster_id, monster.name_en, info.main_damage, info.sub_damage, info.total_damage, info.main_damage_with_atk_latent, info.sub_damage_with_atk_latent, - info.total_damage_with_atk_latent) + info.total_damage_with_atk_latent, card_btn_str, team_btn_str) + + def _get_card_btn_damage(self, card_buttons, info, monster): + lines = [] + card_buttons.sort(key=lambda x: x.mult) + for card in card_buttons: + lines.append(CARD_BUTTON_FORMAT.format(card.name, card.mult, + (info.main_damage_with_atk_latent * card.mult))) + return "\n".join(lines) + + def _get_team_btn_damage(self, team_buttons, info, monster): + # TODO: calculate with oncolor assist damage and ATK+ eq (Oversoul) + lines = [] + team_buttons.sort(key=lambda x: x.mult) + for card in team_buttons: + total_dmg = 0 + if(monster.attr1.value in card.att): + total_dmg += info.main_damage_with_atk_latent + if(monster.attr2.value in card.att): + total_dmg += info.sub_damage_with_atk_latent + colors_str = "" + for i in card.att: + colors_str += COLORS[i] + lines.append(TEAM_BUTTON_FORMAT.format(card.name, card.mult, colors_str, (total_dmg * card.mult))) + return "\n".join(lines) class ButtonInfoResult: From 3ebe89cf86a00d39bf22a61419faeb749a64dabd Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Apr 2021 16:25:58 -0700 Subject: [PATCH 0734/1220] Fix counting for subset awakenings (#996) --- dadguide/find_monster_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 196798ba..19095a68 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -65,7 +65,7 @@ def matches(self, other): if maw == aw: c += 1 elif (eq := AWAKENING_EQUIVALENCES.get(maw.awoken_skill_id)) and eq[1] == aw.awoken_skill_id: - c += eq[0] + c += eq[0] - 1 # Remove one for the plus version that was matched. if c >= self.count: return True return False From 3baa5425a3675727ec39b77ad549c3aae67bce8d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 30 Apr 2021 18:32:43 -0500 Subject: [PATCH 0735/1220] Improve subst awakening counting comment (#997) Also fixed whitespace in token_mappings.py file --- dadguide/find_monster_tokens.py | 9 ++++++++- dadguide/token_mappings.py | 8 +++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 19095a68..3c849409 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -65,7 +65,14 @@ def matches(self, other): if maw == aw: c += 1 elif (eq := AWAKENING_EQUIVALENCES.get(maw.awoken_skill_id)) and eq[1] == aw.awoken_skill_id: - c += eq[0] - 1 # Remove one for the plus version that was matched. + # Hack - if we are matching a plus version of an awakening here, then we're adding + # the correct number of awakenings FOR THE PLUS VERSION here. However, we will + # have also, on another pass, matched the token against the base version of + # the awakening, adding 1 too many to the total. So subtract one from the total + # here to compensate. + # A better approach would be to identify the case when we match the base awakening + # and not add that in the first place, but doing so is difficult and this works. + c += eq[0] - 1 if c >= self.count: return True return False diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index efe6cbbe..0277ddbc 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -7,6 +7,7 @@ K = TypeVar('K') V = TypeVar('V') + def inverse_map(token_map: Mapping[K, Tuple[V]]) -> Mapping[V, Tuple[K]]: ret = defaultdict(tuple) for k, vs in token_map.items(): @@ -14,6 +15,7 @@ def inverse_map(token_map: Mapping[K, Tuple[V]]) -> Mapping[V, Tuple[K]]: ret[v] += (k,) return ret + COLOR_MAP = { Attribute.Fire: ('r', 'red', 'fire'), Attribute.Water: ('b', 'blue', 'water'), @@ -351,7 +353,7 @@ class MiscModifiers(Enum): # This probably doesn't belong in here AWAKENING_EQUIVALENCES = { - 52: (2, 10), # 1 unbindable = 2 bind resistance - 53: (2, 19), # 1 te+ = 2 te - 56: (2, 21), # 1 sb+ = 2 sb + 52: (2, 10), # 1 unbindable = 2 bind resistance + 53: (2, 19), # 1 te+ = 2 te + 56: (2, 21), # 1 sb+ = 2 sb } From c2086d187c21731476b1c28be94a4b66e58c10fd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 30 Apr 2021 18:46:38 -0500 Subject: [PATCH 0736/1220] Fix line-length note (120 char) (#998) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4330866..62e18737 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Red v3 Cogs developed originally for Miru Bot, now for Tsubaki Bot. -Code should be PEP 8 formatted with a 100 character line limit. +Code should be PEP 8 formatted with a 120 character line limit. # Setting up your own Tsubaki instance for contributing code From 820f3c22ceef1c545f38f1819a19f9ee5825eb22 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Apr 2021 18:24:33 -0700 Subject: [PATCH 0737/1220] Improve modded awakening matching logic (#999) * Only allow one SA to contribute per clause (closes #995) * Replace logic of the hack from #996, #997 --- dadguide/find_monster_tokens.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 3c849409..249d43bc 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -54,27 +54,24 @@ def __init__(self, value, *, negated=False, exact=False, database): super().__init__(value, negated=negated, exact=exact, database=database) def matches(self, other): - awlist = other.awakenings - if not self.sa: - awlist = awlist[:-other.superawakening_count] - c = 0 - for aw in inverse_map(AWOKEN_MAP)[self.value]: - aw = self.database.awoken_skill_map[aw.value] - for maw in awlist: - if maw == aw: + for idx, maw in enumerate(other.awakenings): + matched = True + for aw in inverse_map(AWOKEN_MAP)[self.value]: + aw = self.database.awoken_skill_map[aw.value] + if (eq := AWAKENING_EQUIVALENCES.get(maw.awoken_skill_id)) and eq[1] == aw.awoken_skill_id: + c += eq[0] + break + elif maw == aw: c += 1 - elif (eq := AWAKENING_EQUIVALENCES.get(maw.awoken_skill_id)) and eq[1] == aw.awoken_skill_id: - # Hack - if we are matching a plus version of an awakening here, then we're adding - # the correct number of awakenings FOR THE PLUS VERSION here. However, we will - # have also, on another pass, matched the token against the base version of - # the awakening, adding 1 too many to the total. So subtract one from the total - # here to compensate. - # A better approach would be to identify the case when we match the base awakening - # and not add that in the first place, but doing so is difficult and this works. - c += eq[0] - 1 + break + else: + matched = False + if c >= self.count: return True + if idx > other.superawakening_count and (matched or not self.sa): + return False return False def __repr__(self): From 5e8771880b8f1248011b6020076f0409a2d996bf Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Apr 2021 19:26:04 -0700 Subject: [PATCH 0738/1220] Tsubaki! (#1000) Tsubaki!!!!!!!!!! --- padinfo/view/base.py | 14 ++++++++++++++ padinfo/view/components/monster/header.py | 12 ++++++++++++ padinfo/view/evos.py | 11 +++++++---- padinfo/view/id.py | 11 +++++++---- padinfo/view/materials.py | 11 +++++++---- padinfo/view/otherinfo.py | 11 +++++++---- padinfo/view/pantheon.py | 11 +++++++---- padinfo/view/pic.py | 11 +++++++---- 8 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 padinfo/view/base.py diff --git a/padinfo/view/base.py b/padinfo/view/base.py new file mode 100644 index 00000000..a808291e --- /dev/null +++ b/padinfo/view/base.py @@ -0,0 +1,14 @@ +from abc import ABCMeta, abstractmethod + +from discordmenu.embed.view import EmbedView + +from padinfo.view.components.view_state_base_id import ViewStateBaseId + +class BaseIdView(metaclass=ABCMeta): + VIEW_TYPE: str + TSUBAKI = 2141 + + @classmethod + @abstractmethod + def embed(cls, state: ViewStateBaseId) -> EmbedView: + ... diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 08742844..b9655fe6 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -44,6 +44,18 @@ def long_v2(m: "MonsterModel", link=False): msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, MonsterHeader.jp_suffix(m)) return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) + @staticmethod + def long_maybe_tsubaki(m: "MonsterModel", is_tsubaki): + """Returns long_v2 as well as an `!` if the monster is Tsubaki + + To celebrate 1000 issues/PRs in our main Tsubaki repo, we added this easter egg! Yay! + """ + return Text('[{}] {}{}{}'.format( + m.monster_no_na, + m.name_en, + '!' if is_tsubaki else '', + MonsterHeader.jp_suffix(m))) + @staticmethod def short_with_emoji(m: "MonsterModel", link=True, prefix=None): msg = f"{m.monster_no_na} - {m.name_en}" diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index e5a5d2b7..485c5a11 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -6,6 +6,7 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx +from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader @@ -76,7 +77,7 @@ async def query(dgcog, monster): return alt_versions, gem_versions -class EvosView: +class EvosView(BaseIdView): VIEW_TYPE = 'Evos' @staticmethod @@ -88,8 +89,8 @@ def _evo_lines(monsters, current_monster): for ae in sorted(monsters, key=lambda x: int(x.monster_id)) ] - @staticmethod - def embed(state: EvosViewState): + @classmethod + def embed(cls, state: EvosViewState): fields = [ EmbedField( ("{} evolution" if len(state.alt_versions) == 1 else "{} evolutions").format(len(state.alt_versions)), @@ -105,7 +106,9 @@ def embed(state: EvosViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(state.monster).to_markdown(), + title=MonsterHeader.long_maybe_tsubaki(state.monster, + "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + ).to_markdown(), url=puzzledragonx(state.monster)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), embed_footer=pad_info_footer_with_state(state), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 42e9cac6..2709f313 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -9,6 +9,7 @@ from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createMultiplierText +from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader @@ -127,7 +128,7 @@ def evos_embed_field(state: ViewStateBaseId): ) -class IdView: +class IdView(BaseIdView): VIEW_TYPE = 'Id' @staticmethod @@ -244,8 +245,8 @@ def leader_skill_header(m: "MonsterModel"): delimiter=' ' ) - @staticmethod - def embed(state: IdViewState): + @classmethod + def embed(cls, state: IdViewState): m = state.monster fields = [ EmbedField( @@ -279,7 +280,9 @@ def embed(state: IdViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(m).to_markdown(), + title=MonsterHeader.long_maybe_tsubaki(m, + "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + ).to_markdown(), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=pad_info_footer_with_state(state), diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 77452f3d..b4cbded3 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -8,6 +8,7 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill from padinfo.common.external_links import puzzledragonx +from padinfo.view.base import BaseIdView from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -144,16 +145,18 @@ def skillup_field(mons, sec, link): Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:MAX_MONS_TO_SHOW]), text, text2)) -class MaterialsView: +class MaterialsView(BaseIdView): VIEW_TYPE = 'Materials' - @staticmethod - def embed(state: MaterialsViewState): + @classmethod + def embed(cls, state: MaterialsViewState): # m: "MonsterModel", color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(state.monster).to_markdown(), + title=MonsterHeader.long_maybe_tsubaki(state.monster, + "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + ).to_markdown(), url=puzzledragonx(state.monster) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 35e0fb60..73e07a8b 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -8,6 +8,7 @@ from redbot.core.utils.chat_formatting import box from padinfo.common.external_links import puzzledragonx +from padinfo.view.base import BaseIdView from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -42,16 +43,18 @@ def statsbox(m): return box(tbl.get_string()) -class OtherInfoView: +class OtherInfoView(BaseIdView): VIEW_TYPE = 'OtherInfo' - @staticmethod - def embed(state: OtherInfoViewState): + @classmethod + def embed(cls, state: OtherInfoViewState): m: "MonsterModel" = state.monster return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(m).to_markdown(), + title=MonsterHeader.long_maybe_tsubaki(state.monster, + "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + ).to_markdown(), url=puzzledragonx(m)), embed_footer=pad_info_footer_with_state(state), embed_fields=[ diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 440d7c3f..ce3dd9f0 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -8,6 +8,7 @@ from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ get_rarity_emoji from padinfo.common.external_links import puzzledragonx +from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader @@ -155,11 +156,11 @@ def _pantheon_lines(monsters, base_monster): ] -class PantheonView: +class PantheonView(BaseIdView): VIEW_TYPE = 'Pantheon' - @staticmethod - def embed(state: PantheonViewState): + @classmethod + def embed(cls, state: PantheonViewState): fields = [EmbedField( 'Pantheon: {}'.format(state.series_name), Box(*_pantheon_lines(state.pantheon_list, state.base_monster)) @@ -169,7 +170,9 @@ def embed(state: PantheonViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(state.monster).to_markdown(), + title=MonsterHeader.long_maybe_tsubaki(state.monster, + "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + ).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=pad_info_footer_with_state(state), embed_fields=fields, diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 8a741819..e86d5f7c 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -4,6 +4,7 @@ from discordmenu.embed.view import EmbedView from padinfo.common.external_links import puzzledragonx +from padinfo.view.base import BaseIdView from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -21,11 +22,11 @@ def serialize(self): return ret -class PicView: +class PicView(BaseIdView): VIEW_TYPE = 'Pic' - @staticmethod - def embed(state: PicViewState): + @classmethod + def embed(cls, state: PicViewState): url = MonsterImage.picture(state.monster) animated = state.monster.has_animation fields = [EmbedField( @@ -53,7 +54,9 @@ def embed(state: PicViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(state.monster).to_markdown(), + title=MonsterHeader.long_maybe_tsubaki(state.monster, + "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + ).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=pad_info_footer_with_state(state), embed_fields=fields, From f1979db39e373000a39cbc4d1dc4d2e9b383278b Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Apr 2021 21:44:02 -0700 Subject: [PATCH 0739/1220] Fix which awakenings count as SAs (#1002) --- dadguide/find_monster.py | 2 +- dadguide/find_monster_tokens.py | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 83e367bd..0196c628 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -138,7 +138,7 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ # Allow modifiers to match as name tokens if they're alone (Fix for hel and cloud) if not name and modifiers and lastmodpos: - if self.dgcog.index.manual[modifiers[-1].value]: + if self.dgcog.index.manual[modifiers[-1].full_value]: name.add(modifiers[-1]) modifiers = modifiers[:-1] diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 249d43bc..21d2eba8 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -10,7 +10,7 @@ def regexlist(tokens): class Token: def __init__(self, value, *, negated=False, exact=False): - self.value = value + self.value = self.full_value = value self.negated = negated self.exact = exact @@ -27,8 +27,8 @@ def __hash__(self): return hash(self.value) def __repr__(self): - return ("-" if self.negated else "") + (repr(self.value) if self.exact else self.value) - + token = ("-" if self.negated else "") + (repr(self.full_value) if self.exact else self.full_value) + return f"{self.__class__.__name__}<{token}>" class SpecialToken(Token): RE_MATCH = r"" @@ -40,18 +40,16 @@ def __init__(self, value, *, negated=False, exact=False, database): def matches(self, other: MonsterModel) -> bool: return False - def __repr__(self): - return f"{self.__class__.__name__}(token={super().__repr__()})" - class MultipleAwakeningToken(SpecialToken): RE_MATCH = rf"(\d+)-(sa-)?-?({regexlist(AWAKENING_TOKENS)})" - def __init__(self, value, *, negated=False, exact=False, database): - count, sa, value = re.fullmatch(self.RE_MATCH, value).groups() + def __init__(self, fullvalue, *, negated=False, exact=False, database): + count, sa, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() self.count = int(count) self.sa = bool(sa) super().__init__(value, negated=negated, exact=exact, database=database) + self.full_value = fullvalue def matches(self, other): c = 0 @@ -70,13 +68,10 @@ def matches(self, other): if c >= self.count: return True - if idx > other.superawakening_count and (matched or not self.sa): + if maw.is_super and (matched or not self.sa): return False return False - def __repr__(self): - return super().__repr__() + f"" - SPECIAL_TOKEN_TYPES = [ From 3bd70603213ab36927c1b9fea4ee49584368d74a Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 2 May 2021 01:34:21 -0700 Subject: [PATCH 0740/1220] Fix SAs indexing error in moddedfiers (#1011) --- dadguide/find_monster_tokens.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 21d2eba8..5b5c9637 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -54,6 +54,9 @@ def __init__(self, fullvalue, *, negated=False, exact=False, database): def matches(self, other): c = 0 for idx, maw in enumerate(other.awakenings): + if maw.is_super and self.sa: + return False + matched = True for aw in inverse_map(AWOKEN_MAP)[self.value]: aw = self.database.awoken_skill_map[aw.value] @@ -68,7 +71,7 @@ def matches(self, other): if c >= self.count: return True - if maw.is_super and (matched or not self.sa): + if maw.is_super and matched: return False return False From df34cca7b3c19e4bfd93a4db7056dd741cbd09f5 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 2 May 2021 01:46:12 -0700 Subject: [PATCH 0741/1220] Fix SAs indexing error in moddedfiers try 2 (#1012) --- dadguide/find_monster_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 5b5c9637..3247194e 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -54,7 +54,7 @@ def __init__(self, fullvalue, *, negated=False, exact=False, database): def matches(self, other): c = 0 for idx, maw in enumerate(other.awakenings): - if maw.is_super and self.sa: + if maw.is_super and not self.sa: return False matched = True From 34c35c6fce11dc16608a0f77378770b7d7973d31 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 2 May 2021 19:14:32 -0700 Subject: [PATCH 0742/1220] Allow "" to remove name in crud (#1015) --- crud/crud.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 5e1b78a2..2e919c40 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -107,10 +107,16 @@ async def execute_read(self, ctx, sql, replacements): async def crud(self, ctx): """PadGuide database CRUD.""" - @crud.command() + @crud.command(aliases="credit") async def editmonsname(self, ctx, monster_id: int, *, name): - """Change a monster's name_en_override""" - if name.lower() in ["none", "null"]: + """Change a monster's name_en_override + + To remove a name override, set the name to None, NULL, or "" + """ + if name.startswith('"') and name.endswith('"') and name.count('"') <= 2: + name = name[1:-1] + + if name.lower() in ["none", "null", ""]: name = None with await self.get_cursor() as cursor: cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) From c74b8ec44258393b3279bacf07b8841808168021 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 2 May 2021 21:17:22 -0500 Subject: [PATCH 0743/1220] Remove crud editmonsname alias (#1017) --- crud/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crud/crud.py b/crud/crud.py index 2e919c40..a9744ae5 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -107,7 +107,7 @@ async def execute_read(self, ctx, sql, replacements): async def crud(self, ctx): """PadGuide database CRUD.""" - @crud.command(aliases="credit") + @crud.command() async def editmonsname(self, ctx, monster_id: int, *, name): """Change a monster's name_en_override From fec5088253cc9295bee3d6e33d923ea597baf0f5 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 2 May 2021 20:18:46 -0700 Subject: [PATCH 0744/1220] Refactor matches method; Awakenings enum -> AwokenSkills (#1014) Mostly just changing language and adding comments, no significant logic changes --- dadguide/find_monster_tokens.py | 41 ++++---- dadguide/monster_index.py | 4 +- dadguide/token_mappings.py | 175 ++++++++++++++++---------------- padinfo/padinfo.py | 8 +- 4 files changed, 118 insertions(+), 110 deletions(-) diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 3247194e..d958f358 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -1,7 +1,7 @@ import re from dadguide.models.monster_model import MonsterModel -from dadguide.token_mappings import AWAKENING_TOKENS, inverse_map, AWOKEN_MAP, AWAKENING_EQUIVALENCES +from dadguide.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP, AwokenSkills def regexlist(tokens): @@ -14,7 +14,7 @@ def __init__(self, value, *, negated=False, exact=False): self.negated = negated self.exact = exact - def matches(self, other: MonsterModel) -> bool: + def matches(self, monster: MonsterModel) -> bool: return True def __eq__(self, other): @@ -37,7 +37,7 @@ def __init__(self, value, *, negated=False, exact=False, database): self.database = database super().__init__(value, negated=negated, exact=exact) - def matches(self, other: MonsterModel) -> bool: + def matches(self, monster: MonsterModel) -> bool: return False @@ -46,32 +46,39 @@ class MultipleAwakeningToken(SpecialToken): def __init__(self, fullvalue, *, negated=False, exact=False, database): count, sa, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() - self.count = int(count) - self.sa = bool(sa) + self.minimum_count = int(count) + self.allows_super_awakenings = bool(sa) super().__init__(value, negated=negated, exact=exact, database=database) self.full_value = fullvalue - def matches(self, other): - c = 0 - for idx, maw in enumerate(other.awakenings): - if maw.is_super and not self.sa: + def matches(self, monster): + monster_total_awakenings_matching_token = 0 + for awakening in monster.awakenings: + if awakening.is_super and not self.allows_super_awakenings: return False + # Keep track of whether we matched this cycle for SA check at the end matched = True - for aw in inverse_map(AWOKEN_MAP)[self.value]: - aw = self.database.awoken_skill_map[aw.value] - if (eq := AWAKENING_EQUIVALENCES.get(maw.awoken_skill_id)) and eq[1] == aw.awoken_skill_id: - c += eq[0] + + for awoken_skill in (self.database.awoken_skill_map[aws.value] + for aws, tokens in AWOKEN_SKILL_MAP.items() + if self.value in tokens): + if (equivalence := PLUS_AWOKENSKILL_MAP.get(AwokenSkills(awakening.awoken_skill_id))) \ + and equivalence.awoken_skill.value == awoken_skill.awoken_skill_id: + monster_total_awakenings_matching_token += equivalence.value break - elif maw == aw: - c += 1 + elif awoken_skill == awakening: + monster_total_awakenings_matching_token += 1 break else: matched = False - if c >= self.count: + if monster_total_awakenings_matching_token >= self.minimum_count: return True - if maw.is_super and matched: + + # If we already matched an SA and didn't return True, fail immediately. + # We only allow one SA to count towards the total for each MultipleAwakeningToken + if awakening.is_super and matched: return False return False diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 7ee3f529..27c45d10 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -367,13 +367,13 @@ async def get_modifiers(self, monster: MonsterModel): # Awakenings for aw in monster.awakenings: try: - modifiers.update(AWOKEN_MAP[Awakenings(aw.awoken_skill_id)]) + modifiers.update(AWOKEN_SKILL_MAP[AwokenSkills(aw.awoken_skill_id)]) except ValueError: logger.warning(f"Invalid awoken skill ID: {aw.awoken_skill_id}") self.issues.append(f"Invalid awoken skill ID: {aw.awoken_skill_id}") # Numbered Equips - self.add_numbered_modifier(monster, modifiers, AWOKEN_MAP[Awakenings.EQUIP], + self.add_numbered_modifier(monster, modifiers, AWOKEN_SKILL_MAP[AwokenSkills.EQUIP], lambda m: m.is_equip) # Chibi diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 0277ddbc..b8fe3c0c 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -1,4 +1,4 @@ -from collections import defaultdict +from collections import defaultdict, namedtuple from enum import Enum from typing import Mapping, Tuple, TypeVar @@ -88,7 +88,7 @@ class EvoTypes(Enum): } -class Awakenings(Enum): +class AwokenSkills(Enum): ENHANCEDHP = 1 ENHANCEDATK = 2 ENHANCEDRCV = 3 @@ -169,86 +169,86 @@ class Awakenings(Enum): CROSSATTACK = 78 -AWOKEN_MAP = { - Awakenings.ENHANCEDHP: ('hp+', 'hp'), - Awakenings.ENHANCEDATK: ('atk+', 'atk'), - Awakenings.ENHANCEDRCV: ('rcv+', 'rcv'), - Awakenings.REDUCERED: ('elresr', 'elres'), # element resist - Awakenings.REDUCEBLUE: ('elresb', 'elres'), - Awakenings.REDUCEGREEN: ('elresg', 'elres'), - Awakenings.REDUCELIGHT: ('elresl', 'elres'), - Awakenings.REDUCEDARK: ('elresd', 'elres'), - Awakenings.AUTOHEAL: ('autoheal',), - Awakenings.BINDRES: ('unbindable', 'bindres'), - Awakenings.BLINDRES: ('resb',), - Awakenings.JAMMERRES: ('resj',), - Awakenings.POISONRES: ('resp',), - Awakenings.ENHANCEDRED: ('oer', 'oe'), - Awakenings.ENHANCEDBLUE: ('oeb', 'oe'), - Awakenings.ENHANCEDGREEN: ('oeg', 'oe'), - Awakenings.ENHANCEDLIGHT: ('oel', 'oe'), - Awakenings.ENHANCEDDARK: ('oed', 'oe'), - Awakenings.EXTMOVE: ('te', 'finger'), - Awakenings.BINDRECOVERY: ('bindrcv',), - Awakenings.SKILLBOOST: ('sb',), - Awakenings.REDROW: ('rowr', 'row'), - Awakenings.BLUEROW: ('rowb', 'row'), - Awakenings.GREENROW: ('rowg', 'row'), - Awakenings.LIGHTROW: ('rowl', 'row'), - Awakenings.DARKROW: ('rowd', 'row'), - Awakenings.TPA: ('tpa', 'pronged'), - Awakenings.SKILLBINDRES: ('sbr',), - Awakenings.ENHANCEDHEAL: ('htpa', 'oeh'), - Awakenings.MULTIBOOST: ('multi', 'mb'), - Awakenings.DRAGONKILLER: ('dragonkiller', 'dk', 'drk', 'killer'), - Awakenings.GODKILLER: ('godkiller', 'gk', 'gok', 'killer'), - Awakenings.DEVILKILLER: ('devilkiller', 'vk', 'dek', 'killer'), - Awakenings.MACHINEKILLER: ('machinekiller', 'mk', 'mak', 'killer'), - Awakenings.BALANCEDKILLER: ('balancedkiller', 'bk', 'bak', 'killer'), - Awakenings.ATTACKERKILLER: ('attackerkiller', 'ak', 'aak', 'killer'), - Awakenings.PHYSICALKILLER: ('physicalkiller', 'pk', 'phk', 'killer'), - Awakenings.HEALERKILLER: ('healerkiller', 'hk', 'hek', 'killer'), - Awakenings.EVOMATKILLER: ('evokiller', 'evok', 'a2killer'), - Awakenings.AWOKENKILLER: ('awokenkiller', 'awok', 'a2killer'), - Awakenings.FODDERKILLER: ('enhancekiller', 'enhk', 'a2killer'), - Awakenings.REDEEMKILLER: ('vendorkiller', 'vendork', 'a2killer'), - Awakenings.ENHCOMBO7C: ('7c',), - Awakenings.GUARDBREAK: ('gb',), - Awakenings.FUA: ('fua',), - Awakenings.ENHTEAMHP: ('teamhp', 'thp'), - Awakenings.ENHTEAMRCV: ('teamrcv', 'trcv'), - Awakenings.VDP: ('vdp',), - Awakenings.EQUIP: ('equip', 'assist', 'eq'), - Awakenings.SUPERFUA: ('sfua',), - Awakenings.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), - Awakenings.UNBINDABLE: ('unbindable', '' +AWOKEN_SKILL_MAP = { + AwokenSkills.ENHANCEDHP: ('hp+', 'hp'), + AwokenSkills.ENHANCEDATK: ('atk+', 'atk'), + AwokenSkills.ENHANCEDRCV: ('rcv+', 'rcv'), + AwokenSkills.REDUCERED: ('elresr', 'elres'), # element resist + AwokenSkills.REDUCEBLUE: ('elresb', 'elres'), + AwokenSkills.REDUCEGREEN: ('elresg', 'elres'), + AwokenSkills.REDUCELIGHT: ('elresl', 'elres'), + AwokenSkills.REDUCEDARK: ('elresd', 'elres'), + AwokenSkills.AUTOHEAL: ('autoheal',), + AwokenSkills.BINDRES: ('unbindable', 'bindres'), + AwokenSkills.BLINDRES: ('resb',), + AwokenSkills.JAMMERRES: ('resj',), + AwokenSkills.POISONRES: ('resp',), + AwokenSkills.ENHANCEDRED: ('oer', 'oe'), + AwokenSkills.ENHANCEDBLUE: ('oeb', 'oe'), + AwokenSkills.ENHANCEDGREEN: ('oeg', 'oe'), + AwokenSkills.ENHANCEDLIGHT: ('oel', 'oe'), + AwokenSkills.ENHANCEDDARK: ('oed', 'oe'), + AwokenSkills.EXTMOVE: ('te', 'finger'), + AwokenSkills.BINDRECOVERY: ('bindrcv',), + AwokenSkills.SKILLBOOST: ('sb',), + AwokenSkills.REDROW: ('rowr', 'row'), + AwokenSkills.BLUEROW: ('rowb', 'row'), + AwokenSkills.GREENROW: ('rowg', 'row'), + AwokenSkills.LIGHTROW: ('rowl', 'row'), + AwokenSkills.DARKROW: ('rowd', 'row'), + AwokenSkills.TPA: ('tpa', 'pronged'), + AwokenSkills.SKILLBINDRES: ('sbr',), + AwokenSkills.ENHANCEDHEAL: ('htpa', 'oeh'), + AwokenSkills.MULTIBOOST: ('multi', 'mb'), + AwokenSkills.DRAGONKILLER: ('dragonkiller', 'dk', 'drk', 'killer'), + AwokenSkills.GODKILLER: ('godkiller', 'gk', 'gok', 'killer'), + AwokenSkills.DEVILKILLER: ('devilkiller', 'vk', 'dek', 'killer'), + AwokenSkills.MACHINEKILLER: ('machinekiller', 'mk', 'mak', 'killer'), + AwokenSkills.BALANCEDKILLER: ('balancedkiller', 'bk', 'bak', 'killer'), + AwokenSkills.ATTACKERKILLER: ('attackerkiller', 'ak', 'aak', 'killer'), + AwokenSkills.PHYSICALKILLER: ('physicalkiller', 'pk', 'phk', 'killer'), + AwokenSkills.HEALERKILLER: ('healerkiller', 'hk', 'hek', 'killer'), + AwokenSkills.EVOMATKILLER: ('evokiller', 'evok', 'a2killer'), + AwokenSkills.AWOKENKILLER: ('awokenkiller', 'awok', 'a2killer'), + AwokenSkills.FODDERKILLER: ('enhancekiller', 'enhk', 'a2killer'), + AwokenSkills.REDEEMKILLER: ('vendorkiller', 'vendork', 'a2killer'), + AwokenSkills.ENHCOMBO7C: ('7c',), + AwokenSkills.GUARDBREAK: ('gb',), + AwokenSkills.FUA: ('fua',), + AwokenSkills.ENHTEAMHP: ('teamhp', 'thp'), + AwokenSkills.ENHTEAMRCV: ('teamrcv', 'trcv'), + AwokenSkills.VDP: ('vdp',), + AwokenSkills.EQUIP: ('equip', 'assist', 'eq'), + AwokenSkills.SUPERFUA: ('sfua',), + AwokenSkills.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), + AwokenSkills.UNBINDABLE: ('unbindable', '' ''), - Awakenings.EXTMOVEPLUS: ('te+', 'te', 'finger+', 'finger'), - Awakenings.CLOUDRESIST: ('cloudres', 'cloud'), - Awakenings.TAPERESIST: ('taperes', 'tape'), - Awakenings.SKILLBOOSTPLUS: ('sb+', 'sb'), - Awakenings.HP80ORMORE: ('>80', 'highhp'), - Awakenings.HP50ORLESS: ('<50', 'lowhp'), - Awakenings.ELSHIELD: ('elshield', 'elh', 'hel'), - Awakenings.ELATTACK: ('el',), - Awakenings.ENHCOMBO10C: ('10c',), - Awakenings.COMBOORB: ('co', 'corb'), - Awakenings.VOICE: ('voice',), - Awakenings.DUNGEONBONUS: ('dgbonus', 'dgboost'), - Awakenings.REDUCEDHP: ('hp-',), - Awakenings.REDUCEDATK: ('atk-',), - Awakenings.REDUCEDRCV: ('rcv-',), - Awakenings.UNBLINDABLE: ('resb+', 'b+',), - Awakenings.UNJAMMABLE: ('resj+', 'j+',), - Awakenings.UNPOISONABLE: ('resp+', 'p+',), - Awakenings.JAMMERBLESSING: ('jblessing', 'sfj', 'jsurge'), - Awakenings.POISONBLESSING: ('pblessing', 'sfp', 'psurge'), - Awakenings.REDCOMBOCOUNT: ('ccr', 'cc'), - Awakenings.BLUECOMBOCOUNT: ('ccb', 'cc'), - Awakenings.GREENCOMBOCOUNT: ('ccg', 'cc'), - Awakenings.LIGHTCOMBOCOUNT: ('ccl', 'cc'), - Awakenings.DARKCOMBOCOUNT: ('ccd', 'cc'), - Awakenings.CROSSATTACK: ('crossattack', 'crossblind'), + AwokenSkills.EXTMOVEPLUS: ('te+', 'te', 'finger+', 'finger'), + AwokenSkills.CLOUDRESIST: ('cloudres', 'cloud'), + AwokenSkills.TAPERESIST: ('taperes', 'tape'), + AwokenSkills.SKILLBOOSTPLUS: ('sb+', 'sb'), + AwokenSkills.HP80ORMORE: ('>80', 'highhp'), + AwokenSkills.HP50ORLESS: ('<50', 'lowhp'), + AwokenSkills.ELSHIELD: ('elshield', 'elh', 'hel'), + AwokenSkills.ELATTACK: ('el',), + AwokenSkills.ENHCOMBO10C: ('10c',), + AwokenSkills.COMBOORB: ('co', 'corb'), + AwokenSkills.VOICE: ('voice',), + AwokenSkills.DUNGEONBONUS: ('dgbonus', 'dgboost'), + AwokenSkills.REDUCEDHP: ('hp-',), + AwokenSkills.REDUCEDATK: ('atk-',), + AwokenSkills.REDUCEDRCV: ('rcv-',), + AwokenSkills.UNBLINDABLE: ('resb+', 'b+',), + AwokenSkills.UNJAMMABLE: ('resj+', 'j+',), + AwokenSkills.UNPOISONABLE: ('resp+', 'p+',), + AwokenSkills.JAMMERBLESSING: ('jblessing', 'sfj', 'jsurge'), + AwokenSkills.POISONBLESSING: ('pblessing', 'sfp', 'psurge'), + AwokenSkills.REDCOMBOCOUNT: ('ccr', 'cc'), + AwokenSkills.BLUECOMBOCOUNT: ('ccb', 'cc'), + AwokenSkills.GREENCOMBOCOUNT: ('ccg', 'cc'), + AwokenSkills.LIGHTCOMBOCOUNT: ('ccl', 'cc'), + AwokenSkills.DARKCOMBOCOUNT: ('ccd', 'cc'), + AwokenSkills.CROSSATTACK: ('crossattack', 'crossblind'), } @@ -303,7 +303,7 @@ class MiscModifiers(Enum): *SUB_COLOR_MAP.values(), *DUAL_COLOR_MAP.values(), *TYPE_MAP.values(), - *AWOKEN_MAP.values(), + *AWOKEN_SKILL_MAP.values(), *EVO_MAP.values(), *MISC_MAP.values(), } @@ -316,7 +316,7 @@ class MiscModifiers(Enum): *sum(DUAL_COLOR_MAP.values(), ()), } -AWAKENING_TOKENS = {*sum(AWOKEN_MAP.values(), ())} +AWAKENING_TOKENS = {*sum(AWOKEN_SKILL_MAP.values(), ())} EVO_TOKENS = {*sum(EVO_MAP.values(), ())} TYPE_TOKENS = {*sum(TYPE_MAP.values(), ())} @@ -352,8 +352,9 @@ class MiscModifiers(Enum): 'ld', 'lx', 'd', 'dr', 'dg', 'db', 'dl', 'dd', 'dx', 'x', 'xr', 'xg', 'xb', 'xl', 'xd', 'xx'} # This probably doesn't belong in here -AWAKENING_EQUIVALENCES = { - 52: (2, 10), # 1 unbindable = 2 bind resistance - 53: (2, 19), # 1 te+ = 2 te - 56: (2, 21), # 1 sb+ = 2 sb +PlusAwakeningData = namedtuple("PlusAwakeningData", "awoken_skill value") +PLUS_AWOKENSKILL_MAP = { + AwokenSkills.UNBINDABLE: PlusAwakeningData(AwokenSkills.BINDRES, 2), + AwokenSkills.EXTMOVEPLUS: PlusAwakeningData(AwokenSkills.EXTMOVE, 2), + AwokenSkills.SKILLBOOSTPLUS: PlusAwakeningData(AwokenSkills.SKILLBOOST, 2), } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bdea4925..435b39bb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1001,7 +1001,7 @@ async def exportmodifiers(self, ctx): ret += "\n\n### Types\n\n" + tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") mtable = [(k.value, ", ".join(map(inline, v))) for k, v in maps.MISC_MAP.items()] ret += "\n\n### Misc\n\n" + tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") - atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_MAP.items()] + atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_SKILL_MAP.items()] ret += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") stable = [(series[k].name_en, ", ".join(map(inline, v))) for k, v in DGCOG.index.series_id_to_pantheon_nickname.items()] @@ -1026,7 +1026,7 @@ async def idmeaning(self, ctx, *, token): await DGCOG.wait_until_ready() tms = DGCOG.token_maps - awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_MAP.values() for aw in aws) + ")" + awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_SKILL_MAP.values() for aw in aws) + ")" awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} @@ -1081,7 +1081,7 @@ def additmods(ms, om): *["Misc: " + k.value + additmods(v, token) for k, v in tms.MISC_MAP.items() if token in v], *["Awakening: " + get_awakening_emoji(k) + ' ' + awakenings[k.value].name_en + additmods(v, token) - for k, v in tms.AWOKEN_MAP.items() if token in v], + for k, v in tms.AWOKEN_SKILL_MAP.items() if token in v], *["Main attr: " + get_attribute_emoji_by_enum(k, None) + ' ' + k.name.replace("Nil", "None") + additmods(v, token) for k, v in tms.COLOR_MAP.items() if token in v], @@ -1099,7 +1099,7 @@ def additmods(ms, om): *[f"[UNSUPPORTED] Multiple awakenings: {m}x {awakenings[a.value].name_en}" f"{additmods([f'{m}*{d}' for d in v], token)}" for m, ag in re.findall(r"^(\d+)\*{}$".format(awokengroup), token) - for a, v in tms.AWOKEN_MAP.items() if ag in v] + for a, v in tms.AWOKEN_SKILL_MAP.items() if ag in v] ]) if meanings or ret: From d5cc272f1132a7837def9e28d012ba1426bcca0f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 2 May 2021 23:52:47 -0500 Subject: [PATCH 0745/1220] Use discord-menu view state as base class (#1021) Doing this so that the type annotation for `pad_info_footer_with_state` can be just `ViewState` so that we can move it to `tsutils` so that it can be used in every cog without being duplicated --- padinfo/view/components/base.py | 8 ++------ padinfo/view/components/view_state_base.py | 17 ++++++----------- padinfo/view/components/view_state_base_id.py | 19 ++++++++----------- 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/padinfo/view/components/base.py b/padinfo/view/components/base.py index 9f74b341..255f9cd8 100644 --- a/padinfo/view/components/base.py +++ b/padinfo/view/components/base.py @@ -1,11 +1,7 @@ -from typing import Union - from discordmenu.embed.components import EmbedFooter +from discordmenu.embed.view_state import ViewState from discordmenu.intra_message_state import IntraMessageState -from padinfo.view.components.view_state_base import ViewStateBase -from padinfo.view.components.view_state_base_id import ViewStateBaseId - TSUBAKI_FLOWER_ICON_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/tsubaki/tsubakiflower.png' @@ -13,7 +9,7 @@ def pad_info_footer(): return EmbedFooter('Requester may click the reactions below to switch tabs') -def pad_info_footer_with_state(state: Union[ViewStateBase, ViewStateBaseId]): +def pad_info_footer_with_state(state: ViewState): url = IntraMessageState.serialize(TSUBAKI_FLOWER_ICON_URL, state.serialize()) return EmbedFooter( 'Requester may click the reactions below to switch tabs', diff --git a/padinfo/view/components/view_state_base.py b/padinfo/view/components/view_state_base.py index ee53e216..f6971883 100644 --- a/padinfo/view/components/view_state_base.py +++ b/padinfo/view/components/view_state_base.py @@ -1,21 +1,16 @@ from typing import List +from discordmenu.embed.view_state import ViewState -class ViewStateBase: + +class ViewStateBase(ViewState): def __init__(self, original_author_id, menu_type, raw_query, extra_state=None, reaction_list: List = None): - self.extra_state = extra_state or {} - self.menu_type = menu_type - self.original_author_id = original_author_id - self.raw_query = raw_query self.reaction_list = reaction_list + super().__init__(original_author_id=original_author_id, menu_type=menu_type, raw_query=raw_query, + extra_state=extra_state) def serialize(self): - ret = { - 'raw_query': self.raw_query, - 'menu_type': self.menu_type, - 'original_author_id': self.original_author_id, - } - ret.update(self.extra_state) + ret = super().serialize() if self.reaction_list is not None: ret['reaction_list'] = self.reaction_list return ret diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index c4268f72..80cf6ba0 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING, List +from discordmenu.embed.view_state import ViewState + from padinfo.common.config import UserConfig from padinfo.view.common import get_monster_from_ims @@ -7,17 +9,15 @@ from dadguide.models.monster_model import MonsterModel -class ViewStateBaseId: +class ViewStateBaseId(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List["MonsterModel"], use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): + super().__init__(original_author_id=original_author_id, menu_type=menu_type, raw_query=raw_query, + extra_state=extra_state) self.alt_monsters = alt_monsters - self.extra_state = extra_state or {} - self.menu_type = menu_type - self.original_author_id = original_author_id - self.raw_query = raw_query self.reaction_list = reaction_list self.color = color self.monster = monster @@ -25,16 +25,13 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.use_evo_scroll = use_evo_scroll def serialize(self): - ret = { - 'raw_query': self.raw_query, - 'menu_type': self.menu_type, - 'original_author_id': self.original_author_id, + ret = super().serialize() + ret.update({ 'query': self.query, 'resolved_monster_id': self.monster.monster_id, 'use_evo_scroll': str(self.use_evo_scroll), 'reaction_list': self.reaction_list, - } - ret.update(self.extra_state) + }) return ret @classmethod From 839f17a8d578f12bfe2dd106bbf2192e9082a028 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 3 May 2021 04:48:43 -0500 Subject: [PATCH 0746/1220] Replace pad_info_footer with embed_footer (#1022) This PR requires a tsutils version update! --- padinfo/info.json | 2 +- padinfo/view/awakening_help.py | 4 ++-- padinfo/view/base.py | 1 + padinfo/view/components/base.py | 16 ---------------- padinfo/view/evos.py | 4 ++-- padinfo/view/id.py | 4 ++-- padinfo/view/id_traceback.py | 4 ++-- padinfo/view/leader_skill.py | 4 ++-- padinfo/view/leader_skill_single.py | 4 ++-- padinfo/view/materials.py | 4 ++-- padinfo/view/monster_list.py | 5 ++--- padinfo/view/otherinfo.py | 4 ++-- padinfo/view/pantheon.py | 4 ++-- padinfo/view/pic.py | 4 ++-- padinfo/view/series_scroll.py | 5 ++--- padinfo/view/simple_text.py | 4 ++-- padinfo/view/transforminfo.py | 11 +++++------ 17 files changed, 33 insertions(+), 51 deletions(-) delete mode 100644 padinfo/view/components/base.py diff --git a/padinfo/info.json b/padinfo/info.json index 14363409..09a53a4a 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.2.2", + "tsutils>=3.3.2", "prettytable", "python-levenshtein", "discord-menu>=0.16.0" diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index e6e1b615..bbf8114f 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -4,10 +4,10 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedAuthor from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.emoji_map import get_awakening_emoji from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -89,6 +89,6 @@ def embed(state, props: AwakeningHelpViewProps): puzzledragonx(monster), MonsterImage.icon(monster) ), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields ) diff --git a/padinfo/view/base.py b/padinfo/view/base.py index a808291e..da866a6f 100644 --- a/padinfo/view/base.py +++ b/padinfo/view/base.py @@ -4,6 +4,7 @@ from padinfo.view.components.view_state_base_id import ViewStateBaseId + class BaseIdView(metaclass=ABCMeta): VIEW_TYPE: str TSUBAKI = 2141 diff --git a/padinfo/view/components/base.py b/padinfo/view/components/base.py deleted file mode 100644 index 255f9cd8..00000000 --- a/padinfo/view/components/base.py +++ /dev/null @@ -1,16 +0,0 @@ -from discordmenu.embed.components import EmbedFooter -from discordmenu.embed.view_state import ViewState -from discordmenu.intra_message_state import IntraMessageState - -TSUBAKI_FLOWER_ICON_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/tsubaki/tsubakiflower.png' - - -def pad_info_footer(): - return EmbedFooter('Requester may click the reactions below to switch tabs') - - -def pad_info_footer_with_state(state: ViewState): - url = IntraMessageState.serialize(TSUBAKI_FLOWER_ICON_URL, state.serialize()) - return EmbedFooter( - 'Requester may click the reactions below to switch tabs', - icon_url=url) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 485c5a11..c298a476 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -3,12 +3,12 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -111,5 +111,5 @@ def embed(cls, state: EvosViewState): ).to_markdown(), url=puzzledragonx(state.monster)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 2709f313..249ad428 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -4,6 +4,7 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_awakening_emoji, get_emoji @@ -11,7 +12,6 @@ from padinfo.core.leader_skills import createMultiplierText from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -285,5 +285,5 @@ def embed(cls, state: IdViewState): ).to_markdown(), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index c39df622..8b692de0 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -4,9 +4,9 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.text import LabeledText from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.emoji_map import get_attribute_emoji_by_monster -from padinfo.view.components.base import pad_info_footer_with_state if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -51,5 +51,5 @@ def embed(state, props: IdTracebackViewProps): title=get_title(props.monster), description=get_description(props.score) ), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index ae1da345..9c365dff 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -4,11 +4,11 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.core.leader_skills import createMultiplierText from padinfo.core.leader_skills import perform_leaderskill_query -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base import ViewStateBase @@ -60,4 +60,4 @@ def embed(state: LeaderSkillViewState): BoldText(MonsterHeader.name(state.r_mon, link=True, show_jp=True)), Text(rls.desc if rls else 'None')), color=state.color), - embed_footer=pad_info_footer_with_state(state)) + embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index 3e91a6bd..ea95f999 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -4,10 +4,10 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.core.leader_skills import createSingleMultiplierText -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base import ViewStateBase from padinfo.view.common import get_monster_from_ims @@ -45,4 +45,4 @@ def embed(state: LeaderSkillSingleViewState): BoldText(MonsterHeader.name(state.mon, link=True, show_jp=True)), Text(ls.desc if ls else 'None')), color=state.color), - embed_footer=pad_info_footer_with_state(state)) + embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index b4cbded3..7a077433 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -4,12 +4,12 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -160,7 +160,7 @@ def embed(cls, state: MaterialsViewState): url=puzzledragonx(state.monster) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=[f for f in [ mat_use_field(state.mats, "Evo materials") if state.mats or not (state.monster.is_stackable or state.gem_override) else None, diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list.py index b08bd439..1acca799 100644 --- a/padinfo/view/monster_list.py +++ b/padinfo/view/monster_list.py @@ -3,10 +3,9 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView -from tsutils import char_to_emoji +from tsutils import char_to_emoji, embed_footer_with_state from padinfo.common.config import UserConfig -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base import ViewStateBase @@ -89,5 +88,5 @@ def embed(state: MonsterListViewState): EmbedMain( color=state.color, ), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 73e07a8b..5185c14f 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -6,10 +6,10 @@ from discordmenu.embed.text import LabeledText, Text from discordmenu.embed.view import EmbedView from redbot.core.utils.chat_formatting import box +from tsutils import embed_footer_with_state from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId from padinfo.view.id import evos_embed_field @@ -56,7 +56,7 @@ def embed(cls, state: OtherInfoViewState): "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" ).to_markdown(), url=puzzledragonx(m)), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=[ EmbedField( "Stats at +297:", diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index ce3dd9f0..0d4893e9 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -3,6 +3,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ @@ -10,7 +11,6 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -174,7 +174,7 @@ def embed(cls, state: PantheonViewState): "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" ).to_markdown(), url=puzzledragonx(state.monster)), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields, embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), ) diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index e86d5f7c..076becf3 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -2,10 +2,10 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage from discordmenu.embed.text import LinkedText, Text from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -58,7 +58,7 @@ def embed(cls, state: PicViewState): "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" ).to_markdown(), url=puzzledragonx(state.monster)), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields, embed_body_image=EmbedBodyImage(url), ) diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 74eeab53..11c1f43b 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -4,10 +4,9 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView -from tsutils import char_to_emoji +from tsutils import char_to_emoji, embed_footer_with_state from padinfo.common.config import UserConfig -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base import ViewStateBase @@ -147,7 +146,7 @@ def embed(state: SeriesScrollViewState): title=state.title, color=state.color, ), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields) @staticmethod diff --git a/padinfo/view/simple_text.py b/padinfo/view/simple_text.py index 7eca5196..9e49f1c9 100644 --- a/padinfo/view/simple_text.py +++ b/padinfo/view/simple_text.py @@ -2,9 +2,9 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.view_state_base import ViewStateBase @@ -42,5 +42,5 @@ def embed(state: SimpleTextViewState): color=state.color, description=state.message ), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), ) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 609d47b6..4e9ebdc6 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -4,14 +4,13 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx -from padinfo.view.components.base import pad_info_footer_with_state from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base import ViewStateBase -from padinfo.view.leader_skill import createMultiplierText from padinfo.view.id import IdView if TYPE_CHECKING: @@ -163,7 +162,7 @@ def embed(state: TransformInfoViewState): Box( TRANSFORM_EMOJI, IdView.normal_awakenings_row(transformed_mon) - if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), + if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), delimiter=' ' ), base_info(base_mon), @@ -184,7 +183,7 @@ def embed(state: TransformInfoViewState): transformat(transform_active_header(transformed_mon).to_markdown()), Box( Text(transformed_mon.active_skill.desc if transformed_mon.active_skill - else 'None'), + else 'None'), base_active_header(base_mon).to_markdown(), Text(base_mon.active_skill.desc if base_mon.active_skill else 'None') ) @@ -193,7 +192,7 @@ def embed(state: TransformInfoViewState): transformat(leader_header(transformed_mon, False).to_markdown()), Box( Text(transformed_mon.leader_skill.desc if transformed_mon.leader_skill - else 'None'), + else 'None'), leader_header(base_mon, True).to_markdown(), Text(base_mon.leader_skill.desc if base_mon.leader_skill else 'None') ) @@ -207,6 +206,6 @@ def embed(state: TransformInfoViewState): url=puzzledragonx(transformed_mon) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon)), - embed_footer=pad_info_footer_with_state(state), + embed_footer=embed_footer_with_state(state), embed_fields=fields ) From 8458d7873507a1f533e2f9d7c71e669e101c53b6 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Mon, 3 May 2021 15:39:11 -0400 Subject: [PATCH 0747/1220] Remove full command text from ^guide, ^boss (#1023) --- padglobal/padglobal.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 761c16ef..0ddf14cf 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -491,7 +491,7 @@ async def boss(self, ctx, *, term: str = None): else: await ctx.send(inline('No mechanics found')) return - msg = self.boss_to_text(ctx) + msg = await self.boss_to_text(ctx) for page in pagify(msg): await ctx.author.send(page) @@ -519,20 +519,23 @@ async def lookup_boss(self, term, ctx): return name, definition - def boss_to_text(self, ctx): + async def boss_to_text(self, ctx): bosses = self.settings.boss() - msg = '__**PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( + dgcog = self.bot.get_cog('Dadguide') + msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( ctx.prefix) for term in sorted(bosses.keys()): - definition = bosses[term] - msg += '\n**{}**\n{}'.format(term, definition) + m = await dgcog.find_monster(str(term), ctx.author.id) + if not m: # monster not found + continue + msg += '\n[{}] {}'.format(term, m.name_en) return msg def boss_to_text_index(self, ctx): bosses = self.settings.boss() msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( ctx.prefix) - msg = msg + '\n' + ',\n'.join(sorted(bosses.keys())) + msg = msg + '\n' + ',\n'.join(map(str, sorted(bosses.keys()))) return msg @padglobal.command() @@ -1027,8 +1030,7 @@ def guide_to_text(self): msg = '__**Dungeon Guides**__' dungeon_guide = self.settings.dungeonGuide() for term in sorted(dungeon_guide.keys()): - definition = dungeon_guide[term] - msg += '\n**{}** :\n{}\n'.format(term, definition) + msg += '\n{}'.format(term) msg += '\n\n__**Leader Guides**__' for monster_id, definition in self.settings.leaderGuide().items(): @@ -1036,7 +1038,7 @@ def guide_to_text(self): if m is None: continue name = m.name_en.split(', ')[-1].title() - msg += '\n**{}** :\n{}\n'.format(name, definition) + msg += '\n[{}] {}'.format(monster_id, name) return msg From d59a638f95bf1d7cd9e41ed2d1312e4c28fd359d Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 6 May 2021 07:38:48 -0700 Subject: [PATCH 0748/1220] Sort evo trees with a dfs instead of bfs (#1025) This changes the base id of 6 monsters, so the treename sheet will need to be updated. 1 -> 1929 5 -> 1930 9 -> 1931 1803 -> 1810 2926 -> 2978 5802 -> 5810 --- dadguide/find_monster.py | 1 + dadguide/models/monster_model.py | 1 + dadguide/monster_graph.py | 146 ++++++++++-------- dadguide/monster_index.py | 6 +- padinfo/core/transforminfo.py | 2 +- padinfo/menu/id.py | 4 +- padinfo/view/components/view_state_base_id.py | 3 +- 7 files changed, 88 insertions(+), 75 deletions(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 0196c628..2474c5a8 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -212,6 +212,7 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = tokenized_query = [] return (matches[monster].score, + not self.dgcog.database.graph.monster_is_evo_gem(monster), # Don't deprio evos with new modifier not monster.is_equip if not {m[0] for m in matches[monster].mod}.intersection( {'new', 'base'}) else True, diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index ff1a327b..6b1a6e66 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -20,6 +20,7 @@ def __init__(self, **m): self.monster_no_jp = m['monster_no_jp'] self.monster_no_na = m['monster_no_na'] self.monster_no_kr = m['monster_no_kr'] + self.base_evo_id = m['base_evo_id'] # these things are literally named backwards atm self.awakenings = sorted(m['awakenings'], key=lambda a: a.order_idx) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index f739435d..ee9d55f1 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,9 +1,11 @@ import json import re from collections import defaultdict -from typing import Optional, List +from typing import Optional, List, Union, Set -import networkx +from networkx import MultiDiGraph +from networkx.classes.coreviews import AtlasView +from networkx.classes.reportviews import OutMultiEdgeView, NodeView from .database_manager import DadguideDatabase from .models.active_skill_model import ActiveSkillModel @@ -104,15 +106,14 @@ class MonsterGraph(object): def __init__(self, database: DadguideDatabase): self.database = database - self.graph = None - self.graph: networkx.MultiDiGraph - self.edges = None - self.nodes = None + self.graph: Optional[MultiDiGraph] = None + self.edges: Optional[OutMultiEdgeView] = None + self.nodes: Optional[NodeView] = None self.max_monster_id = -1 self.build_graph() def build_graph(self): - self.graph = networkx.MultiDiGraph() + self.graph = MultiDiGraph() ms = self.database.query_many(MONSTER_QUERY, ()) es = self.database.query_many(EVOS_QUERY, ()) @@ -171,6 +172,7 @@ def build_graph(self): ) m_model = MonsterModel(monster_id=m.monster_id, + base_evo_id=m.base_id, monster_no_jp=m.monster_no_jp, monster_no_na=m.monster_no_na, monster_no_kr=m.monster_no_kr, @@ -230,6 +232,10 @@ def build_graph(self): self.graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') self.graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') + if m.evo_gem_id: + self.graph.add_edge(m.monster_id, m.evo_gem_id, type='evo_gem_from') + self.graph.add_edge(m.evo_gem_id, m.monster_id, type='evo_gem_of') + self.max_monster_id = max(self.max_monster_id, m.monster_id) for e in es: @@ -263,12 +269,21 @@ def build_graph(self): self.edges = self.graph.edges self.nodes = self.graph.nodes - @staticmethod - def _get_edges(node, etype): + def _get_edges(self, node: Union[int, AtlasView], etype) -> Set[int]: + if isinstance(node, int): + node = self.graph[node] + return {mid for mid, atlas in node.items() for edge in atlas.values() if edge.get('type') == etype} - @staticmethod - def _get_edge_model(node, etype): + def _get_edge_or_none(self, node: Union[int, AtlasView], etype) -> Optional[int]: + edges = self._get_edges(node, etype) + if edges: + return edges.pop() + + def _get_edge_model(self, node: Union[int, AtlasView], etype): + if isinstance(node, int): + node = self.graph[node] + possible_results = set() for atlas in node.values(): for edge in atlas.values(): @@ -283,48 +298,42 @@ def get_monster(self, monster_id) -> Optional[MonsterModel]: return None return self.graph.nodes[monster_id]['model'] - def get_evo_tree(self, monster_id): - ids = set() - to_check = {monster_id} - while to_check: - mid = to_check.pop() - if mid in ids: - continue - n = self.graph[mid] - to_check.update(self._get_edges(n, 'evolution')) - to_check.update(self._get_edges(n, 'back_evolution')) - ids.add(mid) + def get_evo_tree(self, monster_id) -> List[int]: + while (prev := self._get_edges(monster_id, 'back_evolution')): + monster_id = prev + return self.get_evo_tree_from_base(monster_id) + + def get_evo_tree_from_base(self, base_monster_id) -> List[int]: + ids = [base_monster_id] + for evo in sorted(self._get_edges(base_monster_id, 'evolution')): + ids += self.get_evo_tree_from_base(evo) return ids - def get_transform_tree(self, monster_id): + def get_transform_tree(self, monster_id: int): ids = set() to_check = {monster_id} while to_check: mid = to_check.pop() if mid in ids: continue - n = self.graph[mid] - to_check.update(self._get_edges(n, 'transformation')) - to_check.update(self._get_edges(n, 'back_transformation')) + to_check.update(self._get_edges(mid, 'transformation')) + to_check.update(self._get_edges(mid, 'back_transformation')) ids.add(mid) return ids def get_transform_monsters(self, monster): return {self.get_monster(m) for m in self.get_transform_tree(monster.monster_id)} - def process_alt_versions(self, monster_id): - ids = set() - to_check = {monster_id} - while to_check: - mid = to_check.pop() - if mid in ids: - continue - n = self.graph[mid] - to_check.update(self._get_edges(n, 'evolution')) - to_check.update(self._get_edges(n, 'transformation')) - to_check.update(self._get_edges(n, 'back_evolution')) - to_check.update(self._get_edges(n, 'back_transformation')) - ids.add(mid) + def process_alt_versions(self, monster_id) -> List[int]: + return self.process_alt_versions_from_base(self.get_base_id_by_id(monster_id)) + + def process_alt_versions_from_base(self, base_monster_id) -> List[int]: + ids = [base_monster_id] + for trans in sorted(self._get_edges(base_monster_id, 'transformation')): + if trans > base_monster_id or trans == 5802: # I hate DMG very much + ids += self.process_alt_versions_from_base(trans) + for evo in sorted(self._get_edges(base_monster_id, 'evolution')): + ids += self.process_alt_versions_from_base(evo) return ids def get_alt_ids_by_id(self, monster_id): @@ -332,25 +341,30 @@ def get_alt_ids_by_id(self, monster_id): def get_alt_monsters_by_id(self, monster_id): ids = self.get_alt_ids_by_id(monster_id) - return {self.get_monster(m_id) for m_id in ids} + return [self.get_monster(m_id) for m_id in ids] def get_alt_monsters(self, monster: MonsterModel): return self.get_alt_monsters_by_id(monster.monster_id) def get_base_id_by_id(self, monster_id): - alt_cards = self.get_alt_ids_by_id(monster_id) - if alt_cards is None: - return None - return sorted(alt_cards)[0] + return self.get_base_id(self.get_monster(monster_id)) def get_base_id(self, monster): - return self.get_base_id_by_id(monster.monster_id) + # This fixes DMG. I *hate* DMG. + if monster.base_evo_id == 5802: + return 5810 + + while (prevs := self.get_prev_transforms_by_monster(monster)) \ + and list(prevs)[0].monster_id < monster.monster_id: + monster = prevs.pop() + + return monster.base_evo_id def get_base_monster_by_id(self, monster_id): return self.get_monster(self.get_base_id_by_id(monster_id)) def get_base_monster(self, monster: MonsterModel): - return self.get_monster(self.get_base_id_by_id(monster.monster_id)) + return self.get_monster(self.get_base_id(monster)) def monster_is_base_by_id(self, monster_id: int) -> bool: return self.get_base_id_by_id(monster_id) == monster_id @@ -398,10 +412,10 @@ def get_numerical_sort_top_monster_by_id(self, monster_id): return self.get_monster(self.get_numerical_sort_top_id_by_id(monster_id)) def get_evo_by_monster_id(self, monster_id) -> Optional[EvolutionModel]: - return self._get_edge_model(self.graph[monster_id], 'back_evolution') + return self._get_edge_model(monster_id, 'back_evolution') def get_evo_by_monster(self, monster) -> Optional[EvolutionModel]: - return self._get_edge_model(self.graph[monster.monster_id], 'back_evolution') + return self._get_edge_model(monster.monster_id, 'back_evolution') def cur_evo_type_by_monster_id(self, monster_id: int) -> EvoType: prev_evo = self.get_evo_by_monster_id(monster_id) @@ -471,10 +485,7 @@ def true_evo_type_by_monster(self, monster: MonsterModel) -> InternalEvoType: return self.true_evo_type_by_monster_id(monster.monster_no) def get_prev_evolution_id_by_monster_id(self, monster_id): - bes = self._get_edges(self.graph[monster_id], 'back_evolution') - if bes: - return bes.pop() - return None + return self._get_edge_or_none(monster_id, 'back_evolution') def get_prev_evolution_id_by_monster(self, monster: MonsterModel): return self.get_prev_evolution_id_by_monster_id(monster.monster_no) @@ -484,7 +495,7 @@ def get_prev_evolution_by_monster(self, monster: MonsterModel): return pe and self.get_monster(pe) def get_next_evolution_ids_by_monster_id(self, monster_id): - return self._get_edges(self.graph[monster_id], 'evolution') + return self._get_edges(monster_id, 'evolution') def get_next_evolution_ids_by_monster(self, monster: MonsterModel): return self.get_next_evolution_ids_by_monster_id(monster.monster_no) @@ -493,7 +504,7 @@ def get_next_evolutions_by_monster(self, monster: MonsterModel): return {self.get_monster(mid) for mid in self.get_next_evolution_ids_by_monster(monster)} def get_prev_transform_ids_by_monster_id(self, monster_id): - return self._get_edges(self.graph[monster_id], 'back_transformation') + return self._get_edges(monster_id, 'back_transformation') def get_prev_transform_ids_by_monster(self, monster: MonsterModel): return self.get_prev_transform_ids_by_monster_id(monster.monster_no) @@ -502,10 +513,7 @@ def get_prev_transforms_by_monster(self, monster: MonsterModel): return {self.get_monster(mid) for mid in self.get_prev_transform_ids_by_monster(monster)} def get_next_transform_id_by_monster_id(self, monster_id): - bes = self._get_edges(self.graph[monster_id], 'transformation') - if bes: - return bes.pop() - return None + return self._get_edge_or_none(monster_id, 'transformation') def get_next_transform_id_by_monster(self, monster: MonsterModel): return self.get_next_transform_id_by_monster_id(monster.monster_no) @@ -525,7 +533,7 @@ def evo_mats_by_monster(self, monster: MonsterModel) -> List[MonsterModel]: # farmable def monster_is_farmable_by_id(self, monster_id): - return self.graph.nodes[monster_id]['model'].is_farmable + return self.get_monster(monster_id).is_farmable def monster_is_farmable(self, monster: MonsterModel): return self.monster_is_farmable_by_id(monster.monster_no) @@ -539,7 +547,7 @@ def monster_is_farmable_evo(self, monster: MonsterModel): # mp def monster_is_mp_by_id(self, monster_id): - return self.graph.nodes[monster_id]['model'].in_mpshop + return self.get_monster(monster_id).in_mpshop def monster_is_mp(self, monster: MonsterModel): return self.monster_is_mp_by_id(monster.monster_no) @@ -553,7 +561,7 @@ def monster_is_mp_evo(self, monster: MonsterModel): # pem def monster_is_pem_by_id(self, monster_id): - return self.graph.nodes[monster_id]['model'].in_pem + return self.get_monster(monster_id).in_pem def monster_is_pem(self, monster: MonsterModel): return self.monster_is_pem_by_id(monster.monster_no) @@ -567,7 +575,7 @@ def monster_is_pem_evo(self, monster: MonsterModel): # rem def monster_is_rem_by_id(self, monster_id): - return self.graph.nodes[monster_id]['model'].in_rem + return self.get_monster(monster_id).in_rem def monster_is_rem(self, monster: MonsterModel): return self.monster_is_rem_by_id(monster.monster_no) @@ -581,7 +589,7 @@ def monster_is_rem_evo(self, monster: MonsterModel): # redeemable def monster_is_exchange_by_id(self, monster_id): - return bool(self._get_edges(self.graph[monster_id], 'exchange_from')) + return bool(self._get_edges(monster_id, 'exchange_from')) def monster_is_exchange(self, monster: MonsterModel): return self.monster_is_exchange_by_id(monster.monster_no) @@ -594,7 +602,7 @@ def monster_is_exchange_evo(self, monster: MonsterModel): return self.monster_is_exchange_evo_by_id(monster.monster_no) def get_monster_exchange_mat_ids_by_id(self, monster_id): - return self._get_edges(self.graph[monster_id], 'exchange_from') + return self._get_edges(monster_id, 'exchange_from') def get_monster_exchange_mat_ids(self, monster: MonsterModel): return self.get_monster_exchange_mat_ids_by_id(monster.monster_no) @@ -615,7 +623,7 @@ def monster_is_vendor_exchange(self, monster: MonsterModel): def monster_is_new(self, monster: MonsterModel): latest_time = max(am.reg_date for am in self.get_alt_monsters(monster)) return monster.reg_date == latest_time - + def monster_acquisition(self, monster: MonsterModel): if self.monster_is_mp_evo(monster): return 'MP Shop Card' @@ -667,8 +675,14 @@ def evo_gem_monster_by_id(self, monster_id) -> Optional[MonsterModel]: def evo_gem_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: return self.evo_gem_monster_by_id(monster.monster_no) + def get_monster_from_evo_gem(self, monster: MonsterModel) -> Optional[MonsterModel]: + return self.get_monster(self._get_edge_or_none(monster.monster_id, "evo_gem_of")) + + def monster_is_evo_gem(self, monster: MonsterModel) -> bool: + return bool(self.get_monster_from_evo_gem(monster)) + def material_of_ids_by_id(self, monster_id: int) -> List[int]: - return sorted(self._get_edges(self.graph[monster_id], 'material_of')) + return sorted(self._get_edges(monster_id, 'material_of')) def material_of_ids(self, monster: MonsterModel) -> List[int]: return self.material_of_ids_by_id(monster.monster_no) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 27c45d10..5f3413dd 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -3,7 +3,6 @@ import io import logging import re -from collections import defaultdict import aiohttp import tsutils @@ -49,7 +48,6 @@ async def __ainit__(self, monsters, db): self.remove_mods = defaultdict(set) self.treename_overrides = set() - nickname_data, treenames_data, pantheon_data, nt_alias_data, mod_data, treemod_data = await asyncio.gather( sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 5), sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 5), @@ -417,7 +415,7 @@ def is_story(m, do_transform=True): if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) - else: + else: if self.graph.monster_is_vendor_exchange(monster): modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) if self.graph.monster_is_mp_evo(monster): @@ -450,7 +448,7 @@ def is_story(m, do_transform=True): def add_numbered_modifier(self, monster, curr_mods, added_mods, condition, *, else_mods=None): if condition(monster): curr_mods.update(added_mods) - ms = sorted((m for m in self.graph.get_alt_monsters(monster) if condition(m)), key=lambda m: m.monster_id) + ms = [m for m in self.graph.get_alt_monsters(monster) if condition(m)] if len(ms) > 1: curr_mods.update(f'{mod}-{ms.index(monster) + 1}' for mod in added_mods) elif else_mods: diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index 945c7b80..2a6e81f6 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -9,7 +9,7 @@ async def perform_transforminfo_query(dgcog, raw_query, author_id): transformed_mon = None base_mon = None altversions = mgraph.process_alt_versions(found_monster.monster_id) - for mon_id in sorted(altversions): + for mon_id in altversions: if mgraph.monster_is_transform_base_by_id(mon_id): transformed_mon = dgcog.get_monster(mgraph.get_next_transform_id_by_monster_id(mon_id)) if transformed_mon: diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 0c16644a..e3513753 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -50,7 +50,7 @@ async def respond_with_left(message: Optional[Message], ims, **data): @staticmethod def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): if use_evo_scroll: - evos = sorted({*db_context.graph.get_alt_ids_by_id(monster.monster_id)}) + evos = db_context.graph.get_alt_ids_by_id(monster.monster_id) index = evos.index(monster.monster_id) new_id = evos[index - 1] return new_id @@ -77,7 +77,7 @@ async def respond_with_right(message: Optional[Message], ims, **data): @staticmethod def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): if use_evo_scroll: - evos = sorted({*db_context.graph.get_alt_ids_by_id(monster.monster_id)}) + evos = db_context.graph.get_alt_ids_by_id(monster.monster_id) index = evos.index(monster.monster_id) if index == len(evos) - 1: # cycle back to the beginning of the evos list diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 80cf6ba0..fca442a2 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -56,6 +56,5 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): @classmethod def get_alt_monsters(cls, dgcog, monster): db_context = dgcog.database - alt_monsters = sorted(db_context.graph.get_alt_monsters_by_id(monster.monster_no), - key=lambda x: x.monster_id) + alt_monsters = db_context.graph.get_alt_monsters_by_id(monster.monster_id) return alt_monsters From d966c68a684b7b37b0bacc7280f31fd47da513eb Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 6 May 2021 15:22:15 -0700 Subject: [PATCH 0749/1220] Fix ^ls results when 1 monster has no LS (#1026) --- padinfo/core/leader_skills.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 3e17df1c..e1c47ce1 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,5 +1,7 @@ import re +from dadguide.models.leader_skill_model import LeaderSkillModel + def humanize_number(number, sigfigs=2): n = float("{0:.{1}g}".format(number, sigfigs)) @@ -13,19 +15,23 @@ def humanize_number(number, sigfigs=2): return str(int(n)) -def createMultiplierText(ls1, ls2=None): +def createMultiplierText(ls1, ls2=False): if ls2 and not ls1: ls1, ls2 = ls2, ls1 - if ls1: + if isinstance(ls1, LeaderSkillModel): hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = ls1.data else: hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = 1, 1, 1, 0, 0, 0, 0, 0 - if ls2: + if isinstance(ls2, LeaderSkillModel): hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = ls2.data - else: + elif ls2 is False: hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 + elif ls2 is None: + hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = 1, 1, 1, 0, 0, 0, 0, 0 + else: + raise ValueError(f"ls2 must be LeaderSkillModel or None, not {type(ls2)}") return format_ls_text( hp1 * hp2, From 72a10d61ac8319bafe90b0096cd0b5f8f7b184b3 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 May 2021 18:35:07 -0700 Subject: [PATCH 0750/1220] Add flairs to level-reset evos in minized evo view (#1028) Adds floor & ceiling flairs to irreversible & equip evos, respectively --- padinfo/padinfo.py | 13 +++++---- padinfo/view/components/view_state_base_id.py | 20 ++++++++----- padinfo/view/evos.py | 13 ++++----- padinfo/view/id.py | 28 +++++++++++++------ padinfo/view/materials.py | 8 +++--- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 18 ++++++------ padinfo/view/pic.py | 2 +- 8 files changed, 60 insertions(+), 44 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 435b39bb..ff57f72a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -249,7 +249,7 @@ async def send_message(): if await self.config.do_survey(): asyncio.create_task(self.send_survey_after(ctx, query, monster)) - alt_monsters = IdViewState.get_alt_monsters(dgcog, monster) + alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ await IdViewState.query(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] @@ -336,6 +336,7 @@ async def evos(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) + alt_monsters = EvosViewState.get_alt_monsters_and_evos(dgcog, monster) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) if alt_versions is None: @@ -347,7 +348,7 @@ async def evos(self, ctx, *, query: str): initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, alt_versions, alt_versions, gem_versions, + monster, alt_monsters, alt_versions, gem_versions, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.evos_control) @@ -376,7 +377,7 @@ async def evomats(self, ctx, *, query: str): await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) return - alt_monsters = MaterialsViewState.get_alt_monsters(dgcog, monster) + alt_monsters = MaterialsViewState.get_alt_monsters_and_evos(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) @@ -409,7 +410,7 @@ async def pantheon(self, ctx, *, query: str): await ctx.send('Unable to find a pantheon for the result of your query,' + ' [{}] {}.'.format(monster.monster_id, monster.name_en)) return - alt_monsters = PantheonViewState.get_alt_monsters(dgcog, monster) + alt_monsters = PantheonViewState.get_alt_monsters_and_evos(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) @@ -437,7 +438,7 @@ async def pic(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) - alt_monsters = PicViewState.get_alt_monsters(dgcog, monster) + alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) @@ -465,7 +466,7 @@ async def otherinfo(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) - alt_monsters = PicViewState.get_alt_monsters(dgcog, monster) + alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index fca442a2..9f9d4dc1 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, NamedTuple, Optional from discordmenu.embed.view_state import ViewState @@ -7,11 +7,17 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel + from dadguide.models.evolution_model import EvolutionModel + + +class MonsterEvolution(NamedTuple): + monster: "MonsterModel" + evolution: Optional["EvolutionModel"] class ViewStateBaseId(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List["MonsterModel"], + alt_monsters: List[MonsterEvolution], use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): @@ -40,7 +46,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): return None monster = await get_monster_from_ims(dgcog, ims) - alt_monsters = cls.get_alt_monsters(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query @@ -54,7 +60,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): extra_state=ims) @classmethod - def get_alt_monsters(cls, dgcog, monster): - db_context = dgcog.database - alt_monsters = db_context.graph.get_alt_monsters_by_id(monster.monster_id) - return alt_monsters + def get_alt_monsters_and_evos(cls, dgcog, monster) -> List[MonsterEvolution]: + graph = dgcog.database.graph + alt_monsters = graph.get_alt_monsters_by_id(monster.monster_id) + return [MonsterEvolution(m, graph.get_evo_by_monster_id(m.monster_id)) for m in alt_monsters] diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index c298a476..3a8c996c 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -11,7 +11,7 @@ from padinfo.view.common import get_monster_from_ims from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel @@ -19,15 +19,12 @@ class EvosViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, - monster: "MonsterModel", - - # this param is needed to placate the superclass but we won't duplicate evos in this view - _alt_monsters, + monster: "MonsterModel", alt_monsters: List[MonsterEvolution], alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], reaction_list: List[str] = None, use_evo_scroll: bool = True, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_versions, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -50,7 +47,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if alt_versions is None: return None - alt_monsters = alt_versions + alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -107,7 +104,7 @@ def embed(cls, state: EvosViewState): EmbedMain( color=state.color, title=MonsterHeader.long_maybe_tsubaki(state.monster, - "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI ).to_markdown(), url=puzzledragonx(state.monster)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 249ad428..743b62da 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -14,16 +14,28 @@ from padinfo.view.common import get_monster_from_ims from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel from dadguide.models.awakening_model import AwakeningModel +def alt_fmt(monsterevo, state): + if not monsterevo.evolution: + fmt = "{}" + elif not monsterevo.evolution.reversible: + fmt = "⌊{}⌋" + elif monsterevo.monster.is_equip: + fmt = "⌈{}⌉" + else: + fmt = "{}" + return fmt.format(monsterevo.monster.monster_no_na) + + class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List["MonsterModel"], + alt_monsters: List[MonsterEvolution], transform_base, true_evo_type_raw, acquire_raw, base_rarity, fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, is_child: bool = False, extra_state=None): @@ -53,7 +65,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None monster = await get_monster_from_ims(dgcog, ims) - alt_monsters = cls.get_alt_monsters(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ await IdViewState.query(dgcog, monster) @@ -122,8 +134,8 @@ def evos_embed_field(state: ViewStateBaseId): return EmbedField( "Alternate Evos", HighlightableLinks( - links=[LinkedText(str(m.monster_no_na), puzzledragonx(m)) for m in state.alt_monsters], - highlighted=next(i for i, mon in enumerate(state.alt_monsters) if m.monster_id == mon.monster_id) + links=[LinkedText(alt_fmt(me, state), puzzledragonx(m)) for me in state.alt_monsters], + highlighted=next(i for i, me in enumerate(state.alt_monsters) if m.monster_id == me.monster.monster_id) ) ) @@ -173,8 +185,8 @@ def killers_row(m: "MonsterModel", transform_base): return Box( BoldText('Available killers:'), Text('\N{DOWN-POINTING RED TRIANGLE}' if m != transform_base else ''), - Text('[{} slots]'.format(m.latent_slots if m == transform_base \ - else transform_base.latent_slots)), + Text('[{} slots]'.format(m.latent_slots if m == transform_base + else transform_base.latent_slots)), Text(killers_text), delimiter=' ' ) @@ -281,7 +293,7 @@ def embed(cls, state: IdViewState): EmbedMain( color=state.color, title=MonsterHeader.long_maybe_tsubaki(m, - "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI ).to_markdown(), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 7a077433..6be7ef2b 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -12,7 +12,7 @@ from padinfo.view.base import BaseIdView from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution from padinfo.view.common import get_monster_from_ims from padinfo.view.id import evos_embed_field @@ -24,7 +24,7 @@ class MaterialsViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List["MonsterModel"], + alt_monsters: List[MonsterEvolution], mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, gem_override: bool, @@ -63,7 +63,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if mats is None: return None - alt_monsters = cls.get_alt_monsters(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query menu_type = ims['menu_type'] @@ -155,7 +155,7 @@ def embed(cls, state: MaterialsViewState): EmbedMain( color=state.color, title=MonsterHeader.long_maybe_tsubaki(state.monster, - "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI ).to_markdown(), url=puzzledragonx(state.monster) ), diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 5185c14f..879bebd4 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -53,7 +53,7 @@ def embed(cls, state: OtherInfoViewState): EmbedMain( color=state.color, title=MonsterHeader.long_maybe_tsubaki(state.monster, - "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI ).to_markdown(), url=puzzledragonx(m)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 0d4893e9..00f1acaf 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -13,7 +13,7 @@ from padinfo.view.common import get_monster_from_ims from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution from padinfo.view.id import evos_embed_field if TYPE_CHECKING: @@ -26,7 +26,7 @@ class PantheonViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters, - pantheon_list: List["MonsterModel"], series_name: str, base_monster, + pantheon_list: List[MonsterEvolution], series_name: str, base_monster, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): @@ -55,7 +55,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if pantheon_list is None: return None - alt_monsters = cls.get_alt_monsters(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -122,20 +122,20 @@ async def query(cls, dgcog, monster): if main_att.name == NIL_ATT: att_emoji = get_attribute_emoji_by_enum(sub_att) main_att_list = [m for m in rarity_list if m.attr1.name == sub_att.name - or (m.attr1.name == NIL_ATT - and m.attr2.name == sub_att.name)] + or (m.attr1.name == NIL_ATT + and m.attr2.name == sub_att.name)] else: att_emoji = get_attribute_emoji_by_enum(main_att) main_att_list = [m for m in rarity_list if m.attr1.name == main_att.name - or (m.attr1.name == NIL_ATT - and m.attr2.name == main_att.name)] + or (m.attr1.name == NIL_ATT + and m.attr2.name == main_att.name)] if 0 < len(main_att_list) <= MAX_MONS_TO_SHOW: # append after check this time, because if we go to subatt we only want one emoji filters.append(att_emoji) return main_att_list, cls.make_series_name(series_name, filters), base_mon sub_att_list = [m for m in main_att_list if m.attr1.name == main_att.name - and m.attr2.name == sub_att.name] + and m.attr2.name == sub_att.name] filters.append(get_attribute_emoji_by_monster(base_mon)) if 0 < len(sub_att_list) <= MAX_MONS_TO_SHOW: return sub_att_list, cls.make_series_name(series_name, filters), base_mon @@ -171,7 +171,7 @@ def embed(cls, state: PantheonViewState): EmbedMain( color=state.color, title=MonsterHeader.long_maybe_tsubaki(state.monster, - "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI ).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 076becf3..8f758ba5 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -55,7 +55,7 @@ def embed(cls, state: PicViewState): EmbedMain( color=state.color, title=MonsterHeader.long_maybe_tsubaki(state.monster, - "!" if state.alt_monsters[0].monster_id == cls.TSUBAKI else "" + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI ).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), From e5994dab02cd60f1f216b0163d3c9644139bf520 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 May 2021 19:07:27 -0700 Subject: [PATCH 0751/1220] Fix `&` -> `and` replacement (#1030) Before, we were splitting by any non-word character. Now, you have to use commas or whitespace to separate in sheet. This also fixes issues caused by https://github.com/PyCQA/pycodestyle/issues/988 --- dadguide/monster_index.py | 26 +++++++++++++------------- dadguide/token_mappings.py | 11 +++++------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 5f3413dd..828ec299 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -61,10 +61,10 @@ async def __ainit__(self, monsters, db): if m_id.isdigit() and not i: name = name.strip().lower() mid = int(m_id) - if ov: - self.treename_overrides.add(mid) if lp: self.monster_id_to_nametokens[mid].update(self._name_to_tokens(name)) + if ov: + self.treename_overrides.add(mid) else: if " " in name: self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(mid)) @@ -96,7 +96,7 @@ async def __ainit__(self, monsters, db): next(nt_alias_data) # Skip over heading for tokens, alias in nt_alias_data: - self.replacement_tokens[frozenset(re.split(r'\W+', tokens))].add(alias) + self.replacement_tokens[frozenset(re.split(r'[,\s]+', tokens))].add(alias) self.manual_prefixes = defaultdict(set) for mid, mods, rmv in mod_data: @@ -321,13 +321,13 @@ async def get_modifiers(self, monster: MonsterModel): # Evo self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.EVO], - lambda m: self.graph.monster_is_normal_evo(m) - or self.graph.monster_is_first_evo(m)) + lambda m: (self.graph.monster_is_normal_evo(m) + or self.graph.monster_is_first_evo(m))) # Uvo self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.UVO], - lambda m: self.graph.monster_is_reversible_evo(m) - and not special_evo) + lambda m: (self.graph.monster_is_reversible_evo(m) + and not special_evo)) # UUvo self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.UUVO], @@ -337,8 +337,8 @@ async def get_modifiers(self, monster: MonsterModel): self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.TRANS], lambda m: not self.graph.monster_is_transform_base(m)) self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.BASETRANS], - lambda m: self.graph.monster_is_transform_base(m) - and self.graph.get_next_transform_by_monster(m)) + lambda m: (self.graph.monster_is_transform_base(m) + and self.graph.get_next_transform_by_monster(m))) # Awoken self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.AWOKEN], @@ -358,8 +358,8 @@ async def get_modifiers(self, monster: MonsterModel): # Pixel self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.PIXEL], - lambda m: m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') - or self.graph.true_evo_type_by_monster(m).value == "Pixel", + lambda m: (m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') + or self.graph.true_evo_type_by_monster(m).value == "Pixel"), else_mods=EVO_MAP[EvoTypes.NONPIXEL]) # Awakenings @@ -376,8 +376,8 @@ async def get_modifiers(self, monster: MonsterModel): # Chibi self.add_numbered_modifier(monster, modifiers, MISC_MAP[MiscModifiers.CHIBI], - lambda m: m.name_en == m.name_en.lower() and m.name_en != m.name_ja - or 'ミニ' in m.name_ja or '(chibi)' in m.name_en) + lambda m: (m.name_en == m.name_en.lower() and m.name_en != m.name_ja + or 'ミニ' in m.name_ja or '(chibi)' in m.name_en)) # Series Type if monster.series.series_type == 'regular': diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index b8fe3c0c..09992a03 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -30,16 +30,16 @@ def inverse_map(token_map: Mapping[K, Tuple[V]]) -> Mapping[V, Tuple[K]]: DUAL_COLOR_MAP = {} for cid1, cns1 in COLOR_MAP.items(): for cid2, cns2 in COLOR_MAP.items(): - ts = () + _ts = () for t1 in cns1: for t2 in cns2: if t2 in ("white",): continue if len(t1) + len(t2) == 2: - ts += (t1 + t2,) + _ts += (t1 + t2,) if (len(t1) == 1) == (len(t2) == 1): - ts += (t1 + "/" + t2,) - DUAL_COLOR_MAP[(cid1, cid2)] = ts + _ts += (t1 + "/" + t2,) + DUAL_COLOR_MAP[(cid1, cid2)] = _ts TYPE_MAP = { MonsterType.Evolve: ('evolve',), @@ -221,8 +221,7 @@ class AwokenSkills(Enum): AwokenSkills.EQUIP: ('equip', 'assist', 'eq'), AwokenSkills.SUPERFUA: ('sfua',), AwokenSkills.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), - AwokenSkills.UNBINDABLE: ('unbindable', '' - ''), + AwokenSkills.UNBINDABLE: ('unbindable', 'bindres'), AwokenSkills.EXTMOVEPLUS: ('te+', 'te', 'finger+', 'finger'), AwokenSkills.CLOUDRESIST: ('cloudres', 'cloud'), AwokenSkills.TAPERESIST: ('taperes', 'tape'), From 6344c6943a7623704427eacdbef3becf6f83bbc4 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 May 2021 19:08:34 -0700 Subject: [PATCH 0752/1220] Remove MonsterModel.monster_no (#1029) --- dadguide/models/monster_model.py | 1 - dadguide/monster_graph.py | 52 ++++++++++++++++---------------- padglobal/padglobal.py | 6 ++-- padinfo/view/evos.py | 2 +- padinfo/view/id.py | 2 +- padinfo/view/monster_list.py | 2 +- padmonitor/padmonitor.py | 6 ++-- 7 files changed, 35 insertions(+), 36 deletions(-) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 6b1a6e66..0441284b 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -16,7 +16,6 @@ class MonsterModel(BaseModel): def __init__(self, **m): self.monster_id = m['monster_id'] - self.monster_no = self.monster_id self.monster_no_jp = m['monster_no_jp'] self.monster_no_na = m['monster_no_na'] self.monster_no_kr = m['monster_no_kr'] diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index ee9d55f1..3353ac33 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -400,7 +400,7 @@ def monster_is_transform_base_by_id(self, monster_id: int) -> bool: return self.get_transform_base_id_by_id(monster_id) == monster_id def monster_is_transform_base(self, monster: MonsterModel) -> bool: - return self.monster_is_transform_base_by_id(monster.monster_no) + return self.monster_is_transform_base_by_id(monster.monster_id) def get_numerical_sort_top_id_by_id(self, monster_id): alt_cards = self.get_alt_ids_by_id(monster_id) @@ -422,7 +422,7 @@ def cur_evo_type_by_monster_id(self, monster_id: int) -> EvoType: return EvoType(prev_evo.evolution_type) if prev_evo else EvoType.Base def cur_evo_type_by_monster(self, monster: MonsterModel) -> EvoType: - return self.cur_evo_type_by_monster_id(monster.monster_no) + return self.cur_evo_type_by_monster_id(monster.monster_id) def monster_is_reversible_evo(self, monster: MonsterModel) -> bool: prev_evo = self.get_evo_by_monster(monster) @@ -482,13 +482,13 @@ def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: return InternalEvoType.Normal def true_evo_type_by_monster(self, monster: MonsterModel) -> InternalEvoType: - return self.true_evo_type_by_monster_id(monster.monster_no) + return self.true_evo_type_by_monster_id(monster.monster_id) def get_prev_evolution_id_by_monster_id(self, monster_id): return self._get_edge_or_none(monster_id, 'back_evolution') def get_prev_evolution_id_by_monster(self, monster: MonsterModel): - return self.get_prev_evolution_id_by_monster_id(monster.monster_no) + return self.get_prev_evolution_id_by_monster_id(monster.monster_id) def get_prev_evolution_by_monster(self, monster: MonsterModel): pe = self.get_prev_evolution_id_by_monster(monster) @@ -498,7 +498,7 @@ def get_next_evolution_ids_by_monster_id(self, monster_id): return self._get_edges(monster_id, 'evolution') def get_next_evolution_ids_by_monster(self, monster: MonsterModel): - return self.get_next_evolution_ids_by_monster_id(monster.monster_no) + return self.get_next_evolution_ids_by_monster_id(monster.monster_id) def get_next_evolutions_by_monster(self, monster: MonsterModel): return {self.get_monster(mid) for mid in self.get_next_evolution_ids_by_monster(monster)} @@ -507,7 +507,7 @@ def get_prev_transform_ids_by_monster_id(self, monster_id): return self._get_edges(monster_id, 'back_transformation') def get_prev_transform_ids_by_monster(self, monster: MonsterModel): - return self.get_prev_transform_ids_by_monster_id(monster.monster_no) + return self.get_prev_transform_ids_by_monster_id(monster.monster_id) def get_prev_transforms_by_monster(self, monster: MonsterModel): return {self.get_monster(mid) for mid in self.get_prev_transform_ids_by_monster(monster)} @@ -516,7 +516,7 @@ def get_next_transform_id_by_monster_id(self, monster_id): return self._get_edge_or_none(monster_id, 'transformation') def get_next_transform_id_by_monster(self, monster: MonsterModel): - return self.get_next_transform_id_by_monster_id(monster.monster_no) + return self.get_next_transform_id_by_monster_id(monster.monster_id) def get_next_transform_by_monster(self, monster: MonsterModel): nt = self.get_next_transform_id_by_monster(monster) @@ -529,96 +529,96 @@ def evo_mats_by_monster_id(self, monster_id: int) -> List[MonsterModel]: return [self.get_monster(mat) for mat in evo.mats] def evo_mats_by_monster(self, monster: MonsterModel) -> List[MonsterModel]: - return self.evo_mats_by_monster_id(monster.monster_no) + return self.evo_mats_by_monster_id(monster.monster_id) # farmable def monster_is_farmable_by_id(self, monster_id): return self.get_monster(monster_id).is_farmable def monster_is_farmable(self, monster: MonsterModel): - return self.monster_is_farmable_by_id(monster.monster_no) + return self.monster_is_farmable_by_id(monster.monster_id) def monster_is_farmable_evo_by_id(self, monster_id): return any( m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_farmable_by_id(m)) def monster_is_farmable_evo(self, monster: MonsterModel): - return self.monster_is_farmable_evo_by_id(monster.monster_no) + return self.monster_is_farmable_evo_by_id(monster.monster_id) # mp def monster_is_mp_by_id(self, monster_id): return self.get_monster(monster_id).in_mpshop def monster_is_mp(self, monster: MonsterModel): - return self.monster_is_mp_by_id(monster.monster_no) + return self.monster_is_mp_by_id(monster.monster_id) def monster_is_mp_evo_by_id(self, monster_id): return any( m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_mp_by_id(m)) def monster_is_mp_evo(self, monster: MonsterModel): - return self.monster_is_mp_evo_by_id(monster.monster_no) + return self.monster_is_mp_evo_by_id(monster.monster_id) # pem def monster_is_pem_by_id(self, monster_id): return self.get_monster(monster_id).in_pem def monster_is_pem(self, monster: MonsterModel): - return self.monster_is_pem_by_id(monster.monster_no) + return self.monster_is_pem_by_id(monster.monster_id) def monster_is_pem_evo_by_id(self, monster_id): return any( m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_pem_by_id(m)) def monster_is_pem_evo(self, monster: MonsterModel): - return self.monster_is_pem_evo_by_id(monster.monster_no) + return self.monster_is_pem_evo_by_id(monster.monster_id) # rem def monster_is_rem_by_id(self, monster_id): return self.get_monster(monster_id).in_rem def monster_is_rem(self, monster: MonsterModel): - return self.monster_is_rem_by_id(monster.monster_no) + return self.monster_is_rem_by_id(monster.monster_id) def monster_is_rem_evo_by_id(self, monster_id): return any( m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_rem_by_id(m)) def monster_is_rem_evo(self, monster: MonsterModel): - return self.monster_is_rem_evo_by_id(monster.monster_no) + return self.monster_is_rem_evo_by_id(monster.monster_id) # redeemable def monster_is_exchange_by_id(self, monster_id): return bool(self._get_edges(monster_id, 'exchange_from')) def monster_is_exchange(self, monster: MonsterModel): - return self.monster_is_exchange_by_id(monster.monster_no) + return self.monster_is_exchange_by_id(monster.monster_id) def monster_is_exchange_evo_by_id(self, monster_id): return any( m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_exchange_by_id(m)) def monster_is_exchange_evo(self, monster: MonsterModel): - return self.monster_is_exchange_evo_by_id(monster.monster_no) + return self.monster_is_exchange_evo_by_id(monster.monster_id) def get_monster_exchange_mat_ids_by_id(self, monster_id): return self._get_edges(monster_id, 'exchange_from') def get_monster_exchange_mat_ids(self, monster: MonsterModel): - return self.get_monster_exchange_mat_ids_by_id(monster.monster_no) + return self.get_monster_exchange_mat_ids_by_id(monster.monster_id) def get_monster_exchange_mats_by_id(self, monster_id): return {self.get_monster(mid) for mid in self.get_monster_exchange_mat_ids_by_id(monster_id)} def get_monster_exchange_mats(self, monster: MonsterModel): - return self.get_monster_exchange_mats_by_id(monster.monster_no) + return self.get_monster_exchange_mats_by_id(monster.monster_id) def monster_is_vendor_exchange_by_id(self, monster_id): ids = self.get_monster_exchange_mats_by_id(monster_id) return bool(ids) and all(15 in [t.value for t in m.types] for m in ids) def monster_is_vendor_exchange(self, monster: MonsterModel): - return self.monster_is_vendor_exchange_by_id(monster.monster_no) + return self.monster_is_vendor_exchange_by_id(monster.monster_id) def monster_is_new(self, monster: MonsterModel): latest_time = max(am.reg_date for am in self.get_alt_monsters(monster)) @@ -645,7 +645,7 @@ def numeric_next_monster_id_by_id(self, monster_id: int) -> Optional[int]: return next_monster.monster_id def numeric_next_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: - next_monster_id = self.numeric_next_monster_id_by_id(monster.monster_no) + next_monster_id = self.numeric_next_monster_id_by_id(monster.monster_id) if next_monster_id is None: return None return self.get_monster(next_monster_id) @@ -661,7 +661,7 @@ def numeric_prev_monster_id_by_id(self, monster_id) -> Optional[int]: return prev_monster.monster_id def numeric_prev_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: - prev_monster_id = self.numeric_prev_monster_id_by_id(monster.monster_no) + prev_monster_id = self.numeric_prev_monster_id_by_id(monster.monster_id) if prev_monster_id is None: return None return self.get_monster(prev_monster_id) @@ -673,7 +673,7 @@ def evo_gem_monster_by_id(self, monster_id) -> Optional[MonsterModel]: return self.get_monster(this_monster.evo_gem_id) def evo_gem_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: - return self.evo_gem_monster_by_id(monster.monster_no) + return self.evo_gem_monster_by_id(monster.monster_id) def get_monster_from_evo_gem(self, monster: MonsterModel) -> Optional[MonsterModel]: return self.get_monster(self._get_edge_or_none(monster.monster_id, "evo_gem_of")) @@ -685,8 +685,8 @@ def material_of_ids_by_id(self, monster_id: int) -> List[int]: return sorted(self._get_edges(monster_id, 'material_of')) def material_of_ids(self, monster: MonsterModel) -> List[int]: - return self.material_of_ids_by_id(monster.monster_no) + return self.material_of_ids_by_id(monster.monster_id) def material_of_monsters(self, monster: MonsterModel) -> List[MonsterModel]: return [self.get_monster(m) - for m in self.material_of_ids_by_id(monster.monster_no)] + for m in self.material_of_ids_by_id(monster.monster_id)] diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 0ddf14cf..30e1442d 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -643,8 +643,8 @@ async def _resolve_which(self, ctx, term): elif db_context.graph.monster_is_farmable_evo(monster): return name, FARMABLE_MSG, None, False elif check_simple_tree(monster, db_context): - top_monster = db_context.graph.get_numerical_sort_top_monster_by_id(monster.monster_no) - return name, SIMPLE_TREE_MSG.format(top_monster.monster_no, top_monster.name_en), None, False + top_monster = db_context.graph.get_numerical_sort_top_monster_by_id(monster.monster_id) + return name, SIMPLE_TREE_MSG.format(top_monster.monster_id, top_monster.name_en), None, False else: await ctx.send('No which info for {} (#{})'.format(name, monster_id)) return None, None, None, None @@ -1199,7 +1199,7 @@ def emojify(self, message): def check_simple_tree(monster, db_context): attr1 = monster.attr1 active_skill = monster.active_skill - for m in db_context.graph.get_alt_monsters_by_id(monster.monster_no): + for m in db_context.graph.get_alt_monsters_by_id(monster.monster_id): if m.attr1 != attr1 or m.active_skill.active_skill_id != active_skill.active_skill_id: return False if m.is_equip: diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 3a8c996c..bdefda91 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -66,7 +66,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): @staticmethod async def query(dgcog, monster): db_context = dgcog.database - alt_versions = sorted(db_context.graph.get_alt_monsters_by_id(monster.monster_no), + alt_versions = sorted(db_context.graph.get_alt_monsters_by_id(monster.monster_id), key=lambda x: x.monster_id) gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) if len(alt_versions) == 1 and len(gem_versions) == 0: diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 743b62da..7d58bba7 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -100,7 +100,7 @@ async def _get_monster_misc_info(cls, db_context, monster): transform_base = db_context.graph.get_transform_base(monster) true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value acquire_raw = db_context.graph.monster_acquisition(monster) - base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_no).rarity + base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_id).rarity return acquire_raw, base_rarity, transform_base, true_evo_type_raw diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list.py index 1acca799..580b0ffa 100644 --- a/padinfo/view/monster_list.py +++ b/padinfo/view/monster_list.py @@ -35,7 +35,7 @@ def serialize(self): ret.update({ 'pane_type': MonsterListView.VIEW_TYPE, 'title': self.title, - 'monster_list': [str(m.monster_no) for m in self.monster_list], + 'monster_list': [str(m.monster_id) for m in self.monster_list], 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, 'message': self.message, diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index d9b91195..dcf2d979 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -48,8 +48,8 @@ async def check_seen(self): await DGCOG.wait_until_ready() db_context: "DbContext" = DGCOG.database all_monsters = db_context.get_all_monsters(as_generator=False) - jp_monster_map = {m.monster_no: m for m in all_monsters if m.on_jp} - na_monster_map = {m.monster_no: m for m in all_monsters if m.on_na} + jp_monster_map = {m.monster_id: m for m in all_monsters if m.on_jp} + na_monster_map = {m.monster_id: m for m in all_monsters if m.on_na} def process(existing, new_map, name): if not existing: @@ -68,7 +68,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: - msg += '\n\t[{}] {}'.format(m.monster_no, m.unoverridden_name_en) + msg += '\n\t[{}] {}'.format(m.monster_id, m.unoverridden_name_en) if tsutils.contains_ja(m.name_en) and m.name_en_override is not None: msg += ' ({})'.format(m.name_en_override) return msg From eb2e9c8ed834ed8da7d347e39dffa6f2c58bddac Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 May 2021 19:14:59 -0700 Subject: [PATCH 0753/1220] Check for typechecking before importing cross-cog (#1032) --- padinfo/core/leader_skills.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index e1c47ce1..631abf93 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,6 +1,8 @@ import re +from typing import TYPE_CHECKING -from dadguide.models.leader_skill_model import LeaderSkillModel +if TYPE_CHECKING: + from dadguide.models.leader_skill_model import LeaderSkillModel def humanize_number(number, sigfigs=2): From aa61132ac85380612bad6f27d4ba158ea5ff05dd Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 May 2021 19:21:14 -0700 Subject: [PATCH 0754/1220] Bump TSUtils version (#1033) --- dadguide/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/info.json b/dadguide/info.json index e811d2b5..37a3e8e6 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils>=3.2.3", + "tsutils>=3.3.3", "pytz", "romkan", "networkx" From 9be32afa2f160bdf26398cee7fb3b8d176ace130 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 May 2021 19:26:15 -0700 Subject: [PATCH 0755/1220] Use reflection for LSModel bc what is cross-cog (#1034) --- padinfo/core/leader_skills.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 631abf93..a7eebec4 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,8 +1,4 @@ import re -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from dadguide.models.leader_skill_model import LeaderSkillModel def humanize_number(number, sigfigs=2): @@ -21,12 +17,12 @@ def createMultiplierText(ls1, ls2=False): if ls2 and not ls1: ls1, ls2 = ls2, ls1 - if isinstance(ls1, LeaderSkillModel): + if ls1.__class__.__name__ == "LeaderSkillModel": hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = ls1.data else: hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 = 1, 1, 1, 0, 0, 0, 0, 0 - if isinstance(ls2, LeaderSkillModel): + if ls2.__class__.__name__ == "LeaderSkillModel": hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = ls2.data elif ls2 is False: hp2, atk2, rcv2, resist2, combo2, fua2, mfua2, te2 = hp1, atk1, rcv1, resist1, combo1, fua1, mfua1, te1 From b0aad81a589251999a0a2fd5b842a750188c2d53 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Sun, 9 May 2021 22:44:11 -0400 Subject: [PATCH 0756/1220] Fix links to PDX in alt evos (#1035) --- padinfo/view/id.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 7d58bba7..ced1dcce 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -130,12 +130,12 @@ def _monster_is_enhance(m: "MonsterModel"): def evos_embed_field(state: ViewStateBaseId): - m = state.monster + monster = state.monster return EmbedField( "Alternate Evos", HighlightableLinks( - links=[LinkedText(alt_fmt(me, state), puzzledragonx(m)) for me in state.alt_monsters], - highlighted=next(i for i, me in enumerate(state.alt_monsters) if m.monster_id == me.monster.monster_id) + links=[LinkedText(alt_fmt(me, state), puzzledragonx(me.monster)) for me in state.alt_monsters], + highlighted=next(i for i, me in enumerate(state.alt_monsters) if monster.monster_id == me.monster.monster_id) ) ) From dd9644664b8539093e87c2c9cfbb7759e93080c3 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 May 2021 19:49:15 -0700 Subject: [PATCH 0757/1220] Add monster_name_overrides to monster query (#1027) --- dadguide/monster_graph.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 3353ac33..6a474850 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -19,6 +19,7 @@ MONSTER_QUERY = """SELECT monsters.*, + COALESCE(monster_name_overrides.name_en, monsters.name_en_override) AS name_en_override, leader_skills.name_ja AS ls_name_ja, leader_skills.name_en AS ls_name_en, leader_skills.name_ko AS ls_name_ko, @@ -56,6 +57,7 @@ LEFT OUTER JOIN monsters AS target_monsters ON monsters.name_ja || 'の希石' = target_monsters.name_ja LEFT OUTER JOIN exchanges ON target_monsters.monster_id = exchanges.target_monster_id LEFT OUTER JOIN drops ON monsters.monster_id = drops.monster_id + LEFT OUTER JOIN monster_name_overrides ON monsters.monster_id = monster_name_overrides.monster_id GROUP BY monsters.monster_id""" From f2de70bfc4166c42d8bc0bce89a0650a5d6104b9 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 9 May 2021 23:44:53 -0500 Subject: [PATCH 0758/1220] Add legend to evo list title (#1037) wew embedded assistance --- padinfo/view/id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index ced1dcce..4dfc1936 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -132,7 +132,7 @@ def _monster_is_enhance(m: "MonsterModel"): def evos_embed_field(state: ViewStateBaseId): monster = state.monster return EmbedField( - "Alternate Evos", + "**Evos** - ⌊Irreversible⌋ ⌈Equip⌉", HighlightableLinks( links=[LinkedText(alt_fmt(me, state), puzzledragonx(me.monster)) for me in state.alt_monsters], highlighted=next(i for i, me in enumerate(state.alt_monsters) if monster.monster_id == me.monster.monster_id) From 208b3517295b4be774f5820f80e415e16c372e5c Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 10 May 2021 15:01:58 -0700 Subject: [PATCH 0759/1220] Add series{sid} as modifiers (#1045) Closes #859 --- dadguide/monster_index.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 828ec299..f5d3395b 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -302,6 +302,7 @@ async def get_modifiers(self, monster: MonsterModel): modifiers.update(TYPE_MAP[mt]) # Series + modifiers.add("series" + str(monster.series_id)) if monster.series_id in self.series_id_to_pantheon_nickname: modifiers.update(self.series_id_to_pantheon_nickname[monster.series_id]) @@ -359,7 +360,7 @@ async def get_modifiers(self, monster: MonsterModel): # Pixel self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.PIXEL], lambda m: (m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') - or self.graph.true_evo_type_by_monster(m).value == "Pixel"), + or self.graph.true_evo_type_by_monster(m).value == "Pixel"), else_mods=EVO_MAP[EvoTypes.NONPIXEL]) # Awakenings @@ -377,7 +378,7 @@ async def get_modifiers(self, monster: MonsterModel): # Chibi self.add_numbered_modifier(monster, modifiers, MISC_MAP[MiscModifiers.CHIBI], lambda m: (m.name_en == m.name_en.lower() and m.name_en != m.name_ja - or 'ミニ' in m.name_ja or '(chibi)' in m.name_en)) + or 'ミニ' in m.name_ja or '(chibi)' in m.name_en)) # Series Type if monster.series.series_type == 'regular': From 5be30915fff47577f56da0f2d253cc1eadf3db9b Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 10 May 2021 16:29:11 -0700 Subject: [PATCH 0760/1220] Fix true_evo_type & stop using current evo type (#1046) --- dadguide/models/enum_types.py | 8 ------- dadguide/models/evolution_model.py | 7 ++++--- dadguide/monster_graph.py | 28 ++++++++----------------- tests/dadguide/database_context_test.py | 8 +------ 4 files changed, 14 insertions(+), 37 deletions(-) diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 66976de8..3d69954d 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -27,14 +27,6 @@ class MonsterType(Enum): Vendor = 15 -class EvoType(Enum): - """Evo types supported by DadGuide. Numbers correspond to their id values.""" - Base = 0 # Represents monsters who didn't require evo - Evo = 1 - UvoAwoken = 2 - UuvoReincarnated = 3 - - class InternalEvoType(Enum): """Evo types unsupported by DadGuide.""" Base = "Base" diff --git a/dadguide/models/evolution_model.py b/dadguide/models/evolution_model.py index 518dc543..f26ddfa9 100644 --- a/dadguide/models/evolution_model.py +++ b/dadguide/models/evolution_model.py @@ -4,7 +4,7 @@ class EvolutionModel(BaseModel): def __init__(self, **kwargs): self.evolution_type = kwargs['evolution_type'] - self.reversible = kwargs['reversible'] + self.reversible = bool(kwargs['reversible']) self.from_id = kwargs['from_id'] self.to_id = kwargs['to_id'] self.mat_1_id = kwargs['mat_1_id'] @@ -13,8 +13,9 @@ def __init__(self, **kwargs): self.mat_4_id = kwargs['mat_4_id'] self.mat_5_id = kwargs['mat_5_id'] self.mats = list(filter(None, [self.mat_1_id, self.mat_2_id, self.mat_3_id, self.mat_4_id, self.mat_5_id])) - self.is_pixel = 3826 in self.mats - self.is_super_reincarnated = 5077 in self.mats + self.is_pixel = self.mat_1_id == 3826 + self.is_super_reincarnated = self.mat_1_id == 5077 + self.is_assist = self.mat_1_id == 3911 self.tstamp = kwargs['tstamp'] def to_dict(self): diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 6a474850..98974fa8 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -11,7 +11,7 @@ from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel -from .models.enum_types import EvoType, InternalEvoType +from .models.enum_types import InternalEvoType from .models.evolution_model import EvolutionModel from .models.leader_skill_model import LeaderSkillModel from .models.monster_model import MonsterModel @@ -419,13 +419,6 @@ def get_evo_by_monster_id(self, monster_id) -> Optional[EvolutionModel]: def get_evo_by_monster(self, monster) -> Optional[EvolutionModel]: return self._get_edge_model(monster.monster_id, 'back_evolution') - def cur_evo_type_by_monster_id(self, monster_id: int) -> EvoType: - prev_evo = self.get_evo_by_monster_id(monster_id) - return EvoType(prev_evo.evolution_type) if prev_evo else EvoType.Base - - def cur_evo_type_by_monster(self, monster: MonsterModel) -> EvoType: - return self.cur_evo_type_by_monster_id(monster.monster_id) - def monster_is_reversible_evo(self, monster: MonsterModel) -> bool: prev_evo = self.get_evo_by_monster(monster) return prev_evo is not None and prev_evo.reversible @@ -433,11 +426,9 @@ def monster_is_reversible_evo(self, monster: MonsterModel) -> bool: def monster_is_reincarnated(self, monster: MonsterModel) -> bool: if self.monster_is_reversible_evo(monster): return False - prev = self.get_prev_evolution_by_monster(monster) - while prev: - if not self.monster_is_reversible_evo(prev): + while (monster := self.get_prev_evolution_by_monster(monster)): + if self.monster_is_reversible_evo(monster): return True - prev = self.get_prev_evolution_by_monster(prev) return False def monster_is_normal_evo(self, monster: MonsterModel) -> bool: @@ -459,6 +450,10 @@ def monster_is_second_ultimate(self, monster: MonsterModel) -> bool: return False def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: + monster = self.get_monster(monster_id) + if monster.is_equip: + return InternalEvoType.Assist + if self.get_base_id_by_id(monster_id) == monster_id: return InternalEvoType.Base @@ -471,14 +466,9 @@ def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: elif evo.is_pixel: return InternalEvoType.Pixel - monster = self.get_monster(monster_id) - if monster.is_equip: - return InternalEvoType.Assist - - cur_evo_type = self.cur_evo_type_by_monster_id(monster_id) - if cur_evo_type == EvoType.UuvoReincarnated: + if self.monster_is_reincarnated(monster): return InternalEvoType.Reincarnated - elif cur_evo_type == EvoType.UvoAwoken: + elif self.monster_is_reversible_evo(monster): return InternalEvoType.Ultimate return InternalEvoType.Normal diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index 6bf83f10..d2b02201 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -1,7 +1,7 @@ from dadguide.database_manager import DadguideDatabase from dadguide.database_context import DbContext from dadguide.monster_graph import MonsterGraph -from dadguide.models.enum_types import EvoType, InternalEvoType +from dadguide.models.enum_types import InternalEvoType database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) @@ -17,12 +17,6 @@ # evo types # 5392 is mega dkali assert db_context.graph.get_prev_evolution_id_by_monster_id(5392) == 1588 -assert db_context.graph.cur_evo_type_by_monster_id(5392) == EvoType.UvoAwoken -assert db_context.graph.cur_evo_type_by_monster_id(1587) == EvoType.Base # base dkali -assert db_context.graph.cur_evo_type_by_monster_id(6238) == EvoType.UuvoReincarnated # b sam3 -assert db_context.graph.cur_evo_type_by_monster_id(3270) == EvoType.UuvoReincarnated # revo hades -assert db_context.graph.cur_evo_type_by_monster_id(3391) == EvoType.UuvoReincarnated # revo blodin -assert db_context.graph.cur_evo_type_by_monster_id(1748) == EvoType.UvoAwoken # awoken hades assert db_context.graph.true_evo_type_by_monster_id(5392) == InternalEvoType.Ultimate assert db_context.graph.true_evo_type_by_monster_id(6337) == InternalEvoType.SuperReincarnated # sr hades From a64a21c94827998452d139561f0aafe1f1702490 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 10 May 2021 17:00:16 -0700 Subject: [PATCH 0761/1220] Ensure REM cards are not shown as MP/Shop (#1047) --- dadguide/monster_graph.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 98974fa8..d5b53fc8 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -617,14 +617,14 @@ def monster_is_new(self, monster: MonsterModel): return monster.reg_date == latest_time def monster_acquisition(self, monster: MonsterModel): - if self.monster_is_mp_evo(monster): + if self.monster_is_rem_evo(monster): + return 'REM Card' + elif self.monster_is_mp_evo(monster): return 'MP Shop Card' elif self.monster_is_farmable_evo(monster): return 'Farmable Card' elif self.monster_is_pem_evo(monster): return 'PEM Card' - elif self.monster_is_rem_evo(monster): - return 'REM Card' def numeric_next_monster_id_by_id(self, monster_id: int) -> Optional[int]: next_monster = None From f1f6fafe3c4ab008cc5828e29f86f1cefca2e4d5 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Mon, 10 May 2021 20:45:07 -0400 Subject: [PATCH 0762/1220] Only show equip/irreversible in legend if needed (#1048) --- padinfo/view/id.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 4dfc1936..799b99b4 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -131,11 +131,24 @@ def _monster_is_enhance(m: "MonsterModel"): def evos_embed_field(state: ViewStateBaseId): monster = state.monster + irreversible = False + equip = False + for alt_evo in state.alt_monsters: + if not alt_evo.evolution: + continue + elif not alt_evo.evolution.reversible: + irreversible = True + elif alt_evo.monster.is_equip: + equip = True + else: + continue return EmbedField( - "**Evos** - ⌊Irreversible⌋ ⌈Equip⌉", + "**Evos**" + (" - " if irreversible or equip else "") + + ("⌊Irreversible⌋" if irreversible else "") + ("⌈Equip⌉" if equip else ""), HighlightableLinks( links=[LinkedText(alt_fmt(me, state), puzzledragonx(me.monster)) for me in state.alt_monsters], - highlighted=next(i for i, me in enumerate(state.alt_monsters) if monster.monster_id == me.monster.monster_id) + highlighted=next(i for i, me in enumerate(state.alt_monsters) + if monster.monster_id == me.monster.monster_id) ) ) From d51e14a707c735ecb7c7b6ef71041ef079b99a27 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Wed, 12 May 2021 03:34:07 -0400 Subject: [PATCH 0763/1220] Fix ^currentinvade (off by one hour) (#1049) --- padglobal/padglobal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 30e1442d..4ceddc14 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1131,12 +1131,12 @@ async def whichinvade(self, ctx): """Display which yinyangdra is currently invading for Mystics & Spectres event""" pst = pytz.timezone("America/Los_Angeles") curtime = datetime.datetime.now(pst) - if datetime.time(6) < curtime.time() < datetime.time(18): + if datetime.time(7) < curtime.time() < datetime.time(19): await ctx.send(self.c_commands['redinvadecurrent']) - totime = curtime.replace(hour=18, minute=0, second=0, microsecond=0) + totime = curtime.replace(hour=19, minute=0, second=0, microsecond=0) else: await ctx.send(self.c_commands['blueinvadecurrent']) - totime = curtime.replace(hour=6, minute=0, second=0, microsecond=0) + totime = curtime.replace(hour=7, minute=0, second=0, microsecond=0) if totime < curtime: totime += datetime.timedelta(1) await ctx.send(inline("Invade switches in: " + humanize_timedelta(timedelta=totime - curtime))) From df4f175a0049abcea20b2bb97a65d920548c15e2 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 13 May 2021 21:37:26 -0700 Subject: [PATCH 0764/1220] Don't let non-equips have `equip` as modifier (#1050) --- dadguide/token_mappings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 09992a03..61d68c7d 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -335,6 +335,7 @@ class MiscModifiers(Enum): HAZARDOUS_IN_NAME_MODS = { "reincarnated", "awoken", + "equip", } PROBLEMATIC_SERIES_TOKENS = { From dfb8065340b43cbfc070f2eaa213b61a8754463d Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Fri, 14 May 2021 16:19:20 -0400 Subject: [PATCH 0765/1220] Add padglobal append, appendwhich, prependwhich (#1051) --- padglobal/padglobal.py | 62 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 4ceddc14..d223ac78 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -255,6 +255,29 @@ async def delete(self, ctx, command: str): else: await ctx.send("PAD command doesn't exist.") + @padglobal.command() + async def append(self, ctx, command: str, *, addition): + """Append the additional text to an existing command after a blank line.""" + # the same cleaning that padglobal add does + command = command.lower() + addition = clean_global_mentions(addition) + addition = addition.replace(u'\u200b', '') + addition = replace_emoji_names_with_code(self._get_emojis(), addition) + + corrected_cmd = self._lookup_command(command) + if not corrected_cmd: + await ctx.send('Could not find a good match for command `{}`.'.format(command)) + return + result = self.c_commands.get(corrected_cmd, None) + while result in self.c_commands: + result = self.c_commands[result] + + result = "{}\n\n{}".format(result, addition) + self.c_commands[corrected_cmd] = result + json.dump(self.c_commands, open(self.file_path, 'w+')) + + await ctx.send("Successfully appended to PAD command `{}`.".format(corrected_cmd)) + @padglobal.command() async def setgeneral(self, ctx, command: str): """Sets a command to show up in [p]pad (the default). @@ -737,6 +760,42 @@ async def rmwhich(self, ctx, *, monster_id: int): self.settings.rmWhich(name) await ctx.tick() + @padglobal.command() + async def prependwhich(self, ctx, monster_id: int, *, addition): + """Prepend the additional text to an existing which entry before a blank line.""" + await self._concatenate_which(ctx, monster_id, 'prepend', addition) + + @padglobal.command() + async def appendwhich(self, ctx, monster_id: int, *, addition): + """Append the additional text to an existing which entry after a blank line.""" + await self._concatenate_which(ctx, monster_id, 'append', addition) + + async def _concatenate_which(self, ctx, monster_id: int, operation: str, addition): + m = self.bot.get_cog("Dadguide").get_monster(monster_id) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + if m != base_monster: + m = base_monster + await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) + mon_id = m.monster_id + + if mon_id not in self.settings.which(): + if await confirm_message(ctx, "No which info exists for {}. Would you like to add a new entry?".format( + m.name_en)): + self.settings.addWhich(mon_id, addition) + await ctx.send("PAD which info successfully ADDED.") + return + + definition, _ = self.settings.which().get(mon_id, None) + + if operation == 'prepend': + self.settings.addWhich(mon_id, '{}\n\n{}'.format(addition, definition)) + await ctx.send("Successfully prepended to PAD which info for {}.".format(m.name_en)) + elif operation == 'append': + self.settings.addWhich(mon_id, '{}\n\n{}'.format(definition, addition)) + await ctx.send("Successfully appended to PAD which info for {}.".format(m.name_en)) + else: + raise KeyError("Invalid operation: Must be \'prepend\' or \'append\'") + @padglobal.command() async def getwhich(self, ctx): """Gets a list of all which commands.""" @@ -917,7 +976,8 @@ async def checkCC(self, message): cmd = self.format_cc(result, message) - await message.channel.send(cmd) + for page in pagify(cmd): + await message.channel.send(page) def _lookup_command(self, cmd): """Returns the corrected cmd name. From a51f501b8d311581d6f048a7a2baddc77db8cdc5 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 15 May 2021 20:43:04 -0700 Subject: [PATCH 0766/1220] Show decoration if base evos are equips (#1054) --- padinfo/view/id.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 799b99b4..adf06728 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -22,14 +22,12 @@ def alt_fmt(monsterevo, state): - if not monsterevo.evolution: - fmt = "{}" - elif not monsterevo.evolution.reversible: - fmt = "⌊{}⌋" - elif monsterevo.monster.is_equip: + if monsterevo.monster.is_equip: fmt = "⌈{}⌉" - else: + elif not monsterevo.evolution or monsterevo.evolution.reversible: fmt = "{}" + else: + fmt = "⌊{}⌋" return fmt.format(monsterevo.monster.monster_no_na) @@ -148,7 +146,7 @@ def evos_embed_field(state: ViewStateBaseId): HighlightableLinks( links=[LinkedText(alt_fmt(me, state), puzzledragonx(me.monster)) for me in state.alt_monsters], highlighted=next(i for i, me in enumerate(state.alt_monsters) - if monster.monster_id == me.monster.monster_id) + if state.monster.monster_id == me.monster.monster_id) ) ) From 398b911312d1535b28e71054fee57d70e00102e4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 15 May 2021 22:48:37 -0500 Subject: [PATCH 0767/1220] Update help text for decorated evos (#1055) This finishes PR #1054 to show base equips too --- padinfo/view/id.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index adf06728..9177dc51 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -128,21 +128,19 @@ def _monster_is_enhance(m: "MonsterModel"): def evos_embed_field(state: ViewStateBaseId): - monster = state.monster - irreversible = False - equip = False - for alt_evo in state.alt_monsters: - if not alt_evo.evolution: - continue - elif not alt_evo.evolution.reversible: - irreversible = True - elif alt_evo.monster.is_equip: - equip = True - else: - continue + field_text = "**Evos**" + help_text = "" + # this isn't used right now, but maybe later if discord changes the api for embed titles...? + help_link = "https://github.com/TsubakiBotPad/pad-cogs/wiki/Evolutions-mini-view" + legend_parts = [] + if any([alt_evo.evolution and not alt_evo.evolution.reversible for alt_evo in state.alt_monsters]): + legend_parts.append("⌊Irreversible⌋") + if any([alt_evo.monster.is_equip for alt_evo in state.alt_monsters]): + legend_parts.append("⌈Equip⌉") + if legend_parts: + help_text = ' – Help: {}'.format(" ".join(legend_parts)) return EmbedField( - "**Evos**" + (" - " if irreversible or equip else "") + - ("⌊Irreversible⌋" if irreversible else "") + ("⌈Equip⌉" if equip else ""), + field_text + help_text, HighlightableLinks( links=[LinkedText(alt_fmt(me, state), puzzledragonx(me.monster)) for me in state.alt_monsters], highlighted=next(i for i, me in enumerate(state.alt_monsters) From 7ea22c6d549d11bf084ef45e4f44f3520213506b Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sat, 15 May 2021 23:51:16 -0400 Subject: [PATCH 0768/1220] Fix seriesscroll current page (#1053) --- padinfo/menu/series_scroll.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 26d46d32..388e9ffb 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -229,7 +229,7 @@ def click_child_number(ims, emoji_clicked, **data): return None, {} n = SeriesScrollMenuPanes.emoji_names().index(emoji_clicked) paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, ims) - page = ims.get('page') or 0 + page = ims.get('current_page') or 0 monster_list = paginated_monsters[page] extra_ims = { 'is_child': True, From 15c34bd671a7b658fa28b18020390905bde1ba6f Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 16 May 2021 00:31:54 -0400 Subject: [PATCH 0769/1220] Update handling of MP monsters in padglobal which (#1056) --- padglobal/padglobal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index d223ac78..2907a317 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -48,7 +48,7 @@ def _data_file(file_name: str) -> str: FARMABLE_MSG = 'This monster is **farmable** so make as many copies of whichever evos you like.' MP_BUY_MSG = ('This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' - ', check {}mpdra? for specific recommendations.') + ', check `{}mpdra?` for specific recommendations.') SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution: `[{}] {}`.' @@ -661,7 +661,7 @@ async def _resolve_which(self, ctx, term): monster = dgcog.get_monster(monster_id) - if db_context.graph.monster_is_mp_evo(monster): + if db_context.graph.monster_is_mp_evo(monster) and not db_context.graph.monster_is_rem(monster): return name, MP_BUY_MSG.format(ctx.prefix), None, False elif db_context.graph.monster_is_farmable_evo(monster): return name, FARMABLE_MSG, None, False From 6742144ffc148e116b7abe6e42a02c8302c9cd3c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 16 May 2021 00:51:03 -0500 Subject: [PATCH 0770/1220] Use built-in get_emoji method (#1058) --- padinfo/common/emoji_map.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index ed3b9b1c..7b89730b 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -124,7 +124,4 @@ def get_awakening_emoji(awid: Union[Enum, int], default: str = None): def get_emoji(name): if isinstance(name, int): name = AWAKENING_ID_TO_EMOJI_NAME_MAP[name] - for e in emoji_cache.custom_emojis: - if e.name == name: - return str(e) - return ":{}:".format(name) + return emoji_cache.get_emoji(name) From bf337e3655e41dadc25061201410376fd1c735be Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 16 May 2021 03:01:17 -0500 Subject: [PATCH 0771/1220] Fix some imports & reformat code (#1059) --- padinfo/common/emoji_map.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 7b89730b..c6c2cd77 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -1,5 +1,9 @@ from enum import Enum -from typing import Union, Optional, Literal +from typing import Union, Optional, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + from dadguide.models.enum_types import AwakeningRestrictedLatent, MonsterType from discordmenu.emoji.emoji_cache import emoji_cache @@ -95,8 +99,8 @@ def awakening_restricted_latent_emoji(latent: "AwakeningRestrictedLatent"): return get_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value])) -def get_type_emoji(type: Enum): - return get_emoji('mons_type_'+type.name.lower()) +def get_type_emoji(mons_type: "MonsterType"): + return get_emoji('mons_type_' + mons_type.name.lower()) def get_attribute_emoji_by_monster(monster: "MonsterModel"): @@ -105,6 +109,7 @@ def get_attribute_emoji_by_monster(monster: "MonsterModel"): emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) return get_emoji(emoji) + def get_attribute_emoji_by_enum(type1: Union[Enum, Literal[False]], type2: Optional[Union[Enum, Literal[bool]]] = None): attr1 = 'nil' if type1 is False else type1.name.lower() attr2 = 'nil' if type2 is False else type2.name.lower() if type2 else attr1 From c63c114e9f91336383325a34011aeac52e4f5318 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 16 May 2021 14:13:51 -0400 Subject: [PATCH 0772/1220] Make padglobal append follow aliases (#1061) --- padglobal/padglobal.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 2907a317..478f6737 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -265,18 +265,26 @@ async def append(self, ctx, command: str, *, addition): addition = replace_emoji_names_with_code(self._get_emojis(), addition) corrected_cmd = self._lookup_command(command) + alias = False if not corrected_cmd: - await ctx.send('Could not find a good match for command `{}`.'.format(command)) + await ctx.send("Could not find a good match for command `{}`.".format(command)) return result = self.c_commands.get(corrected_cmd, None) - while result in self.c_commands: + # go a level deeper if trying to append to an alias + if result in self.c_commands: + alias = True + source_cmd = result result = self.c_commands[result] result = "{}\n\n{}".format(result, addition) - self.c_commands[corrected_cmd] = result + if alias: + self.c_commands[source_cmd] = result + else: + self.c_commands[corrected_cmd] = result json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("Successfully appended to PAD command `{}`.".format(corrected_cmd)) + await ctx.send("Successfully appended to {}PAD command `{}`.".format("source " if alias else "", + source_cmd if alias else corrected_cmd)) @padglobal.command() async def setgeneral(self, ctx, command: str): From e68a945a5bdfb8c64c34ba2614678c502fd896ab Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 16 May 2021 15:05:38 -0500 Subject: [PATCH 0773/1220] Use tsutils base for menu panes (#1062) Move the menu panes base classes to tsutils since now we have menus in multiple cogs --- padinfo/info.json | 2 +- padinfo/menu/closable_embed.py | 2 +- padinfo/menu/common.py | 45 ----------------------------- padinfo/menu/id.py | 2 +- padinfo/menu/leader_skill.py | 2 +- padinfo/menu/leader_skill_single.py | 2 +- padinfo/menu/monster_list.py | 2 +- padinfo/menu/series_scroll.py | 2 +- padinfo/menu/simple_text.py | 2 +- padinfo/menu/transforminfo.py | 2 +- 10 files changed, 9 insertions(+), 54 deletions(-) delete mode 100644 padinfo/menu/common.py diff --git a/padinfo/info.json b/padinfo/info.json index 09a53a4a..830eea28 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.3.2", + "tsutils>=3.3.4", "prettytable", "python-levenshtein", "discord-menu>=0.16.0" diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index ff632eaf..456e6539 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -1,7 +1,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from padinfo.menu.common import MenuPanes +from tsutils.menu.panes import MenuPanes from padinfo.view.awakening_help import AwakeningHelpView from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.id_traceback import IdTracebackView diff --git a/padinfo/menu/common.py b/padinfo/menu/common.py deleted file mode 100644 index 38aaed2c..00000000 --- a/padinfo/menu/common.py +++ /dev/null @@ -1,45 +0,0 @@ -emoji_buttons = { - 'home': '\N{HOUSE BUILDING}', - 'reset': '\N{WHITE MEDIUM STAR}', -} - - -class MenuPanes: - INITIAL_EMOJI = emoji_buttons['home'], - DATA = { - } - HIDDEN_EMOJIS = [ - ] - - @classmethod - def emoji_names(cls): - return [k for k, v in cls.DATA.items() if k not in cls.HIDDEN_EMOJIS] - - @classmethod - def transitions(cls): - return {k: v[0] for k, v in cls.DATA.items() if v[0] is not None} - - @classmethod - def pane_types(cls): - return {v[1]: v[0] for k, v in cls.DATA.items() if v[1] and v[1] not in cls.HIDDEN_EMOJIS} - - @classmethod - def respond_to_emoji_with_parent(cls, emoji: str): - """Only defined for menus that support having children""" - if cls.DATA.get(emoji) is None: - return None - return cls.DATA[emoji][0] is not None - - @classmethod - def respond_to_emoji_with_child(cls, emoji: str): - """Only defined for menus that support having children""" - if cls.DATA.get(emoji) is None: - return None - return cls.DATA[emoji][2] is not None - - @classmethod - def get_child_data_func(cls, emoji: str): - """Only defined for menus that support having children""" - if cls.DATA.get(emoji) is None: - return None - return cls.DATA[emoji][2] diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index e3513753..cb564893 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -5,7 +5,7 @@ from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from padinfo.menu.common import MenuPanes +from tsutils.menu.panes import MenuPanes from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.evos import EvosView, EvosViewState from padinfo.view.id import IdView, IdViewState diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 8d0e680a..24353feb 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -5,7 +5,7 @@ from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from padinfo.menu.common import MenuPanes, emoji_buttons +from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillViewState diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index 8a80745f..39477fdb 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -4,7 +4,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache -from padinfo.menu.common import MenuPanes, emoji_buttons +from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill_single import LeaderSkillSingleView, LeaderSkillSingleViewState diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 4db55f60..c9c798cc 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -4,7 +4,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji -from padinfo.menu.common import emoji_buttons, MenuPanes +from tsutils.menu.panes import emoji_buttons, MenuPanes from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView from padinfo.view.monster_list import MonsterListView, MonsterListViewState diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 388e9ffb..6f51d61f 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -5,7 +5,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji -from padinfo.menu.common import MenuPanes, emoji_buttons +from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView from padinfo.view.series_scroll import SeriesScrollView, SeriesScrollViewState diff --git a/padinfo/menu/simple_text.py b/padinfo/menu/simple_text.py index 40eae654..53c6fdbb 100644 --- a/padinfo/menu/simple_text.py +++ b/padinfo/menu/simple_text.py @@ -4,7 +4,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from padinfo.menu.common import MenuPanes, emoji_buttons +from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.view.simple_text import SimpleTextView, SimpleTextViewState diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 8dcb082b..9b1e4675 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -4,7 +4,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji -from padinfo.menu.common import MenuPanes, emoji_buttons +from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.transforminfo import TransformInfoView, TransformInfoViewState From 09ac1272938b32e9e50d105724078952c6760fcd Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 16 May 2021 18:19:46 -0400 Subject: [PATCH 0774/1220] Make which an embed and add padglobal dumpwhich (#1060) --- padglobal/__init__.py | 4 +- padglobal/menu/closable_embed.py | 32 ++++++++++++ padglobal/menu/menu_map.py | 6 +++ padglobal/padglobal.py | 48 +++++++++++++++--- padglobal/view/closable_embed.py | 12 +++++ padglobal/view/which.py | 85 ++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 padglobal/menu/closable_embed.py create mode 100644 padglobal/menu/menu_map.py create mode 100644 padglobal/view/closable_embed.py create mode 100644 padglobal/view/which.py diff --git a/padglobal/__init__.py b/padglobal/__init__.py index 94f35cca..cc77b969 100644 --- a/padglobal/__init__.py +++ b/padglobal/__init__.py @@ -4,4 +4,6 @@ def setup(bot): - bot.add_cog(PadGlobal(bot)) + n = PadGlobal(bot) + bot.add_cog(n) + bot.loop.create_task(n.register_menu()) diff --git a/padglobal/menu/closable_embed.py b/padglobal/menu/closable_embed.py new file mode 100644 index 00000000..e36cb991 --- /dev/null +++ b/padglobal/menu/closable_embed.py @@ -0,0 +1,32 @@ +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu + +from tsutils.menu.panes import MenuPanes +from padglobal.view.closable_embed import ClosableEmbedViewState +from padglobal.view.which import WhichView + +view_types = { + WhichView.VIEW_TYPE: WhichView +} + + +class ClosableEmbedMenu: + MENU_TYPE = 'ClosableEmbedMenu' + message = None + + @staticmethod + def menu(): + embed = EmbedMenu({}, ClosableEmbedMenu.message_control) + return embed + + @staticmethod + def message_control(state: ClosableEmbedViewState): + view = view_types[state.view_type] + return EmbedControl( + [view.embed(state, state.props)], + [] + ) + + +class ClosableEmbedMenuPanes(MenuPanes): + pass diff --git a/padglobal/menu/menu_map.py b/padglobal/menu/menu_map.py new file mode 100644 index 00000000..1f8e2a01 --- /dev/null +++ b/padglobal/menu/menu_map.py @@ -0,0 +1,6 @@ +from padglobal.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes + + +padglobal_menu_map = { + ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), +} diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 478f6737..58b97303 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -13,12 +13,18 @@ import prettytable import pytz import tsutils +from discord import Color from redbot.core import checks, data_manager from redbot.core import commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json, \ auth_check +from padglobal.menu.closable_embed import ClosableEmbedMenu +from padglobal.menu.menu_map import padglobal_menu_map +from padglobal.view.closable_embed import ClosableEmbedViewState +from padglobal.view.which import WhichView, WhichViewProps, UNKNOWN_EDIT_TIMESTAMP + logger = logging.getLogger('red.padbot-cogs.padglobal') global PADGLOBAL_COG @@ -74,6 +80,8 @@ async def check_enabled(ctx): class PadGlobal(commands.Cog): """Global PAD commands.""" + menu_map = padglobal_menu_map + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) global PADGLOBAL_COG @@ -105,6 +113,18 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return + async def register_menu(self): + await self.bot.wait_until_ready() + menulistener = self.bot.get_cog("MenuListener") + if menulistener is None: + logger.warning("MenuListener is not loaded.") + return + await menulistener.register(self) + + async def get_menu_default_data(self, ims): + data = {} + return data + def _export_data(self): faq_and_boards = self.settings.faq() + self.settings.boards() @@ -637,11 +657,14 @@ async def which(self, ctx, *, term: str = None): name, definition, timestamp, success = await self._resolve_which(ctx, term) if name is None or definition is None: return - if not success: - await ctx.send('Which {}\n{}'.format(name, definition)) - return - for page in pagify('Which {} - Last Updated {}\n{}'.format(name, timestamp, self.emojify(definition))): - await ctx.send(page) + # TODO: maybe support different colors one day if the configuration gets moved out of padinfo + color = Color.default() + original_author_id = ctx.message.author.id + menu = ClosableEmbedMenu.menu() + props = WhichViewProps(name=name, definition=definition, timestamp=timestamp, success=success) + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, term, + color, WhichView.VIEW_TYPE, props) + await menu.create(ctx, state) async def _resolve_which(self, ctx, term): dgcog = self.bot.get_cog('Dadguide') @@ -659,7 +682,7 @@ async def _resolve_which(self, ctx, term): name = padinfo.get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() monster_id = m.monster_id definition = self.settings.which().get(monster_id, None) - timestamp = "1969-01-01" + timestamp = UNKNOWN_EDIT_TIMESTAMP if isinstance(definition, list): definition, timestamp = definition @@ -804,6 +827,17 @@ async def _concatenate_which(self, ctx, monster_id: int, operation: str, additio else: raise KeyError("Invalid operation: Must be \'prepend\' or \'append\'") + @padglobal.command(aliases=['whichdump']) + async def dumpwhich(self, ctx, *, term: str): + """Dump the raw text of an existing which entry, boxed.""" + _, definition, _, _ = await self._resolve_which(ctx, term) + + if definition is None: + return + else: + content = box(definition.replace('`', u'\u200b`')) + await ctx.send(content) + @padglobal.command() async def getwhich(self, ctx): """Gets a list of all which commands.""" @@ -819,7 +853,7 @@ async def getwhich(self, ctx): if isinstance(result, list): monsters.append([name, result[1]]) else: - monsters.append([name, "1969-01-01"]) + monsters.append([name, UNKNOWN_EDIT_TIMESTAMP]) tbl = prettytable.PrettyTable(['Monster', 'Timestamp']) tbl.hrules = prettytable.HEADER diff --git a/padglobal/view/closable_embed.py b/padglobal/view/closable_embed.py new file mode 100644 index 00000000..26e5f8d7 --- /dev/null +++ b/padglobal/view/closable_embed.py @@ -0,0 +1,12 @@ +from discordmenu.embed.view_state import ViewState + + +class ClosableEmbedViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, color, view_type, props): + super().__init__(original_author_id, menu_type, raw_query) + self.color = color + self.view_type = view_type + self.props = props + + def serialize(self): + return super().serialize() diff --git a/padglobal/view/which.py b/padglobal/view/which.py new file mode 100644 index 00000000..9a553878 --- /dev/null +++ b/padglobal/view/which.py @@ -0,0 +1,85 @@ +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.text import Text +from discordmenu.embed.view import EmbedView +from discordmenu.emoji.emoji_cache import emoji_cache +from tsutils import embed_footer_with_state + +STAR = '* ' +BULLET_EMOJIS = ['db', 'dp', 'dg'] +UNKNOWN_EDIT_TIMESTAMP = '1970-01-01' + + +class WhichViewProps: + def __init__(self, name: str, definition: str, timestamp: str, success: bool): + self.name = name + self.definition = definition + self.timestamp = timestamp + self.success = success + + +def _get_field(paragraph, color): + lines = paragraph.strip().splitlines() + lines = [_replace_bullets(line, color) for line in lines] + + if len(lines) > 1: + evo = lines[0] + information = '\n'.join(lines[1:]) + if evo.startswith('**'): + return EmbedField(evo, Box(information)) + + # it wasn't multiple lines that started with a bolded line, assume it's not an evo description + return EmbedField('**Additional Information**', Box(paragraph)) + + +def _replace_bullets(line, color_index): + if line.startswith(STAR): + bullet = emoji_cache.get_emoji(BULLET_EMOJIS[color_index]) + return '{} {}'.format(bullet, line[len(STAR):]) + else: + return line + + +def get_description(definition, timestamp, success): + if success: + if timestamp == UNKNOWN_EDIT_TIMESTAMP: + return Text('No last-updated date recorded') + else: + return Text('Last Updated {}'.format(timestamp)) + else: + return definition + + +def get_fields(definition): + paragraphs = definition.split('\n\n') + color = 0 + fields = [] + + for paragraph in paragraphs: + fields.append(_get_field(paragraph, color)) + color = ((color + 1) % len(BULLET_EMOJIS)) + + return fields + + +class WhichView: + VIEW_TYPE = 'Which' + + @staticmethod + def embed(state, props: WhichViewProps): + name = props.name + definition = props.definition + timestamp = props.timestamp + success = props.success + + fields = get_fields(definition) if success else [] + + return EmbedView( + EmbedMain( + color=state.color, + title='Which {}'.format(name), + description=get_description(definition, timestamp, success) + ), + embed_footer=embed_footer_with_state(state), + embed_fields=fields + ) From 1a5c93d5a123b1156ff1e162f772b9dd42e1769d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 19 May 2021 07:35:50 -0500 Subject: [PATCH 0775/1220] Support multi-page scrolling in ^ids (#1067) --- padinfo/menu/monster_list.py | 138 ++++++++++++++++++++++++++++++++++- padinfo/padinfo.py | 5 +- padinfo/view/monster_list.py | 64 ++++++++++++++-- 3 files changed, 194 insertions(+), 13 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index c9c798cc..3924111b 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -1,4 +1,5 @@ -from typing import Optional +from copy import deepcopy +from typing import Optional, List from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl @@ -11,7 +12,12 @@ class MonsterListEmoji: + delete = '\N{CROSS MARK}' home = emoji_buttons['home'] + prev_page = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + prev_mon = '\N{BLACK LEFT-POINTING TRIANGLE}' + next_mon = '\N{BLACK RIGHT-POINTING TRIANGLE}' + next_page = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' zero = char_to_emoji('0') one = char_to_emoji('1') two = char_to_emoji('2') @@ -50,6 +56,30 @@ async def respond_with_reset(message: Optional[Message], ims, **data): # replace with the overview list after the child menu changes return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + @staticmethod + async def respond_with_left(message: Optional[Message], ims, **data): + current_page = ims['current_page'] + if current_page > 0: + ims['current_page'] = current_page - 1 + ims['current_index'] = None + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + paginated_monsters = MonsterListViewState.query_from_ims(data['dgcog'], ims) + ims['current_page'] = len(paginated_monsters) - 1 + ims['current_index'] = None + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + async def respond_with_right(message: Optional[Message], ims, **data): + paginated_monsters = MonsterListViewState.query_from_ims(data['dgcog'], ims) + current_page = ims['current_page'] + if current_page < len(paginated_monsters) - 1: + ims['current_page'] = current_page + 1 + ims['current_index'] = None + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + ims['current_page'] = 0 + ims['current_index'] = None + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + @staticmethod async def respond_with_monster_list(message: Optional[Message], ims, **data): dgcog = data['dgcog'] @@ -59,7 +89,65 @@ async def respond_with_monster_list(message: Optional[Message], ims, **data): return control @staticmethod - async def respond_with_n(message: Optional[Message], ims, _n, **data): + async def respond_with_previous_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + MonsterListMenu._update_ims_prev(dgcog, ims) + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + def _update_ims_prev(dgcog, ims): + current_index = ims['current_index'] + page = ims['current_page'] + paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) + max_index = len(paginated_monsters[page]) - 1 + if current_index is None: + ims['current_index'] = max_index + return + if current_index > 0: + ims['current_index'] = current_index - 1 + return + if current_index == 0 and page > 0: + ims['current_page'] = page - 1 + ims['current_index'] = MonsterListViewState.MAX_ITEMS_PER_PANE + return + if current_index == 0: + # respond with left but then set the current index to the max thing possible + ims['current_page'] = len(paginated_monsters) - 1 + ims['current_index'] = len(paginated_monsters[-1]) - 1 + + @staticmethod + async def respond_with_next_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + MonsterListMenu._update_ims_next(dgcog, ims) + return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + + @staticmethod + def _update_ims_next(dgcog, ims): + current_index = ims['current_index'] + page = ims['current_page'] + paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) + max_index = len(paginated_monsters[page]) - 1 + if current_index is None: + ims['current_index'] = 0 + return + if current_index < max_index: + ims['current_index'] = current_index + 1 + return + if current_index == max_index and page < len(paginated_monsters) - 1: + ims['current_page'] = page + 1 + ims['current_index'] = 0 + return + if current_index == max_index: + ims['current_page'] = 0 + ims['current_index'] = 0 + + @staticmethod + async def respond_with_delete(message: Message, ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_n(message: Optional[Message], ims, n, **data): + ims['current_index'] =n return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod @@ -118,25 +206,63 @@ def monster_list_control(state: MonsterListViewState): @staticmethod def click_child_number(ims, emoji_clicked, **data): + dgcog = data['dgcog'] emoji_response = IdMenuEmoji.refresh \ if MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} n = MonsterListMenuPanes.emoji_names().index(emoji_clicked) + paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) + page = ims.get('current_page') or 0 + monster_list = paginated_monsters[page] extra_ims = { 'is_child': True, - 'resolved_monster_id': int(ims['monster_list'][n]), 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, + 'resolved_monster_id': + monster_list[n - MonsterListMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, } return emoji_response, extra_ims + @staticmethod + def auto_scroll_child_left(ims, _emoji_clicked, **data): + return MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_prev, **data) + + @staticmethod + def auto_scroll_child_right(ims, _emoji_clicked, **data): + return MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_next, **data) + + @staticmethod + def _scroll_child(ims, update_fn, **data): + dgcog = data['dgcog'] + copy_ims = deepcopy(ims) + update_fn(dgcog, copy_ims) + paginated_monsters = MonsterListViewState.query_from_ims(dgcog, copy_ims) + page = copy_ims.get('current_page') or 0 + monster_list = paginated_monsters[page] + # after we figure out new rarity + extra_ims = { + 'is_child': True, + 'reaction_list': IdMenuPanes.emoji_names(), + 'menu_type': IdMenu.MENU_TYPE, + 'resolved_monster_id': monster_list[copy_ims['current_index']].monster_id, + } + return IdMenuEmoji.refresh, extra_ims + class MonsterListMenuPanes(MenuPanes): INITIAL_EMOJI = MonsterListEmoji.home + NON_MONSTER_EMOJI_COUNT = 5 DATA = { # tuple parts: parent_response, pane_type, respond_with_child + MonsterListEmoji.delete: (MonsterListMenu.respond_with_delete, None, None), MonsterListEmoji.home: (MonsterListMenu.respond_with_monster_list, MonsterListEmoji.home, None), + MonsterListEmoji.prev_page: (MonsterListMenu.respond_with_left, MonsterListView.VIEW_TYPE, None), + MonsterListEmoji.prev_mon: ( + MonsterListMenu.respond_with_previous_monster, None, MonsterListMenu.auto_scroll_child_left), + MonsterListEmoji.next_mon: ( + MonsterListMenu.respond_with_next_monster, None, MonsterListMenu.auto_scroll_child_right), + MonsterListEmoji.next_page: (MonsterListMenu.respond_with_right, MonsterListView.VIEW_TYPE, None), MonsterListEmoji.zero: ( MonsterListMenu.respond_with_0, IdView.VIEW_TYPE, MonsterListMenu.click_child_number), MonsterListEmoji.one: ( @@ -172,4 +298,8 @@ class MonsterListMenuPanes(MenuPanes): @classmethod def get_initial_reaction_list(cls, number_of_evos: int): - return cls.emoji_names()[:number_of_evos] + return cls.emoji_names()[:number_of_evos + cls.NON_MONSTER_EMOJI_COUNT] + + @classmethod + def get_previous_reaction_list_num_monsters(cls, reaction_list: List): + return len(reaction_list) - cls.NON_MONSTER_EMOJI_COUNT diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ff57f72a..224b804a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -553,8 +553,10 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' + paginated_monsters = MonsterListViewState.paginate(monster_list) state = MonsterListViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, query, color, - monster_list, title, instruction_message, + paginated_monsters, len(paginated_monsters), 0, + title, instruction_message, reaction_list=initial_reaction_list ) parent_menu = MonsterListMenu.menu() @@ -1181,6 +1183,5 @@ async def idsearch(self, ctx, *, query): if base_id not in used: used.add(base_id) monster_list.append(mon) - monster_list = monster_list[:11] await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results') diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list.py index 580b0ffa..b02e14a0 100644 --- a/padinfo/view/monster_list.py +++ b/padinfo/view/monster_list.py @@ -2,6 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils import char_to_emoji, embed_footer_with_state @@ -11,34 +12,51 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel + from dadguide.database_context import DbContext class MonsterListViewState(ViewStateBase): + MAX_ITEMS_PER_PANE = 11 + def __init__(self, original_author_id, menu_type, raw_query, query, color, - monster_list: List["MonsterModel"], title, message, + paginated_monsters: List[List["MonsterModel"]], page_count, current_page, + title, message, + current_index: int = None, + max_len_so_far: int = None, reaction_list=None, extra_state=None, child_message_id=None ): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.current_index = current_index + self.current_page = current_page + self.page_count = page_count self.message = message self.child_message_id = child_message_id self.title = title - self.monster_list = monster_list + self.paginated_monsters = paginated_monsters + self.monster_list = paginated_monsters[current_page] self.reaction_list = reaction_list self.color = color self.query = query + self.max_len_so_far = max( + max_len_so_far or len(paginated_monsters[current_page]), + len(paginated_monsters[current_page])) def serialize(self): ret = super().serialize() ret.update({ 'pane_type': MonsterListView.VIEW_TYPE, 'title': self.title, - 'monster_list': [str(m.monster_id) for m in self.monster_list], + 'monster_list': [m.monster_id for m in self.monster_list], + 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], + 'current_page': self.current_page, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, 'message': self.message, + 'max_len_so_far': self.max_len_so_far, + 'current_index': self.current_index, }) return ret @@ -46,9 +64,15 @@ def serialize(self): async def deserialize(dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None - monster_list = [dgcog.database.graph.get_monster(int(m)) for m in ims['monster_list']] title = ims['title'] + paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) + page_count = len(paginated_monsters) + current_page = ims['current_page'] + monster_list = paginated_monsters[current_page] + current_index = ims.get('current_index') + max_len_so_far = max(ims['max_len_so_far'] or len(monster_list), len(monster_list)) + raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] @@ -56,14 +80,36 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') child_message_id = ims.get('child_message_id') message = ims.get('message') - return MonsterListViewState(original_author_id, menu_type, raw_query, query, user_config.color, - monster_list, title, message, + paginated_monsters, + page_count, current_page, + title, message, + current_index=current_index, + max_len_so_far=max_len_so_far, reaction_list=reaction_list, extra_state=ims, child_message_id=child_message_id ) + @staticmethod + def query(dgcog, monster_list): + db_context: "DbContext" = dgcog.database + monster_list = [db_context.graph.get_monster(int(m)) for m in monster_list] + paginated_monsters = [monster_list[i:i + MonsterListViewState.MAX_ITEMS_PER_PANE] + for i in range(0, len(monster_list), MonsterListViewState.MAX_ITEMS_PER_PANE)] + return paginated_monsters + + @staticmethod + def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: + monster_list = ims['full_monster_list'] + return MonsterListViewState.query(dgcog, monster_list) + + @staticmethod + def paginate(monster_list: List["MonsterModel"]) -> List[List["MonsterModel"]]: + paginated_monsters = [monster_list[i:i + MonsterListViewState.MAX_ITEMS_PER_PANE] + for i in range(0, len(monster_list), MonsterListViewState.MAX_ITEMS_PER_PANE)] + return paginated_monsters + def _monster_list(monsters): if not len(monsters): @@ -81,7 +127,11 @@ class MonsterListView: def embed(state: MonsterListViewState): fields = [ EmbedField(state.title, - Box(*_monster_list(state.monster_list))) + Box(*_monster_list(state.monster_list))), + EmbedField(BoldText('Page'), + Box('{} of {}'.format(state.current_page + 1, state.page_count)), + inline=True + ) ] return EmbedView( From fa8a59b43c37154fd5f3ae49d28139b55194ba1b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 19 May 2021 08:13:17 -0500 Subject: [PATCH 0776/1220] allmats command to see all evo mats (#1069) --- padinfo/padinfo.py | 30 ++++++++++++++++++++++++++++++ padinfo/view/materials.py | 2 +- padinfo/view/monster_list.py | 9 +++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 224b804a..5f984dec 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -529,6 +529,31 @@ async def buttoninfo(self, ctx, *, query: str): for page in pagify(info_str): await ctx.send(box(page)) + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def allmats(self, ctx, *, query: str): + """Monster info (evo materials tab)""" + dgcog = await self.get_dgcog() + raw_query = query + monster = await dgcog.find_monster(raw_query, ctx.author.id) + + if not monster: + await self.send_id_failure_message(ctx, query) + return + + await self.log_id_result(ctx, monster.monster_id) + + _, usedin, _, gemusedin, _, _, _, _ = \ + await MaterialsViewState.query(dgcog, monster) + + if usedin is None and gemusedin is None: + await ctx.send(inline("This monster is not a mat for anything nor does it have a gem")) + return + + monster_list = usedin or gemusedin + title = 'Material For' if usedin else 'Gem is Material For' + await self._do_monster_list(ctx, dgcog, query, monster_list, title) + @commands.command() @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): @@ -553,10 +578,15 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' + subtitle = None + if len(monster_list) > MonsterListViewState.MAX_INTERNAL_STORE_SIZE: + subtitle = 'List has been truncated to first {} items'.format(MonsterListViewState.MAX_INTERNAL_STORE_SIZE) + monster_list = monster_list[:MonsterListViewState.MAX_INTERNAL_STORE_SIZE] paginated_monsters = MonsterListViewState.paginate(monster_list) state = MonsterListViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, query, color, paginated_monsters, len(paginated_monsters), 0, title, instruction_message, + subtitle, reaction_list=initial_reaction_list ) parent_menu = MonsterListMenu.menu() diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 6be7ef2b..3d23ca10 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -119,7 +119,7 @@ def mat_use_field(mons, title, max_mons=MAX_MONS_TO_SHOW): if len(mons) == 0: text = "None" elif len(mons) > max_mons: - text = f"({len(mons) - max_mons} more monster{'s' if len(mons) - max_mons > 1 else ''})" + text = f"({len(mons) - max_mons} more monster{'s' if len(mons) - max_mons > 1 else ''}, see `^allmats` for full list)" return EmbedField( title, Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:max_mons]), text)) diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list.py index b02e14a0..bfc939a5 100644 --- a/padinfo/view/monster_list.py +++ b/padinfo/view/monster_list.py @@ -17,10 +17,11 @@ class MonsterListViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 11 + MAX_INTERNAL_STORE_SIZE = 100 def __init__(self, original_author_id, menu_type, raw_query, query, color, paginated_monsters: List[List["MonsterModel"]], page_count, current_page, - title, message, + title, message, subtitle: str = None, current_index: int = None, max_len_so_far: int = None, reaction_list=None, @@ -29,6 +30,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, ): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) + self.subtitle = subtitle self.current_index = current_index self.current_page = current_page self.page_count = page_count @@ -49,6 +51,7 @@ def serialize(self): ret.update({ 'pane_type': MonsterListView.VIEW_TYPE, 'title': self.title, + 'subtitle': self.subtitle, 'monster_list': [m.monster_id for m in self.monster_list], 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], 'current_page': self.current_page, @@ -65,6 +68,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None title = ims['title'] + subtitle = ims['subtitle'] paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) page_count = len(paginated_monsters) @@ -84,6 +88,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): paginated_monsters, page_count, current_page, title, message, + subtitle=subtitle, current_index=current_index, max_len_so_far=max_len_so_far, reaction_list=reaction_list, @@ -127,7 +132,7 @@ class MonsterListView: def embed(state: MonsterListViewState): fields = [ EmbedField(state.title, - Box(*_monster_list(state.monster_list))), + Box(state.subtitle, *_monster_list(state.monster_list))), EmbedField(BoldText('Page'), Box('{} of {}'.format(state.current_page + 1, state.page_count)), inline=True From 126823cc2348f311222ebea7ee5910f1bd4ea260 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Thu, 20 May 2021 17:16:18 -0400 Subject: [PATCH 0777/1220] Edit wording for addleaderguide (#1076) --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 58b97303..f2f37049 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1201,7 +1201,7 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): op = 'EDITED' if name in self.settings.leaderGuide() else 'ADDED' if op == 'EDITED' and not await confirm_message(ctx, - "Are you sure you want to edit the boss info for {}?".format( + "Are you sure you want to edit the leader guide for {}?".format( m.name_en)): return self.settings.addLeaderGuide(name, definition) From b983bf06a8aa05d1474f5a136a774b86c032a83a Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Fri, 21 May 2021 22:22:05 -0400 Subject: [PATCH 0778/1220] Update UX for padglobal which (#1080) --- padglobal/padglobal.py | 74 +++++++++++++++++++++++++++++++---------- padglobal/view/which.py | 2 +- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index f2f37049..6300b9ad 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -750,26 +750,43 @@ async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, t await ctx.send("Sent info on {} to {}".format(name, to_user.name)) @padglobal.command() - async def addwhich(self, ctx, monster_id: int, *, definition): + async def addwhich(self, ctx, term, *, definition): """Adds an entry to the which monster evo list. - If you provide a monster ID, the term will be entered for that monster tree. + Accepts queries. The which text will be entered for the resulting monster's tree. e.x. [p]padglobal addwhich 3818 take the pixel one """ - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + dgcog = self.bot.get_cog("Dadguide") + pdicog = self.bot.get_cog("PadInfo") + + term = term.lower() + m = await dgcog.find_monster(term, ctx.author.id) + if m is None: + await ctx.send(f"No monster found for `{term}`") + return + + base_monster = dgcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id + is_int = re.fullmatch(r'\d+', term) + op = 'EDITED' if name in self.settings.which() else 'ADDED' - if op == 'EDITED' and not await confirm_message(ctx, - "Are you sure you want to edit the which info for {}?".format( - m.name_en)): - return + if op == 'ADDED' and not is_int or op == 'EDITED': + if not await confirm_message(ctx, "Are you sure you want to {} which info for {} [{}] {}?".format( + 'edit the' if op == 'EDITED' else 'add new', + pdicog.get_attribute_emoji_by_monster(m), + m.monster_no_na, + m.name_en)): + return + + definition = clean_global_mentions(definition) + definition = definition.replace(u'\u200b', '') + definition = replace_emoji_names_with_code(self._get_emojis(), definition) self.settings.addWhich(name, definition) - await ctx.send("PAD which info successfully {}.".format(op)) + await ctx.send("PAD which info successfully {} for [{}] {}.".format(op, m.monster_no_na, m.name_en)) @padglobal.command() async def rmwhich(self, ctx, *, monster_id: int): @@ -792,23 +809,40 @@ async def rmwhich(self, ctx, *, monster_id: int): await ctx.tick() @padglobal.command() - async def prependwhich(self, ctx, monster_id: int, *, addition): + async def prependwhich(self, ctx, term: str, *, addition): """Prepend the additional text to an existing which entry before a blank line.""" - await self._concatenate_which(ctx, monster_id, 'prepend', addition) + await self._concatenate_which(ctx, term, 'prepend', addition) @padglobal.command() - async def appendwhich(self, ctx, monster_id: int, *, addition): + async def appendwhich(self, ctx, term: str, *, addition): """Append the additional text to an existing which entry after a blank line.""" - await self._concatenate_which(ctx, monster_id, 'append', addition) + await self._concatenate_which(ctx, term, 'append', addition) - async def _concatenate_which(self, ctx, monster_id: int, operation: str, addition): - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + async def _concatenate_which(self, ctx, term: str, operation: str, addition): + dgcog = self.bot.get_cog("Dadguide") + pdicog = self.bot.get_cog("PadInfo") + + term = term.lower() + m = await dgcog.find_monster(term, ctx.author.id) + if m is None: + await ctx.send(f"No monster found for `{term}`") + return + + base_monster = dgcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) mon_id = m.monster_id + # ask for extra confirmation if the term was not an id + if not re.fullmatch(r'\d+', term): + if not await confirm_message(ctx, 'Are you sure you want to {} to the which info for {} [{}] {}?'.format( + operation, + pdicog.get_attribute_emoji_by_monster(m), + m.monster_no_na, + m.name_en)): + return + if mon_id not in self.settings.which(): if await confirm_message(ctx, "No which info exists for {}. Would you like to add a new entry?".format( m.name_en)): @@ -818,12 +852,16 @@ async def _concatenate_which(self, ctx, monster_id: int, operation: str, additio definition, _ = self.settings.which().get(mon_id, None) + addition = clean_global_mentions(addition) + addition = addition.replace(u'\u200b', '') + addition = replace_emoji_names_with_code(self._get_emojis(), addition) + if operation == 'prepend': self.settings.addWhich(mon_id, '{}\n\n{}'.format(addition, definition)) - await ctx.send("Successfully prepended to PAD which info for {}.".format(m.name_en)) + await ctx.send("Successfully PREPENDED to PAD which info for [{}] {}.".format(m.monster_no_na, m.name_en)) elif operation == 'append': self.settings.addWhich(mon_id, '{}\n\n{}'.format(definition, addition)) - await ctx.send("Successfully appended to PAD which info for {}.".format(m.name_en)) + await ctx.send("Successfully APPENDED to PAD which info for [{}] {}.".format(m.monster_no_na, m.name_en)) else: raise KeyError("Invalid operation: Must be \'prepend\' or \'append\'") diff --git a/padglobal/view/which.py b/padglobal/view/which.py index 9a553878..a950a329 100644 --- a/padglobal/view/which.py +++ b/padglobal/view/which.py @@ -20,7 +20,7 @@ def __init__(self, name: str, definition: str, timestamp: str, success: bool): def _get_field(paragraph, color): lines = paragraph.strip().splitlines() - lines = [_replace_bullets(line, color) for line in lines] + lines = [_replace_bullets(line.strip(), color) for line in lines] if len(lines) > 1: evo = lines[0] From 1457e92a87c43cd8f4544903d9dc3c92065f7154 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Sat, 22 May 2021 20:19:12 -0400 Subject: [PATCH 0779/1220] Fix level 120 stats (#1086) --- dadguide/models/monster_stats.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index dd9615ec..04b6c98f 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -68,10 +68,12 @@ def base_stat(self, key, lv, monster_model): s_val = s_min + (s_max - s_min) * ((min(lv, monster_model.level) - 1) / (monster_model.level - 1)) ** scale else: s_val = s_min + val_at_99 = s_val if 99 < lv: s_val *= 1 + (monster_model.limit_mult / 11 * (min(lv, 110) - 99)) / 100 if 110 < lv: - s_val *= 1 + (self.LV_120_MULT_DICT[key] / 10 * (lv - 110)) / 100 + slb_bonus = self.LV_120_MULT_DICT[key] * val_at_99 * (lv-110) / 1000 + s_val += slb_bonus return s_val def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatModifierInput = None): From 76b0605d2034bcb8a306d1c69ab25e1ed6251843 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sat, 22 May 2021 20:37:47 -0400 Subject: [PATCH 0780/1220] Reorganize padglobal command hierarchy (#1085) * Adds root-level command groups pglossary, pboss, pwhich, and pguide. * Adds subgroups dungeon and leader to pguide. * Comments out addemoji. * Removes addadmin and rmadmin. --- padglobal/padglobal.py | 225 +++++++++++++++++++++-------------------- 1 file changed, 118 insertions(+), 107 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 6300b9ad..0acc551d 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -194,7 +194,10 @@ async def togglepadglobal(self, ctx): @commands.group(aliases=['pdg']) @auth_check('contentadmin') async def padglobal(self, ctx): - """PAD global custom commands.""" + """PAD global custom commands. + + See also `[p]pglossary`, `[p]pboss`, `[p]pwhich`, and `[p]pguide`. + """ @padglobal.command() async def say(self, ctx, *, text: str): @@ -497,13 +500,18 @@ def lookup_glossary(self, term): term = matches[0] return term, glossary[term] - @padglobal.command() - async def addglossary(self, ctx, term, *, definition): + @commands.group() + @auth_check('contentadmin') + async def pglossary(self, ctx): + """Commands related to the PAD global glossary.""" + + @pglossary.command(name='add') + async def pglossary_add(self, ctx, term, *, definition): """Adds a term to the glossary. If you want to use a multiple word term, enclose it in quotes. - e.x. [p]padglobal addglossary alb Awoken Liu Bei - e.x. [p]padglobal addglossary "never dathena" NA will never get dathena + e.x. [p]pglossary add alb Awoken Liu Bei + e.x. [p]pglossary add "never dathena" NA will never get dathena """ term = term.lower() definition = clean_global_mentions(definition) @@ -518,8 +526,8 @@ async def addglossary(self, ctx, term, *, definition): self.settings.addGlossary(term, definition) await ctx.send("PAD glossary term successfully {}.".format(op)) - @padglobal.command() - async def rmglossary(self, ctx, *, term): + @pglossary.command(name='remove', aliases=['rm', 'delete', 'del']) + async def pglossary_remove(self, ctx, *, term): """Removes a term from the glossary.""" term = term.lower() if term not in self.settings.glossary(): @@ -589,10 +597,17 @@ def boss_to_text_index(self, ctx): msg = msg + '\n' + ',\n'.join(map(str, sorted(bosses.keys()))) return msg - @padglobal.command() - async def addboss(self, ctx, term, *, definition): + @commands.group() + @auth_check('contentadmin') + async def pboss(self, ctx): + """Commands related to PAD global boss mechanics.""" + + @pboss.command(name='add') + async def pboss_add(self, ctx, term, *, definition): """Adds a set of boss mechanics. - If you want to use a multiple word boss name, enclose it in quotes.""" + + If you want to use a multiple word boss name, enclose it in quotes. + """ dgcog = self.bot.get_cog('Dadguide') pdicog = self.bot.get_cog("PadInfo") @@ -615,9 +630,9 @@ async def addboss(self, ctx, term, *, definition): self.settings.addBoss(base.monster_id, definition) await ctx.send("PAD boss mechanics successfully {}.".format(op)) - @padglobal.command() - async def rmboss(self, ctx, *, term): - """Adds a set of boss mechanics.""" + @pboss.command(name='remove', aliases=['rm', 'delete', 'del']) + async def pboss_remove(self, ctx, *, term): + """Removes a set of boss mechanics.""" dgcog = self.bot.get_cog('Dadguide') pdicog = self.bot.get_cog("PadInfo") @@ -749,12 +764,17 @@ async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, t await to_user.send(page) await ctx.send("Sent info on {} to {}".format(name, to_user.name)) - @padglobal.command() - async def addwhich(self, ctx, term, *, definition): + @commands.group() + @auth_check('contentadmin') + async def pwhich(self, ctx): + """Commands related to PAD global which definitions.""" + + @pwhich.command(name='add') + async def pwhich_add(self, ctx, term, *, definition): """Adds an entry to the which monster evo list. Accepts queries. The which text will be entered for the resulting monster's tree. - e.x. [p]padglobal addwhich 3818 take the pixel one + e.x. [p]pwhich add 3818 take the pixel one """ dgcog = self.bot.get_cog("Dadguide") pdicog = self.bot.get_cog("PadInfo") @@ -788,8 +808,8 @@ async def addwhich(self, ctx, term, *, definition): self.settings.addWhich(name, definition) await ctx.send("PAD which info successfully {} for [{}] {}.".format(op, m.monster_no_na, m.name_en)) - @padglobal.command() - async def rmwhich(self, ctx, *, monster_id: int): + @pwhich.command(name='remove', aliases=['rm', 'delete', 'del']) + async def pwhich_remove(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) @@ -808,13 +828,13 @@ async def rmwhich(self, ctx, *, monster_id: int): self.settings.rmWhich(name) await ctx.tick() - @padglobal.command() - async def prependwhich(self, ctx, term: str, *, addition): + @pwhich.command(name='prepend') + async def pwhich_prepend(self, ctx, term: str, *, addition): """Prepend the additional text to an existing which entry before a blank line.""" await self._concatenate_which(ctx, term, 'prepend', addition) - @padglobal.command() - async def appendwhich(self, ctx, term: str, *, addition): + @pwhich.command(name='append') + async def pwhich_append(self, ctx, term: str, *, addition): """Append the additional text to an existing which entry after a blank line.""" await self._concatenate_which(ctx, term, 'append', addition) @@ -865,8 +885,8 @@ async def _concatenate_which(self, ctx, term: str, operation: str, addition): else: raise KeyError("Invalid operation: Must be \'prepend\' or \'append\'") - @padglobal.command(aliases=['whichdump']) - async def dumpwhich(self, ctx, *, term: str): + @pwhich.command(name='dump') + async def pwhich_dump(self, ctx, *, term: str): """Dump the raw text of an existing which entry, boxed.""" _, definition, _, _ = await self._resolve_which(ctx, term) @@ -876,8 +896,8 @@ async def dumpwhich(self, ctx, *, term: str): content = box(definition.replace('`', u'\u200b`')) await ctx.send(content) - @padglobal.command() - async def getwhich(self, ctx): + @pwhich.command(name='get') + async def pwhich_get(self, ctx): """Gets a list of all which commands.""" items = list() monsters = [] @@ -905,29 +925,6 @@ async def getwhich(self, ctx): for page in pagify(msg): await ctx.send(box(page)) - @padglobal.command() - @checks.is_owner() - async def addadmin(self, ctx, user: discord.Member): - """Adds a user to the pad global admin""" - self.settings.addAdmin(user.id) - await ctx.tick() - - @padglobal.command() - @checks.is_owner() - async def rmadmin(self, ctx, user): - """Removes a user from the pad global admin""" - try: - u = await commands.MemberConverter().convert(ctx, user) - self.settings.rmAdmin(u.id) - except commands.BadArgument as e: - try: - u = int(user) - self.settings.rmAdmin(u) - except ValueError: - await ctx.send(inline("Invalid user id.")) - return - await ctx.tick() - @padglobal.group() @checks.is_owner() async def emojiservers(self, ctx): @@ -971,56 +968,57 @@ def _get_emojis(self): pass return emojis - @padglobal.command() - async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): - """Create padglobal monster emoji by id. - - Uses jp monster IDs by default. You only need to change to na if you want to add - voltron or something. - - If you add a jp ID, it will look like ':pad_123:'. - If you add a na ID, it will look like ':pad_na_123:'. - """ - all_emoji_servers = self.settings.emojiServers() - if not all_emoji_servers: - await ctx.send('No emoji servers set') - return - - if server not in ['na', 'jp']: - await ctx.send('Server must be one of [jp, na]') - return - - if monster_id <= 0: - await ctx.send('Invalid monster id') - return - - server_ids = self.settings.emojiServers() - all_emojis = self._get_emojis() - - source_url = PORTRAIT_TEMPLATE.format(monster_id) - emoji_name = 'pad_' + ('na_' if server == 'na' else '') + str(monster_id) - - for e in all_emojis: - if emoji_name == e.name: - await ctx.send(inline('Already exists')) - return - - for server_id in server_ids: - emoji_server = self.bot.get_guild(int(server_id)) - if len(emoji_server.emojis) < 50: - break - else: - await ctx.send("There is no room. Add a new emoji server to add more emoji.") - return - - try: - async with aiohttp.ClientSession() as sess: - async with sess.get(source_url) as resp: - emoji_content = await resp.read() - await emoji_server.create_custom_emoji(name=emoji_name, image=emoji_content) - await ctx.send(inline('Done creating emoji named {}'.format(emoji_name))) - except Exception as ex: - await ctx.send(box('Error:\n' + str(ex))) + # temporarily removed since emoji servers are full + # @padglobal.command() + # async def addemoji(self, ctx, monster_id: int, server: str = 'jp'): + # """Create padglobal monster emoji by id. + + # Uses jp monster IDs by default. You only need to change to na if you want to add + # voltron or something. + + # If you add a jp ID, it will look like ':pad_123:'. + # If you add a na ID, it will look like ':pad_na_123:'. + # """ + # all_emoji_servers = self.settings.emojiServers() + # if not all_emoji_servers: + # await ctx.send('No emoji servers set') + # return + + # if server not in ['na', 'jp']: + # await ctx.send('Server must be one of [jp, na]') + # return + + # if monster_id <= 0: + # await ctx.send('Invalid monster id') + # return + + # server_ids = self.settings.emojiServers() + # all_emojis = self._get_emojis() + + # source_url = PORTRAIT_TEMPLATE.format(monster_id) + # emoji_name = 'pad_' + ('na_' if server == 'na' else '') + str(monster_id) + + # for e in all_emojis: + # if emoji_name == e.name: + # await ctx.send(inline('Already exists')) + # return + + # for server_id in server_ids: + # emoji_server = self.bot.get_guild(int(server_id)) + # if len(emoji_server.emojis) < 50: + # break + # else: + # await ctx.send("There is no room. Add a new emoji server to add more emoji.") + # return + + # try: + # async with aiohttp.ClientSession() as sess: + # async with sess.get(source_url) as resp: + # emoji_content = await resp.read() + # await emoji_server.create_custom_emoji(name=emoji_name, image=emoji_content) + # await ctx.send(inline('Done creating emoji named {}'.format(emoji_name))) + # except Exception as ex: + # await ctx.send(box('Error:\n' + str(ex))) @commands.Cog.listener('on_message') async def checkCC(self, message): @@ -1201,8 +1199,17 @@ async def guideto(self, ctx, to_user: discord.Member, *, term: str): msg = "Sent guide for {} to {}".format(term, to_user.name) await ctx.send(inline(msg)) - @padglobal.command() - async def adddungeonguide(self, ctx, term: str, *, definition: str): + @commands.group() + @auth_check('contentadmin') + async def pguide(self, ctx): + """Commands related to PAD global dungeon and leader guides.""" + + @pguide.group() + async def dungeon(self, ctx): + """Dungeon guide subcommands.""" + + @dungeon.command(name='add') + async def dungeon_add(self, ctx, term: str, *, definition: str): """Adds a dungeon guide to the [p]guide command""" term = term.lower() op = 'EDITED' if term in self.settings.dungeonGuide() else 'ADDED' @@ -1213,8 +1220,8 @@ async def adddungeonguide(self, ctx, term: str, *, definition: str): self.settings.addDungeonGuide(term, definition) await ctx.send("PAD dungeon guide successfully {}.".format(op)) - @padglobal.command() - async def rmdungeonguide(self, ctx, term: str): + @dungeon.command(name='remove', aliases=['rm', 'delete', 'del']) + async def dungeon_remove(self, ctx, term: str): """Removes a dungeon guide from the [p]guide command""" term = term.lower() if term not in self.settings.dungeonGuide(): @@ -1227,8 +1234,12 @@ async def rmdungeonguide(self, ctx, term: str): self.settings.rmDungeonGuide(term) await ctx.tick() - @padglobal.command() - async def addleaderguide(self, ctx, monster_id: int, *, definition: str): + @pguide.group() + async def leader(self, ctx): + """Leader guide subcommands.""" + + @leader.command(name='add') + async def leader_add(self, ctx, monster_id: int, *, definition: str): """Adds a leader guide to the [p]guide command""" m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) @@ -1245,8 +1256,8 @@ async def addleaderguide(self, ctx, monster_id: int, *, definition: str): self.settings.addLeaderGuide(name, definition) await ctx.send("PAD leader guide info successfully {}.".format(op)) - @padglobal.command() - async def rmleaderguide(self, ctx, monster_id: int): + @leader.command(name='remove', aliases=['rm', 'delete', 'del']) + async def leader_remove(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) From 5b3ccdebf0aab4e95b8d85c5ab0c2ac35347dc1d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 23 May 2021 09:48:24 -0500 Subject: [PATCH 0781/1220] Show list of all awakenings for ^awakenings (#1088) --- padinfo/menu/awakening_list.py | 111 +++++++++++++++++++++++++++++++++ padinfo/menu/menu_map.py | 2 + padinfo/padinfo.py | 23 +++++-- padinfo/view/awakening_help.py | 21 ++----- padinfo/view/awakening_list.py | 83 ++++++++++++++++++++++++ padinfo/view/common.py | 22 +++++++ 6 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 padinfo/menu/awakening_list.py create mode 100644 padinfo/view/awakening_list.py diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py new file mode 100644 index 00000000..359129ea --- /dev/null +++ b/padinfo/menu/awakening_list.py @@ -0,0 +1,111 @@ +from typing import Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from tsutils.menu.panes import emoji_buttons, MenuPanes + +from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListView, AwakeningListSortTypes + + +class AwakeningListEmoji: + delete = '\N{CROSS MARK}' + refresh = '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}' + home = emoji_buttons['home'] + prev_page = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + next_page = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + alphabetical = '\N{INPUT SYMBOL FOR LATIN LETTERS}' + numerical = '\N{INPUT SYMBOL FOR NUMBERS}' + + +class AwakeningListMenu: + MENU_TYPE = 'AwakeningList' + + @staticmethod + def menu(initial_control=None): + if initial_control is None: + initial_control = AwakeningListMenu.awakening_list_control + embed = EmbedMenu(AwakeningListMenuPanes.transitions(), initial_control) + return embed + + @staticmethod + async def respond_with_delete(message: Message, ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_left(message: Optional[Message], ims, **data): + cur_page = ims['current_page'] + if cur_page == 0: + ims['current_page'] = ims['total_pages'] - 1 + else: + ims['current_page'] = cur_page - 1 + return await AwakeningListMenu.respond_with_awakening_list(message, ims, **data) + + @staticmethod + async def respond_with_right(message: Optional[Message], ims, **data): + cur_page = ims['current_page'] + if cur_page == ims['total_pages'] - 1: + ims['current_page'] = 0 + else: + ims['current_page'] = cur_page + 1 + return await AwakeningListMenu.respond_with_awakening_list(message, ims, **data) + + @staticmethod + async def respond_with_alphabetical(message: Optional[Message], ims, **data): + ims['sort_type'] = AwakeningListSortTypes.alphabetical + return await AwakeningListMenu.respond_with_awakening_list(message, ims, **data) + + @staticmethod + async def respond_with_numerical(message: Optional[Message], ims, **data): + ims['sort_type'] = AwakeningListSortTypes.numerical + return await AwakeningListMenu.respond_with_awakening_list(message, ims, **data) + + @staticmethod + async def respond_with_awakening_list(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + view_state = await AwakeningListViewState.deserialize(dgcog, user_config, ims) + control = AwakeningListMenu.awakening_list_control(view_state) + return control + + @staticmethod + def awakening_list_control(state: AwakeningListViewState): + if state is None: + return None + reaction_list = AwakeningListMenuPanes.get_reaction_list(state.sort_type) + return EmbedControl( + [AwakeningListView.embed(state)], + reaction_list + ) + + +class AwakeningListMenuPanes(MenuPanes): + INITIAL_EMOJI = AwakeningListEmoji.home + + DATA = { + AwakeningListEmoji.delete: (AwakeningListMenu.respond_with_delete, None), + AwakeningListEmoji.home: (AwakeningListMenu.respond_with_awakening_list, AwakeningListView.VIEW_TYPE), + AwakeningListEmoji.prev_page: (AwakeningListMenu.respond_with_left, AwakeningListView.VIEW_TYPE), + AwakeningListEmoji.next_page: (AwakeningListMenu.respond_with_right, AwakeningListView.VIEW_TYPE), + AwakeningListEmoji.alphabetical: (AwakeningListMenu.respond_with_alphabetical, AwakeningListView.VIEW_TYPE), + AwakeningListEmoji.numerical: (AwakeningListMenu.respond_with_numerical, AwakeningListView.VIEW_TYPE), + } + + HIDDEN_EMOJIS = [ + AwakeningListEmoji.home, + AwakeningListEmoji.refresh, + ] + + OPTIONAL_EMOJIS = [ + AwakeningListEmoji.alphabetical, + AwakeningListEmoji.numerical, + ] + + @classmethod + def get_reaction_list(cls, cur_sort_type): + other_toggle_reaction = None + if cur_sort_type == AwakeningListSortTypes.alphabetical: + other_toggle_reaction = AwakeningListEmoji.numerical + elif cur_sort_type == AwakeningListSortTypes.numerical: + other_toggle_reaction = AwakeningListEmoji.alphabetical + return [_ for _ in cls.emoji_names() if _ not in cls.OPTIONAL_EMOJIS] + [other_toggle_reaction] diff --git a/padinfo/menu/menu_map.py b/padinfo/menu/menu_map.py index f50a675a..496bd9ad 100644 --- a/padinfo/menu/menu_map.py +++ b/padinfo/menu/menu_map.py @@ -1,3 +1,4 @@ +from padinfo.menu.awakening_list import AwakeningListMenu, AwakeningListMenuPanes from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes @@ -8,6 +9,7 @@ from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes padinfo_menu_map = { + AwakeningListMenu.MENU_TYPE: (AwakeningListMenu, AwakeningListMenuPanes), ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), IdMenu.MENU_TYPE: (IdMenu, IdMenuPanes), LeaderSkillMenu.MENU_TYPE: (LeaderSkillMenu, LeaderSkillMenuPanes), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5f984dec..9963ac9f 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -23,6 +23,7 @@ from padinfo.core.leader_skills import perform_leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query +from padinfo.menu.awakening_list import AwakeningListMenu, AwakeningListMenuPanes from padinfo.menu.closable_embed import ClosableEmbedMenu from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu @@ -34,6 +35,7 @@ from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.awakening_help import AwakeningHelpView, AwakeningHelpViewProps +from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListSortTypes from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.evos import EvosViewState @@ -759,18 +761,31 @@ async def transforminfo(self, ctx, *, query): menu = TransformInfoMenu.menu() await menu.create(ctx, state) - @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp', 'awakenings']) + @commands.command(aliases=['awakehelp', 'awakeningshelp', 'awohelp', 'awokenhelp', 'awakeninghelp']) @checks.bot_has_permissions(embed_links=True) - async def awakeninghelp(self, ctx, *, query): - """Describe a monster's regular and super awakenings in detail.""" + async def awakenings(self, ctx, *, query=None): + """Describe a monster's regular and super awakenings in detail. + + Leave blank to see a list of every awakening in the game.""" dgcog = await self.get_dgcog() + color = await self.get_user_embed_color(ctx) + + if not query: + sort_type = AwakeningListSortTypes.numerical + paginated_skills = await AwakeningListViewState.query(dgcog, sort_type) + menu = AwakeningListMenu.menu() + state = AwakeningListViewState(ctx.message.author.id, AwakeningListMenu.MENU_TYPE, color, + sort_type, paginated_skills, 0, + reaction_list=AwakeningListMenuPanes.get_reaction_list(sort_type)) + await menu.create(ctx, state) + return + monster = await dgcog.find_monster(query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) return - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() props = AwakeningHelpViewProps(monster=monster) diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index bbf8114f..5afd9ed1 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -8,6 +8,7 @@ from padinfo.common.emoji_map import get_awakening_emoji from padinfo.common.external_links import puzzledragonx +from padinfo.view.common import get_awoken_skill_description from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -23,16 +24,6 @@ def __init__(self, monster: "MonsterModel"): self.monster = monster -def _get_long_desc(awakening: "AwakeningModel"): - emoji_text = get_awakening_emoji(awakening.awoken_skill_id, awakening.name) - desc = awakening.awoken_skill.desc_en - return Box( - Text(emoji_text), - Text(desc), - delimiter=' ' - ) - - def _get_short_desc(prev_index: int, awakening: "AwakeningModel"): emoji_text = get_awakening_emoji(awakening.awoken_skill_id, awakening.name) return Box( @@ -48,20 +39,20 @@ def _get_all_awakening_descs(awakening_list): for index, awakening in enumerate(awakening_list): if awakening.name not in appearances: appearances[awakening.name] = index - awakening_descs.append(_get_long_desc(awakening)) + awakening_descs.append(get_awoken_skill_description(awakening.awoken_skill)) else: awakening_descs.append(_get_short_desc(appearances[awakening.name], awakening)) return Box(*awakening_descs) -def normal_awakenings(monster: "MonsterModel"): +def get_normal_awakenings(monster: "MonsterModel"): normal_awakening_count = len(monster.awakenings) - monster.superawakening_count normal_awakenings = monster.awakenings[:normal_awakening_count] return _get_all_awakening_descs(normal_awakenings) -def super_awakenings(monster: "MonsterModel"): +def get_super_awakenings(monster: "MonsterModel"): normal_awakening_count = len(monster.awakenings) - monster.superawakening_count super_awakenings = monster.awakenings[normal_awakening_count:] return _get_all_awakening_descs(super_awakenings) @@ -75,8 +66,8 @@ def embed(state, props: AwakeningHelpViewProps): monster = props.monster fields = [ - EmbedField('Normal Awakenings', normal_awakenings(monster)), - EmbedField('Super Awakenings', super_awakenings(monster)) + EmbedField('Normal Awakenings', get_normal_awakenings(monster)), + EmbedField('Super Awakenings', get_super_awakenings(monster)) ] return EmbedView( diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py new file mode 100644 index 00000000..a86dcd67 --- /dev/null +++ b/padinfo/view/awakening_list.py @@ -0,0 +1,83 @@ +from typing import List + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state + +from padinfo.common.config import UserConfig +from padinfo.view.common import get_awoken_skill_description +from padinfo.view.components.view_state_base import ViewStateBase + + +class AwakeningListSortTypes: + alphabetical = "Alphabetical" + numerical = "Numerical" + + +class AwakeningListViewState(ViewStateBase): + MAX_ITEMS_PER_PANE = 10 + + def __init__(self, original_author_id, menu_type, color, sort_type, paginated_skills, current_page, + extra_state=None, + reaction_list: List[str] = None): + super().__init__(original_author_id, menu_type, '', extra_state=extra_state, reaction_list=reaction_list) + self.sort_type = sort_type + self.paginated_skills = paginated_skills + self.total_pages = len(self.paginated_skills) + self.current_page = current_page + self.color = color + + def serialize(self): + ret = super().serialize() + ret.update({ + 'current_page': self.current_page, + 'total_pages': self.total_pages, + 'sort_type': self.sort_type, + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + sort_type = ims['sort_type'] + paginated_skills = await cls.query(dgcog, sort_type) + original_author_id = ims['original_author_id'] + current_page = ims['current_page'] + menu_type = ims['menu_type'] + reaction_list = ims['reaction_list'] + return AwakeningListViewState(original_author_id, menu_type, user_config.color, sort_type, paginated_skills, + current_page, reaction_list=reaction_list) + + @classmethod + async def query(cls, dgcog, sort_type): + awoken_skills = dgcog.database.get_all_awoken_skills() + if sort_type == AwakeningListSortTypes.alphabetical: + awoken_skills = sorted(awoken_skills, key=lambda awo: awo.name_en) + elif sort_type == AwakeningListSortTypes.numerical: + awoken_skills = sorted(awoken_skills, key=lambda awo: awo.awoken_skill_id) + + paginated_skills = [awoken_skills[i:i + AwakeningListViewState.MAX_ITEMS_PER_PANE] + for i in range(0, len(awoken_skills), AwakeningListViewState.MAX_ITEMS_PER_PANE)] + return paginated_skills + + +class AwakeningListView: + VIEW_TYPE = 'AwakeningList' + + @staticmethod + def embed(state: AwakeningListViewState): + fields = [ + EmbedField('Awakenings - by {}'.format('name' if state.sort_type == AwakeningListSortTypes.alphabetical else 'id number'), + Box(*[get_awoken_skill_description(awo) for awo in state.paginated_skills[state.current_page]])), + EmbedField('Page', Box( + '{} of {}'.format(str(state.current_page + 1), str(len(state.paginated_skills))) + )) + ] + + return EmbedView( + EmbedMain( + color=state.color, + ), + embed_footer=embed_footer_with_state(state), + embed_fields=fields + ) diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 83d2a9e6..382234d5 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -1,3 +1,14 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.text import Text, BoldText + +from padinfo.common.emoji_map import get_awakening_emoji + +if TYPE_CHECKING: + from dadguide.models.awakening_model import AwokenSkillModel + + async def get_monster_from_ims(dgcog, ims: dict): query = ims.get('query') or ims['raw_query'] @@ -7,3 +18,14 @@ async def get_monster_from_ims(dgcog, ims: dict): return dgcog.database.graph.get_monster(resolved_monster_id) monster = await dgcog.find_monster(query, ims['original_author_id']) return monster + + +def get_awoken_skill_description(awoken_skill: "AwokenSkillModel"): + emoji_text = get_awakening_emoji(awoken_skill.awoken_skill_id, awoken_skill.name) + desc = awoken_skill.desc_en + return Box( + Text(emoji_text), + BoldText(awoken_skill.name_en), + Text(desc), + delimiter=' ' + ) From 634a00fd18c8bc8ac14702039c0136bd1f202a56 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 23 May 2021 11:57:34 -0400 Subject: [PATCH 0782/1220] Update pwhich dump and list (#1087) --- padglobal/padglobal.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 0acc551d..d04d7bbe 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -18,7 +18,7 @@ from redbot.core import commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json, \ - auth_check + auth_check, get_reaction from padglobal.menu.closable_embed import ClosableEmbedMenu from padglobal.menu.menu_map import padglobal_menu_map @@ -57,6 +57,8 @@ def _data_file(file_name: str) -> str: ', check `{}mpdra?` for specific recommendations.') SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution: `[{}] {}`.' +MAX_WHICH_LIST_BEFORE_DM_PROMPT = 30 + def mod_help(self, ctx, help_type): hs = getattr(self, help_type) @@ -893,12 +895,27 @@ async def pwhich_dump(self, ctx, *, term: str): if definition is None: return else: - content = box(definition.replace('`', u'\u200b`')) - await ctx.send(content) + for page in pagify(definition): + content = box(page.replace('`', u'\u200b`')) + await ctx.send(content) + + @pwhich.command(name='list') + async def pwhich_list(self, ctx): + """List all which commands.""" + channel = '\N{WHITE HEAVY CHECK MARK}' + send_as_dm = '\N{ENVELOPE}' + cancel = '\N{CROSS MARK}' + destination = channel + if len(self.settings.which()) > MAX_WHICH_LIST_BEFORE_DM_PROMPT: + destination = await get_reaction(ctx, + 'This will send a lot of messages. Are you sure? ' + + '(Yes / DM me instead / Cancel)', + channel, + send_as_dm, + cancel) + if destination == cancel or destination is None: + return - @pwhich.command(name='get') - async def pwhich_get(self, ctx): - """Gets a list of all which commands.""" items = list() monsters = [] for w in self.settings.which(): @@ -923,7 +940,10 @@ async def pwhich_get(self, ctx): msg = tsutils.strip_right_multiline(tbl.get_string()) for page in pagify(msg): - await ctx.send(box(page)) + if destination == channel: + await ctx.send(box(page)) + else: + await ctx.author.send(box(page)) @padglobal.group() @checks.is_owner() From 3317406109dd869077469182c9d09fbde43170de Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 23 May 2021 19:12:18 -0500 Subject: [PATCH 0783/1220] Rename Balance to Balanced (#1091) --- dadguide/models/enum_types.py | 2 +- dadguide/models/monster_model.py | 4 ++-- dadguide/token_mappings.py | 2 +- padbuildimg/padbuildimg.py | 2 +- padsearch/padsearch.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 3d69954d..525e773c 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -14,7 +14,7 @@ class Attribute(Enum): class MonsterType(Enum): Evolve = 0 - Balance = 1 + Balanced = 1 Physical = 2 Healer = 3 Dragon = 4 diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 0441284b..471c0d08 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -103,13 +103,13 @@ def killers(self): type_to_killers_map = { MonsterType.God: ['Devil'], MonsterType.Devil: ['God'], - MonsterType.Machine: ['God', 'Balance'], + MonsterType.Machine: ['God', 'Balanced'], MonsterType.Dragon: ['Machine', 'Healer'], MonsterType.Physical: ['Machine', 'Healer'], MonsterType.Attacker: ['Devil', 'Physical'], MonsterType.Healer: ['Dragon', 'Attacker'], } - if MonsterType.Balance in self.types: + if MonsterType.Balanced in self.types: return ['Any'] killers = set() for t in self.types: diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 61d68c7d..584e64d6 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -43,7 +43,7 @@ def inverse_map(token_map: Mapping[K, Tuple[V]]) -> Mapping[V, Tuple[K]]: TYPE_MAP = { MonsterType.Evolve: ('evolve',), - MonsterType.Balance: ('balanced', 'bal'), + MonsterType.Balanced: ('balanced', 'bal', 'balance'), MonsterType.Physical: ('physical', 'phys'), MonsterType.Healer: ('healer',), MonsterType.Dragon: ('dragon', 'dra'), diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 9eb7aae6..5970cb82 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -145,7 +145,7 @@ 'Physical': [208, 203], # machine healer 'Attacker': [207, 202], # devil physical 'Healer': [204, 206], # dragon attacker - 'Balance': [201, 202, 203, 204, 205, 206, 207, 208] + 'Balanced': [201, 202, 203, 204, 205, 206, 207, 208] } AWK_CIRCLE = 'circle' diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index c140e2b7..d806b9de 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -73,7 +73,7 @@ def filt_timeout(filts, ms): TYPES = [ "attacker", "awoken", - "balance", + "balanced", "devil", "dragon", "enhance", From 0fae56b42c30ac7180def9788cebdf64e884fbdf Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Sun, 23 May 2021 20:13:00 -0400 Subject: [PATCH 0784/1220] Include equip effects in ^buttoninfo (#1084) --- dadguide/models/monster_stats.py | 58 ++++++++++++++++++++++------- padinfo/core/button_info.py | 63 ++++++++++++++++++++------------ padinfo/padinfo.py | 2 +- 3 files changed, 85 insertions(+), 38 deletions(-) diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 04b6c98f..9a62580c 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -1,4 +1,5 @@ from typing import Literal +from dadguide.token_mappings import AwokenSkills StatType = Literal['hp', 'atk', 'rcv'] @@ -41,10 +42,11 @@ class MonsterStats: PLUS_DICT = {'hp': 10, 'atk': 5, 'rcv': 3} LV_120_MULT_DICT = {'hp': 10, 'atk': 5, 'rcv': 5} - def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus_297=True, - stat_latents: MonsterStatModifierInput = None): - s_val = self.base_stat(key, lv, monster_model) - + def stat(self, monster_model, key: StatType, lv, *, plus=99, inherit=False, is_plus_297=True, + stat_latents: MonsterStatModifierInput = None, multiplayer=False, inherited_monster: "MonsterModel" = None, inherited_monster_lvl=99): + # TODO: deal with atk-, rcv-, and hp- awakenings + # PAD rounds stats from the start before any calculations + s_val = round(self.base_stat(key, lv, monster_model)) if stat_latents and not (monster_model.is_equip or inherit): latents = s_val * stat_latents.get_latent_multiplier(key) stat_awakenings = stat_latents.get_awakening_addition(key) @@ -58,14 +60,39 @@ def stat(self, monster_model, key: StatType, lv, plus=99, inherit=False, is_plus if not is_plus_297: s_val -= self.PLUS_DICT[key] * max(min(plus, 99), 0) s_val *= inherit_dict[key] - return int(round(s_val)) + + if inherited_monster: + # add base stats of inherit only if main atts are the same and not no-main-att + if monster_model.attr1.value == inherited_monster.attr1.value or monster_model.attr1.value == 6 or inherited_monster.attr1.value == 6: + # recursion is not possible because inherits do not have inherits on top of them + # inherit=True to calculate inherit stats and multiplayer=False in case the inherit has a multiboost awakening + s_val += round(self.stat(inherited_monster, key, inherited_monster_lvl, + inherit=True, multiplayer=False)) + # add bonus atk, hp, or rcv awakenings + inherit_bonus = MonsterStatModifierInput( + num_hp_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDHP.value), + num_atk_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDATK.value), + num_rcv_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDRCV.value) + ) + s_val += inherit_bonus.get_awakening_addition(key) + + if multiplayer: + print(s_val) + num_multiboost = monster_model.awakening_count(AwokenSkills.MULTIBOOST.value) + num_multiboost += inherited_monster.awakening_count( + AwokenSkills.MULTIBOOST.value) if inherited_monster else 0 + s_val = round(round(s_val) * (1.5 ** num_multiboost)) + print(s_val) + + return s_val def base_stat(self, key, lv, monster_model): s_min = float(monster_model.stat_values[key]['min']) s_max = float(monster_model.stat_values[key]['max']) if monster_model.level > 1: scale = monster_model.stat_values[key]['scale'] - s_val = s_min + (s_max - s_min) * ((min(lv, monster_model.level) - 1) / (monster_model.level - 1)) ** scale + s_val = s_min + (s_max - s_min) * ((min(lv, monster_model.level) - 1) / + (monster_model.level - 1)) ** scale else: s_val = s_min val_at_99 = s_val @@ -86,15 +113,18 @@ def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: Monst if not stat_latents: stat_latents = MonsterStatModifierInput( - num_hp_awakening=monster_model.awakening_count(1), - num_atk_awakening=monster_model.awakening_count(2), - num_rcv_awakening=monster_model.awakening_count(3), - num_voice_awakening=monster_model.awakening_count(63) + num_hp_awakening=monster_model.awakening_count(AwokenSkills.ENHANCEDHP.value), + num_atk_awakening=monster_model.awakening_count(AwokenSkills.ENHANCEDATK.value), + num_rcv_awakening=monster_model.awakening_count(AwokenSkills.ENHANCEDRCV.value), + num_voice_awakening=monster_model.awakening_count(AwokenSkills.VOICE.value) ) - - hp = self.stat(monster_model, 'hp', lv, plus[0], inherit, is_plus_297, stat_latents) - atk = self.stat(monster_model, 'atk', lv, plus[1], inherit, is_plus_297, stat_latents) - rcv = self.stat(monster_model, 'rcv', lv, plus[2], inherit, is_plus_297, stat_latents) + # TODO: Fix rounding + hp = int(round(self.stat(monster_model, 'hp', lv, + plus=plus[0], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents))) + atk = int(round(self.stat(monster_model, 'atk', lv, + plus=plus[1], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents))) + rcv = int(round(self.stat(monster_model, 'rcv', lv, + plus=plus[2], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents))) weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) return hp, atk, rcv, weighted diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 63984f59..a704649a 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -16,12 +16,13 @@ Total: {} -------------- +Inherits are assumed to be the max possible level (up to 110) and +297 {} {} """ -TEAM_BUTTON_FORMAT = "As {} base ({}x {}): Contributes {}" -CARD_BUTTON_FORMAT = "As {} base ({}x): Deals {}" +TEAM_BUTTON_FORMAT = "As [{}] {} base ({}x {}): Contributes {}" +CARD_BUTTON_FORMAT = "As [{}] {} base ({}x): Deals {}" MonsterStat = namedtuple('MonsterStat', 'name id mult att') @@ -32,7 +33,8 @@ ] CARD_BUTTONS = [ MonsterStat("Satan", 4286, 300, []), - MonsterStat("Brachydios", 4134, 350, []), + MonsterStat("Durandalf Equip", 4723, 300, []), + MonsterStat("Brachydios Equip", 4152, 350, []), MonsterStat("Rajang", 5527, 550, []), MonsterStat("Balrog", 5108, 450, []) ] @@ -70,20 +72,21 @@ def get_info(self, dgcog, monster_model): result.sub_damage = result.main_damage * sub_attr_multiplier result.total_damage = result.main_damage + result.sub_damage - result.main_damage_with_atk_latent = self._calculate_damage(dgcog, monster_model, max_level, max_atk_latents) + result.main_damage_with_atk_latent = self._calculate_damage( + dgcog, monster_model, max_level, max_atk_latents) result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent return result def _calculate_damage(self, dgcog, monster_model, level, num_atkpp_latent=0): stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=num_atkpp_latent) - stat_latents.num_atk_awakening = len([x for x in monster_model.awakenings if x.awoken_skill_id == 1]) + stat_latents.num_atk_awakening = len( + [x for x in monster_model.awakenings if x.awoken_skill_id == 1]) - dmg = dgcog.monster_stats.stat(monster_model, 'atk', level, stat_latents=stat_latents) - num_mult_boost = len([x for x in monster_model.awakenings if x.awoken_skill_id == 30]) + dmg = dgcog.monster_stats.stat(monster_model, 'atk', level, + stat_latents=stat_latents, multiplayer=True) - dmg *= 1.5 ** num_mult_boost - return dmg + return int(round(dmg)) def _get_sub_attr_multiplier(self, monster_model): if monster_model.attr2.value == 6 or monster_model.attr1.value == 6: @@ -92,36 +95,50 @@ def _get_sub_attr_multiplier(self, monster_model): return 1 / 10 return 1 / 3 - def to_string(self, monster, info): - card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, info, monster) - team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, info, monster) - return INFO_STRING.format(monster.monster_id, monster.name_en, info.main_damage, info.sub_damage, - info.total_damage, - info.main_damage_with_atk_latent, info.sub_damage_with_atk_latent, - info.total_damage_with_atk_latent, card_btn_str, team_btn_str) + def to_string(self, dgcog, monster, info): + card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dgcog, monster) + team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dgcog, monster) + return INFO_STRING.format(monster.monster_id, monster.name_en, int(round(info.main_damage)), int(round(info.sub_damage)), + int(round(info.total_damage)), + int(round(info.main_damage_with_atk_latent)), int( + round(info.sub_damage_with_atk_latent)), + int(round(info.total_damage_with_atk_latent)), card_btn_str, team_btn_str) - def _get_card_btn_damage(self, card_buttons, info, monster): + def _get_card_btn_damage(self, card_buttons, dgcog, monster): lines = [] card_buttons.sort(key=lambda x: x.mult) for card in card_buttons: - lines.append(CARD_BUTTON_FORMAT.format(card.name, card.mult, - (info.main_damage_with_atk_latent * card.mult))) + inherit_model = dgcog.get_monster(card.id) + max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level + inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level + stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=monster.latent_slots / 2) + dmg = int(round(dgcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, + inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level))) + lines.append(CARD_BUTTON_FORMAT.format( + card.id, card.name, card.mult, (dmg * card.mult))) return "\n".join(lines) - def _get_team_btn_damage(self, team_buttons, info, monster): + def _get_team_btn_damage(self, team_buttons, dgcog, monster): # TODO: calculate with oncolor assist damage and ATK+ eq (Oversoul) lines = [] team_buttons.sort(key=lambda x: x.mult) for card in team_buttons: total_dmg = 0 + inherit_model = dgcog.get_monster(card.id) + max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level + inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level + stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=monster.latent_slots / 2) + dmg = dgcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, + inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level) if(monster.attr1.value in card.att): - total_dmg += info.main_damage_with_atk_latent + total_dmg += dmg if(monster.attr2.value in card.att): - total_dmg += info.sub_damage_with_atk_latent + total_dmg += dmg * self._get_sub_attr_multiplier(monster) colors_str = "" for i in card.att: colors_str += COLORS[i] - lines.append(TEAM_BUTTON_FORMAT.format(card.name, card.mult, colors_str, (total_dmg * card.mult))) + lines.append(TEAM_BUTTON_FORMAT.format(card.id, card.name, + card.mult, colors_str, (total_dmg * card.mult))) return "\n".join(lines) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9963ac9f..bee81b71 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -527,7 +527,7 @@ async def buttoninfo(self, ctx, *, query: str): return DGCOG = self.bot.get_cog("Dadguide") info = button_info.get_info(DGCOG, monster) - info_str = button_info.to_string(monster, info) + info_str = button_info.to_string(DGCOG, monster, info) for page in pagify(info_str): await ctx.send(box(page)) From 463f30de79db9fb6337c0fe8b72fd8c7c8619dac Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 26 May 2021 19:52:51 -0500 Subject: [PATCH 0785/1220] Support "generators" for monster lists (#1099) --- padinfo/menu/menu_map.py | 6 ++ padinfo/menu/monster_list.py | 57 +++++++++------ padinfo/menu/series_scroll.py | 28 ++++---- padinfo/padinfo.py | 72 ++++++++----------- padinfo/view/monster_list/__init__.py | 0 padinfo/view/monster_list/all_mats.py | 25 +++++++ padinfo/view/monster_list/id_search.py | 32 +++++++++ .../view/{ => monster_list}/monster_list.py | 62 ++++++---------- .../view/monster_list/static_monster_list.py | 30 ++++++++ padinfo/view/series_scroll.py | 8 +-- 10 files changed, 196 insertions(+), 124 deletions(-) create mode 100644 padinfo/view/monster_list/__init__.py create mode 100644 padinfo/view/monster_list/all_mats.py create mode 100644 padinfo/view/monster_list/id_search.py rename padinfo/view/{ => monster_list}/monster_list.py (63%) create mode 100644 padinfo/view/monster_list/static_monster_list.py diff --git a/padinfo/menu/menu_map.py b/padinfo/menu/menu_map.py index 496bd9ad..23ea164c 100644 --- a/padinfo/menu/menu_map.py +++ b/padinfo/menu/menu_map.py @@ -7,6 +7,9 @@ from padinfo.menu.series_scroll import SeriesScrollMenu, SeriesScrollMenuPanes from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes +from padinfo.view.monster_list.all_mats import AllMatsViewState +from padinfo.view.monster_list.id_search import IdSearchViewState +from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState padinfo_menu_map = { AwakeningListMenu.MENU_TYPE: (AwakeningListMenu, AwakeningListMenuPanes), @@ -15,6 +18,9 @@ LeaderSkillMenu.MENU_TYPE: (LeaderSkillMenu, LeaderSkillMenuPanes), LeaderSkillSingleMenu.MENU_TYPE: (LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes), MonsterListMenu.MENU_TYPE: (MonsterListMenu, MonsterListMenuPanes), + AllMatsViewState.VIEW_STATE_TYPE: (MonsterListMenu, MonsterListMenuPanes), + IdSearchViewState.VIEW_STATE_TYPE: (MonsterListMenu, MonsterListMenuPanes), + StaticMonsterListViewState.VIEW_STATE_TYPE: (MonsterListMenu, MonsterListMenuPanes), SeriesScrollMenu.MENU_TYPE: (SeriesScrollMenu, SeriesScrollMenuPanes), SimpleTextMenu.MENU_TYPE: (SimpleTextMenu, SimpleTextMenuPanes), TransformInfoMenu.MENU_TYPE: (TransformInfoMenu, TransformInfoMenuPanes), diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 3924111b..74df57ea 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -8,7 +8,10 @@ from tsutils.menu.panes import emoji_buttons, MenuPanes from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView -from padinfo.view.monster_list import MonsterListView, MonsterListViewState +from padinfo.view.monster_list.all_mats import AllMatsViewState +from padinfo.view.monster_list.id_search import IdSearchViewState +from padinfo.view.monster_list.monster_list import MonsterListView, MonsterListViewState +from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState class MonsterListEmoji: @@ -33,6 +36,17 @@ class MonsterListEmoji: reset = emoji_buttons['reset'] +view_state_types = { + AllMatsViewState.VIEW_STATE_TYPE: AllMatsViewState, + IdSearchViewState.VIEW_STATE_TYPE: IdSearchViewState, + StaticMonsterListViewState.VIEW_STATE_TYPE: StaticMonsterListViewState, +} + + +def _get_view_state(ims: dict): + return view_state_types.get(ims['menu_type']) or MonsterListMenu.MENU_TYPE + + class MonsterListMenu: MENU_TYPE = 'MonsterListMenu' CHILD_MENU_TYPE = 'IdMenu' @@ -63,14 +77,14 @@ async def respond_with_left(message: Optional[Message], ims, **data): ims['current_page'] = current_page - 1 ims['current_index'] = None return await MonsterListMenu.respond_with_monster_list(message, ims, **data) - paginated_monsters = MonsterListViewState.query_from_ims(data['dgcog'], ims) + paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(data['dgcog'], ims) ims['current_page'] = len(paginated_monsters) - 1 ims['current_index'] = None return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): - paginated_monsters = MonsterListViewState.query_from_ims(data['dgcog'], ims) + paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(data['dgcog'], ims) current_page = ims['current_page'] if current_page < len(paginated_monsters) - 1: ims['current_page'] = current_page + 1 @@ -84,21 +98,21 @@ async def respond_with_right(message: Optional[Message], ims, **data): async def respond_with_monster_list(message: Optional[Message], ims, **data): dgcog = data['dgcog'] user_config = data['user_config'] - view_state = await MonsterListViewState.deserialize(dgcog, user_config, ims) + view_state = await _get_view_state(ims).deserialize(dgcog, user_config, ims) control = MonsterListMenu.monster_list_control(view_state) return control @staticmethod async def respond_with_previous_monster(message: Optional[Message], ims, **data): dgcog = data['dgcog'] - MonsterListMenu._update_ims_prev(dgcog, ims) + await MonsterListMenu._update_ims_prev(dgcog, ims) return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod - def _update_ims_prev(dgcog, ims): + async def _update_ims_prev(dgcog, ims): current_index = ims['current_index'] page = ims['current_page'] - paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) + paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, ims) max_index = len(paginated_monsters[page]) - 1 if current_index is None: ims['current_index'] = max_index @@ -108,7 +122,7 @@ def _update_ims_prev(dgcog, ims): return if current_index == 0 and page > 0: ims['current_page'] = page - 1 - ims['current_index'] = MonsterListViewState.MAX_ITEMS_PER_PANE + ims['current_index'] = MonsterListViewState.MAX_ITEMS_PER_PANE - 1 return if current_index == 0: # respond with left but then set the current index to the max thing possible @@ -118,14 +132,14 @@ def _update_ims_prev(dgcog, ims): @staticmethod async def respond_with_next_monster(message: Optional[Message], ims, **data): dgcog = data['dgcog'] - MonsterListMenu._update_ims_next(dgcog, ims) + await MonsterListMenu._update_ims_next(dgcog, ims) return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod - def _update_ims_next(dgcog, ims): + async def _update_ims_next(dgcog, ims): current_index = ims['current_index'] page = ims['current_page'] - paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) + paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, ims) max_index = len(paginated_monsters[page]) - 1 if current_index is None: ims['current_index'] = 0 @@ -147,7 +161,7 @@ async def respond_with_delete(message: Message, ims, **data): @staticmethod async def respond_with_n(message: Optional[Message], ims, n, **data): - ims['current_index'] =n + ims['current_index'] = n return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod @@ -205,14 +219,14 @@ def monster_list_control(state: MonsterListViewState): ) @staticmethod - def click_child_number(ims, emoji_clicked, **data): + async def click_child_number(ims, emoji_clicked, **data): dgcog = data['dgcog'] emoji_response = IdMenuEmoji.refresh \ if MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} n = MonsterListMenuPanes.emoji_names().index(emoji_clicked) - paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) + paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, ims) page = ims.get('current_page') or 0 monster_list = paginated_monsters[page] extra_ims = { @@ -225,22 +239,21 @@ def click_child_number(ims, emoji_clicked, **data): return emoji_response, extra_ims @staticmethod - def auto_scroll_child_left(ims, _emoji_clicked, **data): - return MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_prev, **data) + async def auto_scroll_child_left(ims, _emoji_clicked, **data): + return await MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_prev, **data) @staticmethod - def auto_scroll_child_right(ims, _emoji_clicked, **data): - return MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_next, **data) + async def auto_scroll_child_right(ims, _emoji_clicked, **data): + return await MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_next, **data) @staticmethod - def _scroll_child(ims, update_fn, **data): + async def _scroll_child(ims, update_fn, **data): dgcog = data['dgcog'] copy_ims = deepcopy(ims) - update_fn(dgcog, copy_ims) - paginated_monsters = MonsterListViewState.query_from_ims(dgcog, copy_ims) + await update_fn(dgcog, copy_ims) + paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, copy_ims) page = copy_ims.get('current_page') or 0 monster_list = paginated_monsters[page] - # after we figure out new rarity extra_ims = { 'is_child': True, 'reaction_list': IdMenuPanes.emoji_names(), diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 6f51d61f..1d32ed88 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -68,14 +68,14 @@ async def respond_with_left(message: Optional[Message], ims, **data): return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) current_rarity_index = ims['all_rarities'].index(ims['rarity']) ims['rarity'] = ims['all_rarities'][current_rarity_index - 1] - paginated_monsters = SeriesScrollViewState.query_from_ims(data['dgcog'], ims) + paginated_monsters = await SeriesScrollViewState.query_from_ims(data['dgcog'], ims) ims['current_page'] = len(paginated_monsters) - 1 ims['current_index'] = None return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): - paginated_monsters = SeriesScrollViewState.query_from_ims(data['dgcog'], ims) + paginated_monsters = await SeriesScrollViewState.query_from_ims(data['dgcog'], ims) current_page = ims['current_page'] if current_page < len(paginated_monsters) - 1: ims['current_page'] = current_page + 1 @@ -101,14 +101,14 @@ async def respond_with_monster_list(message: Optional[Message], ims, **data): @staticmethod async def respond_with_previous_monster(message: Optional[Message], ims, **data): dgcog = data['dgcog'] - SeriesScrollMenu._update_ims_prev(dgcog, ims) + await SeriesScrollMenu._update_ims_prev(dgcog, ims) return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) @staticmethod - def _update_ims_prev(dgcog, ims): + async def _update_ims_prev(dgcog, ims): current_index = ims['current_index'] page = ims['current_page'] - paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, ims) + paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, ims) max_index = len(paginated_monsters[page]) - 1 if current_index is None: ims['current_index'] = max_index @@ -124,21 +124,21 @@ def _update_ims_prev(dgcog, ims): # respond with left but then set the current index to the max thing possible current_rarity_index = ims['all_rarities'].index(ims['rarity']) ims['rarity'] = ims['all_rarities'][current_rarity_index - 1] - paginated_monsters_new = SeriesScrollViewState.query_from_ims(dgcog, ims) + paginated_monsters_new = await SeriesScrollViewState.query_from_ims(dgcog, ims) ims['current_page'] = len(paginated_monsters_new) - 1 ims['current_index'] = len(paginated_monsters_new[-1]) - 1 @staticmethod async def respond_with_next_monster(message: Optional[Message], ims, **data): dgcog = data['dgcog'] - SeriesScrollMenu._update_ims_next(dgcog, ims) + await SeriesScrollMenu._update_ims_next(dgcog, ims) return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) @staticmethod - def _update_ims_next(dgcog, ims): + async def _update_ims_next(dgcog, ims): current_index = ims['current_index'] page = ims['current_page'] - paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, ims) + paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, ims) max_index = len(paginated_monsters[page]) - 1 if current_index is None: ims['current_index'] = 0 @@ -221,14 +221,14 @@ def monster_list_control(state: SeriesScrollViewState): ) @staticmethod - def click_child_number(ims, emoji_clicked, **data): + async def click_child_number(ims, emoji_clicked, **data): dgcog = data['dgcog'] emoji_response = IdMenuEmoji.refresh \ if SeriesScrollMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} n = SeriesScrollMenuPanes.emoji_names().index(emoji_clicked) - paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, ims) + paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, ims) page = ims.get('current_page') or 0 monster_list = paginated_monsters[page] extra_ims = { @@ -249,11 +249,11 @@ def auto_scroll_child_right(ims, _emoji_clicked, **data): return SeriesScrollMenu._scroll_child(ims, SeriesScrollMenu._update_ims_next, **data) @staticmethod - def _scroll_child(ims, update_fn, **data): + async def _scroll_child(ims, update_fn, **data): dgcog = data['dgcog'] copy_ims = deepcopy(ims) - update_fn(dgcog, copy_ims) - paginated_monsters = SeriesScrollViewState.query_from_ims(dgcog, copy_ims) + await update_fn(dgcog, copy_ims) + paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, copy_ims) page = copy_ims.get('current_page') or 0 monster_list = paginated_monsters[page] # after we figure out new rarity diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index bee81b71..fd222735 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -46,7 +46,9 @@ from padinfo.view.links import LinksView from padinfo.view.lookup import LookupView from padinfo.view.materials import MaterialsViewState -from padinfo.view.monster_list import MonsterListViewState +from padinfo.view.monster_list.all_mats import AllMatsViewState +from padinfo.view.monster_list.id_search import IdSearchViewState +from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState from padinfo.view.otherinfo import OtherInfoViewState from padinfo.view.pantheon import PantheonViewState from padinfo.view.pic import PicViewState @@ -536,25 +538,20 @@ async def buttoninfo(self, ctx, *, query: str): async def allmats(self, ctx, *, query: str): """Monster info (evo materials tab)""" dgcog = await self.get_dgcog() - raw_query = query - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dgcog.find_monster(query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) return - - await self.log_id_result(ctx, monster.monster_id) - - _, usedin, _, gemusedin, _, _, _, _ = \ - await MaterialsViewState.query(dgcog, monster) - - if usedin is None and gemusedin is None: + monster_list = await AllMatsViewState.query(dgcog, monster) + if monster_list is None: await ctx.send(inline("This monster is not a mat for anything nor does it have a gem")) return - monster_list = usedin or gemusedin + _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.query(dgcog, monster) + title = 'Material For' if usedin else 'Gem is Material For' - await self._do_monster_list(ctx, dgcog, query, monster_list, title) + await self._do_monster_list(ctx, dgcog, query, monster_list, title, AllMatsViewState) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -566,31 +563,26 @@ async def evolist(self, ctx, *, query): await self.send_id_failure_message(ctx, query) return - alt_versions, _ = await EvosViewState.query(dgcog, monster) - if alt_versions is None: + monster_list, _ = await EvosViewState.query(dgcog, monster) + if monster_list is None: await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, monster.name_en) + 'which has no alt evos.') return - await self._do_monster_list(ctx, dgcog, query, alt_versions, 'Evolution List') + await self._do_monster_list(ctx, dgcog, query, monster_list, 'Evolution List', StaticMonsterListViewState) - async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], title): + async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], + title, view_state_type): raw_query = query original_author_id = ctx.message.author.id color = await self.get_user_embed_color(ctx) initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' - subtitle = None - if len(monster_list) > MonsterListViewState.MAX_INTERNAL_STORE_SIZE: - subtitle = 'List has been truncated to first {} items'.format(MonsterListViewState.MAX_INTERNAL_STORE_SIZE) - monster_list = monster_list[:MonsterListViewState.MAX_INTERNAL_STORE_SIZE] - paginated_monsters = MonsterListViewState.paginate(monster_list) - state = MonsterListViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, query, color, - paginated_monsters, len(paginated_monsters), 0, - title, instruction_message, - subtitle, - reaction_list=initial_reaction_list - ) + state = view_state_type(original_author_id, view_state_type.VIEW_STATE_TYPE, query, color, + monster_list, + title, instruction_message, + reaction_list=initial_reaction_list + ) parent_menu = MonsterListMenu.menu() message = await ctx.send('Setting up!') @@ -600,7 +592,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM 'dgcog': dgcog, 'user_config': user_config, } - child_state = SimpleTextViewState(original_author_id, MonsterListMenu.MENU_TYPE, raw_query, color, + child_state = SimpleTextViewState(original_author_id, view_state_type.VIEW_STATE_TYPE, raw_query, color, instruction_message, reaction_list=[] ) @@ -630,7 +622,7 @@ async def collabscroll(self, ctx, *, query): paginated_monsters = None rarity = None for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: - paginated_monsters = SeriesScrollViewState.query(dgcog, monster.series_id, rarity) + paginated_monsters = await SeriesScrollViewState.query(dgcog, monster.series_id, rarity) if paginated_monsters: break all_rarities = SeriesScrollViewState.query_all_rarities(dgcog, series_id) @@ -800,13 +792,12 @@ async def idhistory(self, ctx): dgcog = await self.get_dgcog() history = await self.config.user(ctx.author).id_history() - monsters = [dgcog.get_monster(m) for m in history] + monster_list = [dgcog.get_monster(m) for m in history] - if not monsters: + if not monster_list: await ctx.send('Did not find any recent queries in history.') return - - await self._do_monster_list(ctx, dgcog, '', monsters, 'Result History') + await self._do_monster_list(ctx, dgcog, '', monster_list, 'Result History', StaticMonsterListViewState) @commands.command() async def padsay(self, ctx, server, *, query: str = None): @@ -1215,18 +1206,11 @@ async def idtraceback(self, ctx, *, query): async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") - matched_monsters = await dgcog.find_monsters(query, ctx.author.id) + monster_list = await IdSearchViewState.query(dgcog, query, ctx.author.id) - if not matched_monsters: + if monster_list is None: await ctx.send("No monster matched.") return - used = set() - monster_list = [] - for mon in matched_monsters: - base_id = dgcog.database.graph.get_base_id(mon) - if base_id not in used: - used.add(base_id) - monster_list.append(mon) - - await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results') + print(monster_list) + await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results', IdSearchViewState) diff --git a/padinfo/view/monster_list/__init__.py b/padinfo/view/monster_list/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padinfo/view/monster_list/all_mats.py b/padinfo/view/monster_list/all_mats.py new file mode 100644 index 00000000..5554b879 --- /dev/null +++ b/padinfo/view/monster_list/all_mats.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING, List, Optional + +from padinfo.view.materials import MaterialsViewState +from padinfo.view.monster_list.monster_list import MonsterListViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class AllMatsViewState(MonsterListViewState): + VIEW_STATE_TYPE = "AllMats" + + @classmethod + async def query(cls, dgcog, monster: "MonsterModel") -> Optional[List["MonsterModel"]]: + _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.query(dgcog, monster) + if usedin is None and gemusedin is None: + return None + monster_list = usedin or gemusedin + return monster_list + + @classmethod + async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: + monster = await dgcog.find_monster(ims['raw_query'], ims['original_author_id']) + monster_list = await cls.query(dgcog, monster) + return monster_list diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py new file mode 100644 index 00000000..1d7c327d --- /dev/null +++ b/padinfo/view/monster_list/id_search.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING, List + +from padinfo.view.monster_list.monster_list import MonsterListViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class IdSearchViewState(MonsterListViewState): + VIEW_STATE_TYPE = "IdSearch" + + @classmethod + async def query(cls, dgcog, query, original_author_id) -> List["MonsterModel"]: + found_monsters = await dgcog.find_monsters(query, original_author_id) + + if not found_monsters: + return None + + used = set() + monster_list = [] + for mon in found_monsters: + base_id = dgcog.database.graph.get_base_id(mon) + if base_id not in used: + used.add(base_id) + monster_list.append(mon) + + return monster_list + + @classmethod + async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: + monster_list = await cls.query(dgcog, ims['raw_query'], ims['original_author_id']) + return monster_list diff --git a/padinfo/view/monster_list.py b/padinfo/view/monster_list/monster_list.py similarity index 63% rename from padinfo/view/monster_list.py rename to padinfo/view/monster_list/monster_list.py index bfc939a5..3a61b70d 100644 --- a/padinfo/view/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -12,28 +12,27 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel - from dadguide.database_context import DbContext class MonsterListViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 11 - MAX_INTERNAL_STORE_SIZE = 100 - def __init__(self, original_author_id, menu_type, raw_query, query, color, - paginated_monsters: List[List["MonsterModel"]], page_count, current_page, - title, message, subtitle: str = None, + def __init__(self, original_author_id, menu_type, query, color, + monster_list: List["MonsterModel"], + title, message, + *, + current_page: int = 0, current_index: int = None, - max_len_so_far: int = None, reaction_list=None, extra_state=None, child_message_id=None ): - super().__init__(original_author_id, menu_type, raw_query, + super().__init__(original_author_id, menu_type, query, extra_state=extra_state) - self.subtitle = subtitle + paginated_monsters = self.paginate(monster_list) self.current_index = current_index self.current_page = current_page - self.page_count = page_count + self.page_count = len(paginated_monsters) self.message = message self.child_message_id = child_message_id self.title = title @@ -42,40 +41,30 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, self.reaction_list = reaction_list self.color = color self.query = query - self.max_len_so_far = max( - max_len_so_far or len(paginated_monsters[current_page]), - len(paginated_monsters[current_page])) def serialize(self): ret = super().serialize() ret.update({ 'pane_type': MonsterListView.VIEW_TYPE, 'title': self.title, - 'subtitle': self.subtitle, 'monster_list': [m.monster_id for m in self.monster_list], - 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], 'current_page': self.current_page, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, 'message': self.message, - 'max_len_so_far': self.max_len_so_far, 'current_index': self.current_index, }) return ret - @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): + @classmethod + async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None title = ims['title'] - subtitle = ims['subtitle'] - paginated_monsters = MonsterListViewState.query_from_ims(dgcog, ims) - page_count = len(paginated_monsters) + monster_list = await cls.query_from_ims(dgcog, ims) current_page = ims['current_page'] - monster_list = paginated_monsters[current_page] current_index = ims.get('current_index') - max_len_so_far = max(ims['max_len_so_far'] or len(monster_list), len(monster_list)) raw_query = ims['raw_query'] query = ims.get('query') or raw_query @@ -84,30 +73,19 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') child_message_id = ims.get('child_message_id') message = ims.get('message') - return MonsterListViewState(original_author_id, menu_type, raw_query, query, user_config.color, - paginated_monsters, - page_count, current_page, + return MonsterListViewState(original_author_id, menu_type, query, user_config.color, + monster_list, title, message, - subtitle=subtitle, + current_page=current_page, current_index=current_index, - max_len_so_far=max_len_so_far, reaction_list=reaction_list, extra_state=ims, child_message_id=child_message_id ) - @staticmethod - def query(dgcog, monster_list): - db_context: "DbContext" = dgcog.database - monster_list = [db_context.graph.get_monster(int(m)) for m in monster_list] - paginated_monsters = [monster_list[i:i + MonsterListViewState.MAX_ITEMS_PER_PANE] - for i in range(0, len(monster_list), MonsterListViewState.MAX_ITEMS_PER_PANE)] - return paginated_monsters - - @staticmethod - def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: - monster_list = ims['full_monster_list'] - return MonsterListViewState.query(dgcog, monster_list) + @classmethod + async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: + ... @staticmethod def paginate(monster_list: List["MonsterModel"]) -> List[List["MonsterModel"]]: @@ -115,6 +93,10 @@ def paginate(monster_list: List["MonsterModel"]) -> List[List["MonsterModel"]]: for i in range(0, len(monster_list), MonsterListViewState.MAX_ITEMS_PER_PANE)] return paginated_monsters + @classmethod + async def query_paginated_from_ims(cls, dgcog, ims) -> List[List["MonsterModel"]]: + return cls.paginate(await cls.query_from_ims(dgcog, ims)) + def _monster_list(monsters): if not len(monsters): @@ -132,7 +114,7 @@ class MonsterListView: def embed(state: MonsterListViewState): fields = [ EmbedField(state.title, - Box(state.subtitle, *_monster_list(state.monster_list))), + Box(*_monster_list(state.monster_list))), EmbedField(BoldText('Page'), Box('{} of {}'.format(state.current_page + 1, state.page_count)), inline=True diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py new file mode 100644 index 00000000..6e607507 --- /dev/null +++ b/padinfo/view/monster_list/static_monster_list.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING, List + +from padinfo.view.monster_list.monster_list import MonsterListViewState + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + from dadguide.database_context import DbContext + + +class StaticMonsterListViewState(MonsterListViewState): + VIEW_STATE_TYPE = "StaticMonsterList" + + def serialize(self): + ret = super().serialize() + ret.update({ + 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], + }) + return ret + + @classmethod + async def query(cls, dgcog, monster_list) -> List["MonsterModel"]: + db_context: "DbContext" = dgcog.database + monster_list = [db_context.graph.get_monster(int(m)) for m in monster_list] + return monster_list + + @classmethod + async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: + monster_ids = ims['full_monster_list'] + monster_list = await cls.query(dgcog, monster_ids) + return monster_list diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 11c1f43b..1f317056 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -69,7 +69,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): series_id = ims['series_id'] rarity = ims['rarity'] all_rarities = ims['all_rarities'] - paginated_monsters = SeriesScrollViewState.query(dgcog, series_id, rarity) + paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity) current_page = ims['current_page'] monster_list = paginated_monsters[current_page] title = ims['title'] @@ -96,7 +96,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): child_message_id=child_message_id) @staticmethod - def query(dgcog, series_id, rarity): + async def query(dgcog, series_id, rarity): db_context: "DbContext" = dgcog.database all_series_monsters = db_context.get_monsters_by_series(series_id) base_monsters_of_rarity = list(filter( @@ -113,10 +113,10 @@ def query_all_rarities(dgcog, series_id): m.series_id == series_id and db_context.graph.monster_is_base(m)}) @staticmethod - def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: + async def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: series_id = ims['series_id'] rarity = ims['rarity'] - paginated_monsters = SeriesScrollViewState.query(dgcog, series_id, rarity) + paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity) return paginated_monsters From 2eb390608b1c771ab6225a4ae21bd3284c24e26c Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 26 May 2021 17:53:38 -0700 Subject: [PATCH 0786/1220] Make breakglass restart bot (#1103) --- padglobal/padglobal.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index d04d7bbe..1bff6b6c 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -14,8 +14,7 @@ import pytz import tsutils from discord import Color -from redbot.core import checks, data_manager -from redbot.core import commands, errors +from redbot.core import checks, data_manager, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json, \ auth_check, get_reaction @@ -128,7 +127,6 @@ async def get_menu_default_data(self, ims): return data def _export_data(self): - faq_and_boards = self.settings.faq() + self.settings.boards() general = {k: v for k, v in self.c_commands.items() if k not in faq_and_boards} faq = {k: v for k, v in self.c_commands.items() if k in self.settings.faq()} @@ -172,14 +170,13 @@ class of problems where the bot can read messages but not respond, so this logger.critical(msg) try: - app_info = await self.bot.application_info() - owner = app_info.owner - await owner.send(msg) - await ctx.send("Owner has been notified, shutting down...") + for uid in self.bot.owner_ids: + await self.bot.get_user(uid).send(msg) + await ctx.send("Owners have been notified, shutting down...") except Exception as ex: - logger.exception('Failed to notifiy for breakglass: ' + str(ex)) + logger.exception("Failed to notifiy for breakglass.") - await self.bot.shutdown() + await self.bot.shutdown(restart=True) @commands.command() @checks.admin_or_permissions(manage_guild=True) From 846aceb84a1796d4fb517cad7bd3652b079dbd04 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 26 May 2021 20:07:39 -0500 Subject: [PATCH 0787/1220] Retain index if only one page (#1104) --- padinfo/menu/monster_list.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 74df57ea..509cfd8e 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -79,7 +79,8 @@ async def respond_with_left(message: Optional[Message], ims, **data): return await MonsterListMenu.respond_with_monster_list(message, ims, **data) paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(data['dgcog'], ims) ims['current_page'] = len(paginated_monsters) - 1 - ims['current_index'] = None + if len(paginated_monsters) > 1: + ims['current_index'] = None return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod @@ -91,7 +92,8 @@ async def respond_with_right(message: Optional[Message], ims, **data): ims['current_index'] = None return await MonsterListMenu.respond_with_monster_list(message, ims, **data) ims['current_page'] = 0 - ims['current_index'] = None + if len(paginated_monsters) > 1: + ims['current_index'] = None return await MonsterListMenu.respond_with_monster_list(message, ims, **data) @staticmethod From 42b46a76ff20e2ed361d1a01b649eba17b65c497 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 26 May 2021 22:41:40 -0500 Subject: [PATCH 0788/1220] Improve `which` "uncontroversial" message (#1105) --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 1bff6b6c..25fb56f1 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -54,7 +54,7 @@ def _data_file(file_name: str) -> str: FARMABLE_MSG = 'This monster is **farmable** so make as many copies of whichever evos you like.' MP_BUY_MSG = ('This monster can be purchased with MP. **DO NOT** buy MP cards without a good reason' ', check `{}mpdra?` for specific recommendations.') -SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial; use the highest evolution: `[{}] {}`.' +SIMPLE_TREE_MSG = 'This monster appears to be uncontroversial because the skill never changes & there are no branching evos; use the highest evolution: `[{}] {}`.' MAX_WHICH_LIST_BEFORE_DM_PROMPT = 30 From 324c9d784634dcd13c4aa7e432866c4e36a45fbd Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Thu, 27 May 2021 16:11:33 -0400 Subject: [PATCH 0789/1220] Warn of aliases when using padglobal delete (#1108) --- padglobal/padglobal.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 25fb56f1..4fc98cd5 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -233,6 +233,9 @@ async def _add(self, ctx, command, text, confirm=True): if text in self.c_commands: op = 'ALIASED' + if text == command: + await ctx.send('You cannot alias something to itself.') + return if self.c_commands[text] in self.c_commands: await ctx.send("You cannot alias an alias") return @@ -262,7 +265,17 @@ async def delete(self, ctx, command: str): Example: [p]padglobal delete yourcommand""" command = command.lower() + + aliases = await self._find_aliases(command) + if aliases: + if not await confirm_message(ctx, + 'Are you sure? `{}` has **{}** alias(es): `{}` which will also be deleted.' + .format(command, len(aliases), '`, `'.join(aliases))): + await ctx.send('Cancelling delete of `{}`.'.format(command)) + return + if command in self.c_commands: + alias = self.c_commands[command] in self.c_commands ocm = self.c_commands.copy() self.c_commands.pop(command, None) todel = [command] @@ -273,7 +286,7 @@ async def delete(self, ctx, command: str): self.c_commands.pop(comm, None) todel.append(comm) json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("PAD command successfully deleted.") + await ctx.send("PAD {} successfully deleted.".format('ALIAS' if alias else 'COMMAND')) else: await ctx.send("PAD command doesn't exist.") @@ -308,6 +321,13 @@ async def append(self, ctx, command: str, *, addition): await ctx.send("Successfully appended to {}PAD command `{}`.".format("source " if alias else "", source_cmd if alias else corrected_cmd)) + async def _find_aliases(self, command: str): + aliases = [] + for cmd in self.c_commands: + if self.c_commands[cmd] == command: + aliases.append(cmd) + return aliases + @padglobal.command() async def setgeneral(self, ctx, command: str): """Sets a command to show up in [p]pad (the default). From c97890c5109c0973662d7b7bcb37ddd6ddfc681b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 27 May 2021 21:52:26 -0500 Subject: [PATCH 0790/1220] Add bindclear, rowclear as aliases (#1112) --- dadguide/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 584e64d6..9b5a244f 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -189,7 +189,7 @@ class AwokenSkills(Enum): AwokenSkills.ENHANCEDLIGHT: ('oel', 'oe'), AwokenSkills.ENHANCEDDARK: ('oed', 'oe'), AwokenSkills.EXTMOVE: ('te', 'finger'), - AwokenSkills.BINDRECOVERY: ('bindrcv',), + AwokenSkills.BINDRECOVERY: ('bindrcv', 'bindclear', 'rowclear'), AwokenSkills.SKILLBOOST: ('sb',), AwokenSkills.REDROW: ('rowr', 'row'), AwokenSkills.BLUEROW: ('rowb', 'row'), From fbd436b7c369fb0d4e43236396844806cc2510e3 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Fri, 28 May 2021 22:20:57 -0400 Subject: [PATCH 0791/1220] Distinguish padglobal operations with bold text (#1113) --- padglobal/padglobal.py | 56 ++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 4fc98cd5..e224ce11 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -15,7 +15,7 @@ import tsutils from discord import Color from redbot.core import checks, data_manager, commands, errors -from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta +from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta, bold from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json, \ auth_check, get_reaction @@ -232,7 +232,7 @@ async def _add(self, ctx, command, text, confirm=True): self.c_commands = {} if text in self.c_commands: - op = 'ALIASED' + op = 'aliased' if text == command: await ctx.send('You cannot alias something to itself.') return @@ -240,7 +240,7 @@ async def _add(self, ctx, command, text, confirm=True): await ctx.send("You cannot alias an alias") return elif command in self.c_commands: - op = 'EDITED' + op = 'edited' ted = self.c_commands[command] alias = False while ted in self.c_commands: @@ -252,11 +252,11 @@ async def _add(self, ctx, command, text, confirm=True): if not conf: return else: - op = 'ADDED' + op = 'added' self.c_commands[command] = text json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("PAD command successfully {}.".format(op)) + await ctx.send("PAD command successfully {}.".format(bold(op))) @padglobal.command(aliases=['rmalias', 'delalias', 'remove', 'rm', 'del']) async def delete(self, ctx, command: str): @@ -269,8 +269,8 @@ async def delete(self, ctx, command: str): aliases = await self._find_aliases(command) if aliases: if not await confirm_message(ctx, - 'Are you sure? `{}` has **{}** alias(es): `{}` which will also be deleted.' - .format(command, len(aliases), '`, `'.join(aliases))): + 'Are you sure? `{}` has {} alias(es): `{}` which will also be deleted.' + .format(command, bold(len(aliases)), '`, `'.join(aliases))): await ctx.send('Cancelling delete of `{}`.'.format(command)) return @@ -286,7 +286,7 @@ async def delete(self, ctx, command: str): self.c_commands.pop(comm, None) todel.append(comm) json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("PAD {} successfully deleted.".format('ALIAS' if alias else 'COMMAND')) + await ctx.send("PAD {} successfully deleted.".format(bold('alias' if alias else 'command'))) else: await ctx.send("PAD command doesn't exist.") @@ -537,13 +537,13 @@ async def pglossary_add(self, ctx, term, *, definition): definition = definition.replace(u'\u200b', '') definition = replace_emoji_names_with_code(self._get_emojis(), definition) - op = 'EDITED' if term in self.settings.glossary() else 'ADDED' - if op == 'EDITED' and not await confirm_message(ctx, + op = 'edited' if term in self.settings.glossary() else 'added' + if op == 'edited' and not await confirm_message(ctx, "Are you sure you want to edit the glossary info for {}?".format( term)): return self.settings.addGlossary(term, definition) - await ctx.send("PAD glossary term successfully {}.".format(op)) + await ctx.send("PAD glossary term successfully {}.".format(bold(op))) @pglossary.command(name='remove', aliases=['rm', 'delete', 'del']) async def pglossary_remove(self, ctx, *, term): @@ -638,8 +638,8 @@ async def pboss_add(self, ctx, term, *, definition): base = dgcog.database.graph.get_base_monster(m) - op = 'EDITED' if base.monster_id in self.settings.boss() else 'ADDED' - if op == 'EDITED' and not await confirm_message(ctx, + op = 'edited' if base.monster_id in self.settings.boss() else 'added' + if op == 'edited' and not await confirm_message(ctx, "Are you sure you want to edit the boss info for {}?".format( base.name_en)): return @@ -647,7 +647,7 @@ async def pboss_add(self, ctx, term, *, definition): definition = definition.replace(u'\u200b', '') definition = replace_emoji_names_with_code(self._get_emojis(), definition) self.settings.addBoss(base.monster_id, definition) - await ctx.send("PAD boss mechanics successfully {}.".format(op)) + await ctx.send("PAD boss mechanics successfully {}.".format(bold(op))) @pboss.command(name='remove', aliases=['rm', 'delete', 'del']) async def pboss_remove(self, ctx, *, term): @@ -812,10 +812,10 @@ async def pwhich_add(self, ctx, term, *, definition): is_int = re.fullmatch(r'\d+', term) - op = 'EDITED' if name in self.settings.which() else 'ADDED' - if op == 'ADDED' and not is_int or op == 'EDITED': + op = 'edited' if name in self.settings.which() else 'added' + if op == 'added' and not is_int or op == 'edited': if not await confirm_message(ctx, "Are you sure you want to {} which info for {} [{}] {}?".format( - 'edit the' if op == 'EDITED' else 'add new', + 'edit the' if op == 'edited' else 'add new', pdicog.get_attribute_emoji_by_monster(m), m.monster_no_na, m.name_en)): @@ -825,7 +825,7 @@ async def pwhich_add(self, ctx, term, *, definition): definition = definition.replace(u'\u200b', '') definition = replace_emoji_names_with_code(self._get_emojis(), definition) self.settings.addWhich(name, definition) - await ctx.send("PAD which info successfully {} for [{}] {}.".format(op, m.monster_no_na, m.name_en)) + await ctx.send("PAD which info successfully {} for [{}] {}.".format(bold(op), m.monster_no_na, m.name_en)) @pwhich.command(name='remove', aliases=['rm', 'delete', 'del']) async def pwhich_remove(self, ctx, *, monster_id: int): @@ -886,7 +886,7 @@ async def _concatenate_which(self, ctx, term: str, operation: str, addition): if await confirm_message(ctx, "No which info exists for {}. Would you like to add a new entry?".format( m.name_en)): self.settings.addWhich(mon_id, addition) - await ctx.send("PAD which info successfully ADDED.") + await ctx.send("PAD which info successfully {}.".format(bold('added'))) return definition, _ = self.settings.which().get(mon_id, None) @@ -897,10 +897,12 @@ async def _concatenate_which(self, ctx, term: str, operation: str, addition): if operation == 'prepend': self.settings.addWhich(mon_id, '{}\n\n{}'.format(addition, definition)) - await ctx.send("Successfully PREPENDED to PAD which info for [{}] {}.".format(m.monster_no_na, m.name_en)) + await ctx.send("Successfully {} to PAD which info for [{}] {}.".format(bold('prepended'), m.monster_no_na, + m.name_en)) elif operation == 'append': self.settings.addWhich(mon_id, '{}\n\n{}'.format(definition, addition)) - await ctx.send("Successfully APPENDED to PAD which info for [{}] {}.".format(m.monster_no_na, m.name_en)) + await ctx.send("Successfully {} to PAD which info for [{}] {}.".format(bold('appended'), m.monster_no_na, + m.name_en)) else: raise KeyError("Invalid operation: Must be \'prepend\' or \'append\'") @@ -1249,13 +1251,13 @@ async def dungeon(self, ctx): async def dungeon_add(self, ctx, term: str, *, definition: str): """Adds a dungeon guide to the [p]guide command""" term = term.lower() - op = 'EDITED' if term in self.settings.dungeonGuide() else 'ADDED' - if op == 'EDITED' and not await confirm_message(ctx, + op = 'edited' if term in self.settings.dungeonGuide() else 'added' + if op == 'edited' and not await confirm_message(ctx, "Are you sure you want to edit the dungeon guide info for {}?".format( term)): return self.settings.addDungeonGuide(term, definition) - await ctx.send("PAD dungeon guide successfully {}.".format(op)) + await ctx.send("PAD dungeon guide successfully {}.".format(bold(op))) @dungeon.command(name='remove', aliases=['rm', 'delete', 'del']) async def dungeon_remove(self, ctx, term: str): @@ -1285,13 +1287,13 @@ async def leader_add(self, ctx, monster_id: int, *, definition: str): await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) name = m.monster_id - op = 'EDITED' if name in self.settings.leaderGuide() else 'ADDED' - if op == 'EDITED' and not await confirm_message(ctx, + op = 'edited' if name in self.settings.leaderGuide() else 'added' + if op == 'edited' and not await confirm_message(ctx, "Are you sure you want to edit the leader guide for {}?".format( m.name_en)): return self.settings.addLeaderGuide(name, definition) - await ctx.send("PAD leader guide info successfully {}.".format(op)) + await ctx.send("PAD leader guide info successfully {}.".format(bold(op))) @leader.command(name='remove', aliases=['rm', 'delete', 'del']) async def leader_remove(self, ctx, monster_id: int): From 3171a22b375aad817019603d797d42acea767152 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sat, 29 May 2021 15:23:15 -0400 Subject: [PATCH 0792/1220] Resolve double-aliases for the user (#1119) --- padglobal/padglobal.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index e224ce11..241e8938 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -237,8 +237,14 @@ async def _add(self, ctx, command, text, confirm=True): await ctx.send('You cannot alias something to itself.') return if self.c_commands[text] in self.c_commands: - await ctx.send("You cannot alias an alias") - return + source = self.c_commands[text] + if await confirm_message(ctx, '{} is already an alias for {}, and you can\'t alias to an alias.' + .format(inline(text), inline(source)) + + ' Would you like to alias to {} instead?'.format(inline(source))): + # change target + text = source + else: + return elif command in self.c_commands: op = 'edited' ted = self.c_commands[command] @@ -270,7 +276,7 @@ async def delete(self, ctx, command: str): if aliases: if not await confirm_message(ctx, 'Are you sure? `{}` has {} alias(es): `{}` which will also be deleted.' - .format(command, bold(len(aliases)), '`, `'.join(aliases))): + .format(command, bold(str(len(aliases))), '`, `'.join(aliases))): await ctx.send('Cancelling delete of `{}`.'.format(command)) return From debde645bfd58f496dddbca9ed496b227ad70f3a Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 30 May 2021 04:44:20 -0700 Subject: [PATCH 0793/1220] idna!!!!!!!!!!!!!!!!!!!!!!!!!!!!! (#1118) --- dadguide/common/__init__.py | 0 dadguide/common/enums.py | 5 + dadguide/dadguide.py | 22 +- dadguide/database_context.py | 42 +- dadguide/find_monster.py | 52 +- dadguide/idtest_mixin.py | 33 +- dadguide/models/active_skill_model.py | 8 + dadguide/models/awakening_model.py | 5 +- dadguide/models/leader_skill_model.py | 9 +- dadguide/models/monster/__init__.py | 0 dadguide/models/monster/monster_difference.py | 80 +++ dadguide/models/monster_model.py | 18 +- dadguide/monster_graph.py | 560 +++++++----------- dadguide/monster_index.py | 48 +- padglobal/padglobal.py | 6 +- padinfo/core/button_info.py | 4 +- padinfo/core/leader_skills.py | 3 +- padinfo/core/transforminfo.py | 20 +- padinfo/menu/id.py | 12 +- padinfo/menu/monster_list.py | 1 + padinfo/menu/series_scroll.py | 3 + padinfo/menu/transforminfo.py | 9 +- padinfo/padinfo.py | 98 +-- padinfo/view/common.py | 4 +- padinfo/view/components/monster/header.py | 48 +- padinfo/view/components/view_state_base_id.py | 16 +- padinfo/view/evos.py | 21 +- padinfo/view/id.py | 23 +- padinfo/view/materials.py | 20 +- padinfo/view/monster_list/monster_list.py | 1 + .../view/monster_list/static_monster_list.py | 8 +- padinfo/view/otherinfo.py | 6 +- padinfo/view/pantheon.py | 18 +- padinfo/view/pic.py | 6 +- padinfo/view/series_scroll.py | 14 +- padinfo/view/transforminfo.py | 16 +- padmonitor/padmonitor.py | 2 +- tests/dadguide/database_context_test.py | 33 +- 38 files changed, 667 insertions(+), 607 deletions(-) create mode 100644 dadguide/common/__init__.py create mode 100644 dadguide/common/enums.py create mode 100644 dadguide/models/monster/__init__.py create mode 100644 dadguide/models/monster/monster_difference.py diff --git a/dadguide/common/__init__.py b/dadguide/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dadguide/common/enums.py b/dadguide/common/enums.py new file mode 100644 index 00000000..2a4a1b8b --- /dev/null +++ b/dadguide/common/enums.py @@ -0,0 +1,5 @@ +from typing import Literal + +DEFAULT_SERVER = "COMBINED" +SERVERS = ["NA", "COMBINED"] +Server = Literal["COMBINED", "NA"] diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 010f6dff..e95b2832 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -11,7 +11,7 @@ import os import shutil from io import BytesIO -from typing import Optional, List +from typing import List, Dict, Optional import discord import time @@ -26,6 +26,7 @@ from .idtest_mixin import IdTest from .models.monster_model import MonsterModel from .models.monster_stats import monster_stats, MonsterStatModifierInput +from .common.enums import DEFAULT_SERVER, SERVERS, Server from .monster_index import MonsterIndex logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -67,7 +68,7 @@ def __init__(self, bot, *args, **kwargs): self._is_ready = asyncio.Event() self.database = None - self.index = None # type: Optional[MonsterIndex] + self.indexes = {} # type: Dict[str, MonsterIndex] self.fir_lock = asyncio.Lock() self.fir3_lock = asyncio.Lock() @@ -83,7 +84,7 @@ def __init__(self, bot, *args, **kwargs): self.MonsterStatModifierInput = MonsterStatModifierInput self.token_maps = token_mappings - self.mon_finder = FindMonster(self, self.fm_flags_default) + self.mon_finder = None # type: Optional[FindMonster] GADMIN_COG = self.bot.get_cog("GlobalAdmin") if GADMIN_COG: @@ -138,23 +139,26 @@ async def forceindexreload3(self, ctx): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" - self.index = await MonsterIndex(self.database.get_all_monsters(), self.database) # noqa + for server in SERVERS: + self.indexes[server] = await MonsterIndex(self.database.graph, server) # noqa + self.mon_finder = FindMonster(self, self.fm_flags_default) asyncio.create_task(self.check_index()) async def check_index(self): if not await self.config.indexlog(): return - self.index.issues.extend((await self.run_tests())[:25]) + index = self.indexes["COMBINED"] + index.issues.extend((await self.run_tests())[:25]) channel = self.bot.get_channel(await self.config.indexlog()) - if self.index.issues: - for page in pagify("Index Load Warnings:\n" + "\n".join(self.index.issues[:100])): + if index.issues: + for page in pagify(f"Index Load Warnings:\n" + "\n".join(index.issues[:100])): await channel.send(box(page)) - def get_monster(self, monster_id: int) -> MonsterModel: + def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" - return self.database.graph.get_monster(monster_id) + return self.database.graph.get_monster(monster_id, server=server) def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty diff --git a/dadguide/database_context.py b/dadguide/database_context.py index b508b45c..7784133a 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -1,4 +1,4 @@ -from typing import Generator, List, Optional +from typing import Generator, List from .database_manager import DadguideDatabase from .models.awoken_skill_model import AwokenSkillModel @@ -6,6 +6,7 @@ from .models.scheduled_event_model import ScheduledEventModel from .models.series_model import SeriesModel from .monster_graph import MonsterGraph +from .common.enums import DEFAULT_SERVER, Server SCHEDULED_EVENT_QUERY = """SELECT schedule.*, @@ -25,38 +26,29 @@ def __init__(self, database: DadguideDatabase, graph: MonsterGraph): self.awoken_skill_map = {awsk.awoken_skill_id: awsk for awsk in self.get_all_awoken_skills()} def get_awoken_skill_ids(self): - SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id from awoken_skills' + SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id FROM awoken_skills' return [r.awoken_skill_id for r in self.database.query_many( SELECT_AWOKEN_SKILL_IDS, (), as_generator=True)] - def get_monsters_where(self, f): - return [m for m in self.get_all_monsters() if f(m)] + def get_monsters_where(self, f, *, server: Server): + return [m for m in self.get_all_monsters(server) if f(m)] - def get_first_monster_where(self, f): - ms = self.get_monsters_where(f) - if ms: - return min(ms, key=lambda m: m.monster_id) + def get_monsters_by_series(self, series_id: int, *, server: Server): + return self.get_monsters_where(lambda m: m.series_id == series_id, server=server) - def get_monsters_by_series(self, series_id: int): - return self.get_monsters_where(lambda m: m.series_id == series_id) + def get_monsters_by_active(self, active_skill_id: int, *, server: Server): + return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id, server=server) - def get_monsters_by_active(self, active_skill_id: int): - return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id) - - def get_all_monster_ids_query(self, as_generator=True): + def get_all_monster_ids_query(self, server: Server): + table = 'monsters_na' if server == "NA" else 'monsters' query = self.database.query_many( - self.database.select_builder(tables={'monsters': ('monster_id',)}), (), - as_generator=as_generator) - if as_generator: - return map(lambda m: m.monster_id, query) - return [m.monster_id for m in query] - - def get_all_monsters(self, as_generator=True): - monsters = (self.graph.get_monster(mid) for mid in self.get_all_monster_ids_query()) - if not as_generator: - return [*monsters] - return monsters + self.database.select_builder(tables={table: ('monster_id',)}), (), + as_generator=True) + return map(lambda m: m.monster_id, query) + + def get_all_monsters(self, server: Server = DEFAULT_SERVER): + return (self.graph.get_monster(mid, server=server) for mid in self.get_all_monster_ids_query(server)) def get_all_events(self) -> Generator[ScheduledEventModel, None, None]: result = self.database.query_many(SCHEDULED_EVENT_QUERY, ()) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 2474c5a8..cc0319b0 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -8,6 +8,7 @@ from dadguide.find_monster_tokens import Token, SPECIAL_TOKEN_TYPES from dadguide.models.monster_model import MonsterModel +from dadguide.common.enums import DEFAULT_SERVER SERIES_TYPE_PRIORITY = { "regular": 4, @@ -41,12 +42,25 @@ class FindMonster: def __init__(self, dgcog, flags: Mapping[str, Any]): self.dgcog = dgcog + self.index = self.dgcog.indexes["COMBINED"] self.flags = flags + def _process_settings(self, original_query: str) -> str: + settings = re.findall(r'--\w+(?:{.+?})?', original_query) + + for setting in settings: + if self.calc_ratio_modifier(setting, '--na') >= self.MODIFIER_JW_DISTANCE: + self.index = self.dgcog.indexes['NA'] + else: + self.index = self.dgcog.indexes[DEFAULT_SERVER] + + return re.sub(r'\s*--\w+(?:{.+?})?\s*', ' ', original_query) + def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: result = [] skip = 0 - multi_word_tokens_sorted = sorted(self.dgcog.index.multi_word_tokens, + + multi_word_tokens_sorted = sorted(self.index.multi_word_tokens, key=lambda x: (len(x), len(''.join(x))), reverse=True) for c1, token1 in enumerate(tokens): @@ -73,12 +87,12 @@ def _monster_has_modifier(self, monster: MonsterModel, token: Token, matches: Ma return False if len(token.value) < 6: - if token.value in self.dgcog.index.modifiers[monster]: + if token.value in self.index.modifiers[monster]: matches[monster].mod.add((token.value, token.value)) matches[monster].score += 1 return True else: - closest = max(self.dgcog.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(token, m)) + closest = max(self.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(token, m)) ratio = self.calc_ratio_modifier(closest, token.value) if ratio > self.MODIFIER_JW_DISTANCE: matches[monster].mod.add((token.value, closest)) @@ -100,7 +114,7 @@ def _string_to_token(self, value: str) -> Token: def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: modifiers = [] name = set() - longmods = [p for p in self.dgcog.index.all_modifiers if len(p) > 8] + longmods = [p for p in self.index.all_modifiers if len(p) > 8] lastmodpos = False # Suffixes @@ -108,7 +122,7 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ token = self._string_to_token(value) if any(self.calc_ratio_modifier(m, token.value.split('-')[0], .1) > self.MODIFIER_JW_DISTANCE - for m in self.dgcog.index.suffixes): + for m in self.index.suffixes): # TODO: Store this as a list of regexes. Don't split on '-' anymore. modifiers.append(token) else: @@ -120,9 +134,9 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ for i, value in enumerate(tokenized_query): token = self._string_to_token(value) - if token.value in self.dgcog.index.all_modifiers or ( + if token.value in self.index.all_modifiers or ( any(self.calc_ratio_modifier(token, m, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) - and token.value not in self.dgcog.index.all_name_tokens + and token.value not in self.index.all_name_tokens and len(token.value) >= 8): lastmodpos = not token.negated modifiers.append(token) @@ -138,7 +152,7 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ # Allow modifiers to match as name tokens if they're alone (Fix for hel and cloud) if not name and modifiers and lastmodpos: - if self.dgcog.index.manual[modifiers[-1].full_value]: + if self.index.manual[modifiers[-1].full_value]: name.add(modifiers[-1]) modifiers = modifiers[:-1] @@ -153,7 +167,7 @@ def _process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) if matched_mons is not None: matched_mons.difference_update(invalid) else: - matched_mons = set(self.dgcog.database.get_all_monsters()).difference(invalid) + matched_mons = set(self.dgcog.database.get_all_monsters(self.index.server)).difference(invalid) else: valid = self._get_valid_monsters_from_name_token(name_token, matches) if matched_mons is not None: @@ -167,23 +181,23 @@ def _get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, mult: Union[int, float] = 1) -> Set[MonsterModel]: valid_monsters = set() all_monsters_name_tokens_scores = {nt: self.calc_ratio_name(token, nt) for nt in - self.dgcog.index.all_name_tokens} + self.index.all_name_tokens} matched_tokens = sorted((nt for nt, s in all_monsters_name_tokens_scores.items() if s > self.TOKEN_JW_DISTANCE), key=lambda nt: all_monsters_name_tokens_scores[nt], reverse=True) - matched_tokens += [t for t in self.dgcog.index.all_name_tokens if t.startswith(token.value)] + matched_tokens += [t for t in self.index.all_name_tokens if t.startswith(token.value)] for match in matched_tokens: score = all_monsters_name_tokens_scores[match] - for matched_monster in self.dgcog.index.manual[match]: + for matched_monster in self.index.manual[match]: if matched_monster not in valid_monsters and token.matches(matched_monster): matches[matched_monster].name.add((token.value, match, '(manual)')) matches[matched_monster].score += (score + .001) * mult valid_monsters.add(matched_monster) - for matched_monster in self.dgcog.index.name_tokens[match]: + for matched_monster in self.index.name_tokens[match]: if matched_monster not in valid_monsters and token.matches(matched_monster): matches[matched_monster].name.add((token.value, match, '(name)')) matches[matched_monster].score += score * mult valid_monsters.add(matched_monster) - for matched_monster in self.dgcog.index.fluff_tokens[match]: + for matched_monster in self.index.fluff_tokens[match]: if matched_monster not in valid_monsters and token.matches(matched_monster): matches[matched_monster].name.add((token.value, match, '(fluff)')) matches[matched_monster].score += score * mult / 2 @@ -258,7 +272,7 @@ async def _find_monster_search(self, tokenized_query: List[str]) -> \ name_query_tokens.difference_update({'|'}) for mod_token in mod_tokens: - if mod_token.value not in self.dgcog.index.all_modifiers: + if mod_token.value not in self.index.all_modifiers: async with self.dgcog.config.typo_mods() as typo_mods: typo_mods.append(mod_token.value) @@ -271,7 +285,7 @@ async def _find_monster_search(self, tokenized_query: List[str]) -> \ matched_mons = self._get_monster_evos(matched_mons, matches) else: # There are no name tokens in the query - matched_mons = {*self.dgcog.database.get_all_monsters()} + matched_mons = {*self.dgcog.database.get_all_monsters(self.index.server)} monster_score = defaultdict(int) # Expand search to the evo tree @@ -294,7 +308,7 @@ async def find_monster_debug(self, query: str) -> \ await self.dgcog.wait_until_ready() query = rmdiacritics(query).lower().replace(",", "") - tokenized_query = query.split() + tokenized_query = self._process_settings(query).split() mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) best_monster, matches_dict, valid_monsters = max( @@ -333,7 +347,7 @@ def calc_ratio_name(self, token: Union[Token, str], full_word: str, prefix_weigh """Calculate the name distance between two tokens""" string = token.value if isinstance(token, Token) else token - mw = self.dgcog.index.mwt_to_len[full_word] != 1 + mw = self.index.mwt_to_len[full_word] != 1 jw = jaro_winkler(string, full_word, prefix_weight) if isinstance(token, Token) and token.exact and string != full_word: @@ -352,6 +366,6 @@ def calc_ratio_name(self, token: Union[Token, str], full_word: str, prefix_weigh score = jw if mw: - score = score ** 10 * self.dgcog.index.mwt_to_len[full_word] + score = score ** 10 * self.index.mwt_to_len[full_word] return score diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index e11cb7b8..6ae2a456 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -1,15 +1,18 @@ import re from datetime import datetime -from typing import List +from typing import List, Dict, Optional import tsutils from redbot.core import commands, Config, checks from redbot.core.utils.chat_formatting import box, pagify +from dadguide.common.enums import DEFAULT_SERVER, Server +from dadguide.monster_index import MonsterIndex + class IdTest: bot = None - index = None + indexes: Dict[str, MonsterIndex] find_monster = None def __init__(self): @@ -64,17 +67,17 @@ async def idt_fluff(self, ctx): @idt_name.command(name="add") @checks.is_owner() - async def idtn_add(self, ctx, mid: int, token, *, reason=""): + async def idtf_add(self, ctx, mid: int, token, server: Optional[Server] = DEFAULT_SERVER, *, reason=""): """Add a name token test to the id3 test suite""" - await self.norf_add(ctx, mid, token, reason, False) + await self.norf_add(ctx, mid, token, server, reason, False) @idt_fluff.command(name="add") @checks.is_owner() - async def idtf_add(self, ctx, mid: int, token, *, reason=""): + async def idtf_add(self, ctx, mid: int, token, server: Optional[Server] = DEFAULT_SERVER, *, reason=""): """Add a fluff token test to the id3 test suite""" - await self.norf_add(ctx, mid, token, reason, True) + await self.norf_add(ctx, mid, token, server, reason, True) - async def norf_add(self, ctx, mid: int, token, reason, fluffy): + async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.confirm_message(ctx, @@ -104,6 +107,7 @@ async def norf_add(self, ctx, mid: int, token, reason, fluffy): 'token': token, 'fluff': fluffy, 'reason': reason, + 'server': server, 'ts': datetime.now().timestamp() } @@ -158,7 +162,6 @@ async def norf_import(self, ctx, queries): cases = re.findall(r'\s*(?:\d+. )?(.+?) +- (\d+)\s+(\w*) *(.*)', queries) async with self.config.fluff_suite() as suite: for query, result, fluff, reason in cases: - # print(query, result, fluff, reason) if not any(c['id'] == int(result) and c['token'] == query for c in suite): suite.append({ 'id': int(result), @@ -414,11 +417,11 @@ async def norf_run(self, ctx): o = "" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' async with ctx.typing(): - for i, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff']))): + for i, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff'], v['server']))): fluff = case['id'] in [m.monster_id for m in - self.bot.get_cog("Dadguide").index.fluff_tokens[case['token']]] + self.indexes[case['server']].fluff_tokens[case['token']]] name = case['id'] in [m.monster_id for m in - self.bot.get_cog("Dadguide").index.name_tokens[case['token']]] + self.indexes[case['server']].name_tokens[case['token']]] if (case['fluff'] and not fluff) or (not case['fluff'] and not name): q = '"{}"'.format(case['token']) @@ -463,8 +466,8 @@ async def idt_runall(self, ctx): fc = 0 async with ctx.typing(): for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in self.index.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.index.name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.indexes[v['server']].fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.indexes[v['server']].name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) @@ -509,8 +512,8 @@ async def run_tests(self) -> List[str]: fsuite = await self.config.fluff_suite() for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in self.index.fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.index.name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.indexes[v['server']].fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.indexes[v['server']].name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) diff --git a/dadguide/models/active_skill_model.py b/dadguide/models/active_skill_model.py index 665a790d..4643ae21 100644 --- a/dadguide/models/active_skill_model.py +++ b/dadguide/models/active_skill_model.py @@ -29,3 +29,11 @@ def to_dict(self): 'name_ja': self.name_ja, 'name_en': self.name_en, } + + def __eq__(self, other): + if isinstance(other, ActiveSkillModel): + return self.active_skill_id == other.active_skill_id \ + and self.desc_en == other.desc_en \ + and self.turn_max == other.turn_max \ + and self.turn_min == other.turn_min + return False diff --git a/dadguide/models/awakening_model.py b/dadguide/models/awakening_model.py index d185fc15..a92961eb 100644 --- a/dadguide/models/awakening_model.py +++ b/dadguide/models/awakening_model.py @@ -23,8 +23,7 @@ def to_dict(self): def __eq__(self, other): if isinstance(other, AwakeningModel): - return self.awakening_id == other.awakening_id + return self.awoken_skill_id == other.awoken_skill_id and self.is_super == other.is_super elif isinstance(other, AwokenSkillModel): return self.awoken_skill == other - else: - return False + return False diff --git a/dadguide/models/leader_skill_model.py b/dadguide/models/leader_skill_model.py index 535e411d..b7153e2b 100644 --- a/dadguide/models/leader_skill_model.py +++ b/dadguide/models/leader_skill_model.py @@ -40,7 +40,14 @@ def name(self): def to_dict(self): return { - 'monster_id': self.leader_skill_id, + 'leader_skill_id': self.leader_skill_id, 'name_ja': self.name_ja, 'name_en': self.name_en, } + + def __eq__(self, other): + if isinstance(other, LeaderSkillModel): + return self.leader_skill_id == other.leader_skill_id \ + and self.data == other.data \ + and self.desc_en == other.desc_en + return False diff --git a/dadguide/models/monster/__init__.py b/dadguide/models/monster/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dadguide/models/monster/monster_difference.py b/dadguide/models/monster/monster_difference.py new file mode 100644 index 00000000..d88f7c36 --- /dev/null +++ b/dadguide/models/monster/monster_difference.py @@ -0,0 +1,80 @@ +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class MonsterDifference: + __slots__ = 'types', 'awakenings', 'leader_skill', 'active_skill', 'rarity', \ + 'sell_gold', 'is_inheritable', 'cost', 'exp', 'fodder_exp', 'level', \ + 'limit_mult', 'latent_slots', 'stat_values', 'existance' + + def __init__(self, types=False, awakenings=False, leader_skill=False, active_skill=False, rarity=False, + sell_gold=False, is_inheritable=False, cost=False, exp=False, fodder_exp=False, level=False, + limit_mult=False, latent_slots=False, stat_values=False, existance=False): + self.types = types + self.awakenings = awakenings + self.leader_skill = leader_skill + self.active_skill = active_skill + self.rarity = rarity + self.sell_gold = sell_gold + self.is_inheritable = is_inheritable + self.cost = cost + self.exp = exp + self.fodder_exp = fodder_exp + self.level = level + self.limit_mult = limit_mult + self.latent_slots = latent_slots + self.stat_values = stat_values + self.existance = existance + + @classmethod + def from_monsters(cls, monster1: "MonsterModel", monster2: Optional["MonsterModel"]): + if monster2 is None: + return cls(existance=True) + + return cls( + monster1.types != monster2.types, + monster1.awakenings != monster2.awakenings, + monster1.leader_skill != monster2.leader_skill, + monster1.active_skill != monster2.active_skill, + monster1.rarity != monster2.rarity, + monster1.sell_gold != monster2.sell_gold, + monster1.is_inheritable != monster2.is_inheritable, + monster1.cost != monster2.cost, + monster1.exp != monster2.exp, + monster1.fodder_exp != monster2.fodder_exp, + monster1.level != monster2.level, + monster1.limit_mult != monster2.limit_mult, + monster1.latent_slots != monster2.latent_slots, + monster1.stat_values != monster2.stat_values, + False) + + @classmethod + def from_int(cls, value: int): + return cls(*(value & 1 << exp for exp in range(len(cls.__slots__)))) + + @property + def value(self): + return (self.types << 0 + | self.awakenings << 1 + | self.leader_skill << 2 + | self.active_skill << 3 + | self.rarity << 4 + | self.sell_gold << 5 + | self.is_inheritable << 6 + | self.cost << 7 + | self.exp << 8 + | self.fodder_exp << 9 + | self.level << 10 + | self.limit_mult << 11 + | self.latent_slots << 12 + | self.stat_values << 13 + | self.existance << 14) + + def __bool__(self): + return bool(self.value) + + def __repr__(self): + vals = ', '.join(f"{value}=True" for value in self.__slots__ if getattr(self, value)) + return f"MonsterDifference({vals or 'False'})" diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 471c0d08..9d3ec75e 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -10,6 +10,7 @@ from collections import defaultdict import romkan +from .monster.monster_difference import MonsterDifference from .monster_stats import monster_stats @@ -21,7 +22,6 @@ def __init__(self, **m): self.monster_no_kr = m['monster_no_kr'] self.base_evo_id = m['base_evo_id'] - # these things are literally named backwards atm self.awakenings = sorted(m['awakenings'], key=lambda a: a.order_idx) self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) self.leader_skill: LeaderSkillModel = m['leader_skill'] @@ -96,6 +96,8 @@ def __init__(self, **m): self.has_animation = m['has_animation'] self.has_hqimage = m['has_hqimage'] + self.server_priority = m['server_priority'] + self.search = MonsterSearchHelper(self) @property @@ -163,11 +165,17 @@ def to_dict(self): 'name_en': self.name_en, } + def get_difference(self, other): + if other and self.monster_id != other.monster_id: + raise ValueError("You cannot get the difference of monsters with different ids.") + return MonsterDifference.from_monsters(self, other) + def __repr__(self): - return "Monster<{} ({})>".format(self.name_en, self.monster_id) + server_prefix = self.server_priority + " " if self.server_priority != "COMBINED" else "" + return "Monster<{}{} ({})>".format(server_prefix, self.name_en, self.monster_id) -class MonsterSearchHelper(object): +class MonsterSearchHelper: def __init__(self, m: MonsterModel): self.name = '{} {}'.format(m.name_en, m.name_ja).lower() @@ -187,8 +195,8 @@ def __init__(self, m: MonsterModel): self.types = [t.name for t in m.types] - def replace_colors(text: str): - return text.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') + def replace_colors(txt: str): + return txt.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') self.leader = replace_colors(self.leader) self.active = replace_colors(self.active) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index d5b53fc8..5aad408c 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,48 +1,49 @@ import json import re from collections import defaultdict -from typing import Optional, List, Union, Set +from typing import Optional, List, Set, Dict from networkx import MultiDiGraph -from networkx.classes.coreviews import AtlasView -from networkx.classes.reportviews import OutMultiEdgeView, NodeView +from .common.enums import DEFAULT_SERVER, Server, SERVERS from .database_manager import DadguideDatabase from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel +from .models.base_model import BaseModel from .models.enum_types import InternalEvoType from .models.evolution_model import EvolutionModel from .models.leader_skill_model import LeaderSkillModel from .models.monster_model import MonsterModel +from .models.monster.monster_difference import MonsterDifference from .models.series_model import SeriesModel MONSTER_QUERY = """SELECT - monsters.*, - COALESCE(monster_name_overrides.name_en, monsters.name_en_override) AS name_en_override, - leader_skills.name_ja AS ls_name_ja, - leader_skills.name_en AS ls_name_en, - leader_skills.name_ko AS ls_name_ko, - leader_skills.desc_ja AS ls_desc_ja, - leader_skills.desc_en AS ls_desc_en, - leader_skills.desc_ko AS ls_desc_ko, - leader_skills.max_hp, - leader_skills.max_atk, - leader_skills.max_rcv, - leader_skills.max_rcv, - leader_skills.max_shield, - leader_skills.max_combos, - leader_skills.bonus_damage, - leader_skills.mult_bonus_damage, - leader_skills.extra_time, - active_skills.name_ja AS as_name_ja, - active_skills.name_en AS as_name_en, - active_skills.name_ko AS as_name_ko, - active_skills.desc_ja AS as_desc_ja, - active_skills.desc_en AS as_desc_en, - active_skills.desc_ko AS as_desc_ko, - active_skills.turn_max, - active_skills.turn_min, + monsters{0}.*, + COALESCE(monster_name_overrides.name_en, monsters{0}.name_en_override) AS name_en_override, + leader_skills{0}.name_ja AS ls_name_ja, + leader_skills{0}.name_en AS ls_name_en, + leader_skills{0}.name_ko AS ls_name_ko, + leader_skills{0}.desc_ja AS ls_desc_ja, + leader_skills{0}.desc_en AS ls_desc_en, + leader_skills{0}.desc_ko AS ls_desc_ko, + leader_skills{0}.max_hp, + leader_skills{0}.max_atk, + leader_skills{0}.max_rcv, + leader_skills{0}.max_rcv, + leader_skills{0}.max_shield, + leader_skills{0}.max_combos, + leader_skills{0}.bonus_damage, + leader_skills{0}.mult_bonus_damage, + leader_skills{0}.extra_time, + active_skills{0}.name_ja AS as_name_ja, + active_skills{0}.name_en AS as_name_en, + active_skills{0}.name_ko AS as_name_ko, + active_skills{0}.desc_ja AS as_desc_ja, + active_skills{0}.desc_en AS as_desc_en, + active_skills{0}.desc_ko AS as_desc_ko, + active_skills{0}.turn_max, + active_skills{0}.turn_min, series.name_ja AS s_name_ja, series.name_en AS s_name_en, series.name_ko AS s_name_ko, @@ -50,45 +51,45 @@ exchanges.target_monster_id AS evo_gem_id, drops.drop_id FROM - monsters - LEFT OUTER JOIN leader_skills ON monsters.leader_skill_id = leader_skills.leader_skill_id - LEFT OUTER JOIN active_skills ON monsters.active_skill_id = active_skills.active_skill_id - LEFT OUTER JOIN series ON monsters.series_id = series.series_id - LEFT OUTER JOIN monsters AS target_monsters ON monsters.name_ja || 'の希石' = target_monsters.name_ja + monsters{0} + LEFT OUTER JOIN leader_skills{0} ON monsters{0}.leader_skill_id = leader_skills{0}.leader_skill_id + LEFT OUTER JOIN active_skills{0} ON monsters{0}.active_skill_id = active_skills{0}.active_skill_id + LEFT OUTER JOIN series ON monsters{0}.series_id = series.series_id + LEFT OUTER JOIN monsters{0} AS target_monsters ON monsters{0}.name_ja || 'の希石' = target_monsters.name_ja LEFT OUTER JOIN exchanges ON target_monsters.monster_id = exchanges.target_monster_id - LEFT OUTER JOIN drops ON monsters.monster_id = drops.monster_id - LEFT OUTER JOIN monster_name_overrides ON monsters.monster_id = monster_name_overrides.monster_id + LEFT OUTER JOIN drops ON monsters{0}.monster_id = drops.monster_id + LEFT OUTER JOIN monster_name_overrides ON monsters{0}.monster_id = monster_name_overrides.monster_id GROUP BY - monsters.monster_id""" + monsters{0}.monster_id""" # make sure we're only looking at the most recent row for any evolution # since the database might have old data in it still # group by `to_id` and not `evolution_id` because PAD monsters can only have 1 recipe, and # the evolution_id changes when data changes so grouping by evolution_id is unhelpful EVOS_QUERY = """SELECT - evolutions.* + evolutions{0}.* FROM ( SELECT evolution_id, MAX(tstamp) AS tstamp FROM - evolutions + evolutions{0} GROUP BY to_id ) AS latest_evolutions - INNER JOIN evolutions ON evolutions.evolution_id = latest_evolutions.evolution_id - AND evolutions.tstamp = latest_evolutions.tstamp""" + INNER JOIN evolutions{0} ON evolutions{0}.evolution_id = latest_evolutions.evolution_id + AND evolutions{0}.tstamp = latest_evolutions.tstamp""" AWAKENINGS_QUERY = """SELECT - awakenings.awakening_id, - awakenings.monster_id, - awakenings.is_super, - awakenings.order_idx, + awakenings{0}.awakening_id, + awakenings{0}.monster_id, + awakenings{0}.is_super, + awakenings{0}.order_idx, awoken_skills.* FROM - awakenings - JOIN awoken_skills ON awakenings.awoken_skill_id = awoken_skills.awoken_skill_id""" + awakenings{0} + JOIN awoken_skills ON awakenings{0}.awoken_skill_id = awoken_skills.awoken_skill_id""" EGG_QUERY = """SELECT d_egg_machine_types.name AS type, @@ -104,24 +105,34 @@ exchanges """ +SPECIFY_SERVER = " WHERE server_id = {}" + class MonsterGraph(object): def __init__(self, database: DadguideDatabase): self.database = database - self.graph: Optional[MultiDiGraph] = None - self.edges: Optional[OutMultiEdgeView] = None - self.nodes: Optional[NodeView] = None self.max_monster_id = -1 - self.build_graph() + self.graph_dict: Dict[Server, MultiDiGraph] = { # noqa + "COMBINED": self.build_graph("COMBINED"), + "NA": self.build_graph("NA"), + } + + self._cache_graphs() - def build_graph(self): - self.graph = MultiDiGraph() + def build_graph(self, server: Server) -> MultiDiGraph: + graph = MultiDiGraph() - ms = self.database.query_many(MONSTER_QUERY, ()) - es = self.database.query_many(EVOS_QUERY, ()) - aws = self.database.query_many(AWAKENINGS_QUERY, ()) - ems = self.database.query_many(EGG_QUERY, ()) - exs = self.database.query_many(EXCHANGE_QUERY, ()) + table_suffix = "" + where = "" + if server != "COMBINED": + table_suffix = "_" + server.lower() + where = SPECIFY_SERVER.format(["JP", "NA", "KR"].index(server)) + + ms = self.database.query_many(MONSTER_QUERY.format(table_suffix), ()) + es = self.database.query_many(EVOS_QUERY.format(table_suffix), ()) + aws = self.database.query_many(AWAKENINGS_QUERY.format(table_suffix), ()) + ems = self.database.query_many(EGG_QUERY.format(table_suffix) + where, ()) + exs = self.database.query_many(EXCHANGE_QUERY.format(table_suffix) + where, ()) mtoawo = defaultdict(list) for a in aws: @@ -227,25 +238,26 @@ def build_graph(self): latent_slots=m.latent_slots, has_animation=m.has_animation == 1, has_hqimage=m.has_hqimage == 1, + server_priority=server, ) - self.graph.add_node(m.monster_id, model=m_model) + graph.add_node(m.monster_id, model=m_model) if m.linked_monster_id: - self.graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') - self.graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') + graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') + graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') if m.evo_gem_id: - self.graph.add_edge(m.monster_id, m.evo_gem_id, type='evo_gem_from') - self.graph.add_edge(m.evo_gem_id, m.monster_id, type='evo_gem_of') + graph.add_edge(m.monster_id, m.evo_gem_id, type='evo_gem_from') + graph.add_edge(m.evo_gem_id, m.monster_id, type='evo_gem_of') self.max_monster_id = max(self.max_monster_id, m.monster_id) for e in es: evo_model = EvolutionModel(**e) - self.graph.add_edge( + graph.add_edge( evo_model.from_id, evo_model.to_id, type='evolution', model=evo_model) - self.graph.add_edge( + graph.add_edge( evo_model.to_id, evo_model.from_id, type='back_evolution', model=evo_model) # for material_of queries @@ -253,41 +265,37 @@ def build_graph(self): for mat in evo_model.mats: if mat in already_used_in_this_evo: continue - self.graph.add_edge( + graph.add_edge( mat, evo_model.to_id, type="material_of", model=evo_model) already_used_in_this_evo.append(mat) for ex in exs: for vendor_id in re.findall(r'\d+', ex.required_monster_ids): - self.graph.add_edge( + graph.add_edge( ex.target_monster_id, int(vendor_id), type='exchange_from') - self.graph.add_edge( + graph.add_edge( int(vendor_id), ex.target_monster_id, type='exchange_for') - # Caching - for mid in self.graph.nodes: - self.graph.nodes[mid]['alt_versions'] = self.process_alt_versions(mid) - - self.edges = self.graph.edges - self.nodes = self.graph.nodes + return graph - def _get_edges(self, node: Union[int, AtlasView], etype) -> Set[int]: - if isinstance(node, int): - node = self.graph[node] + def _cache_graphs(self) -> None: + for server in self.graph_dict: + for mid in self.graph_dict[server].nodes: + self.graph_dict[server].nodes[mid]['alt_versions'] = self.process_alt_ids( + self.get_monster(mid, server=server)) - return {mid for mid, atlas in node.items() for edge in atlas.values() if edge.get('type') == etype} + def _get_edges(self, monster: MonsterModel, etype) -> Set[int]: + return {mid for mid, atlas in self.graph_dict[monster.server_priority][monster.monster_id].items() + for edge in atlas.values() if edge.get('type') == etype} - def _get_edge_or_none(self, node: Union[int, AtlasView], etype) -> Optional[int]: - edges = self._get_edges(node, etype) + def _get_edge_or_none(self, monster: MonsterModel, etype) -> Optional[int]: + edges = self._get_edges(monster, etype) if edges: return edges.pop() - def _get_edge_model(self, node: Union[int, AtlasView], etype): - if isinstance(node, int): - node = self.graph[node] - + def _get_edge_model(self, monster: MonsterModel, etype) -> Optional[BaseModel]: possible_results = set() - for atlas in node.values(): + for atlas in self.graph_dict[monster.server_priority][monster.monster_id].values(): for edge in atlas.values(): if edge.get('type') == etype: possible_results.add(edge['model']) @@ -295,138 +303,116 @@ def _get_edge_model(self, node: Union[int, AtlasView], etype): return None return sorted(possible_results, key=lambda x: x.tstamp)[-1] - def get_monster(self, monster_id) -> Optional[MonsterModel]: - if monster_id not in self.graph.nodes: + def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[MonsterModel]: + if monster_id not in self.graph_dict[server].nodes: return None - return self.graph.nodes[monster_id]['model'] + return self.graph_dict[server].nodes[monster_id]['model'] - def get_evo_tree(self, monster_id) -> List[int]: - while (prev := self._get_edges(monster_id, 'back_evolution')): - monster_id = prev - return self.get_evo_tree_from_base(monster_id) + def get_all_monsters(self, server: Server) -> Set[MonsterModel]: + return {mdata['model'] for mdata in self.graph_dict[server].nodes.values()} - def get_evo_tree_from_base(self, base_monster_id) -> List[int]: - ids = [base_monster_id] - for evo in sorted(self._get_edges(base_monster_id, 'evolution')): - ids += self.get_evo_tree_from_base(evo) - return ids + def get_evo_tree(self, monster: MonsterModel) -> List[MonsterModel]: + while (prev := self.get_prev_evolution(monster)): + monster = prev + return self.get_evo_tree_from_base(monster) - def get_transform_tree(self, monster_id: int): - ids = set() - to_check = {monster_id} + def get_evo_tree_from_base(self, base_monster: MonsterModel) -> List[MonsterModel]: + mons = [base_monster] + for evo in self.get_next_evolutions(base_monster): + mons += self.get_evo_tree_from_base(evo) + return mons + + def get_transform_monsters(self, monster: MonsterModel) -> Set[MonsterModel]: + mons = set() + to_check = {monster} while to_check: - mid = to_check.pop() - if mid in ids: + mon = to_check.pop() + if mon in mons: continue - to_check.update(self._get_edges(mid, 'transformation')) - to_check.update(self._get_edges(mid, 'back_transformation')) - ids.add(mid) + if (next_transform := self.get_next_transform(mon)): + to_check.add(next_transform) + to_check.update(self.get_prev_transforms(mon)) + mons.add(mon) + return mons + + def process_alt_ids(self, monster: MonsterModel) -> List[int]: + return [m.monster_id for m in self.process_alt_monsters_from_base(self.get_base_monster(monster))] + + def process_alt_monsters_from_base(self, base_monster: MonsterModel) -> List[MonsterModel]: + ids = [base_monster] + transform = self.get_next_transform(base_monster) + if transform and (transform.monster_id > base_monster.monster_id + or transform.monster_id == 5802): # I hate DMG very much + ids += self.process_alt_monsters_from_base(transform) + for evo in self.get_next_evolutions(base_monster): + ids += self.process_alt_monsters_from_base(evo) return ids - def get_transform_monsters(self, monster): - return {self.get_monster(m) for m in self.get_transform_tree(monster.monster_id)} + def get_alt_ids_fast_by_id_and_server(self, monster_id, server: Server) -> List[int]: + # This is for the index. I'm too lazy to remove this, and it might have impacts on speed. + return self.graph_dict[server].nodes[monster_id]['alt_versions'] - def process_alt_versions(self, monster_id) -> List[int]: - return self.process_alt_versions_from_base(self.get_base_id_by_id(monster_id)) + def get_alt_ids(self, monster: MonsterModel) -> List[int]: + return self.graph_dict[monster.server_priority].nodes[monster.monster_id]['alt_versions'] - def process_alt_versions_from_base(self, base_monster_id) -> List[int]: - ids = [base_monster_id] - for trans in sorted(self._get_edges(base_monster_id, 'transformation')): - if trans > base_monster_id or trans == 5802: # I hate DMG very much - ids += self.process_alt_versions_from_base(trans) - for evo in sorted(self._get_edges(base_monster_id, 'evolution')): - ids += self.process_alt_versions_from_base(evo) - return ids + def get_alt_monsters(self, monster: MonsterModel) -> List[MonsterModel]: + return [self.get_monster(m_id, server=monster.server_priority) for m_id in self.get_alt_ids(monster)] - def get_alt_ids_by_id(self, monster_id): - return self.nodes[monster_id]['alt_versions'] - - def get_alt_monsters_by_id(self, monster_id): - ids = self.get_alt_ids_by_id(monster_id) - return [self.get_monster(m_id) for m_id in ids] - - def get_alt_monsters(self, monster: MonsterModel): - return self.get_alt_monsters_by_id(monster.monster_id) - - def get_base_id_by_id(self, monster_id): - return self.get_base_id(self.get_monster(monster_id)) - - def get_base_id(self, monster): + def get_base_id(self, monster) -> int: # This fixes DMG. I *hate* DMG. if monster.base_evo_id == 5802: return 5810 - while (prevs := self.get_prev_transforms_by_monster(monster)) \ + while (prevs := self.get_prev_transforms(monster)) \ and list(prevs)[0].monster_id < monster.monster_id: monster = prevs.pop() return monster.base_evo_id - def get_base_monster_by_id(self, monster_id): - return self.get_monster(self.get_base_id_by_id(monster_id)) - - def get_base_monster(self, monster: MonsterModel): - return self.get_monster(self.get_base_id(monster)) - - def monster_is_base_by_id(self, monster_id: int) -> bool: - return self.get_base_id_by_id(monster_id) == monster_id + def get_base_monster(self, monster: MonsterModel) -> MonsterModel: + return self.get_monster(self.get_base_id(monster), server=monster.server_priority) def monster_is_base(self, monster: MonsterModel) -> bool: - return self.monster_is_base_by_id(monster.monster_id) + return self.get_base_id(monster) == monster.monster_id - def get_transform_base_id_by_id(self, monster_id): + def get_transform_base(self, monster: MonsterModel) -> MonsterModel: # NOTE: This assumes that no two monsters will transform to the same monster. This # also doesn't work for monsters like DMG which are transforms but also base # cards. This also assumes that the "base" monster will be the lowest id in # the case of a cyclical transform. seen = set() - curr = monster_id + curr = monster while curr not in seen: seen.add(curr) - next_ids = self.get_prev_transform_ids_by_monster_id(curr) - if next_ids: - curr = next_ids.pop() + next_mons = self.get_prev_transforms(curr) + if next_mons: + curr = next_mons.pop() else: break else: - curr = min(seen) + curr = min(seen, key=lambda m: m.monster_id) return curr - def get_transform_base_by_id(self, monster_id): - return self.get_monster(self.get_transform_base_id_by_id(monster_id)) - - def get_transform_base(self, monster): - return self.get_transform_base_by_id(monster.monster_id) - - def monster_is_transform_base_by_id(self, monster_id: int) -> bool: - return self.get_transform_base_id_by_id(monster_id) == monster_id - def monster_is_transform_base(self, monster: MonsterModel) -> bool: - return self.monster_is_transform_base_by_id(monster.monster_id) + return self.get_transform_base(monster) == monster - def get_numerical_sort_top_id_by_id(self, monster_id): - alt_cards = self.get_alt_ids_by_id(monster_id) + def get_numerical_sort_top_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: + alt_cards = self.get_alt_monsters(monster) if alt_cards is None: return None - return sorted(alt_cards)[-1] - - def get_numerical_sort_top_monster_by_id(self, monster_id): - return self.get_monster(self.get_numerical_sort_top_id_by_id(monster_id)) - - def get_evo_by_monster_id(self, monster_id) -> Optional[EvolutionModel]: - return self._get_edge_model(monster_id, 'back_evolution') + return sorted(alt_cards, key=lambda m: m.monster_id)[-1] - def get_evo_by_monster(self, monster) -> Optional[EvolutionModel]: - return self._get_edge_model(monster.monster_id, 'back_evolution') + def get_evolution(self, monster) -> Optional[EvolutionModel]: + return self._get_edge_model(monster, 'back_evolution') def monster_is_reversible_evo(self, monster: MonsterModel) -> bool: - prev_evo = self.get_evo_by_monster(monster) + prev_evo = self.get_evolution(monster) return prev_evo is not None and prev_evo.reversible def monster_is_reincarnated(self, monster: MonsterModel) -> bool: if self.monster_is_reversible_evo(monster): return False - while (monster := self.get_prev_evolution_by_monster(monster)): + while (monster := self.get_prev_evolution(monster)): if self.monster_is_reversible_evo(monster): return True return False @@ -437,29 +423,26 @@ def monster_is_normal_evo(self, monster: MonsterModel) -> bool: or self.monster_is_base(monster)) def monster_is_first_evo(self, monster: MonsterModel) -> bool: - prev = self.get_prev_evolution_by_monster(monster) + prev = self.get_prev_evolution(monster) if prev: - return self.get_prev_evolution_by_monster(prev) is None + return self.get_prev_evolution(prev) is None return False def monster_is_second_ultimate(self, monster: MonsterModel) -> bool: if self.monster_is_reversible_evo(monster): - prev = self.get_prev_evolution_by_monster(monster) + prev = self.get_prev_evolution(monster) if prev is not None: return self.monster_is_reversible_evo(prev) return False - def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: - monster = self.get_monster(monster_id) + def true_evo_type(self, monster: MonsterModel) -> InternalEvoType: if monster.is_equip: return InternalEvoType.Assist - - if self.get_base_id_by_id(monster_id) == monster_id: + if self.monster_is_base(monster): return InternalEvoType.Base - evo = self.get_evo_by_monster_id(monster_id) + evo = self.get_evolution(monster) if evo is None: - # this is possible without being the above case for transforms return InternalEvoType.Base if evo.is_super_reincarnated: return InternalEvoType.SuperReincarnated @@ -470,153 +453,76 @@ def true_evo_type_by_monster_id(self, monster_id: int) -> InternalEvoType: return InternalEvoType.Reincarnated elif self.monster_is_reversible_evo(monster): return InternalEvoType.Ultimate - return InternalEvoType.Normal - def true_evo_type_by_monster(self, monster: MonsterModel) -> InternalEvoType: - return self.true_evo_type_by_monster_id(monster.monster_id) - - def get_prev_evolution_id_by_monster_id(self, monster_id): - return self._get_edge_or_none(monster_id, 'back_evolution') + def get_prev_evolution_id(self, monster: MonsterModel) -> Optional[int]: + return self._get_edge_or_none(monster, 'back_evolution') - def get_prev_evolution_id_by_monster(self, monster: MonsterModel): - return self.get_prev_evolution_id_by_monster_id(monster.monster_id) + def get_prev_evolution(self, monster: MonsterModel) -> Optional[MonsterModel]: + pe = self.get_prev_evolution_id(monster) + return pe and self.get_monster(pe, server=monster.server_priority) - def get_prev_evolution_by_monster(self, monster: MonsterModel): - pe = self.get_prev_evolution_id_by_monster(monster) - return pe and self.get_monster(pe) + def get_next_evolution_ids(self, monster: MonsterModel) -> Set[int]: + return self._get_edges(monster, 'evolution') - def get_next_evolution_ids_by_monster_id(self, monster_id): - return self._get_edges(monster_id, 'evolution') + def get_next_evolutions(self, monster: MonsterModel) -> Set[MonsterModel]: + return {self.get_monster(mid, server=monster.server_priority) + for mid in self.get_next_evolution_ids(monster)} - def get_next_evolution_ids_by_monster(self, monster: MonsterModel): - return self.get_next_evolution_ids_by_monster_id(monster.monster_id) + def get_prev_transform_ids(self, monster: MonsterModel) -> Set[int]: + return self._get_edges(monster, 'back_transformation') - def get_next_evolutions_by_monster(self, monster: MonsterModel): - return {self.get_monster(mid) for mid in self.get_next_evolution_ids_by_monster(monster)} + def get_prev_transforms(self, monster: MonsterModel) -> Set[MonsterModel]: + return {self.get_monster(mid, server=monster.server_priority) + for mid in self.get_prev_transform_ids(monster)} - def get_prev_transform_ids_by_monster_id(self, monster_id): - return self._get_edges(monster_id, 'back_transformation') + def get_next_transform_id(self, monster: MonsterModel) -> Optional[int]: + return self._get_edge_or_none(monster, 'transformation') - def get_prev_transform_ids_by_monster(self, monster: MonsterModel): - return self.get_prev_transform_ids_by_monster_id(monster.monster_id) + def get_next_transform(self, monster: MonsterModel) -> Optional[MonsterModel]: + nt = self.get_next_transform_id(monster) + return nt and self.get_monster(nt, server=monster.server_priority) - def get_prev_transforms_by_monster(self, monster: MonsterModel): - return {self.get_monster(mid) for mid in self.get_prev_transform_ids_by_monster(monster)} - - def get_next_transform_id_by_monster_id(self, monster_id): - return self._get_edge_or_none(monster_id, 'transformation') - - def get_next_transform_id_by_monster(self, monster: MonsterModel): - return self.get_next_transform_id_by_monster_id(monster.monster_id) - - def get_next_transform_by_monster(self, monster: MonsterModel): - nt = self.get_next_transform_id_by_monster(monster) - return nt and self.get_monster(nt) - - def evo_mats_by_monster_id(self, monster_id: int) -> List[MonsterModel]: - evo = self.get_evo_by_monster_id(monster_id) + def evo_mats(self, monster: MonsterModel) -> List[MonsterModel]: + evo = self.get_evolution(monster) if evo is None: return [] - return [self.get_monster(mat) for mat in evo.mats] - - def evo_mats_by_monster(self, monster: MonsterModel) -> List[MonsterModel]: - return self.evo_mats_by_monster_id(monster.monster_id) - - # farmable - def monster_is_farmable_by_id(self, monster_id): - return self.get_monster(monster_id).is_farmable - - def monster_is_farmable(self, monster: MonsterModel): - return self.monster_is_farmable_by_id(monster.monster_id) + return [self.get_monster(mat, server=monster.server_priority) for mat in evo.mats] - def monster_is_farmable_evo_by_id(self, monster_id): - return any( - m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_farmable_by_id(m)) + def monster_is_farmable_evo(self, monster: MonsterModel) -> bool: + return any(alt.is_farmable for alt in self.get_alt_monsters(monster)) - def monster_is_farmable_evo(self, monster: MonsterModel): - return self.monster_is_farmable_evo_by_id(monster.monster_id) + def monster_is_mp_evo(self, monster: MonsterModel) -> bool: + return any(alt.in_mpshop for alt in self.get_alt_monsters(monster)) - # mp - def monster_is_mp_by_id(self, monster_id): - return self.get_monster(monster_id).in_mpshop + def monster_is_pem_evo(self, monster: MonsterModel) -> bool: + return any(alt.in_pem for alt in self.get_alt_monsters(monster)) - def monster_is_mp(self, monster: MonsterModel): - return self.monster_is_mp_by_id(monster.monster_id) + def monster_is_rem_evo(self, monster: MonsterModel) -> bool: + return any(alt.in_rem for alt in self.get_alt_monsters(monster)) - def monster_is_mp_evo_by_id(self, monster_id): - return any( - m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_mp_by_id(m)) + def monster_is_exchange(self, monster: MonsterModel) -> bool: + return bool(self._get_edges(monster, 'exchange_from')) - def monster_is_mp_evo(self, monster: MonsterModel): - return self.monster_is_mp_evo_by_id(monster.monster_id) + def monster_is_exchange_evo(self, monster: MonsterModel) -> bool: + return any(self.monster_is_exchange(alt) for alt in self.get_alt_monsters(monster)) - # pem - def monster_is_pem_by_id(self, monster_id): - return self.get_monster(monster_id).in_pem + def get_monster_exchange_mat_ids(self, monster: MonsterModel) -> Set[int]: + return self._get_edges(monster, 'exchange_from') - def monster_is_pem(self, monster: MonsterModel): - return self.monster_is_pem_by_id(monster.monster_id) + def get_monster_exchange_mats(self, monster: MonsterModel) -> Set[MonsterModel]: + return {self.get_monster(mid, server=monster.server_priority) + for mid in self.get_monster_exchange_mat_ids(monster)} - def monster_is_pem_evo_by_id(self, monster_id): - return any( - m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_pem_by_id(m)) - - def monster_is_pem_evo(self, monster: MonsterModel): - return self.monster_is_pem_evo_by_id(monster.monster_id) - - # rem - def monster_is_rem_by_id(self, monster_id): - return self.get_monster(monster_id).in_rem - - def monster_is_rem(self, monster: MonsterModel): - return self.monster_is_rem_by_id(monster.monster_id) - - def monster_is_rem_evo_by_id(self, monster_id): - return any( - m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_rem_by_id(m)) - - def monster_is_rem_evo(self, monster: MonsterModel): - return self.monster_is_rem_evo_by_id(monster.monster_id) - - # redeemable - def monster_is_exchange_by_id(self, monster_id): - return bool(self._get_edges(monster_id, 'exchange_from')) - - def monster_is_exchange(self, monster: MonsterModel): - return self.monster_is_exchange_by_id(monster.monster_id) - - def monster_is_exchange_evo_by_id(self, monster_id): - return any( - m for m in self.get_alt_ids_by_id(monster_id) if self.monster_is_exchange_by_id(m)) - - def monster_is_exchange_evo(self, monster: MonsterModel): - return self.monster_is_exchange_evo_by_id(monster.monster_id) - - def get_monster_exchange_mat_ids_by_id(self, monster_id): - return self._get_edges(monster_id, 'exchange_from') - - def get_monster_exchange_mat_ids(self, monster: MonsterModel): - return self.get_monster_exchange_mat_ids_by_id(monster.monster_id) - - def get_monster_exchange_mats_by_id(self, monster_id): - return {self.get_monster(mid) for mid in self.get_monster_exchange_mat_ids_by_id(monster_id)} - - def get_monster_exchange_mats(self, monster: MonsterModel): - return self.get_monster_exchange_mats_by_id(monster.monster_id) - - def monster_is_vendor_exchange_by_id(self, monster_id): - ids = self.get_monster_exchange_mats_by_id(monster_id) + def monster_is_vendor_exchange(self, monster: MonsterModel) -> bool: + ids = self.get_monster_exchange_mats(monster) return bool(ids) and all(15 in [t.value for t in m.types] for m in ids) - def monster_is_vendor_exchange(self, monster: MonsterModel): - return self.monster_is_vendor_exchange_by_id(monster.monster_id) - - def monster_is_new(self, monster: MonsterModel): + def monster_is_new(self, monster: MonsterModel) -> bool: latest_time = max(am.reg_date for am in self.get_alt_monsters(monster)) return monster.reg_date == latest_time - def monster_acquisition(self, monster: MonsterModel): + def monster_acquisition(self, monster: MonsterModel) -> str: if self.monster_is_rem_evo(monster): return 'REM Card' elif self.monster_is_mp_evo(monster): @@ -626,59 +532,47 @@ def monster_acquisition(self, monster: MonsterModel): elif self.monster_is_pem_evo(monster): return 'PEM Card' - def numeric_next_monster_id_by_id(self, monster_id: int) -> Optional[int]: + def numeric_next_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: next_monster = None offset = 1 - while next_monster is None and monster_id + offset <= self.max_monster_id: - next_monster = self.get_monster(monster_id + offset) + while next_monster is None and monster.monster_id + offset <= self.max_monster_id: + next_monster = self.get_monster(monster.monster_id + offset, server=monster.server_priority) offset += 1 if next_monster is None: return None - return next_monster.monster_id - - def numeric_next_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: - next_monster_id = self.numeric_next_monster_id_by_id(monster.monster_id) - if next_monster_id is None: - return None - return self.get_monster(next_monster_id) + return next_monster - def numeric_prev_monster_id_by_id(self, monster_id) -> Optional[int]: + def numeric_prev_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: prev_monster = None offset = 1 - while prev_monster is None and monster_id - offset >= 1: - prev_monster = self.get_monster(monster_id - offset) + while prev_monster is None and monster.monster_id - offset >= 1: + prev_monster = self.get_monster(monster.monster_id - offset, server=monster.server_priority) offset += 1 if prev_monster is None: return None - return prev_monster.monster_id - - def numeric_prev_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: - prev_monster_id = self.numeric_prev_monster_id_by_id(monster.monster_id) - if prev_monster_id is None: - return None - return self.get_monster(prev_monster_id) - - def evo_gem_monster_by_id(self, monster_id) -> Optional[MonsterModel]: - this_monster = self.get_monster(monster_id) - if this_monster.evo_gem_id is None: - return None - return self.get_monster(this_monster.evo_gem_id) + return prev_monster def evo_gem_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: - return self.evo_gem_monster_by_id(monster.monster_id) + if monster.evo_gem_id is None: + return None + return self.get_monster(monster.evo_gem_id, server=monster.server_priority) def get_monster_from_evo_gem(self, monster: MonsterModel) -> Optional[MonsterModel]: - return self.get_monster(self._get_edge_or_none(monster.monster_id, "evo_gem_of")) + return self.get_monster(self._get_edge_or_none(monster, "evo_gem_of"), server=monster.server_priority) def monster_is_evo_gem(self, monster: MonsterModel) -> bool: return bool(self.get_monster_from_evo_gem(monster)) - def material_of_ids_by_id(self, monster_id: int) -> List[int]: - return sorted(self._get_edges(monster_id, 'material_of')) - def material_of_ids(self, monster: MonsterModel) -> List[int]: - return self.material_of_ids_by_id(monster.monster_id) + return sorted(self._get_edges(monster, 'material_of')) def material_of_monsters(self, monster: MonsterModel) -> List[MonsterModel]: - return [self.get_monster(m) - for m in self.material_of_ids_by_id(monster.monster_id)] + return [self.get_monster(m, server=monster.server_priority) + for m in self.material_of_ids(monster)] + + def monster_difference(self, monster: MonsterModel, server) -> MonsterDifference: + return monster.get_difference(self.get_monster(monster.monster_id, server=server)) + + def monster_is_discrepant(self, monster: MonsterModel) -> bool: + return any((md := self.monster_difference(monster, server)) and not md.existance + for server in SERVERS) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index f5d3395b..75e492dc 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -9,6 +9,8 @@ from redbot.core.utils import AsyncIter from .models.monster_model import MonsterModel +from .monster_graph import MonsterGraph +from .common.enums import DEFAULT_SERVER, Server from .token_mappings import * SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY' \ @@ -24,8 +26,10 @@ class MonsterIndex(tsutils.aobject): - async def __ainit__(self, monsters, db): - self.graph = db.graph + async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): + self.graph = graph + self.server = server + monsters = graph.get_all_monsters(server) self.issues = [] @@ -34,14 +38,14 @@ async def __ainit__(self, monsters, db): self.monster_id_to_treename = defaultdict(set) self.series_id_to_pantheon_nickname = \ defaultdict(set, {m.series_id: {m.series.name_en.lower().replace(" ", "")} - for m in db.get_all_monsters() + for m in monsters if m.series.name_en.lower() not in PROBLEMATIC_SERIES_TOKENS}) self.mwtoken_creators = defaultdict(set) self.multi_word_tokens = {tuple(m.series.name_en.lower().split()) for m - in db.get_all_monsters() + in monsters if " " in m.series.name_en.strip()}.union(MULTI_WORD_TOKENS) self.replacement_tokens = defaultdict(set) @@ -58,7 +62,7 @@ async def __ainit__(self, monsters, db): ) for m_id, name, lp, ov, i in nickname_data: - if m_id.isdigit() and not i: + if m_id.isdigit() and not i and graph.get_monster(int(m_id), server=server): name = name.strip().lower() mid = int(m_id) if lp: @@ -67,23 +71,23 @@ async def __ainit__(self, monsters, db): self.treename_overrides.add(mid) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(mid)) + self.mwtoken_creators[name.lower().replace(" ", "")].add(graph.get_monster(mid, server=server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_nickname[mid].add(name.lower().replace(" ", "")) for m_id, name, mp, ov, i in treenames_data: - if m_id.isdigit() and not i: + if m_id.isdigit() and not i and graph.get_monster(int(m_id), server=server): name = name.strip().lower() mid = int(m_id) if ov: - for emid in self.graph.get_alt_ids_by_id(mid): + for emid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): self.treename_overrides.add(emid) if mp: - for emid in self.graph.get_alt_ids_by_id(mid): + for emid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): self.monster_id_to_nametokens[emid].update(self._name_to_tokens(name)) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")].add(db.graph.get_monster(mid)) + self.mwtoken_creators[name.lower().replace(" ", "")].add(graph.get_monster(mid, server=server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_treename[mid].add(name.lower().replace(" ", "")) @@ -100,7 +104,7 @@ async def __ainit__(self, monsters, db): self.manual_prefixes = defaultdict(set) for mid, mods, rmv in mod_data: - if mid.isdigit(): + if mid.isdigit() and graph.get_monster(int(mid), server=server): mid = int(mid) for mod in mods.split(","): mod = mod.strip().lower() @@ -113,7 +117,7 @@ async def __ainit__(self, monsters, db): self.manual_prefixes[mid].update(get_modifier_aliases(mod)) for mid, mods in treemod_data: - if mid.isdigit(): + if mid.isdigit() and graph.get_monster(int(mid), server=server): mid = int(mid) for mod in mods.split(","): mod = mod.strip().lower() @@ -121,7 +125,7 @@ async def __ainit__(self, monsters, db): self.multi_word_tokens.add(tuple(mod.split(" "))) mod = mod.replace(" ", "") aliases = get_modifier_aliases(mod) - for emid in self.graph.get_alt_ids_by_id(mid): + for emid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): self.manual_prefixes[emid].update(aliases) self._known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() @@ -315,9 +319,9 @@ async def get_modifiers(self, monster: MonsterModel): modifiers.update(EVO_MAP[EvoTypes.BASE]) special_evo = ('覚醒' in monster.name_ja or 'awoken' in monster.name_en or '転生' in monster.name_ja or - self.graph.true_evo_type_by_monster(monster).value == "Reincarnated" or + self.graph.true_evo_type(monster).value == "Reincarnated" or 'reincarnated' in monster.name_en or - self.graph.true_evo_type_by_monster(monster).value == "Super Reincarnated" or + self.graph.true_evo_type(monster).value == "Super Reincarnated" or monster.is_equip or '極醒' in monster.name_ja) # Evo @@ -339,7 +343,7 @@ async def get_modifiers(self, monster: MonsterModel): lambda m: not self.graph.monster_is_transform_base(m)) self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.BASETRANS], lambda m: (self.graph.monster_is_transform_base(m) - and self.graph.get_next_transform_by_monster(m))) + and self.graph.get_next_transform(m))) # Awoken self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.AWOKEN], @@ -351,16 +355,16 @@ async def get_modifiers(self, monster: MonsterModel): # Reincarnated self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.REVO], - lambda m: self.graph.true_evo_type_by_monster(m).value == "Reincarnated") + lambda m: self.graph.true_evo_type(m).value == "Reincarnated") # Super Reincarnated self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.SREVO], - lambda m: self.graph.true_evo_type_by_monster(m).value == "Super Reincarnated") + lambda m: self.graph.true_evo_type(m).value == "Super Reincarnated") # Pixel self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.PIXEL], lambda m: (m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') - or self.graph.true_evo_type_by_monster(m).value == "Pixel"), + or self.graph.true_evo_type(m).value == "Pixel"), else_mods=EVO_MAP[EvoTypes.NONPIXEL]) # Awakenings @@ -392,13 +396,13 @@ async def get_modifiers(self, monster: MonsterModel): # Story def is_story(m, do_transform=True): - if m.series_id == 196 or any(mat.series_id == 196 for mat in self.graph.evo_mats_by_monster(m)): + if m.series_id == 196 or any(mat.series_id == 196 for mat in self.graph.evo_mats(m)): return True if do_transform: for pt in self.graph.get_transform_monsters(m): if is_story(pt, False): return True - pe = self.graph.get_prev_evolution_by_monster(m) + pe = self.graph.get_prev_evolution(m) if pe and is_story(pe): return True return False @@ -437,7 +441,7 @@ def is_story(m, do_transform=True): modifiers.update(MISC_MAP[MiscModifiers.INNA]) if not monster.on_jp: modifiers.update(MISC_MAP[MiscModifiers.ONLYNA]) - if monster.monster_id + 10000 in self.graph.nodes: + if monster.monster_id + 10000 in self.graph.graph_dict[monster.server_priority].nodes: modifiers.add("idjp") if monster.monster_id > 10000: modifiers.add("idna") diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 241e8938..b3500156 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -732,12 +732,12 @@ async def _resolve_which(self, ctx, term): monster = dgcog.get_monster(monster_id) - if db_context.graph.monster_is_mp_evo(monster) and not db_context.graph.monster_is_rem(monster): + if db_context.graph.monster_is_mp_evo(monster) and not monster.in_rem: return name, MP_BUY_MSG.format(ctx.prefix), None, False elif db_context.graph.monster_is_farmable_evo(monster): return name, FARMABLE_MSG, None, False elif check_simple_tree(monster, db_context): - top_monster = db_context.graph.get_numerical_sort_top_monster_by_id(monster.monster_id) + top_monster = db_context.graph.get_numerical_sort_top_monster(monster) return name, SIMPLE_TREE_MSG.format(top_monster.monster_id, top_monster.name_en), None, False else: await ctx.send('No which info for {} (#{})'.format(name, monster_id)) @@ -1395,7 +1395,7 @@ def emojify(self, message): def check_simple_tree(monster, db_context): attr1 = monster.attr1 active_skill = monster.active_skill - for m in db_context.graph.get_alt_monsters_by_id(monster.monster_id): + for m in db_context.graph.get_alt_monsters(monster): if m.attr1 != attr1 or m.active_skill.active_skill_id != active_skill.active_skill_id: return False if m.is_equip: diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index a704649a..13cb6cbc 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -108,7 +108,7 @@ def _get_card_btn_damage(self, card_buttons, dgcog, monster): lines = [] card_buttons.sort(key=lambda x: x.mult) for card in card_buttons: - inherit_model = dgcog.get_monster(card.id) + inherit_model = dgcog.get_monster(card.id, server=monster.server_priority) max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=monster.latent_slots / 2) @@ -124,7 +124,7 @@ def _get_team_btn_damage(self, team_buttons, dgcog, monster): team_buttons.sort(key=lambda x: x.mult) for card in team_buttons: total_dmg = 0 - inherit_model = dgcog.get_monster(card.id) + inherit_model = dgcog.get_monster(card.id, server=monster.server_priority) max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=monster.latent_slots / 2) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index a7eebec4..24c4631c 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -85,10 +85,9 @@ async def perform_leaderskill_query(dgcog, raw_query, author_id): if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" m = await dgcog.find_monster(query, author_id) - if m and left_query in dgcog.index.modifiers[m]: + if m and left_query in dgcog.indexes[m.server_priority].modifiers[m]: left_query = query right_query = None - break else: # no separators diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index 2a6e81f6..4c87808c 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -8,24 +8,24 @@ async def perform_transforminfo_query(dgcog, raw_query, author_id): transformed_mon = None base_mon = None - altversions = mgraph.process_alt_versions(found_monster.monster_id) - for mon_id in altversions: - if mgraph.monster_is_transform_base_by_id(mon_id): - transformed_mon = dgcog.get_monster(mgraph.get_next_transform_id_by_monster_id(mon_id)) + altversions = mgraph.get_alt_monsters(found_monster) + for mon in altversions: + if mgraph.monster_is_transform_base(mon): + transformed_mon = mgraph.get_next_transform(mon) if transformed_mon: - base_mon = dgcog.get_monster(mon_id) + base_mon = mon break if not transformed_mon: return found_monster, None, None reaction_ids = [base_mon.monster_id, transformed_mon.monster_id] - current_id = transformed_mon.monster_id + current = transformed_mon while True: - next_transform_id = mgraph.get_next_transform_id_by_monster_id(current_id) - if next_transform_id is not None and next_transform_id not in reaction_ids: - reaction_ids.append(next_transform_id) - current_id = next_transform_id + next_transform = mgraph.get_next_transform(current) + if next_transform is not None and next_transform.monster_id not in reaction_ids: + reaction_ids.append(next_transform.monster_id) + current = next_transform else: break diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index cb564893..b8661734 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -35,7 +35,7 @@ def menu(initial_control=None): async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] db_context: "DbContext" = dgcog.database - m = db_context.graph.get_monster(int(ims['resolved_monster_id'])) + m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=ims['resolved_monster_server']) use_evo_scroll = ims.get('use_evo_scroll') != 'False' new_monster_id = IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll) @@ -50,7 +50,7 @@ async def respond_with_left(message: Optional[Message], ims, **data): @staticmethod def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): if use_evo_scroll: - evos = db_context.graph.get_alt_ids_by_id(monster.monster_id) + evos = db_context.graph.get_alt_ids(monster) index = evos.index(monster.monster_id) new_id = evos[index - 1] return new_id @@ -62,13 +62,13 @@ def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_ev async def respond_with_right(message: Optional[Message], ims, **data): dgcog = data['dgcog'] db_context: "DbContext" = dgcog.database - m = db_context.graph.get_monster(int(ims['resolved_monster_id'])) + m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=ims['resolved_monster_server']) use_evo_scroll = ims.get('use_evo_scroll') != 'False' - new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll)) + new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll) or '') if new_monster_id is None: ims['unsupported_transition'] = True - ims['resolved_monster_id'] = str(new_monster_id) if new_monster_id else None + ims['resolved_monster_id'] = new_monster_id pane_type = ims.get('pane_type') pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] @@ -77,7 +77,7 @@ async def respond_with_right(message: Optional[Message], ims, **data): @staticmethod def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): if use_evo_scroll: - evos = db_context.graph.get_alt_ids_by_id(monster.monster_id) + evos = db_context.graph.get_alt_ids(monster) index = evos.index(monster.monster_id) if index == len(evos) - 1: # cycle back to the beginning of the evos list diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 509cfd8e..f6d9b69c 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -237,6 +237,7 @@ async def click_child_number(ims, emoji_clicked, **data): 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster_list[n - MonsterListMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, + 'resolved_monster_server': ims['monster_server'], } return emoji_response, extra_ims diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 1d32ed88..23ad5470 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -237,6 +237,8 @@ async def click_child_number(ims, emoji_clicked, **data): 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster_list[n - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, + 'resolved_monster_server': + monster_list[n - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT].server_priority, } return emoji_response, extra_ims @@ -262,6 +264,7 @@ async def _scroll_child(ims, update_fn, **data): 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster_list[copy_ims['current_index']].monster_id, + 'resolved_monster_server': monster_list[copy_ims['current_index']].server_priority, } return IdMenuEmoji.refresh, extra_ims diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 9b1e4675..920cf1d7 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -22,8 +22,7 @@ async def respond_with_base(message: Optional[Message], ims, **data): user_config = data['user_config'] # base is always first - ims['query'] = str(ims['resolved_monster_ids'][0]) - ims['resolved_monster_id'] = None + ims['resolved_monster_id'] = ims['resolved_monster_ids'][0] id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) reaction_list = ims['reaction_list'] id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) @@ -35,8 +34,7 @@ async def respond_with_transform(message: Optional[Message], ims, **data): user_config = data['user_config'] # transform is always second - ims['query'] = str(ims['resolved_monster_ids'][1]) - ims['resolved_monster_id'] = None + ims['resolved_monster_id'] = ims['resolved_monster_ids'][1] id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) reaction_list = ims['reaction_list'] id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) @@ -51,8 +49,7 @@ async def respond_with_n(message: Optional[Message], ims, **data): if n is None: return None - ims['query'] = str(ims['resolved_monster_ids'][n]) - ims['resolved_monster_id'] = None + ims['resolved_monster_id'] = ims['resolved_monster_ids'][n] id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) reaction_list = ims['reaction_list'] id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index fd222735..e8c193f6 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -5,7 +5,7 @@ import re import urllib.parse from io import BytesIO -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional, Literal import discord import tsutils @@ -189,7 +189,7 @@ async def _id(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def idna(self, ctx, *, query: str): """Monster info (limited to NA monsters ONLY)""" - await self._do_id(ctx, "inna " + query) + await self._do_id(ctx, "--na " + query) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -210,20 +210,20 @@ async def _do_id(self, ctx, query: str): original_author_id = ctx.message.author.id goodquery = None - if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.index.all_name_tokens: + if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.indexes["COMBINED"].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.index.all_name_tokens: + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes["COMBINED"].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: bad = False - for monster in dgcog.index.all_name_tokens[goodquery[1]]: - for modifier in dgcog.index.modifiers[monster]: + for monster in dgcog.indexes["COMBINED"].all_name_tokens[goodquery[1]]: + for modifier in dgcog.indexes["COMBINED"].modifiers[monster]: if modifier == 'xm' and goodquery[0] == 'x': goodquery[0] = 'xm' if modifier == goodquery[0]: bad = True - if bad and query not in dgcog.index.all_name_tokens: + if bad and query not in dgcog.indexes["COMBINED"].all_name_tokens: async def send_message(): await asyncio.sleep(1) await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" @@ -245,8 +245,8 @@ async def send_message(): # id3 messaging stuff if monster and monster.monster_no_na != monster.monster_no_jp: - await ctx.send("The NA ID and JP ID of this card differ! " - "The JP ID is 1053 you can query with {0.prefix}id jp1053.".format(ctx) + + await ctx.send(f"The NA ID and JP ID of this card differ! The JP ID is {monster.monster_id}, so " + f"you can query with {ctx.prefix}id jp{monster.monster_id}." + (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) @@ -258,8 +258,10 @@ async def send_message(): await IdViewState.query(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + discrep = dgcog.database.graph.monster_is_discrepant(monster) - state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, + state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, discrep, transform_base, true_evo_type_raw, acquire_raw, base_rarity, use_evo_scroll=settings.checkEvoID(ctx.author.id), reaction_list=initial_reaction_list) menu = IdMenu.menu() @@ -342,6 +344,7 @@ async def evos(self, ctx, *, query: str): alt_monsters = EvosViewState.get_alt_monsters_and_evos(dgcog, monster) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + discrep = dgcog.database.graph.monster_is_discrepant(monster) if alt_versions is None: await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, @@ -351,8 +354,9 @@ async def evos(self, ctx, *, query: str): full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, alt_monsters, alt_versions, gem_versions, + state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, discrep, + alt_versions, gem_versions, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.evos_control) @@ -382,10 +386,12 @@ async def evomats(self, ctx, *, query: str): return alt_monsters = MaterialsViewState.get_alt_monsters_and_evos(dgcog, monster) + discrep = dgcog.database.graph.monster_is_discrepant(monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, + state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, discrep, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) @@ -415,11 +421,13 @@ async def pantheon(self, ctx, *, query: str): + ' [{}] {}.'.format(monster.monster_id, monster.name_en)) return alt_monsters = PantheonViewState.get_alt_monsters_and_evos(dgcog, monster) + discrep = dgcog.database.graph.monster_is_discrepant(monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, alt_monsters, pantheon_list, series_name, base_monster, + state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, discrep, + pantheon_list, series_name, base_monster, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.pantheon_control) @@ -443,11 +451,12 @@ async def pic(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) + discrep = dgcog.database.graph.monster_is_discrepant(monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, alt_monsters, + state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, discrep, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.pic_control) @@ -471,11 +480,12 @@ async def otherinfo(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) + discrep = dgcog.database.graph.monster_is_discrepant(monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, - monster, alt_monsters, + state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, discrep, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.otherinfo_control) @@ -622,10 +632,11 @@ async def collabscroll(self, ctx, *, query): paginated_monsters = None rarity = None for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: - paginated_monsters = await SeriesScrollViewState.query(dgcog, monster.series_id, rarity) + paginated_monsters = await SeriesScrollViewState.query(dgcog, monster.series_id, + rarity, monster.server_priority) if paginated_monsters: break - all_rarities = SeriesScrollViewState.query_all_rarities(dgcog, series_id) + all_rarities = SeriesScrollViewState.query_all_rarities(dgcog, series_id, monster.server_priority) raw_query = query original_author_id = ctx.message.author.id @@ -746,9 +757,11 @@ async def transforminfo(self, ctx, *, query): original_author_id = ctx.message.author.id acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) reaction_list = TransformInfoMenuPanes.get_reaction_list(len(monster_ids)) + discrep = dgcog.database.graph.monster_is_discrepant(base_mon) \ + or dgcog.database.graph.monster_is_discrepant(transformed_mon) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, - color, base_mon, transformed_mon, acquire_raw, monster_ids, + color, base_mon, transformed_mon, acquire_raw, monster_ids, discrep, reaction_list=reaction_list) menu = TransformInfoMenu.menu() await menu.create(ctx, state) @@ -959,7 +972,7 @@ async def send_id_failure_message(ctx, query: str): "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) @commands.command(aliases=["iddebug", "dbid", "iddb"]) - async def debugid(self, ctx, *, query): + async def debugid(self, ctx, server: Optional[Literal["COMBINED", "NA"]] = "COMBINED", *, query): """Get helpful id information about a monster""" dgcog = self.bot.get_cog("Dadguide") mon = await dgcog.find_monster(query, ctx.author.id) @@ -967,17 +980,17 @@ async def debugid(self, ctx, *, query): await ctx.send(box("Your query didn't match any monsters.")) return base_monster = dgcog.database.graph.get_base_monster(mon) - mods = dgcog.index.modifiers[mon] - manual_modifiers = dgcog.index.manual_prefixes[mon.monster_id] + mods = dgcog.indexes[server].modifiers[mon] + manual_modifiers = dgcog.indexes[server].manual_prefixes[mon.monster_id] EVOANDTYPE = dgcog.token_maps.EVO_TOKENS.union(dgcog.token_maps.TYPE_TOKENS) ret = (f"[{mon.monster_id}] {mon.name_en}\n" f"Base: [{base_monster.monster_id}] {base_monster.name_en}\n" f"Series: {mon.series.name_en} ({mon.series_id}, {mon.series.series_type})\n\n" - f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.index.name_tokens.items() if mon in ms))}\n" - f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.index.fluff_tokens.items() if mon in ms))}\n\n" + f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.indexes[server].name_tokens.items() if mon in ms))}\n" + f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.indexes[server].fluff_tokens.items() if mon in ms))}\n\n" f"[Manual Tokens]\n" - f" Treenames: {' '.join(sorted(t for t, ms in dgcog.index.manual_tree.items() if mon in ms))}\n" - f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.index.manual_nick.items() if mon in ms))}\n\n" + f" Treenames: {' '.join(sorted(t for t, ms in dgcog.indexes[server].manual_tree.items() if mon in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.indexes[server].manual_nick.items() if mon in ms))}\n\n" f"[Modifier Tokens]\n" f" Attribute: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.COLOR_TOKENS))}\n" f" Awakening: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.AWAKENING_TOKENS))}\n" @@ -1020,7 +1033,7 @@ async def idhelp(self, ctx, *, query=""): await self.debugid(ctx, query=query) @commands.command() - async def exportmodifiers(self, ctx): + async def exportmodifiers(self, ctx, server: Optional[Literal["COMBINED", "NA"]] = "COMBINED"): DGCOG = self.bot.get_cog("Dadguide") maps = DGCOG.token_maps awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} @@ -1043,7 +1056,7 @@ async def exportmodifiers(self, ctx): atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_SKILL_MAP.items()] ret += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") stable = [(series[k].name_en, ", ".join(map(inline, v))) - for k, v in DGCOG.index.series_id_to_pantheon_nickname.items()] + for k, v in DGCOG.indexes[server].series_id_to_pantheon_nickname.items()] ret += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in maps.COLOR_MAP.items()] ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in @@ -1057,7 +1070,7 @@ async def exportmodifiers(self, ctx): await ctx.send(file=text_to_file(ret, filename="table.md")) @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod", "luid", "idlu"]) - async def idmeaning(self, ctx, *, token): + async def idmeaning(self, ctx, token, server: Optional[Literal["COMBINED", "NA"]] = "COMBINED"): """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") @@ -1078,7 +1091,7 @@ def f(m, s): token_ret = "" so = [] for mon in sorted(token_dict[token], key=lambda m: m.monster_id): - if (mon in DGCOG.index.mwtoken_creators[token]) == is_multiword: + if (mon in DGCOG.indexes[server].mwtoken_creators[token]) == is_multiword: so.append(mon) if len(so) > 5: token_ret += f"\n\n{token_type}\n" + ", ".join(f(m, str(m.monster_id)) for m in so[:10]) @@ -1088,22 +1101,22 @@ def f(m, s): f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) return token_ret - ret += write_name_token(DGCOG.index.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) - ret += write_name_token(DGCOG.index.manual, "[Manual Tokens]") - ret += write_name_token(DGCOG.index.name_tokens, "[Name Tokens]") - ret += write_name_token(DGCOG.index.fluff_tokens, "[Fluff Tokens]") + ret += write_name_token(DGCOG.indexes[server].manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) + ret += write_name_token(DGCOG.indexes[server].manual, "[Manual Tokens]") + ret += write_name_token(DGCOG.indexes[server].name_tokens, "[Name Tokens]") + ret += write_name_token(DGCOG.indexes[server].fluff_tokens, "[Fluff Tokens]") - submwtokens = [t for t in DGCOG.index.multi_word_tokens if token in t] + submwtokens = [t for t in DGCOG.indexes[server].multi_word_tokens if token in t] if submwtokens: ret += "\n\n[Multi-word Super-tokens]\n" for t in submwtokens: - if not DGCOG.index.all_name_tokens[''.join(t)]: + if not DGCOG.indexes[server].all_name_tokens[''.join(t)]: continue - creators = sorted(DGCOG.index.mwtoken_creators["".join(t)], key=lambda m: m.monster_id) + creators = sorted(DGCOG.indexes[server].mwtoken_creators["".join(t)], key=lambda m: m.monster_id) ret += f"{' '.join(t).title()}" ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + - str(DGCOG.mon_finder.get_most_eligable_monster(DGCOG.index.all_name_tokens[''.join(t)], + str(DGCOG.mon_finder.get_most_eligable_monster(DGCOG.indexes[server].all_name_tokens[''.join(t)], DGCOG).monster_id) + ")\n") @@ -1131,7 +1144,7 @@ def additmods(ms, om): '/' + k[1].name.replace("Nil", "None") + additmods(v, token) for k, v in tms.DUAL_COLOR_MAP.items() if token in v], *["Series: " + series[k].name_en + additmods(v, token) - for k, v in DGCOG.index.series_id_to_pantheon_nickname.items() if token in v], + for k, v in DGCOG.indexes[server].series_id_to_pantheon_nickname.items() if token in v], *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", token)], *["Base rarity: " + m for m in re.findall(r"^(\d+)\*b$", token)], @@ -1212,5 +1225,4 @@ async def idsearch(self, ctx, *, query): await ctx.send("No monster matched.") return - print(monster_list) await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results', IdSearchViewState) diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 382234d5..66036889 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -13,9 +13,9 @@ async def get_monster_from_ims(dgcog, ims: dict): query = ims.get('query') or ims['raw_query'] resolved_monster_id_str = ims.get('resolved_monster_id') - resolved_monster_id = int(resolved_monster_id_str) if resolved_monster_id_str else None + resolved_monster_id = int(resolved_monster_id_str or 0) if resolved_monster_id: - return dgcog.database.graph.get_monster(resolved_monster_id) + return dgcog.database.graph.get_monster(resolved_monster_id, server=ims.get('resolved_monster_server')) monster = await dgcog.find_monster(query, ims['original_author_id']) return monster diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index b9655fe6..f3cbe8aa 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -11,58 +11,66 @@ class MonsterHeader: - @staticmethod - def jp_suffix(m: "MonsterModel", subname_on_override=True): + @classmethod + def jp_suffix(cls, m: "MonsterModel", discrepant=False, subname_on_override=True): suffix = "" if m.roma_subname and (subname_on_override or m.name_en_override is None): suffix += ' [{}]'.format(m.roma_subname) if not m.on_na: suffix += ' (JP only)' + if discrepant: + suffix += ' (JP Buffed)' return suffix - @staticmethod - def short(m: "MonsterModel", link=False): + @classmethod + def short(cls, m: "MonsterModel", link=False): type_emojis = '' msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) return '[{}]({})'.format(msg, puzzledragonx(m)) if link else msg - @staticmethod - def long(m: "MonsterModel", link=False): - msg = MonsterHeader.short(m) + MonsterHeader.jp_suffix(m) + @classmethod + def long(cls, m: "MonsterModel", link=False): + msg = cls.short(m) + cls.jp_suffix(m) return '[{}]({})'.format(msg, puzzledragonx(m)) if link else msg - @staticmethod - def name(m: "MonsterModel", link=False, show_jp=False): + @classmethod + def name(cls, m: "MonsterModel", link=False, show_jp=False): msg = '[{}] {}{}'.format( m.monster_no_na, m.name_en, - MonsterHeader.jp_suffix(m) if show_jp else '') + cls.jp_suffix(m) if show_jp else '') return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) - @staticmethod - def long_v2(m: "MonsterModel", link=False): - msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, MonsterHeader.jp_suffix(m)) + @classmethod + def long_v2(cls, m: "MonsterModel", link=False): + msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, cls.jp_suffix(m)) return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) - @staticmethod - def long_maybe_tsubaki(m: "MonsterModel", is_tsubaki): + @classmethod + def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, discrepant=False): """Returns long_v2 as well as an `!` if the monster is Tsubaki To celebrate 1000 issues/PRs in our main Tsubaki repo, we added this easter egg! Yay! """ - return Text('[{}] {}{}{}'.format( + return '[{}] {}{}{}'.format( m.monster_no_na, m.name_en, '!' if is_tsubaki else '', - MonsterHeader.jp_suffix(m))) + cls.jp_suffix(m, discrepant)) - @staticmethod - def short_with_emoji(m: "MonsterModel", link=True, prefix=None): + @classmethod + def fmt_id_header(cls, m: "MonsterModel", is_tsubaki, discrepant): + return Text('{} {}'.strip().format( + '\N{EARTH GLOBE AMERICAS}' if m.server_priority == "NA" else '', + cls.long_maybe_tsubaki(m, is_tsubaki, bool(discrepant)))) + + @classmethod + def short_with_emoji(cls, m: "MonsterModel", link=True, prefix=None): msg = f"{m.monster_no_na} - {m.name_en}" return Box( prefix, Text(get_attribute_emoji_by_monster(m)), LinkedText(msg, puzzledragonx(m)) if link else Text(msg), - Text(MonsterHeader.jp_suffix(m, False)), + Text(cls.jp_suffix(m, False, False)), delimiter=' ' ) diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 9f9d4dc1..52167719 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel + from dadguide.models.monster.monster_difference import MonsterDifference from dadguide.models.evolution_model import EvolutionModel @@ -17,7 +18,7 @@ class MonsterEvolution(NamedTuple): class ViewStateBaseId(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List[MonsterEvolution], + alt_monsters: List[MonsterEvolution], monster_difference: "MonsterDifference", use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): @@ -27,6 +28,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.reaction_list = reaction_list self.color = color self.monster = monster + self.discrepant = monster_difference self.query = query self.use_evo_scroll = use_evo_scroll @@ -35,6 +37,7 @@ def serialize(self): ret.update({ 'query': self.query, 'resolved_monster_id': self.monster.monster_id, + 'resolved_monster_server': self.monster.server_priority, 'use_evo_scroll': str(self.use_evo_scroll), 'reaction_list': self.reaction_list, }) @@ -54,13 +57,16 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') + discrep = dgcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + alt_monsters, discrep, + use_evo_scroll=use_evo_scroll, + reaction_list=reaction_list, extra_state=ims) @classmethod def get_alt_monsters_and_evos(cls, dgcog, monster) -> List[MonsterEvolution]: graph = dgcog.database.graph - alt_monsters = graph.get_alt_monsters_by_id(monster.monster_id) - return [MonsterEvolution(m, graph.get_evo_by_monster_id(m.monster_id)) for m in alt_monsters] + alt_monsters = graph.get_alt_monsters(monster) + return [MonsterEvolution(m, graph.get_evolution(m)) for m in alt_monsters] diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index bdefda91..af32dc7e 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -18,13 +18,14 @@ class EvosViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, - monster: "MonsterModel", alt_monsters: List[MonsterEvolution], + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + alt_monsters: List[MonsterEvolution], discrepant, alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], reaction_list: List[str] = None, use_evo_scroll: bool = True, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + alt_monsters, discrepant, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -54,10 +55,10 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') + discrep = dgcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, - monster, - alt_monsters, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + alt_monsters, discrep, alt_versions, gem_versions, reaction_list=reaction_list, use_evo_scroll=use_evo_scroll, @@ -66,7 +67,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): @staticmethod async def query(dgcog, monster): db_context = dgcog.database - alt_versions = sorted(db_context.graph.get_alt_monsters_by_id(monster.monster_id), + alt_versions = sorted(db_context.graph.get_alt_monsters(monster), key=lambda x: x.monster_id) gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) if len(alt_versions) == 1 and len(gem_versions) == 0: @@ -103,9 +104,9 @@ def embed(cls, state: EvosViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_maybe_tsubaki(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI - ).to_markdown(), + title=MonsterHeader.fmt_id_header(state.monster, + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + state.discrepant).to_markdown(), url=puzzledragonx(state.monster)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 9177dc51..42497974 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -33,11 +33,12 @@ def alt_fmt(monsterevo, state): class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List[MonsterEvolution], + alt_monsters: List[MonsterEvolution], discrepant, transform_base, true_evo_type_raw, acquire_raw, base_rarity, fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, is_child: bool = False, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + alt_monsters, discrepant, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -76,8 +77,10 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') fallback_message = ims.get('message') is_child = ims.get('is_child') + discrep = dgcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + alt_monsters, discrep, transform_base, true_evo_type_raw, acquire_raw, base_rarity, fallback_message=fallback_message, use_evo_scroll=use_evo_scroll, @@ -96,9 +99,9 @@ async def query(cls, dgcog, monster): @classmethod async def _get_monster_misc_info(cls, db_context, monster): transform_base = db_context.graph.get_transform_base(monster) - true_evo_type_raw = db_context.graph.true_evo_type_by_monster(monster).value + true_evo_type_raw = db_context.graph.true_evo_type(monster).value acquire_raw = db_context.graph.monster_acquisition(monster) - base_rarity = db_context.graph.get_base_monster_by_id(monster.monster_id).rarity + base_rarity = db_context.graph.get_base_monster(monster).rarity return acquire_raw, base_rarity, transform_base, true_evo_type_raw @@ -133,9 +136,9 @@ def evos_embed_field(state: ViewStateBaseId): # this isn't used right now, but maybe later if discord changes the api for embed titles...? help_link = "https://github.com/TsubakiBotPad/pad-cogs/wiki/Evolutions-mini-view" legend_parts = [] - if any([alt_evo.evolution and not alt_evo.evolution.reversible for alt_evo in state.alt_monsters]): + if any(not alt_evo.evolution.reversible for alt_evo in state.alt_monsters if alt_evo.evolution): legend_parts.append("⌊Irreversible⌋") - if any([alt_evo.monster.is_equip for alt_evo in state.alt_monsters]): + if any(alt_evo.monster.is_equip for alt_evo in state.alt_monsters): legend_parts.append("⌈Equip⌉") if legend_parts: help_text = ' – Help: {}'.format(" ".join(legend_parts)) @@ -301,9 +304,9 @@ def embed(cls, state: IdViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_maybe_tsubaki(m, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI - ).to_markdown(), + title=MonsterHeader.fmt_id_header(m, + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + state.discrepant).to_markdown(), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 3d23ca10..ef681829 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -24,14 +24,15 @@ class MaterialsViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List[MonsterEvolution], + alt_monsters: List[MonsterEvolution], discrepant, mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, gem_override: bool, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + alt_monsters, discrepant, reaction_list=reaction_list, use_evo_scroll=use_evo_scroll, extra_state=extra_state) @@ -70,8 +71,10 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = ims.get('reaction_list') + discrep = dgcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + alt_monsters, discrep, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, @@ -80,7 +83,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): @staticmethod async def query(dgcog, monster): db_context = dgcog.database - mats = db_context.graph.evo_mats_by_monster(monster) + mats = db_context.graph.evo_mats(monster) usedin = db_context.graph.material_of_monsters(monster) evo_gem = db_context.graph.evo_gem_monster(monster) gemid = str(evo_gem.monster_no_na) if evo_gem else None @@ -90,7 +93,8 @@ async def query(dgcog, monster): link = ilmina_skill(monster) if monster.active_skill: - sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id) + sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id, + server=monster.server_priority) if db_context.graph.monster_is_farmable_evo(m)] sugs = [db_context.graph.evo_gem_monster(su) for su in sums] vsums = [] @@ -154,9 +158,9 @@ def embed(cls, state: MaterialsViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_maybe_tsubaki(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI - ).to_markdown(), + title=MonsterHeader.fmt_id_header(state.monster, + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + state.discrepant).to_markdown(), url=puzzledragonx(state.monster) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 3a61b70d..02633990 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -48,6 +48,7 @@ def serialize(self): 'pane_type': MonsterListView.VIEW_TYPE, 'title': self.title, 'monster_list': [m.monster_id for m in self.monster_list], + 'monster_server': self.monster_list[0].server_priority if self.monster_list else "COMBINED", 'current_page': self.current_page, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index 6e607507..43d238b4 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -14,17 +14,19 @@ def serialize(self): ret = super().serialize() ret.update({ 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], + 'monster_server': self.paginated_monsters[0].server_priority if self.paginated_monsters else "COMBINED", }) return ret @classmethod - async def query(cls, dgcog, monster_list) -> List["MonsterModel"]: + async def query(cls, dgcog, monster_list, server) -> List["MonsterModel"]: db_context: "DbContext" = dgcog.database - monster_list = [db_context.graph.get_monster(int(m)) for m in monster_list] + monster_list = [db_context.graph.get_monster(int(m), server=server) for m in monster_list] return monster_list @classmethod async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: monster_ids = ims['full_monster_list'] - monster_list = await cls.query(dgcog, monster_ids) + server = ims['monster_server'] + monster_list = await cls.query(dgcog, monster_ids, server) return monster_list diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 879bebd4..cde2c78b 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -52,9 +52,9 @@ def embed(cls, state: OtherInfoViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_maybe_tsubaki(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI - ).to_markdown(), + title=MonsterHeader.fmt_id_header(state.monster, + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + state.discrepant).to_markdown(), url=puzzledragonx(m)), embed_footer=embed_footer_with_state(state), embed_fields=[ diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 00f1acaf..2b93ba59 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -25,12 +25,14 @@ class PantheonViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters, + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + alt_monsters, discrepant, pantheon_list: List[MonsterEvolution], series_name: str, base_monster, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, + super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + alt_monsters, discrepant, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -62,8 +64,10 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') + discrep = dgcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, + return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + alt_monsters, discrep, pantheon_list, series_name, base_monster, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, @@ -85,7 +89,7 @@ async def query(cls, dgcog, monster): db_context = dgcog.database series_name = monster.series.name_en - full_pantheon = db_context.get_monsters_by_series(monster.series_id) + full_pantheon = db_context.get_monsters_by_series(monster.series_id, server=monster.server_priority) if not full_pantheon: return None, None, None @@ -170,9 +174,9 @@ def embed(cls, state: PantheonViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_maybe_tsubaki(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI - ).to_markdown(), + title=MonsterHeader.fmt_id_header(state.monster, + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + state.discrepant).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), embed_fields=fields, diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 8f758ba5..2eb22722 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -54,9 +54,9 @@ def embed(cls, state: PicViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_maybe_tsubaki(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI - ).to_markdown(), + title=MonsterHeader.fmt_id_header(state.monster, + state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + state.discrepant).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), embed_fields=fields, diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 1f317056..efb116d1 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -48,6 +48,7 @@ def serialize(self): ret.update({ 'pane_type': SeriesScrollView.VIEW_TYPE, 'series_id': self.series_id, + 'server': self.monster_list[0].monster_id if self.monster_list else "COMBINED", 'current_page': self.current_page, 'pages_in_rarity': self.pages_in_rarity, 'title': self.title, @@ -63,13 +64,12 @@ def serialize(self): @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): - # print(ims) if ims.get('unsupported_transition'): return None series_id = ims['series_id'] rarity = ims['rarity'] all_rarities = ims['all_rarities'] - paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity) + paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, ims['server']) current_page = ims['current_page'] monster_list = paginated_monsters[current_page] title = ims['title'] @@ -96,9 +96,9 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): child_message_id=child_message_id) @staticmethod - async def query(dgcog, series_id, rarity): + async def query(dgcog, series_id, rarity, server): db_context: "DbContext" = dgcog.database - all_series_monsters = db_context.get_monsters_by_series(series_id) + all_series_monsters = db_context.get_monsters_by_series(series_id, server=server) base_monsters_of_rarity = list(filter( lambda m: db_context.graph.monster_is_base(m) and m.rarity == rarity, all_series_monsters)) paginated_monsters = [base_monsters_of_rarity[i:i + SeriesScrollViewState.MAX_ITEMS_PER_PANE] @@ -107,16 +107,16 @@ async def query(dgcog, series_id, rarity): return paginated_monsters @staticmethod - def query_all_rarities(dgcog, series_id): + def query_all_rarities(dgcog, series_id, server): db_context: "DbContext" = dgcog.database - return sorted({m.rarity for m in db_context.get_all_monsters() if + return sorted({m.rarity for m in db_context.get_all_monsters(server) if m.series_id == series_id and db_context.graph.monster_is_base(m)}) @staticmethod async def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: series_id = ims['series_id'] rarity = ims['rarity'] - paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity) + paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, ims['server']) return paginated_monsters diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 4e9ebdc6..41af89cd 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -22,7 +22,7 @@ class TransformInfoViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, - acquire_raw, monster_ids, reaction_list): + acquire_raw, monster_ids, discrepant, reaction_list): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color self.base_mon = base_mon @@ -30,11 +30,13 @@ def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, tr self.acquire_raw = acquire_raw self.monster_ids = monster_ids self.reaction_list = reaction_list + self.discrepant = discrepant def serialize(self): ret = super().serialize() ret.update({ 'resolved_monster_ids': self.monster_ids, + 'resolved_monster_server': self.base_mon.server_priority, 'reaction_list': self.reaction_list }) return ret @@ -48,14 +50,16 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): base_mon_id = monster_ids[0] transformed_mon_id = monster_ids[1] - base_mon = dgcog.get_monster(base_mon_id) - transformed_mon = dgcog.get_monster(transformed_mon_id) + base_mon = dgcog.get_monster(base_mon_id, server=ims['resolved_monster_server']) + transformed_mon = dgcog.get_monster(transformed_mon_id, server=ims['resolved_monster_server']) acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) reaction_list = ims['reaction_list'] + discrep = dgcog.database.graph.monster_is_discrepant(base_mon) \ + or dgcog.database.graph.monster_is_discrepant(transformed_mon) return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, - base_mon, transformed_mon, acquire_raw, monster_ids, + base_mon, transformed_mon, acquire_raw, monster_ids, discrep, reaction_list=reaction_list) @staticmethod @@ -202,7 +206,9 @@ def embed(state: TransformInfoViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.long_v2(transformed_mon).to_markdown(), + title=MonsterHeader.fmt_id_header(transformed_mon, + False, + state.discrepant).to_markdown(), url=puzzledragonx(transformed_mon) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon)), diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index dcf2d979..31875086 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -47,7 +47,7 @@ async def check_seen(self): DGCOG = self.bot.get_cog('Dadguide') await DGCOG.wait_until_ready() db_context: "DbContext" = DGCOG.database - all_monsters = db_context.get_all_monsters(as_generator=False) + all_monsters = [*db_context.get_all_monsters()] jp_monster_map = {m.monster_id: m for m in all_monsters if m.on_jp} na_monster_map = {m.monster_id: m for m in all_monsters if m.on_na} diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index d2b02201..53fd495f 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -6,26 +6,21 @@ database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) db_context = DbContext(database, graph) +get_monster = lambda mid, server="COMBINED": graph.get_monster(mid, server=server) -# print(ctx.get_awoken_skill_ids()) -assert db_context.graph.monster_is_farmable_by_id(4) -assert not db_context.graph.monster_is_farmable_by_id(5156) -assert db_context.graph.get_base_monster_by_id(1074).monster_id == 1073 # evo pandora -assert db_context.get_monsters_by_series(1)[0].name_en == 'Tyrra' -assert db_context.get_monsters_by_active(1)[0].name_en == 'Tyrra' +assert get_monster(4).is_farmable +assert not get_monster(5156).is_farmable +assert graph.get_base_monster(get_monster(1074)).monster_id == 1073 # evo pandora +assert db_context.get_monsters_by_series(1, server="COMBINED")[0].name_en == 'Tyrra' +assert db_context.get_monsters_by_active(1, server="COMBINED")[0].name_en == 'Tyrra' # evo types -# 5392 is mega dkali -assert db_context.graph.get_prev_evolution_id_by_monster_id(5392) == 1588 +assert graph.true_evo_type(get_monster(5392)) == InternalEvoType.Ultimate +assert graph.true_evo_type(get_monster(6337)) == InternalEvoType.SuperReincarnated # sr hades +assert graph.true_evo_type(get_monster(5871)) == InternalEvoType.Pixel # px sakuya +assert graph.true_evo_type(get_monster(1465)) == InternalEvoType.Normal # awoken thoth +assert graph.true_evo_type(get_monster(1464)) == InternalEvoType.Base # base thoth -assert db_context.graph.true_evo_type_by_monster_id(5392) == InternalEvoType.Ultimate -assert db_context.graph.true_evo_type_by_monster_id(6337) == InternalEvoType.SuperReincarnated # sr hades -assert db_context.graph.true_evo_type_by_monster_id(5871) == InternalEvoType.Pixel # px sakuya -assert db_context.graph.true_evo_type_by_monster_id(1465) == InternalEvoType.Normal # awoken thoth -assert db_context.graph.true_evo_type_by_monster_id(1464) == InternalEvoType.Base # base thoth - -# check that we're using the most recent data -assert db_context.graph.true_evo_type_by_monster_id(3908) == InternalEvoType.Reincarnated # revo typhon - -assert len(db_context.graph.get_evo_by_monster_id(3).mats) == 2 -assert 153 in db_context.graph.get_evo_by_monster_id(3).mats +# mats +assert len(graph.get_evolution(get_monster(3)).mats) == 2 +assert 153 in graph.get_evolution(get_monster(3)).mats From ed890000e0301a435eaaa725aa0a5eef3fd33889 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 31 May 2021 00:53:41 -0500 Subject: [PATCH 0794/1220] "JP buffed" instead of "JP Buffed" (#1134) --- padinfo/view/components/monster/header.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index f3cbe8aa..307b8bd8 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -19,7 +19,7 @@ def jp_suffix(cls, m: "MonsterModel", discrepant=False, subname_on_override=True if not m.on_na: suffix += ' (JP only)' if discrepant: - suffix += ' (JP Buffed)' + suffix += ' (JP buffed)' return suffix @classmethod From b501aee0225ba5f982ad2a378a70663c37ad9643 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 31 May 2021 03:58:34 -0700 Subject: [PATCH 0795/1220] Order evos in graph functions (#1138) --- dadguide/monster_graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 5aad408c..c78c586f 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -318,7 +318,7 @@ def get_evo_tree(self, monster: MonsterModel) -> List[MonsterModel]: def get_evo_tree_from_base(self, base_monster: MonsterModel) -> List[MonsterModel]: mons = [base_monster] - for evo in self.get_next_evolutions(base_monster): + for evo in sorted(self.get_next_evolutions(base_monster), key=lambda m: m.monster_id) mons += self.get_evo_tree_from_base(evo) return mons @@ -344,7 +344,7 @@ def process_alt_monsters_from_base(self, base_monster: MonsterModel) -> List[Mon if transform and (transform.monster_id > base_monster.monster_id or transform.monster_id == 5802): # I hate DMG very much ids += self.process_alt_monsters_from_base(transform) - for evo in self.get_next_evolutions(base_monster): + for evo in sorted(self.get_next_evolutions(base_monster), key=lambda m: m.monster_id): ids += self.process_alt_monsters_from_base(evo) return ids From b839a0ae9d4af53d0b7117f2d691c21c609478f9 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 31 May 2021 04:01:57 -0700 Subject: [PATCH 0796/1220] Fix typo (#1139) --- dadguide/monster_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index c78c586f..c7247c6e 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -318,7 +318,7 @@ def get_evo_tree(self, monster: MonsterModel) -> List[MonsterModel]: def get_evo_tree_from_base(self, base_monster: MonsterModel) -> List[MonsterModel]: mons = [base_monster] - for evo in sorted(self.get_next_evolutions(base_monster), key=lambda m: m.monster_id) + for evo in sorted(self.get_next_evolutions(base_monster), key=lambda m: m.monster_id): mons += self.get_evo_tree_from_base(evo) return mons From f150f3136b9b4ef4cd6a990b37ca2ee43fbd5dbc Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 31 May 2021 07:05:39 -0500 Subject: [PATCH 0797/1220] Change JP Added wording & add sell gold (#1145) --- padinfo/view/otherinfo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index cde2c78b..101391d1 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -64,12 +64,15 @@ def embed(cls, state: OtherInfoViewState): Text(statsbox(m)), LabeledText("JP Name", m.name_ja), LinksView.linksbox(m), - LabeledText("History", m.history_us) if m.history_us else None, + LabeledText("JP Added", str(m.reg_date)) if m.reg_date else None, LabeledText("Series", m.series.name_en), Box( LabeledText("Sell MP", '{:,}'.format(m.sell_mp)), LabeledText("Buy MP", '{:,}'.format(m.buy_mp)) if m.buy_mp else None, delimiter=' '), + Box( + LabeledText("Sell Gold", '{:,}'.format(m.sell_gold)) + ), Box( LabeledText("XP to Max", '{:.1f}'.format(m.exp / 1000000).rstrip('0').rstrip('.') + 'M' if m.exp >= 1000000 else '{:,}'.format(m.exp)), From 671d24eae7c5d883270eef5adb1a0c311e17884a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 31 May 2021 05:13:18 -0700 Subject: [PATCH 0798/1220] Add ^idset server and --notnaonly setting (#1146) --- dadguide/dadguide.py | 4 +++- dadguide/find_monster.py | 8 +++++--- padinfo/padinfo.py | 27 ++++++++++++++++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index e95b2832..23d4b3aa 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -73,7 +73,7 @@ def __init__(self, bot, *args, **kwargs): self.fir_lock = asyncio.Lock() self.fir3_lock = asyncio.Lock() - self.fm_flags_default = {'na_prio': True} + self.fm_flags_default = {'na_prio': True, 'server': DEFAULT_SERVER} self.config = Config.get_conf(self, identifier=64667103) self.config.register_global(datafile='', indexlog=0, test_suite={}, fluff_suite=[], typo_mods=[]) self.config.register_user(lastaction=None, fm_flags={}) @@ -84,6 +84,8 @@ def __init__(self, bot, *args, **kwargs): self.MonsterStatModifierInput = MonsterStatModifierInput self.token_maps = token_mappings + self.DEFAULT_SERVER = DEFAULT_SERVER + self.SERVERS = SERVERS self.mon_finder = None # type: Optional[FindMonster] GADMIN_COG = self.bot.get_cog("GlobalAdmin") diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index cc0319b0..52aa4d4d 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -42,17 +42,19 @@ class FindMonster: def __init__(self, dgcog, flags: Mapping[str, Any]): self.dgcog = dgcog - self.index = self.dgcog.indexes["COMBINED"] self.flags = flags + self.index = self.dgcog.indexes[flags['server']] def _process_settings(self, original_query: str) -> str: settings = re.findall(r'--\w+(?:{.+?})?', original_query) for setting in settings: - if self.calc_ratio_modifier(setting, '--na') >= self.MODIFIER_JW_DISTANCE: + if setting == '--na': self.index = self.dgcog.indexes['NA'] + if setting == '--notnaonly': + self.index = self.dgcog.indexes['COMBINED'] else: - self.index = self.dgcog.indexes[DEFAULT_SERVER] + self.index = self.dgcog.indexes[self.flags['server']] return re.sub(r'\s*--\w+(?:{.+?})?\s*', ' ', original_query) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e8c193f6..dda159f4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -57,6 +57,7 @@ from padinfo.view.transforminfo import TransformInfoViewState if TYPE_CHECKING: + from dadguide.dadguide import Dadguide from dadguide.models.monster_model import MonsterModel from dadguide.models.series_model import SeriesModel @@ -150,7 +151,7 @@ async def reload_nicknames(self): await asyncio.sleep(wait_time) - async def get_dgcog(self): + async def get_dgcog(self) -> "Dadguide": dgcog = self.bot.get_cog("Dadguide") if dgcog is None: raise ValueError("Dadguide cog is not loaded") @@ -757,8 +758,7 @@ async def transforminfo(self, ctx, *, query): original_author_id = ctx.message.author.id acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) reaction_list = TransformInfoMenuPanes.get_reaction_list(len(monster_ids)) - discrep = dgcog.database.graph.monster_is_discrepant(base_mon) \ - or dgcog.database.graph.monster_is_discrepant(transformed_mon) + discrep = dgcog.database.graph.monster_is_discrepant(base_mon) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, color, base_mon, transformed_mon, acquire_raw, monster_ids, discrep, @@ -914,6 +914,22 @@ async def naprio(self, ctx, value: bool): fm_flags['na_prio'] = value await ctx.send(f"NA monster prioritization has been **{'en' if value else 'dis'}abled**.") + @idset.command() + async def server(self, ctx, server: str): + dgcog = await self.get_dgcog() + async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + if server.upper() == "DEFAULT": + fm_flags['server'] = dgcog.DEFAULT_SERVER + elif server.upper() in dgcog.SERVERS: + fm_flags['server'] = server.upper() + else: + if dgcog.DEFAULT_SERVER == "COMBINED": + await ctx.send("Server must be `default` or `na`") + else: + await ctx.send("Server must be `na` or `combined`") + return + await ctx.tick() + @commands.group() @checks.is_owner() async def padinfo(self, ctx): @@ -1116,8 +1132,9 @@ def f(m, s): ret += f"{' '.join(t).title()}" ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + - str(DGCOG.mon_finder.get_most_eligable_monster(DGCOG.indexes[server].all_name_tokens[''.join(t)], - DGCOG).monster_id) + str(DGCOG.mon_finder.get_most_eligable_monster( + DGCOG.indexes[server].all_name_tokens[''.join(t)], + DGCOG).monster_id) + ")\n") def additmods(ms, om): From 38fe08fee8fc08ae3153d79dc572d9b5943c6553 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 31 May 2021 07:31:33 -0500 Subject: [PATCH 0799/1220] Add idset server docstring (#1147) --- padinfo/padinfo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index dda159f4..33dd4aa7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -916,6 +916,11 @@ async def naprio(self, ctx, value: bool): @idset.command() async def server(self, ctx, server: str): + """Change the server to be used for your `[p]id` queries + + `[p]idset server default`: Include both NA & JP cards. You can still use `--na` as needed. + `[p]idset server na`: Include only NA cards. You can still use `--notnaonly` as needed. + """ dgcog = await self.get_dgcog() async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: if server.upper() == "DEFAULT": From 35a8938c4824ffb34a43d1b1722c4b4239c2612e Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 31 May 2021 08:26:31 -0500 Subject: [PATCH 0800/1220] Fix left-right arrows in MonsterListMenu (#1148) --- padinfo/menu/monster_list.py | 6 ++++-- padinfo/view/monster_list/monster_list.py | 2 +- padinfo/view/monster_list/static_monster_list.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index f6d9b69c..cd0bd367 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -237,7 +237,7 @@ async def click_child_number(ims, emoji_clicked, **data): 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster_list[n - MonsterListMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, - 'resolved_monster_server': ims['monster_server'], + 'resolved_monster_server': ims['resolved_monster_server'], } return emoji_response, extra_ims @@ -257,11 +257,13 @@ async def _scroll_child(ims, update_fn, **data): paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, copy_ims) page = copy_ims.get('current_page') or 0 monster_list = paginated_monsters[page] + monster = monster_list[copy_ims['current_index']] extra_ims = { 'is_child': True, 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, - 'resolved_monster_id': monster_list[copy_ims['current_index']].monster_id, + 'resolved_monster_id': monster.monster_id, + 'resolved_monster_server': monster.server_priority, } return IdMenuEmoji.refresh, extra_ims diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 02633990..4fb7e32d 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -48,7 +48,7 @@ def serialize(self): 'pane_type': MonsterListView.VIEW_TYPE, 'title': self.title, 'monster_list': [m.monster_id for m in self.monster_list], - 'monster_server': self.monster_list[0].server_priority if self.monster_list else "COMBINED", + 'resolved_monster_server': self.monster_list[0].server_priority if self.monster_list else "COMBINED", 'current_page': self.current_page, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index 43d238b4..9813f8d7 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -14,7 +14,7 @@ def serialize(self): ret = super().serialize() ret.update({ 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], - 'monster_server': self.paginated_monsters[0].server_priority if self.paginated_monsters else "COMBINED", + 'resolved_monster_server': self.paginated_monsters[0].server_priority if self.paginated_monsters else "COMBINED", }) return ret @@ -27,6 +27,6 @@ async def query(cls, dgcog, monster_list, server) -> List["MonsterModel"]: @classmethod async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: monster_ids = ims['full_monster_list'] - server = ims['monster_server'] + server = ims['resolved_monster_server'] monster_list = await cls.query(dgcog, monster_ids, server) return monster_list From 76c1a6b577ccbae0f1ec08c39dd1206516ff8464 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 31 May 2021 08:32:23 -0500 Subject: [PATCH 0801/1220] Move AwokenSkills enum to common/enums.py (#1149) --- dadguide/common/enums.py | 82 +++++++++++++++++++++++++++++++ dadguide/find_monster_tokens.py | 3 +- dadguide/models/monster_stats.py | 2 +- dadguide/token_mappings.py | 83 +------------------------------- 4 files changed, 86 insertions(+), 84 deletions(-) diff --git a/dadguide/common/enums.py b/dadguide/common/enums.py index 2a4a1b8b..8639f94c 100644 --- a/dadguide/common/enums.py +++ b/dadguide/common/enums.py @@ -1,5 +1,87 @@ +from enum import Enum from typing import Literal DEFAULT_SERVER = "COMBINED" SERVERS = ["NA", "COMBINED"] Server = Literal["COMBINED", "NA"] + + +class AwokenSkills(Enum): + ENHANCEDHP = 1 + ENHANCEDATK = 2 + ENHANCEDRCV = 3 + REDUCERED = 4 + REDUCEBLUE = 5 + REDUCEGREEN = 6 + REDUCELIGHT = 7 + REDUCEDARK = 8 + AUTOHEAL = 9 + BINDRES = 10 + BLINDRES = 11 + JAMMERRES = 12 + POISONRES = 13 + ENHANCEDRED = 14 + ENHANCEDBLUE = 15 + ENHANCEDGREEN = 16 + ENHANCEDLIGHT = 17 + ENHANCEDDARK = 18 + EXTMOVE = 19 + BINDRECOVERY = 20 + SKILLBOOST = 21 + REDROW = 22 + BLUEROW = 23 + GREENROW = 24 + LIGHTROW = 25 + DARKROW = 26 + TPA = 27 + SKILLBINDRES = 28 + ENHANCEDHEAL = 29 + MULTIBOOST = 30 + DRAGONKILLER = 31 + GODKILLER = 32 + DEVILKILLER = 33 + MACHINEKILLER = 34 + BALANCEDKILLER = 35 + ATTACKERKILLER = 36 + PHYSICALKILLER = 37 + HEALERKILLER = 38 + EVOMATKILLER = 39 + AWOKENKILLER = 40 + FODDERKILLER = 41 + REDEEMKILLER = 42 + ENHCOMBO7C = 43 + GUARDBREAK = 44 + FUA = 45 + ENHTEAMHP = 46 + ENHTEAMRCV = 47 + VDP = 48 + EQUIP = 49 + SUPERFUA = 50 + SKILLCHARGE = 51 + UNBINDABLE = 52 + EXTMOVEPLUS = 53 + CLOUDRESIST = 54 + TAPERESIST = 55 + SKILLBOOSTPLUS = 56 + HP80ORMORE = 57 + HP50ORLESS = 58 + ELSHIELD = 59 + ELATTACK = 60 + ENHCOMBO10C = 61 + COMBOORB = 62 + VOICE = 63 + DUNGEONBONUS = 64 + REDUCEDHP = 65 + REDUCEDATK = 66 + REDUCEDRCV = 67 + UNBLINDABLE = 68 + UNJAMMABLE = 69 + UNPOISONABLE = 70 + JAMMERBLESSING = 71 + POISONBLESSING = 72 + REDCOMBOCOUNT = 73 + BLUECOMBOCOUNT = 74 + GREENCOMBOCOUNT = 75 + LIGHTCOMBOCOUNT = 76 + DARKCOMBOCOUNT = 77 + CROSSATTACK = 78 diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index d958f358..6e5d9ddb 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -1,7 +1,8 @@ import re from dadguide.models.monster_model import MonsterModel -from dadguide.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP, AwokenSkills +from dadguide.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP +from dadguide.common.enums import AwokenSkills def regexlist(tokens): diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 9a62580c..02b2734f 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -1,5 +1,5 @@ from typing import Literal -from dadguide.token_mappings import AwokenSkills +from dadguide.common.enums import AwokenSkills StatType = Literal['hp', 'atk', 'rcv'] diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 9b5a244f..2726175c 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -2,6 +2,7 @@ from enum import Enum from typing import Mapping, Tuple, TypeVar +from .common.enums import AwokenSkills from .models.enum_types import Attribute, MonsterType K = TypeVar('K') @@ -87,88 +88,6 @@ class EvoTypes(Enum): EvoTypes.NONPIXEL: ('nonpixel', 'np'), } - -class AwokenSkills(Enum): - ENHANCEDHP = 1 - ENHANCEDATK = 2 - ENHANCEDRCV = 3 - REDUCERED = 4 - REDUCEBLUE = 5 - REDUCEGREEN = 6 - REDUCELIGHT = 7 - REDUCEDARK = 8 - AUTOHEAL = 9 - BINDRES = 10 - BLINDRES = 11 - JAMMERRES = 12 - POISONRES = 13 - ENHANCEDRED = 14 - ENHANCEDBLUE = 15 - ENHANCEDGREEN = 16 - ENHANCEDLIGHT = 17 - ENHANCEDDARK = 18 - EXTMOVE = 19 - BINDRECOVERY = 20 - SKILLBOOST = 21 - REDROW = 22 - BLUEROW = 23 - GREENROW = 24 - LIGHTROW = 25 - DARKROW = 26 - TPA = 27 - SKILLBINDRES = 28 - ENHANCEDHEAL = 29 - MULTIBOOST = 30 - DRAGONKILLER = 31 - GODKILLER = 32 - DEVILKILLER = 33 - MACHINEKILLER = 34 - BALANCEDKILLER = 35 - ATTACKERKILLER = 36 - PHYSICALKILLER = 37 - HEALERKILLER = 38 - EVOMATKILLER = 39 - AWOKENKILLER = 40 - FODDERKILLER = 41 - REDEEMKILLER = 42 - ENHCOMBO7C = 43 - GUARDBREAK = 44 - FUA = 45 - ENHTEAMHP = 46 - ENHTEAMRCV = 47 - VDP = 48 - EQUIP = 49 - SUPERFUA = 50 - SKILLCHARGE = 51 - UNBINDABLE = 52 - EXTMOVEPLUS = 53 - CLOUDRESIST = 54 - TAPERESIST = 55 - SKILLBOOSTPLUS = 56 - HP80ORMORE = 57 - HP50ORLESS = 58 - ELSHIELD = 59 - ELATTACK = 60 - ENHCOMBO10C = 61 - COMBOORB = 62 - VOICE = 63 - DUNGEONBONUS = 64 - REDUCEDHP = 65 - REDUCEDATK = 66 - REDUCEDRCV = 67 - UNBLINDABLE = 68 - UNJAMMABLE = 69 - UNPOISONABLE = 70 - JAMMERBLESSING = 71 - POISONBLESSING = 72 - REDCOMBOCOUNT = 73 - BLUECOMBOCOUNT = 74 - GREENCOMBOCOUNT = 75 - LIGHTCOMBOCOUNT = 76 - DARKCOMBOCOUNT = 77 - CROSSATTACK = 78 - - AWOKEN_SKILL_MAP = { AwokenSkills.ENHANCEDHP: ('hp+', 'hp'), AwokenSkills.ENHANCEDATK: ('atk+', 'atk'), From 50cabd56be6ad3a56ce58b886c2ca9d6f3259c5a Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Mon, 31 May 2021 21:56:20 -0400 Subject: [PATCH 0802/1220] Fix evolist getting server_priority (#1150) --- padinfo/view/monster_list/static_monster_list.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index 9813f8d7..5e084fa6 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -14,7 +14,8 @@ def serialize(self): ret = super().serialize() ret.update({ 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], - 'resolved_monster_server': self.paginated_monsters[0].server_priority if self.paginated_monsters else "COMBINED", + 'resolved_monster_server': (self.paginated_monsters[0][0].server_priority + if self.paginated_monsters else "COMBINED"), }) return ret From a7766eecc45604690dc89bfd684a8f885ed57d32 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Jun 2021 10:02:31 -0700 Subject: [PATCH 0803/1220] Make single quotes work for exact matching (#1155) Resolves #1071 --- dadguide/find_monster.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 52aa4d4d..3a868cd5 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -310,6 +310,7 @@ async def find_monster_debug(self, query: str) -> \ await self.dgcog.wait_until_ready() query = rmdiacritics(query).lower().replace(",", "") + query = re.sub(r'(\s|^)\'(\S+)\'(\s|$)', r'\1"\2"\3', query) # Replace ' with " around tokens tokenized_query = self._process_settings(query).split() mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) From 9f909cc4626102cad12108e0fa315c384a532090 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Jun 2021 10:03:31 -0700 Subject: [PATCH 0804/1220] Change notnaonly to allservers (#1153) Closes #1151 --- dadguide/find_monster.py | 2 +- padinfo/padinfo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 3a868cd5..2dd20514 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -51,7 +51,7 @@ def _process_settings(self, original_query: str) -> str: for setting in settings: if setting == '--na': self.index = self.dgcog.indexes['NA'] - if setting == '--notnaonly': + if setting == '--allservers': self.index = self.dgcog.indexes['COMBINED'] else: self.index = self.dgcog.indexes[self.flags['server']] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 33dd4aa7..3a006618 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -919,7 +919,7 @@ async def server(self, ctx, server: str): """Change the server to be used for your `[p]id` queries `[p]idset server default`: Include both NA & JP cards. You can still use `--na` as needed. - `[p]idset server na`: Include only NA cards. You can still use `--notnaonly` as needed. + `[p]idset server na`: Include only NA cards. You can still use `--allservers` as needed. """ dgcog = await self.get_dgcog() async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: From 446aab97612256f242f1b638a123bdbe43be96ee Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Jun 2021 10:06:16 -0700 Subject: [PATCH 0805/1220] Fix --na setting (#1154) Co-authored-by: River <18037011+RheingoldRiver@users.noreply.github.com> --- dadguide/find_monster.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 2dd20514..8f2e2473 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -46,15 +46,13 @@ def __init__(self, dgcog, flags: Mapping[str, Any]): self.index = self.dgcog.indexes[flags['server']] def _process_settings(self, original_query: str) -> str: - settings = re.findall(r'--\w+(?:{.+?})?', original_query) + settings = re.findall(r'--(\w+)(?::{(.+?)})?', original_query) - for setting in settings: - if setting == '--na': + for setting, data in settings: + if setting == 'na': self.index = self.dgcog.indexes['NA'] if setting == '--allservers': self.index = self.dgcog.indexes['COMBINED'] - else: - self.index = self.dgcog.indexes[self.flags['server']] return re.sub(r'\s*--\w+(?:{.+?})?\s*', ' ', original_query) From d97cf0699191dba09ca07c365913fdefaaae5820 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Jun 2021 10:07:16 -0700 Subject: [PATCH 0806/1220] Move other dadguide enums into common (#1152) --- dadguide/common/enums.py | 57 ++++++++++++++++++++++++ dadguide/models/enum_types.py | 58 ------------------------- dadguide/models/monster_model.py | 5 +-- dadguide/monster_graph.py | 3 +- dadguide/token_mappings.py | 3 +- padinfo/common/emoji_map.py | 2 +- tests/dadguide/database_context_test.py | 2 +- 7 files changed, 62 insertions(+), 68 deletions(-) delete mode 100644 dadguide/models/enum_types.py diff --git a/dadguide/common/enums.py b/dadguide/common/enums.py index 8639f94c..dfd5fb55 100644 --- a/dadguide/common/enums.py +++ b/dadguide/common/enums.py @@ -6,6 +6,56 @@ Server = Literal["COMBINED", "NA"] +class Attribute(Enum): + """Standard 5 PAD colors in enum form. Values correspond to Gungho values.""" + Fire = 0 + Water = 1 + Wood = 2 + Light = 3 + Dark = 4 + Unknown = 5 + Nil = 6 + + +class MonsterType(Enum): + Evolve = 0 + Balanced = 1 + Physical = 2 + Healer = 3 + Dragon = 4 + God = 5 + Attacker = 6 + Devil = 7 + Machine = 8 + Awoken = 12 + Enhance = 14 + Vendor = 15 + + +class InternalEvoType(Enum): + """The official GH evo types.""" + Base = "Base" + Normal = "Normal" + Ultimate = "Ultimate" + Reincarnated = "Reincarnated" + Assist = "Assist" + Pixel = "Pixel" + SuperReincarnated = "Super Reincarnated" + + +class Server(Enum): + JP = 0 + NA = 1 + KR = 2 + + +class AwakeningRestrictedLatent(Enum): + """Latent awakenings with availability gated by having an awakening""" + UnmatchableClear = 606 + SpinnerClear = 607 + AbsorbPierce = 608 + + class AwokenSkills(Enum): ENHANCEDHP = 1 ENHANCEDATK = 2 @@ -85,3 +135,10 @@ class AwokenSkills(Enum): LIGHTCOMBOCOUNT = 76 DARKCOMBOCOUNT = 77 CROSSATTACK = 78 + + +def enum_or_none(enum, value, default=None): + if value is not None: + return enum(value) + else: + return default diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py deleted file mode 100644 index 525e773c..00000000 --- a/dadguide/models/enum_types.py +++ /dev/null @@ -1,58 +0,0 @@ -from enum import Enum - - -class Attribute(Enum): - """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" - Fire = 0 - Water = 1 - Wood = 2 - Light = 3 - Dark = 4 - Unknown = 5 - Nil = 6 - - -class MonsterType(Enum): - Evolve = 0 - Balanced = 1 - Physical = 2 - Healer = 3 - Dragon = 4 - God = 5 - Attacker = 6 - Devil = 7 - Machine = 8 - Awoken = 12 - Enhance = 14 - Vendor = 15 - - -class InternalEvoType(Enum): - """Evo types unsupported by DadGuide.""" - Base = "Base" - Normal = "Normal" - Ultimate = "Ultimate" - Reincarnated = "Reincarnated" - Assist = "Assist" - Pixel = "Pixel" - SuperReincarnated = "Super Reincarnated" - - -class Server(Enum): - JP = 0 - NA = 1 - KR = 2 - - -class AwakeningRestrictedLatent(Enum): - """Latent awakenings with availability gated by having an awakening""" - UnmatchableClear = 606 - SpinnerClear = 607 - AbsorbPierce = 608 - - -def enum_or_none(enum, value, default=None): - if value is not None: - return enum(value) - else: - return default diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 9d3ec75e..e367103b 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -1,8 +1,5 @@ from .base_model import BaseModel -from .enum_types import Attribute -from .enum_types import MonsterType -from .enum_types import AwakeningRestrictedLatent -from .enum_types import enum_or_none +from ..common.enums import Attribute, MonsterType, AwakeningRestrictedLatent, enum_or_none from .active_skill_model import ActiveSkillModel from .leader_skill_model import LeaderSkillModel import tsutils diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index c7247c6e..e1acc31f 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -5,13 +5,12 @@ from networkx import MultiDiGraph -from .common.enums import DEFAULT_SERVER, Server, SERVERS +from .common.enums import DEFAULT_SERVER, Server, SERVERS, InternalEvoType from .database_manager import DadguideDatabase from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel from .models.base_model import BaseModel -from .models.enum_types import InternalEvoType from .models.evolution_model import EvolutionModel from .models.leader_skill_model import LeaderSkillModel from .models.monster_model import MonsterModel diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 2726175c..71520dda 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -2,8 +2,7 @@ from enum import Enum from typing import Mapping, Tuple, TypeVar -from .common.enums import AwokenSkills -from .models.enum_types import Attribute, MonsterType +from .common.enums import AwokenSkills, Attribute, MonsterType K = TypeVar('K') V = TypeVar('V') diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index c6c2cd77..1457ddbf 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -3,7 +3,7 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel - from dadguide.models.enum_types import AwakeningRestrictedLatent, MonsterType + from dadguide.common.enums import MonsterType, AwakeningRestrictedLatent from discordmenu.emoji.emoji_cache import emoji_cache diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index 53fd495f..7f4b74e8 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -1,7 +1,7 @@ from dadguide.database_manager import DadguideDatabase from dadguide.database_context import DbContext from dadguide.monster_graph import MonsterGraph -from dadguide.models.enum_types import InternalEvoType +from dadguide.common.enums import InternalEvoType database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) From b84dada8b1000f347a6a8304d51161740a8d5a12 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:10:00 -0500 Subject: [PATCH 0807/1220] Revert "Move other dadguide enums into common" (#1156) This reverts commit d97cf0699191dba09ca07c365913fdefaaae5820. --- dadguide/common/enums.py | 57 ------------------------ dadguide/models/enum_types.py | 58 +++++++++++++++++++++++++ dadguide/models/monster_model.py | 5 ++- dadguide/monster_graph.py | 3 +- dadguide/token_mappings.py | 3 +- padinfo/common/emoji_map.py | 2 +- tests/dadguide/database_context_test.py | 2 +- 7 files changed, 68 insertions(+), 62 deletions(-) create mode 100644 dadguide/models/enum_types.py diff --git a/dadguide/common/enums.py b/dadguide/common/enums.py index dfd5fb55..8639f94c 100644 --- a/dadguide/common/enums.py +++ b/dadguide/common/enums.py @@ -6,56 +6,6 @@ Server = Literal["COMBINED", "NA"] -class Attribute(Enum): - """Standard 5 PAD colors in enum form. Values correspond to Gungho values.""" - Fire = 0 - Water = 1 - Wood = 2 - Light = 3 - Dark = 4 - Unknown = 5 - Nil = 6 - - -class MonsterType(Enum): - Evolve = 0 - Balanced = 1 - Physical = 2 - Healer = 3 - Dragon = 4 - God = 5 - Attacker = 6 - Devil = 7 - Machine = 8 - Awoken = 12 - Enhance = 14 - Vendor = 15 - - -class InternalEvoType(Enum): - """The official GH evo types.""" - Base = "Base" - Normal = "Normal" - Ultimate = "Ultimate" - Reincarnated = "Reincarnated" - Assist = "Assist" - Pixel = "Pixel" - SuperReincarnated = "Super Reincarnated" - - -class Server(Enum): - JP = 0 - NA = 1 - KR = 2 - - -class AwakeningRestrictedLatent(Enum): - """Latent awakenings with availability gated by having an awakening""" - UnmatchableClear = 606 - SpinnerClear = 607 - AbsorbPierce = 608 - - class AwokenSkills(Enum): ENHANCEDHP = 1 ENHANCEDATK = 2 @@ -135,10 +85,3 @@ class AwokenSkills(Enum): LIGHTCOMBOCOUNT = 76 DARKCOMBOCOUNT = 77 CROSSATTACK = 78 - - -def enum_or_none(enum, value, default=None): - if value is not None: - return enum(value) - else: - return default diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py new file mode 100644 index 00000000..525e773c --- /dev/null +++ b/dadguide/models/enum_types.py @@ -0,0 +1,58 @@ +from enum import Enum + + +class Attribute(Enum): + """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" + Fire = 0 + Water = 1 + Wood = 2 + Light = 3 + Dark = 4 + Unknown = 5 + Nil = 6 + + +class MonsterType(Enum): + Evolve = 0 + Balanced = 1 + Physical = 2 + Healer = 3 + Dragon = 4 + God = 5 + Attacker = 6 + Devil = 7 + Machine = 8 + Awoken = 12 + Enhance = 14 + Vendor = 15 + + +class InternalEvoType(Enum): + """Evo types unsupported by DadGuide.""" + Base = "Base" + Normal = "Normal" + Ultimate = "Ultimate" + Reincarnated = "Reincarnated" + Assist = "Assist" + Pixel = "Pixel" + SuperReincarnated = "Super Reincarnated" + + +class Server(Enum): + JP = 0 + NA = 1 + KR = 2 + + +class AwakeningRestrictedLatent(Enum): + """Latent awakenings with availability gated by having an awakening""" + UnmatchableClear = 606 + SpinnerClear = 607 + AbsorbPierce = 608 + + +def enum_or_none(enum, value, default=None): + if value is not None: + return enum(value) + else: + return default diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index e367103b..9d3ec75e 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -1,5 +1,8 @@ from .base_model import BaseModel -from ..common.enums import Attribute, MonsterType, AwakeningRestrictedLatent, enum_or_none +from .enum_types import Attribute +from .enum_types import MonsterType +from .enum_types import AwakeningRestrictedLatent +from .enum_types import enum_or_none from .active_skill_model import ActiveSkillModel from .leader_skill_model import LeaderSkillModel import tsutils diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index e1acc31f..c7247c6e 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -5,12 +5,13 @@ from networkx import MultiDiGraph -from .common.enums import DEFAULT_SERVER, Server, SERVERS, InternalEvoType +from .common.enums import DEFAULT_SERVER, Server, SERVERS from .database_manager import DadguideDatabase from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel from .models.base_model import BaseModel +from .models.enum_types import InternalEvoType from .models.evolution_model import EvolutionModel from .models.leader_skill_model import LeaderSkillModel from .models.monster_model import MonsterModel diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 71520dda..2726175c 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -2,7 +2,8 @@ from enum import Enum from typing import Mapping, Tuple, TypeVar -from .common.enums import AwokenSkills, Attribute, MonsterType +from .common.enums import AwokenSkills +from .models.enum_types import Attribute, MonsterType K = TypeVar('K') V = TypeVar('V') diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 1457ddbf..c6c2cd77 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -3,7 +3,7 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel - from dadguide.common.enums import MonsterType, AwakeningRestrictedLatent + from dadguide.models.enum_types import AwakeningRestrictedLatent, MonsterType from discordmenu.emoji.emoji_cache import emoji_cache diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index 7f4b74e8..53fd495f 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -1,7 +1,7 @@ from dadguide.database_manager import DadguideDatabase from dadguide.database_context import DbContext from dadguide.monster_graph import MonsterGraph -from dadguide.common.enums import InternalEvoType +from dadguide.models.enum_types import InternalEvoType database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) From c1e8e79125d7f6047e69043afb85477d15783dbc Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Jun 2021 12:40:43 -0700 Subject: [PATCH 0808/1220] allow em dash to specify settings (#1158) Also fixes allservers setting --- dadguide/find_monster.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 8f2e2473..fe0faaf9 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -46,15 +46,15 @@ def __init__(self, dgcog, flags: Mapping[str, Any]): self.index = self.dgcog.indexes[flags['server']] def _process_settings(self, original_query: str) -> str: - settings = re.findall(r'--(\w+)(?::{(.+?)})?', original_query) + settings = re.findall(r'(?:--|—)(\w+)(?::{(.+?)})?', original_query) for setting, data in settings: if setting == 'na': self.index = self.dgcog.indexes['NA'] - if setting == '--allservers': + if setting == 'allservers': self.index = self.dgcog.indexes['COMBINED'] - return re.sub(r'\s*--\w+(?:{.+?})?\s*', ' ', original_query) + return re.sub(r'\s*(--|—)\w+(:{.+?})?\s*', ' ', original_query) def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: result = [] From f1bf3312eace89f3bd019ff757152bbf1c76b166 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 2 Jun 2021 00:34:05 -0700 Subject: [PATCH 0809/1220] Fix seriesscroll (#1161) --- padinfo/view/series_scroll.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index efb116d1..72f1b0d5 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -48,7 +48,7 @@ def serialize(self): ret.update({ 'pane_type': SeriesScrollView.VIEW_TYPE, 'series_id': self.series_id, - 'server': self.monster_list[0].monster_id if self.monster_list else "COMBINED", + 'server': self.monster_list[0].series_priority if self.monster_list else "COMBINED", 'current_page': self.current_page, 'pages_in_rarity': self.pages_in_rarity, 'title': self.title, From 7fa4723d143749f22ec7a9e5740813d0d2d9bb9d Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 2 Jun 2021 00:35:41 -0700 Subject: [PATCH 0810/1220] series_priority -> server_priority (#1162) --- padinfo/view/series_scroll.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 72f1b0d5..464b6459 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -48,7 +48,7 @@ def serialize(self): ret.update({ 'pane_type': SeriesScrollView.VIEW_TYPE, 'series_id': self.series_id, - 'server': self.monster_list[0].series_priority if self.monster_list else "COMBINED", + 'server': self.monster_list[0].server_priority if self.monster_list else "COMBINED", 'current_page': self.current_page, 'pages_in_rarity': self.pages_in_rarity, 'title': self.title, From 521df68755746807aa37453ccef201db51791e20 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 2 Jun 2021 03:44:11 -0700 Subject: [PATCH 0811/1220] Implement QuerySettings (#1164) --- dadguide/common/enums.py | 7 +- dadguide/dadguide.py | 7 +- dadguide/database_context.py | 6 +- dadguide/find_monster.py | 17 ++--- dadguide/idtest_mixin.py | 17 +++-- dadguide/models/monster_model.py | 5 +- dadguide/monster_graph.py | 15 ++-- dadguide/monster_index.py | 3 +- padinfo/menu/id.py | 10 ++- padinfo/menu/monster_list.py | 4 +- padinfo/menu/series_scroll.py | 5 +- padinfo/padinfo.py | 76 ++++++++++++------- padinfo/view/common.py | 4 +- padinfo/view/components/monster/header.py | 15 ++-- padinfo/view/components/view_state_base_id.py | 14 ++-- padinfo/view/evos.py | 12 +-- padinfo/view/id.py | 12 +-- padinfo/view/materials.py | 12 +-- padinfo/view/monster_list/id_search.py | 4 +- padinfo/view/monster_list/monster_list.py | 11 ++- .../view/monster_list/static_monster_list.py | 9 ++- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 12 +-- padinfo/view/pic.py | 2 +- padinfo/view/series_scroll.py | 15 ++-- padinfo/view/transforminfo.py | 21 +++-- tests/dadguide/database_context_test.py | 8 +- 27 files changed, 192 insertions(+), 133 deletions(-) diff --git a/dadguide/common/enums.py b/dadguide/common/enums.py index 8639f94c..5b1925a4 100644 --- a/dadguide/common/enums.py +++ b/dadguide/common/enums.py @@ -1,9 +1,8 @@ from enum import Enum -from typing import Literal +from tsutils.enums import Server -DEFAULT_SERVER = "COMBINED" -SERVERS = ["NA", "COMBINED"] -Server = Literal["COMBINED", "NA"] +DEFAULT_SERVER = Server.COMBINED +SERVERS = [Server.NA, Server.COMBINED] class AwokenSkills(Enum): diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 23d4b3aa..f610207d 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -19,6 +19,7 @@ from redbot.core import checks, data_manager, commands, Config from redbot.core.utils.chat_formatting import pagify, box from tsutils import auth_check, safe_read_json +from tsutils.enums import Server from . import token_mappings from .find_monster import FindMonster @@ -26,7 +27,7 @@ from .idtest_mixin import IdTest from .models.monster_model import MonsterModel from .models.monster_stats import monster_stats, MonsterStatModifierInput -from .common.enums import DEFAULT_SERVER, SERVERS, Server +from .common.enums import DEFAULT_SERVER, SERVERS from .monster_index import MonsterIndex logger = logging.getLogger('red.padbot-cogs.dadguide') @@ -68,7 +69,7 @@ def __init__(self, bot, *args, **kwargs): self._is_ready = asyncio.Event() self.database = None - self.indexes = {} # type: Dict[str, MonsterIndex] + self.indexes = {} # type: Dict[Server, MonsterIndex] self.fir_lock = asyncio.Lock() self.fir3_lock = asyncio.Lock() @@ -150,7 +151,7 @@ async def check_index(self): if not await self.config.indexlog(): return - index = self.indexes["COMBINED"] + index = self.indexes[Server.COMBINED] index.issues.extend((await self.run_tests())[:25]) channel = self.bot.get_channel(await self.config.indexlog()) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 7784133a..49238195 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -1,12 +1,14 @@ from typing import Generator, List +from tsutils.enums import Server + from .database_manager import DadguideDatabase from .models.awoken_skill_model import AwokenSkillModel from .models.dungeon_model import DungeonModel from .models.scheduled_event_model import ScheduledEventModel from .models.series_model import SeriesModel from .monster_graph import MonsterGraph -from .common.enums import DEFAULT_SERVER, Server +from .common.enums import DEFAULT_SERVER SCHEDULED_EVENT_QUERY = """SELECT schedule.*, @@ -41,7 +43,7 @@ def get_monsters_by_active(self, active_skill_id: int, *, server: Server): return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id, server=server) def get_all_monster_ids_query(self, server: Server): - table = 'monsters_na' if server == "NA" else 'monsters' + table = 'monsters_na' if server == Server.NA else 'monsters' query = self.database.query_many( self.database.select_builder(tables={table: ('monster_id',)}), (), as_generator=True) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index fe0faaf9..8d008759 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -1,14 +1,15 @@ import json import re from collections import defaultdict -from typing import Set, List, Tuple, Optional, Mapping, Union, Iterable, Any +from typing import Set, List, Tuple, Optional, Mapping, Union, Iterable, Any, Dict from Levenshtein import jaro_winkler from tsutils import rmdiacritics +from tsutils.query_settings import QuerySettings +from tsutils.enums import Server from dadguide.find_monster_tokens import Token, SPECIAL_TOKEN_TYPES from dadguide.models.monster_model import MonsterModel -from dadguide.common.enums import DEFAULT_SERVER SERIES_TYPE_PRIORITY = { "regular": 4, @@ -40,19 +41,15 @@ class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 - def __init__(self, dgcog, flags: Mapping[str, Any]): + def __init__(self, dgcog, flags: Dict[str, Any]): self.dgcog = dgcog self.flags = flags - self.index = self.dgcog.indexes[flags['server']] + self.index = self.dgcog.indexes[Server(flags['server'])] def _process_settings(self, original_query: str) -> str: - settings = re.findall(r'(?:--|—)(\w+)(?::{(.+?)})?', original_query) + query_settings = QuerySettings.extract(self.flags, original_query) - for setting, data in settings: - if setting == 'na': - self.index = self.dgcog.indexes['NA'] - if setting == 'allservers': - self.index = self.dgcog.indexes['COMBINED'] + self.index = self.dgcog.indexes[query_settings.server] return re.sub(r'\s*(--|—)\w+(:{.+?})?\s*', ' ', original_query) diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index 6ae2a456..22f50f19 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -3,16 +3,17 @@ from typing import List, Dict, Optional import tsutils +from tsutils.enums import Server from redbot.core import commands, Config, checks from redbot.core.utils.chat_formatting import box, pagify -from dadguide.common.enums import DEFAULT_SERVER, Server +from dadguide.common.enums import DEFAULT_SERVER from dadguide.monster_index import MonsterIndex class IdTest: bot = None - indexes: Dict[str, MonsterIndex] + indexes: Dict[Server, MonsterIndex] find_monster = None def __init__(self): @@ -419,9 +420,9 @@ async def norf_run(self, ctx): async with ctx.typing(): for i, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff'], v['server']))): fluff = case['id'] in [m.monster_id for m in - self.indexes[case['server']].fluff_tokens[case['token']]] + self.indexes[Server(case['server'])].fluff_tokens[case['token']]] name = case['id'] in [m.monster_id for m in - self.indexes[case['server']].name_tokens[case['token']]] + self.indexes[Server(case['server'])].name_tokens[case['token']]] if (case['fluff'] and not fluff) or (not case['fluff'] and not name): q = '"{}"'.format(case['token']) @@ -466,8 +467,8 @@ async def idt_runall(self, ctx): fc = 0 async with ctx.typing(): for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in self.indexes[v['server']].fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.indexes[v['server']].name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) @@ -512,8 +513,8 @@ async def run_tests(self) -> List[str]: fsuite = await self.config.fluff_suite() for c, v in enumerate(fsuite): - fluff = v['id'] in [m.monster_id for m in self.indexes[v['server']].fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.indexes[v['server']].name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 9d3ec75e..1238de98 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -1,3 +1,5 @@ +from tsutils.enums import Server + from .base_model import BaseModel from .enum_types import Attribute from .enum_types import MonsterType @@ -171,13 +173,12 @@ def get_difference(self, other): return MonsterDifference.from_monsters(self, other) def __repr__(self): - server_prefix = self.server_priority + " " if self.server_priority != "COMBINED" else "" + server_prefix = self.server_priority + " " if self.server_priority != Server.COMBINED else "" return "Monster<{}{} ({})>".format(server_prefix, self.name_en, self.monster_id) class MonsterSearchHelper: def __init__(self, m: MonsterModel): - self.name = '{} {}'.format(m.name_en, m.name_ja).lower() leader_skill = m.leader_skill self.leader = leader_skill.desc.lower() if leader_skill else '' diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index c7247c6e..ba3efbe3 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -4,8 +4,9 @@ from typing import Optional, List, Set, Dict from networkx import MultiDiGraph +from tsutils.enums import Server -from .common.enums import DEFAULT_SERVER, Server, SERVERS +from .common.enums import DEFAULT_SERVER, SERVERS from .database_manager import DadguideDatabase from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel @@ -105,7 +106,7 @@ exchanges """ -SPECIFY_SERVER = " WHERE server_id = {}" +SERVER_ID_WHERE_CONDITION = " WHERE server_id = {}" class MonsterGraph(object): @@ -113,8 +114,8 @@ def __init__(self, database: DadguideDatabase): self.database = database self.max_monster_id = -1 self.graph_dict: Dict[Server, MultiDiGraph] = { # noqa - "COMBINED": self.build_graph("COMBINED"), - "NA": self.build_graph("NA"), + Server.COMBINED: self.build_graph(Server.COMBINED), + Server.NA: self.build_graph(Server.NA), } self._cache_graphs() @@ -124,9 +125,9 @@ def build_graph(self, server: Server) -> MultiDiGraph: table_suffix = "" where = "" - if server != "COMBINED": - table_suffix = "_" + server.lower() - where = SPECIFY_SERVER.format(["JP", "NA", "KR"].index(server)) + if server != Server.COMBINED: + table_suffix = "_" + server.value.lower() + where = SERVER_ID_WHERE_CONDITION.format(["JP", "NA", "KR"].index(server.value)) ms = self.database.query_many(MONSTER_QUERY.format(table_suffix), ()) es = self.database.query_many(EVOS_QUERY.format(table_suffix), ()) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 75e492dc..be889bae 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -7,10 +7,11 @@ import aiohttp import tsutils from redbot.core.utils import AsyncIter +from tsutils.enums import Server from .models.monster_model import MonsterModel from .monster_graph import MonsterGraph -from .common.enums import DEFAULT_SERVER, Server +from .common.enums import DEFAULT_SERVER from .token_mappings import * SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY' \ diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index b8661734..a68e6cf9 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -6,6 +6,8 @@ from tsutils import char_to_emoji from tsutils.menu.panes import MenuPanes +from tsutils.query_settings import QuerySettings + from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.evos import EvosView, EvosViewState from padinfo.view.id import IdView, IdViewState @@ -35,7 +37,9 @@ def menu(initial_control=None): async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] db_context: "DbContext" = dgcog.database - m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=ims['resolved_monster_server']) + query_settings = QuerySettings.deserialize(ims.get('query_settings')) + + m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=query_settings.server) use_evo_scroll = ims.get('use_evo_scroll') != 'False' new_monster_id = IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll) @@ -62,7 +66,9 @@ def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_ev async def respond_with_right(message: Optional[Message], ims, **data): dgcog = data['dgcog'] db_context: "DbContext" = dgcog.database - m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=ims['resolved_monster_server']) + query_settings = QuerySettings.deserialize(ims.get('query_settings')) + + m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=query_settings.server) use_evo_scroll = ims.get('use_evo_scroll') != 'False' new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll) or '') diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index cd0bd367..84ae39ca 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -237,7 +237,7 @@ async def click_child_number(ims, emoji_clicked, **data): 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster_list[n - MonsterListMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, - 'resolved_monster_server': ims['resolved_monster_server'], + 'query_settings': ims['query_settings'], } return emoji_response, extra_ims @@ -263,7 +263,7 @@ async def _scroll_child(ims, update_fn, **data): 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster.monster_id, - 'resolved_monster_server': monster.server_priority, + 'query_settings': ims['query_settings'], } return IdMenuEmoji.refresh, extra_ims diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 23ad5470..8db23ae6 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -237,8 +237,7 @@ async def click_child_number(ims, emoji_clicked, **data): 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster_list[n - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, - 'resolved_monster_server': - monster_list[n - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT].server_priority, + 'query_settings': ims['query_settings'] } return emoji_response, extra_ims @@ -264,7 +263,7 @@ async def _scroll_child(ims, update_fn, **data): 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, 'resolved_monster_id': monster_list[copy_ims['current_index']].monster_id, - 'resolved_monster_server': monster_list[copy_ims['current_index']].server_priority, + 'query_settings': ims['query_settings'] } return IdMenuEmoji.refresh, extra_ims diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3a006618..f8b3c0ef 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -5,16 +5,19 @@ import re import urllib.parse from io import BytesIO -from typing import TYPE_CHECKING, List, Optional, Literal +from typing import TYPE_CHECKING, List, Optional, Type import discord import tsutils from discord import Color from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import checks, commands, data_manager, Config +from redbot.core.commands import Literal as LiteralConverter from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate from tsutils import char_to_emoji, is_donor, safe_read_json +from tsutils.enums import Server +from tsutils.query_settings import QuerySettings from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ @@ -48,6 +51,7 @@ from padinfo.view.materials import MaterialsViewState from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState +from padinfo.view.monster_list.monster_list import MonsterListViewState from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState from padinfo.view.otherinfo import OtherInfoViewState from padinfo.view.pantheon import PantheonViewState @@ -211,20 +215,20 @@ async def _do_id(self, ctx, query: str): original_author_id = ctx.message.author.id goodquery = None - if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.indexes["COMBINED"].all_name_tokens: + if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes["COMBINED"].all_name_tokens: + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: bad = False - for monster in dgcog.indexes["COMBINED"].all_name_tokens[goodquery[1]]: - for modifier in dgcog.indexes["COMBINED"].modifiers[monster]: + for monster in dgcog.indexes[Server.COMBINED].all_name_tokens[goodquery[1]]: + for modifier in dgcog.indexes[Server.COMBINED].modifiers[monster]: if modifier == 'xm' and goodquery[0] == 'x': goodquery[0] = 'xm' if modifier == goodquery[0]: bad = True - if bad and query not in dgcog.indexes["COMBINED"].all_name_tokens: + if bad and query not in dgcog.indexes[Server.COMBINED].all_name_tokens: async def send_message(): await asyncio.sleep(1) await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" @@ -259,10 +263,11 @@ async def send_message(): await IdViewState.query(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, transform_base, true_evo_type_raw, acquire_raw, base_rarity, use_evo_scroll=settings.checkEvoID(ctx.author.id), reaction_list=initial_reaction_list) menu = IdMenu.menu() @@ -345,7 +350,8 @@ async def evos(self, ctx, *, query: str): alt_monsters = EvosViewState.get_alt_monsters_and_evos(dgcog, monster) alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) if alt_versions is None: await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, @@ -356,7 +362,7 @@ async def evos(self, ctx, *, query: str): initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, alt_versions, gem_versions, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) @@ -387,12 +393,13 @@ async def evomats(self, ctx, *, query: str): return alt_monsters = MaterialsViewState.get_alt_monsters_and_evos(dgcog, monster) - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) @@ -422,12 +429,13 @@ async def pantheon(self, ctx, *, query: str): + ' [{}] {}.'.format(monster.monster_id, monster.name_en)) return alt_monsters = PantheonViewState.get_alt_monsters_and_evos(dgcog, monster) - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, pantheon_list, series_name, base_monster, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) @@ -452,12 +460,13 @@ async def pic(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.pic_control) @@ -481,12 +490,13 @@ async def otherinfo(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, reaction_list=initial_reaction_list, use_evo_scroll=settings.checkEvoID(ctx.author.id)) menu = IdMenu.menu(initial_control=IdMenu.otherinfo_control) @@ -582,15 +592,16 @@ async def evolist(self, ctx, *, query): await self._do_monster_list(ctx, dgcog, query, monster_list, 'Evolution List', StaticMonsterListViewState) async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], - title, view_state_type): + title, view_state_type: Type[MonsterListViewState]): raw_query = query original_author_id = ctx.message.author.id color = await self.get_user_embed_color(ctx) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' state = view_state_type(original_author_id, view_state_type.VIEW_STATE_TYPE, query, color, - monster_list, + monster_list, query_settings, title, instruction_message, reaction_list=initial_reaction_list ) @@ -631,6 +642,7 @@ async def collabscroll(self, ctx, *, query): series_object: "SeriesModel" = monster.series title = series_object.name_en paginated_monsters = None + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) rarity = None for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: paginated_monsters = await SeriesScrollViewState.query(dgcog, monster.series_id, @@ -647,6 +659,7 @@ async def collabscroll(self, ctx, *, query): state = SeriesScrollViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, query, color, series_id, 0, paginated_monsters[0], int(rarity), len(paginated_monsters), + query_settings, all_rarities, title, instruction_message, reaction_list=initial_reaction_list) @@ -758,10 +771,12 @@ async def transforminfo(self, ctx, *, query): original_author_id = ctx.message.author.id acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) reaction_list = TransformInfoMenuPanes.get_reaction_list(len(monster_ids)) - discrep = dgcog.database.graph.monster_is_discrepant(base_mon) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, - color, base_mon, transformed_mon, acquire_raw, monster_ids, discrep, + color, base_mon, transformed_mon, acquire_raw, monster_ids, is_jp_buffed, + query_settings, reaction_list=reaction_list) menu = TransformInfoMenu.menu() await menu.create(ctx, state) @@ -911,7 +926,8 @@ async def embedcolor(self, ctx, *, color): async def naprio(self, ctx, value: bool): """Change whether [p]id will default away from new evos of monsters that aren't in NA yet""" async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: - fm_flags['na_prio'] = value + # The current na_prio enum has 0 and 1 instead of False and True as its values + fm_flags['na_prio'] = int(value) await ctx.send(f"NA monster prioritization has been **{'en' if value else 'dis'}abled**.") @idset.command() @@ -925,10 +941,10 @@ async def server(self, ctx, server: str): async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: if server.upper() == "DEFAULT": fm_flags['server'] = dgcog.DEFAULT_SERVER - elif server.upper() in dgcog.SERVERS: + elif server.upper() in [s.value for s in dgcog.SERVERS]: fm_flags['server'] = server.upper() else: - if dgcog.DEFAULT_SERVER == "COMBINED": + if dgcog.DEFAULT_SERVER == Server.COMBINED: await ctx.send("Server must be `default` or `na`") else: await ctx.send("Server must be `na` or `combined`") @@ -993,7 +1009,7 @@ async def send_id_failure_message(ctx, query: str): "help with querying a specific monster.".format(inline(query), ctx, IDGUIDE)) @commands.command(aliases=["iddebug", "dbid", "iddb"]) - async def debugid(self, ctx, server: Optional[Literal["COMBINED", "NA"]] = "COMBINED", *, query): + async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, query): """Get helpful id information about a monster""" dgcog = self.bot.get_cog("Dadguide") mon = await dgcog.find_monster(query, ctx.author.id) @@ -1054,7 +1070,8 @@ async def idhelp(self, ctx, *, query=""): await self.debugid(ctx, query=query) @commands.command() - async def exportmodifiers(self, ctx, server: Optional[Literal["COMBINED", "NA"]] = "COMBINED"): + async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] = "COMBINED"): + server = Server(server) DGCOG = self.bot.get_cog("Dadguide") maps = DGCOG.token_maps awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} @@ -1091,7 +1108,7 @@ async def exportmodifiers(self, ctx, server: Optional[Literal["COMBINED", "NA"]] await ctx.send(file=text_to_file(ret, filename="table.md")) @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod", "luid", "idlu"]) - async def idmeaning(self, ctx, token, server: Optional[Literal["COMBINED", "NA"]] = "COMBINED"): + async def idmeaning(self, ctx, token, server: Optional[Server] = Server.COMBINED): """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") DGCOG = self.bot.get_cog("Dadguide") @@ -1248,3 +1265,6 @@ async def idsearch(self, ctx, *, query): return await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results', IdSearchViewState) + + async def get_fm_flags(self, user: discord.User): + return await self.bot.get_cog("Dadguide").config.user(user).fm_flags() diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 66036889..cf449818 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -2,6 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import Text, BoldText +from tsutils.query_settings import QuerySettings from padinfo.common.emoji_map import get_awakening_emoji @@ -11,11 +12,12 @@ async def get_monster_from_ims(dgcog, ims: dict): query = ims.get('query') or ims['raw_query'] + query_settings = QuerySettings.deserialize(ims.get('query_settings')) resolved_monster_id_str = ims.get('resolved_monster_id') resolved_monster_id = int(resolved_monster_id_str or 0) if resolved_monster_id: - return dgcog.database.graph.get_monster(resolved_monster_id, server=ims.get('resolved_monster_server')) + return dgcog.database.graph.get_monster(resolved_monster_id, server=query_settings.server) monster = await dgcog.find_monster(query, ims['original_author_id']) return monster diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 307b8bd8..3af9b59d 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -2,6 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import LinkedText, Text +from tsutils.enums import Server from padinfo.common.emoji_map import get_attribute_emoji_by_monster from padinfo.common.external_links import puzzledragonx @@ -12,13 +13,13 @@ class MonsterHeader: @classmethod - def jp_suffix(cls, m: "MonsterModel", discrepant=False, subname_on_override=True): + def jp_suffix(cls, m: "MonsterModel", is_jp_buffed=False, subname_on_override=True): suffix = "" if m.roma_subname and (subname_on_override or m.name_en_override is None): suffix += ' [{}]'.format(m.roma_subname) if not m.on_na: suffix += ' (JP only)' - if discrepant: + if is_jp_buffed: suffix += ' (JP buffed)' return suffix @@ -47,7 +48,7 @@ def long_v2(cls, m: "MonsterModel", link=False): return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) @classmethod - def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, discrepant=False): + def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed=False): """Returns long_v2 as well as an `!` if the monster is Tsubaki To celebrate 1000 issues/PRs in our main Tsubaki repo, we added this easter egg! Yay! @@ -56,13 +57,13 @@ def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, discrepant=False): m.monster_no_na, m.name_en, '!' if is_tsubaki else '', - cls.jp_suffix(m, discrepant)) + cls.jp_suffix(m, is_jp_buffed)) @classmethod - def fmt_id_header(cls, m: "MonsterModel", is_tsubaki, discrepant): + def fmt_id_header(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed): return Text('{} {}'.strip().format( - '\N{EARTH GLOBE AMERICAS}' if m.server_priority == "NA" else '', - cls.long_maybe_tsubaki(m, is_tsubaki, bool(discrepant)))) + '\N{EARTH GLOBE AMERICAS}' if m.server_priority == Server.NA else '', + cls.long_maybe_tsubaki(m, is_tsubaki, bool(is_jp_buffed)))) @classmethod def short_with_emoji(cls, m: "MonsterModel", link=True, prefix=None): diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 52167719..0b4356f3 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING, List, NamedTuple, Optional from discordmenu.embed.view_state import ViewState +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel - from dadguide.models.monster.monster_difference import MonsterDifference from dadguide.models.evolution_model import EvolutionModel @@ -18,7 +18,7 @@ class MonsterEvolution(NamedTuple): class ViewStateBaseId(ViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List[MonsterEvolution], monster_difference: "MonsterDifference", + alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): @@ -28,16 +28,17 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.reaction_list = reaction_list self.color = color self.monster = monster - self.discrepant = monster_difference + self.is_jp_buffed = is_jp_buffed self.query = query + self.query_settings = query_settings self.use_evo_scroll = use_evo_scroll def serialize(self): ret = super().serialize() ret.update({ 'query': self.query, + 'query_settings': self.query_settings.serialize(), 'resolved_monster_id': self.monster.monster_id, - 'resolved_monster_server': self.monster.server_priority, 'use_evo_scroll': str(self.use_evo_scroll), 'reaction_list': self.reaction_list, }) @@ -53,14 +54,15 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] query = ims.get('query') or raw_query + query_settings = QuerySettings.deserialize(ims.get('query_settings')) original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index af32dc7e..e0cac0c2 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -4,6 +4,7 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx @@ -19,13 +20,13 @@ class EvosViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List[MonsterEvolution], discrepant, + alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], reaction_list: List[str] = None, use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - alt_monsters, discrepant, + alt_monsters, is_jp_buffed, query_settings, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -51,14 +52,15 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query + query_settings = QuerySettings.deserialize(ims.get('query_settings')) original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, alt_versions, gem_versions, reaction_list=reaction_list, use_evo_scroll=use_evo_scroll, @@ -106,7 +108,7 @@ def embed(cls, state: EvosViewState): color=state.color, title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.discrepant).to_markdown(), + state.is_jp_buffed).to_markdown(), url=puzzledragonx(state.monster)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 42497974..32a9a352 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -5,6 +5,7 @@ from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_awakening_emoji, get_emoji @@ -33,12 +34,12 @@ def alt_fmt(monsterevo, state): class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List[MonsterEvolution], discrepant, + alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, transform_base, true_evo_type_raw, acquire_raw, base_rarity, fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, is_child: bool = False, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - alt_monsters, discrepant, + alt_monsters, is_jp_buffed, query_settings, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -71,16 +72,17 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] # This is to support the 2 vs 1 monster query difference between ^ls and ^id query = ims.get('query') or raw_query + query_settings = QuerySettings.deserialize(ims.get('query_settings')) menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = ims.get('reaction_list') fallback_message = ims.get('message') is_child = ims.get('is_child') - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, transform_base, true_evo_type_raw, acquire_raw, base_rarity, fallback_message=fallback_message, use_evo_scroll=use_evo_scroll, @@ -306,7 +308,7 @@ def embed(cls, state: IdViewState): color=state.color, title=MonsterHeader.fmt_id_header(m, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.discrepant).to_markdown(), + state.is_jp_buffed).to_markdown(), url=puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index ef681829..7dd74c5c 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -5,6 +5,7 @@ from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill @@ -24,7 +25,7 @@ class MaterialsViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters: List[MonsterEvolution], discrepant, + alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, gem_override: bool, @@ -32,7 +33,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - alt_monsters, discrepant, + alt_monsters, is_jp_buffed, query_settings, reaction_list=reaction_list, use_evo_scroll=use_evo_scroll, extra_state=extra_state) @@ -67,14 +68,15 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query + query_settings = QuerySettings.deserialize(ims.get('query_settings')) menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = ims.get('reaction_list') - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, @@ -160,7 +162,7 @@ def embed(cls, state: MaterialsViewState): color=state.color, title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.discrepant).to_markdown(), + state.is_jp_buffed).to_markdown(), url=puzzledragonx(state.monster) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py index 1d7c327d..5526e8cd 100644 --- a/padinfo/view/monster_list/id_search.py +++ b/padinfo/view/monster_list/id_search.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional from padinfo.view.monster_list.monster_list import MonsterListViewState @@ -10,7 +10,7 @@ class IdSearchViewState(MonsterListViewState): VIEW_STATE_TYPE = "IdSearch" @classmethod - async def query(cls, dgcog, query, original_author_id) -> List["MonsterModel"]: + async def query(cls, dgcog, query, original_author_id) -> Optional[List["MonsterModel"]]: found_monsters = await dgcog.find_monsters(query, original_author_id) if not found_monsters: diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 4fb7e32d..2ab029da 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -5,6 +5,8 @@ from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils import char_to_emoji, embed_footer_with_state +from tsutils.enums import Server +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.view.components.monster.header import MonsterHeader @@ -15,10 +17,11 @@ class MonsterListViewState(ViewStateBase): + VIEW_STATE_TYPE: str MAX_ITEMS_PER_PANE = 11 def __init__(self, original_author_id, menu_type, query, color, - monster_list: List["MonsterModel"], + monster_list: List["MonsterModel"], query_settings: QuerySettings, title, message, *, current_page: int = 0, @@ -41,6 +44,7 @@ def __init__(self, original_author_id, menu_type, query, color, self.reaction_list = reaction_list self.color = color self.query = query + self.query_settings = query_settings def serialize(self): ret = super().serialize() @@ -48,7 +52,7 @@ def serialize(self): 'pane_type': MonsterListView.VIEW_TYPE, 'title': self.title, 'monster_list': [m.monster_id for m in self.monster_list], - 'resolved_monster_server': self.monster_list[0].server_priority if self.monster_list else "COMBINED", + 'query_settings': self.query_settings.serialize(), 'current_page': self.current_page, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, @@ -69,13 +73,14 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] query = ims.get('query') or raw_query + query_settings = QuerySettings.deserialize(ims.get('query_settings')) original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') child_message_id = ims.get('child_message_id') message = ims.get('message') return MonsterListViewState(original_author_id, menu_type, query, user_config.color, - monster_list, + monster_list, query_settings, title, message, current_page=current_page, current_index=current_index, diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index 5e084fa6..35875b87 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -1,5 +1,8 @@ from typing import TYPE_CHECKING, List +from tsutils.enums import Server +from tsutils.query_settings import QuerySettings + from padinfo.view.monster_list.monster_list import MonsterListViewState if TYPE_CHECKING: @@ -14,8 +17,6 @@ def serialize(self): ret = super().serialize() ret.update({ 'full_monster_list': [m.monster_id for page in self.paginated_monsters for m in page], - 'resolved_monster_server': (self.paginated_monsters[0][0].server_priority - if self.paginated_monsters else "COMBINED"), }) return ret @@ -28,6 +29,6 @@ async def query(cls, dgcog, monster_list, server) -> List["MonsterModel"]: @classmethod async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: monster_ids = ims['full_monster_list'] - server = ims['resolved_monster_server'] - monster_list = await cls.query(dgcog, monster_ids, server) + query_settings = QuerySettings.deserialize(ims['query_settings']) + monster_list = await cls.query(dgcog, monster_ids, query_settings.server) return monster_list diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 101391d1..69f15709 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -54,7 +54,7 @@ def embed(cls, state: OtherInfoViewState): color=state.color, title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.discrepant).to_markdown(), + state.is_jp_buffed).to_markdown(), url=puzzledragonx(m)), embed_footer=embed_footer_with_state(state), embed_fields=[ diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 2b93ba59..628dff92 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -4,6 +4,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ @@ -26,13 +27,13 @@ class PantheonViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", - alt_monsters, discrepant, + alt_monsters, is_jp_buffed, query_settings, pantheon_list: List[MonsterEvolution], series_name: str, base_monster, use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, - alt_monsters, discrepant, + alt_monsters, is_jp_buffed, query_settings, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) @@ -60,14 +61,15 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query + query_settings = QuerySettings.deserialize(ims.get('query_settings')) original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - discrep = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, - alt_monsters, discrep, + alt_monsters, is_jp_buffed, query_settings, pantheon_list, series_name, base_monster, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, @@ -176,7 +178,7 @@ def embed(cls, state: PantheonViewState): color=state.color, title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.discrepant).to_markdown(), + state.is_jp_buffed).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), embed_fields=fields, diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 2eb22722..5d2addd6 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -56,7 +56,7 @@ def embed(cls, state: PicViewState): color=state.color, title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.discrepant).to_markdown(), + state.is_jp_buffed).to_markdown(), url=puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), embed_fields=fields, diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 464b6459..048c96ff 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -5,6 +5,8 @@ from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils import char_to_emoji, embed_footer_with_state +from tsutils.enums import Server +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.view.components.monster.header import MonsterHeader @@ -19,7 +21,7 @@ class SeriesScrollViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 11 def __init__(self, original_author_id, menu_type, raw_query, query, color, series_id, current_page, - monster_list: List["MonsterModel"], rarity: int, pages_in_rarity: int, + monster_list: List["MonsterModel"], rarity: int, pages_in_rarity: int, query_settings: QuerySettings, all_rarities: List[int], title, message, current_index: int = None, @@ -34,6 +36,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, serie self.current_page = current_page or 0 self.series_id = series_id self.rarity = rarity + self.query_settings = query_settings self.message = message self.child_message_id = child_message_id self.title = title @@ -48,7 +51,7 @@ def serialize(self): ret.update({ 'pane_type': SeriesScrollView.VIEW_TYPE, 'series_id': self.series_id, - 'server': self.monster_list[0].server_priority if self.monster_list else "COMBINED", + 'query_settings': self.query_settings.serialize(), 'current_page': self.current_page, 'pages_in_rarity': self.pages_in_rarity, 'title': self.title, @@ -69,7 +72,8 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): series_id = ims['series_id'] rarity = ims['rarity'] all_rarities = ims['all_rarities'] - paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, ims['server']) + query_settings = QuerySettings.deserialize(ims.get('query_settings')) + paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, query_settings.server) current_page = ims['current_page'] monster_list = paginated_monsters[current_page] title = ims['title'] @@ -86,7 +90,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): current_index = ims.get('current_index') return SeriesScrollViewState(original_author_id, menu_type, raw_query, query, user_config.color, series_id, - current_page, monster_list, rarity, pages_in_rarity, + current_page, monster_list, rarity, pages_in_rarity, query_settings, all_rarities, title, message, current_index=current_index, @@ -116,7 +120,8 @@ def query_all_rarities(dgcog, series_id, server): async def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: series_id = ims['series_id'] rarity = ims['rarity'] - paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, ims['server']) + query_settings = QuerySettings.deserialize(ims['query_settings']) + paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, query_settings.server) return paginated_monsters diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 41af89cd..3b816331 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -5,6 +5,7 @@ from discordmenu.embed.text import Text, BoldText, LabeledText from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx @@ -22,21 +23,23 @@ class TransformInfoViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, - acquire_raw, monster_ids, discrepant, reaction_list): + acquire_raw, monster_ids, is_jp_buffed, query_settings, + reaction_list): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color self.base_mon = base_mon self.transformed_mon = transformed_mon self.acquire_raw = acquire_raw self.monster_ids = monster_ids + self.is_jp_buffed = is_jp_buffed + self.query_settings = query_settings self.reaction_list = reaction_list - self.discrepant = discrepant def serialize(self): ret = super().serialize() ret.update({ + 'query_settings': self.query_settings.serialize(), 'resolved_monster_ids': self.monster_ids, - 'resolved_monster_server': self.base_mon.server_priority, 'reaction_list': self.reaction_list }) return ret @@ -49,17 +52,19 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): monster_ids = ims['resolved_monster_ids'] base_mon_id = monster_ids[0] transformed_mon_id = monster_ids[1] + query_settings = QuerySettings.deserialize(ims.get('query_settings')) - base_mon = dgcog.get_monster(base_mon_id, server=ims['resolved_monster_server']) - transformed_mon = dgcog.get_monster(transformed_mon_id, server=ims['resolved_monster_server']) + base_mon = dgcog.get_monster(base_mon_id, server=query_settings.server) + transformed_mon = dgcog.get_monster(transformed_mon_id, server=query_settings.server) acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) reaction_list = ims['reaction_list'] - discrep = dgcog.database.graph.monster_is_discrepant(base_mon) \ + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) \ or dgcog.database.graph.monster_is_discrepant(transformed_mon) return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, - base_mon, transformed_mon, acquire_raw, monster_ids, discrep, + base_mon, transformed_mon, acquire_raw, monster_ids, is_jp_buffed, + query_settings, reaction_list=reaction_list) @staticmethod @@ -208,7 +213,7 @@ def embed(state: TransformInfoViewState): color=state.color, title=MonsterHeader.fmt_id_header(transformed_mon, False, - state.discrepant).to_markdown(), + state.is_jp_buffed).to_markdown(), url=puzzledragonx(transformed_mon) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon)), diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index 53fd495f..923a6219 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -1,3 +1,5 @@ +from tsutils.enums import Server + from dadguide.database_manager import DadguideDatabase from dadguide.database_context import DbContext from dadguide.monster_graph import MonsterGraph @@ -6,13 +8,13 @@ database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) db_context = DbContext(database, graph) -get_monster = lambda mid, server="COMBINED": graph.get_monster(mid, server=server) +get_monster = lambda mid, server=Server.COMBINED: graph.get_monster(mid, server=server) assert get_monster(4).is_farmable assert not get_monster(5156).is_farmable assert graph.get_base_monster(get_monster(1074)).monster_id == 1073 # evo pandora -assert db_context.get_monsters_by_series(1, server="COMBINED")[0].name_en == 'Tyrra' -assert db_context.get_monsters_by_active(1, server="COMBINED")[0].name_en == 'Tyrra' +assert db_context.get_monsters_by_series(1, server=Server.COMBINED)[0].name_en == 'Tyrra' +assert db_context.get_monsters_by_active(1, server=Server.COMBINED)[0].name_en == 'Tyrra' # evo types assert graph.true_evo_type(get_monster(5392)) == InternalEvoType.Ultimate From fbb60a7c01a2350ee8b5dfbad27614f345a2f81c Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 2 Jun 2021 04:27:50 -0700 Subject: [PATCH 0812/1220] Fix ls and lss (#1165) --- padinfo/menu/leader_skill.py | 2 ++ padinfo/padinfo.py | 15 +++++++++++---- padinfo/view/leader_skill.py | 12 ++++++++++-- padinfo/view/leader_skill_single.py | 14 ++++++++++++-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 24353feb..5bf5c1d5 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -24,6 +24,7 @@ async def respond_with_right(message: Optional[Message], ims, **data): # Extract the query from the ls state ims['query'] = ims['r_query'] + ims['query_settings'] = ims['r_query_settings'] ims['resolved_monster_id'] = None id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) id_control = LeaderSkillMenu.id_control(id_view_state) @@ -36,6 +37,7 @@ async def respond_with_left(message: Optional[Message], ims, **data): # Extract the query from the ls state ims['query'] = ims['l_query'] + ims['query_settings'] = ims['l_query_settings'] ims['resolved_monster_id'] = None id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) id_control = LeaderSkillMenu.id_control(id_view_state) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f8b3c0ef..8dbb6fdd 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -217,7 +217,8 @@ async def _do_id(self, ctx, query: str): goodquery = None if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes[Server.COMBINED].all_name_tokens: + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes[ + Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: @@ -710,10 +711,13 @@ async def leaderskill(self, ctx, *, raw_query): await ctx.send(inline(err_msg.format('Right', r_query))) return + l_query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), l_query) + r_query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), r_query) + color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, color, l_mon, r_mon, - l_query, r_query) + l_query, r_query, l_query_settings, r_query_settings) menu = LeaderSkillMenu.menu() await menu.create(ctx, state) @@ -744,9 +748,12 @@ async def leaderskillsingle(self, ctx, *, query): await self.send_id_failure_message(ctx, query) return + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, color, monster) + state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, query_settings, + color, monster) menu = LeaderSkillSingleMenu.menu() await menu.create(ctx, state) @@ -940,7 +947,7 @@ async def server(self, ctx, server: str): dgcog = await self.get_dgcog() async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: if server.upper() == "DEFAULT": - fm_flags['server'] = dgcog.DEFAULT_SERVER + fm_flags['server'] = dgcog.DEFAULT_SERVER.value elif server.upper() in [s.value for s in dgcog.SERVERS]: fm_flags['server'] = server.upper() else: diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 9c365dff..44c1af04 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -5,6 +5,7 @@ from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.core.leader_skills import createMultiplierText @@ -17,19 +18,24 @@ class LeaderSkillViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, l_mon, r_mon, l_query, r_query): + def __init__(self, original_author_id, menu_type, raw_query, color, l_mon, r_mon, l_query, r_query, + l_query_settings, r_query_settings): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color self.l_mon = l_mon self.l_query = l_query + self.l_query_settings = l_query_settings self.r_mon = r_mon self.r_query = r_query + self.r_query_settings = r_query_settings def serialize(self): ret = super().serialize() ret.update({ 'l_query': self.l_query, 'r_query': self.r_query, + 'l_query_settings': self.l_query_settings.serialize(), + 'r_query_settings': self.r_query_settings.serialize() }) return ret @@ -38,10 +44,12 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] + l_query_settings = QuerySettings.deserialize(ims['l_query_settings']) + r_query_settings = QuerySettings.deserialize(ims['r_query_settings']) l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, ims['original_author_id']) return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, - r_query) + r_query, l_query_settings, r_query_settings) class LeaderSkillView: diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index ea95f999..c05b93a3 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -5,6 +5,7 @@ from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.core.leader_skills import createSingleMultiplierText @@ -17,19 +18,28 @@ class LeaderSkillSingleViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, mon): + def __init__(self, original_author_id, menu_type, raw_query, query_settings, color, mon): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color self.mon = mon + self.query_settings = query_settings + + def serialize(self): + ret = super().serialize() + ret.update({ + 'query_settings': self.query_settings.serialize() + }) + return ret @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] + query_settings = QuerySettings.deserialize(ims['query_settings']) mon = await get_monster_from_ims(dgcog, ims) - return LeaderSkillSingleViewState(original_author_id, menu_type, raw_query, user_config.color, mon) + return LeaderSkillSingleViewState(original_author_id, menu_type, raw_query, query_settings, user_config.color, mon) class LeaderSkillSingleView: From 0b86177044e04313592c547a745e7401bbdd7262 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 2 Jun 2021 04:37:10 -0700 Subject: [PATCH 0813/1220] r_query can be None (#1166) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8dbb6fdd..c31ee7b9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -712,7 +712,7 @@ async def leaderskill(self, ctx, *, raw_query): return l_query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), l_query) - r_query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), r_query) + r_query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), r_query or l_query) color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id From 0b49706a49b749361e7e09d03f8d04dc215c4825 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 04:38:50 -0500 Subject: [PATCH 0814/1220] Stop playing with ims directly in MonsterListMenu (#1168) --- padinfo/menu/monster_list.py | 182 ++++++++-------------- padinfo/view/monster_list/monster_list.py | 65 +++++++- 2 files changed, 125 insertions(+), 122 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 84ae39ca..70de210c 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -1,11 +1,10 @@ -from copy import deepcopy from typing import Optional, List from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji - from tsutils.menu.panes import emoji_buttons, MenuPanes + from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView from padinfo.view.monster_list.all_mats import AllMatsViewState @@ -36,21 +35,16 @@ class MonsterListEmoji: reset = emoji_buttons['reset'] -view_state_types = { - AllMatsViewState.VIEW_STATE_TYPE: AllMatsViewState, - IdSearchViewState.VIEW_STATE_TYPE: IdSearchViewState, - StaticMonsterListViewState.VIEW_STATE_TYPE: StaticMonsterListViewState, -} - - -def _get_view_state(ims: dict): - return view_state_types.get(ims['menu_type']) or MonsterListMenu.MENU_TYPE - - class MonsterListMenu: MENU_TYPE = 'MonsterListMenu' CHILD_MENU_TYPE = 'IdMenu' + view_state_types = { + AllMatsViewState.VIEW_STATE_TYPE: AllMatsViewState, + IdSearchViewState.VIEW_STATE_TYPE: IdSearchViewState, + StaticMonsterListViewState.VIEW_STATE_TYPE: StaticMonsterListViewState, + } + @staticmethod def menu(initial_control=None): if initial_control is None: @@ -58,6 +52,13 @@ def menu(initial_control=None): embed = EmbedMenu(MonsterListMenuPanes.transitions(), initial_control) return embed + @classmethod + async def _get_view_state(cls, ims: dict, **data) -> MonsterListViewState: + dgcog = data['dgcog'] + user_config = data['user_config'] + view_state_class = cls.view_state_types.get(ims['menu_type']) or MonsterListMenu.MENU_TYPE + return await view_state_class.deserialize(dgcog, user_config, ims) + @staticmethod async def respond_with_refresh(message: Optional[Message], ims, **data): # this is only called once on message load @@ -70,92 +71,34 @@ async def respond_with_reset(message: Optional[Message], ims, **data): # replace with the overview list after the child menu changes return await MonsterListMenu.respond_with_monster_list(message, ims, **data) - @staticmethod - async def respond_with_left(message: Optional[Message], ims, **data): - current_page = ims['current_page'] - if current_page > 0: - ims['current_page'] = current_page - 1 - ims['current_index'] = None - return await MonsterListMenu.respond_with_monster_list(message, ims, **data) - paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(data['dgcog'], ims) - ims['current_page'] = len(paginated_monsters) - 1 - if len(paginated_monsters) > 1: - ims['current_index'] = None - return await MonsterListMenu.respond_with_monster_list(message, ims, **data) - - @staticmethod - async def respond_with_right(message: Optional[Message], ims, **data): - paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(data['dgcog'], ims) - current_page = ims['current_page'] - if current_page < len(paginated_monsters) - 1: - ims['current_page'] = current_page + 1 - ims['current_index'] = None - return await MonsterListMenu.respond_with_monster_list(message, ims, **data) - ims['current_page'] = 0 - if len(paginated_monsters) > 1: - ims['current_index'] = None - return await MonsterListMenu.respond_with_monster_list(message, ims, **data) - - @staticmethod - async def respond_with_monster_list(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] - user_config = data['user_config'] - view_state = await _get_view_state(ims).deserialize(dgcog, user_config, ims) - control = MonsterListMenu.monster_list_control(view_state) - return control + @classmethod + async def respond_with_left(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.decrement_page() + return MonsterListMenu.monster_list_control(view_state) - @staticmethod - async def respond_with_previous_monster(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] - await MonsterListMenu._update_ims_prev(dgcog, ims) - return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + @classmethod + async def respond_with_right(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.increment_page() + return MonsterListMenu.monster_list_control(view_state) - @staticmethod - async def _update_ims_prev(dgcog, ims): - current_index = ims['current_index'] - page = ims['current_page'] - paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, ims) - max_index = len(paginated_monsters[page]) - 1 - if current_index is None: - ims['current_index'] = max_index - return - if current_index > 0: - ims['current_index'] = current_index - 1 - return - if current_index == 0 and page > 0: - ims['current_page'] = page - 1 - ims['current_index'] = MonsterListViewState.MAX_ITEMS_PER_PANE - 1 - return - if current_index == 0: - # respond with left but then set the current index to the max thing possible - ims['current_page'] = len(paginated_monsters) - 1 - ims['current_index'] = len(paginated_monsters[-1]) - 1 + @classmethod + async def respond_with_monster_list(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + return MonsterListMenu.monster_list_control(view_state) - @staticmethod - async def respond_with_next_monster(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] - await MonsterListMenu._update_ims_next(dgcog, ims) - return await MonsterListMenu.respond_with_monster_list(message, ims, **data) + @classmethod + async def respond_with_previous_monster(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.decrement_index() + return MonsterListMenu.monster_list_control(view_state) - @staticmethod - async def _update_ims_next(dgcog, ims): - current_index = ims['current_index'] - page = ims['current_page'] - paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, ims) - max_index = len(paginated_monsters[page]) - 1 - if current_index is None: - ims['current_index'] = 0 - return - if current_index < max_index: - ims['current_index'] = current_index + 1 - return - if current_index == max_index and page < len(paginated_monsters) - 1: - ims['current_page'] = page + 1 - ims['current_index'] = 0 - return - if current_index == max_index: - ims['current_page'] = 0 - ims['current_index'] = 0 + @classmethod + async def respond_with_next_monster(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.increment_index() + return MonsterListMenu.monster_list_control(view_state) @staticmethod async def respond_with_delete(message: Message, ims, **data): @@ -220,50 +163,45 @@ def monster_list_control(state: MonsterListViewState): reaction_list ) - @staticmethod - async def click_child_number(ims, emoji_clicked, **data): - dgcog = data['dgcog'] + @classmethod + async def click_child_number(cls, ims, emoji_clicked, **data): + view_state = await cls._get_view_state(ims, **data) emoji_response = IdMenuEmoji.refresh \ if MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} n = MonsterListMenuPanes.emoji_names().index(emoji_clicked) - paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, ims) - page = ims.get('current_page') or 0 - monster_list = paginated_monsters[page] extra_ims = { 'is_child': True, 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, - 'resolved_monster_id': - monster_list[n - MonsterListMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, - 'query_settings': ims['query_settings'], + 'resolved_monster_id': view_state.monster_list[ + MonsterListMenuPanes.get_monster_index(n)].monster_id, + 'query_settings': view_state.query_settings.serialize(), } return emoji_response, extra_ims - @staticmethod - async def auto_scroll_child_left(ims, _emoji_clicked, **data): - return await MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_prev, **data) + @classmethod + async def auto_scroll_child_left(cls, ims, _emoji_clicked, **data): + view_state = await cls._get_view_state(ims, **data) + return await MonsterListMenu._scroll_child(view_state, view_state.decrement_index) - @staticmethod - async def auto_scroll_child_right(ims, _emoji_clicked, **data): - return await MonsterListMenu._scroll_child(ims, MonsterListMenu._update_ims_next, **data) + @classmethod + async def auto_scroll_child_right(cls, ims, _emoji_clicked, **data): + view_state = await cls._get_view_state(ims, **data) + return await MonsterListMenu._scroll_child(view_state, view_state.increment_index) @staticmethod - async def _scroll_child(ims, update_fn, **data): - dgcog = data['dgcog'] - copy_ims = deepcopy(ims) - await update_fn(dgcog, copy_ims) - paginated_monsters = await _get_view_state(ims).query_paginated_from_ims(dgcog, copy_ims) - page = copy_ims.get('current_page') or 0 - monster_list = paginated_monsters[page] - monster = monster_list[copy_ims['current_index']] + async def _scroll_child(view_state: MonsterListViewState, update_function): + # when the parent ims updated, it was a deepcopy of the ims / view_state we have now, so we have to + # redo the update function that we did before + update_function() extra_ims = { 'is_child': True, 'reaction_list': IdMenuPanes.emoji_names(), 'menu_type': IdMenu.MENU_TYPE, - 'resolved_monster_id': monster.monster_id, - 'query_settings': ims['query_settings'], + 'resolved_monster_id': view_state.monster_list[view_state.current_index].monster_id, + 'query_settings': view_state.query_settings.serialize(), } return IdMenuEmoji.refresh, extra_ims @@ -321,3 +259,7 @@ def get_initial_reaction_list(cls, number_of_evos: int): @classmethod def get_previous_reaction_list_num_monsters(cls, reaction_list: List): return len(reaction_list) - cls.NON_MONSTER_EMOJI_COUNT + + @classmethod + def get_monster_index(cls, n: int): + return n - cls.NON_MONSTER_EMOJI_COUNT diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 2ab029da..85e14aae 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -5,7 +5,6 @@ from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils import char_to_emoji, embed_footer_with_state -from tsutils.enums import Server from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -40,12 +39,16 @@ def __init__(self, original_author_id, menu_type, query, color, self.child_message_id = child_message_id self.title = title self.paginated_monsters = paginated_monsters - self.monster_list = paginated_monsters[current_page] + self.monster_list = None + self._update_monster_list() self.reaction_list = reaction_list self.color = color self.query = query self.query_settings = query_settings + def _update_monster_list(self): + self.monster_list = self.paginated_monsters[self.current_page] + def serialize(self): ret = super().serialize() ret.update({ @@ -103,6 +106,64 @@ def paginate(monster_list: List["MonsterModel"]) -> List[List["MonsterModel"]]: async def query_paginated_from_ims(cls, dgcog, ims) -> List[List["MonsterModel"]]: return cls.paginate(await cls.query_from_ims(dgcog, ims)) + def increment_page(self): + if self.current_page < len(self.paginated_monsters) - 1: + self.current_page = self.current_page + 1 + self._update_monster_list() + self.current_index = None + return + self.current_page = 0 + if len(self.paginated_monsters) > 1: + self.current_index = None + + def decrement_page(self): + if self.current_page > 0: + self.current_page = self.current_page - 1 + self._update_monster_list() + self.current_index = None + return + self.current_page = len(self.paginated_monsters) - 1 + self._update_monster_list() + if len(self.paginated_monsters) > 1: + self.current_index = None + + def increment_index(self): + max_index = len(self.paginated_monsters[self.current_page]) - 1 + if self.current_index is None: + self.current_index = 0 + return + if self.current_index < max_index: + self.current_index = self.current_index + 1 + return + if self.current_index == max_index and self.current_page < len(self.paginated_monsters) - 1: + self.current_page = self.current_page + 1 + self._update_monster_list() + self.current_index = 0 + return + if self.current_index == max_index: + self.current_page = 0 + self._update_monster_list() + self.current_index = 0 + + def decrement_index(self): + max_index = len(self.paginated_monsters[self.current_page]) - 1 + if self.current_index is None: + self.current_index = max_index + return + if self.current_index > 0: + self.current_index = self.current_index - 1 + return + if self.current_index == 0 and self.current_page > 0: + self.current_page = self.current_page - 1 + self._update_monster_list() + self.current_index = MonsterListViewState.MAX_ITEMS_PER_PANE - 1 + return + if self.current_index == 0: + # respond with left but then set the current index to the max thing possible + self.current_page = len(self.paginated_monsters) - 1 + self._update_monster_list() + self.current_index = len(self.paginated_monsters[-1]) - 1 + def _monster_list(monsters): if not len(monsters): From f4b1d2355c04897ab906cf50ed24a0e1f6a3a3ab Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 04:57:53 -0500 Subject: [PATCH 0815/1220] Move all dadguide enum types to enum_types.py (#1170) --- dadguide/common/__init__.py | 0 dadguide/common/enums.py | 86 ------------------------------- dadguide/dadguide.py | 2 +- dadguide/database_context.py | 2 +- dadguide/find_monster_tokens.py | 2 +- dadguide/idtest_mixin.py | 2 +- dadguide/models/enum_types.py | 87 ++++++++++++++++++++++++++++++++ dadguide/models/monster_stats.py | 2 +- dadguide/monster_graph.py | 3 +- dadguide/monster_index.py | 2 +- dadguide/token_mappings.py | 3 +- 11 files changed, 95 insertions(+), 96 deletions(-) delete mode 100644 dadguide/common/__init__.py delete mode 100644 dadguide/common/enums.py diff --git a/dadguide/common/__init__.py b/dadguide/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dadguide/common/enums.py b/dadguide/common/enums.py deleted file mode 100644 index 5b1925a4..00000000 --- a/dadguide/common/enums.py +++ /dev/null @@ -1,86 +0,0 @@ -from enum import Enum -from tsutils.enums import Server - -DEFAULT_SERVER = Server.COMBINED -SERVERS = [Server.NA, Server.COMBINED] - - -class AwokenSkills(Enum): - ENHANCEDHP = 1 - ENHANCEDATK = 2 - ENHANCEDRCV = 3 - REDUCERED = 4 - REDUCEBLUE = 5 - REDUCEGREEN = 6 - REDUCELIGHT = 7 - REDUCEDARK = 8 - AUTOHEAL = 9 - BINDRES = 10 - BLINDRES = 11 - JAMMERRES = 12 - POISONRES = 13 - ENHANCEDRED = 14 - ENHANCEDBLUE = 15 - ENHANCEDGREEN = 16 - ENHANCEDLIGHT = 17 - ENHANCEDDARK = 18 - EXTMOVE = 19 - BINDRECOVERY = 20 - SKILLBOOST = 21 - REDROW = 22 - BLUEROW = 23 - GREENROW = 24 - LIGHTROW = 25 - DARKROW = 26 - TPA = 27 - SKILLBINDRES = 28 - ENHANCEDHEAL = 29 - MULTIBOOST = 30 - DRAGONKILLER = 31 - GODKILLER = 32 - DEVILKILLER = 33 - MACHINEKILLER = 34 - BALANCEDKILLER = 35 - ATTACKERKILLER = 36 - PHYSICALKILLER = 37 - HEALERKILLER = 38 - EVOMATKILLER = 39 - AWOKENKILLER = 40 - FODDERKILLER = 41 - REDEEMKILLER = 42 - ENHCOMBO7C = 43 - GUARDBREAK = 44 - FUA = 45 - ENHTEAMHP = 46 - ENHTEAMRCV = 47 - VDP = 48 - EQUIP = 49 - SUPERFUA = 50 - SKILLCHARGE = 51 - UNBINDABLE = 52 - EXTMOVEPLUS = 53 - CLOUDRESIST = 54 - TAPERESIST = 55 - SKILLBOOSTPLUS = 56 - HP80ORMORE = 57 - HP50ORLESS = 58 - ELSHIELD = 59 - ELATTACK = 60 - ENHCOMBO10C = 61 - COMBOORB = 62 - VOICE = 63 - DUNGEONBONUS = 64 - REDUCEDHP = 65 - REDUCEDATK = 66 - REDUCEDRCV = 67 - UNBLINDABLE = 68 - UNJAMMABLE = 69 - UNPOISONABLE = 70 - JAMMERBLESSING = 71 - POISONBLESSING = 72 - REDCOMBOCOUNT = 73 - BLUECOMBOCOUNT = 74 - GREENCOMBOCOUNT = 75 - LIGHTCOMBOCOUNT = 76 - DARKCOMBOCOUNT = 77 - CROSSATTACK = 78 diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index f610207d..5e14beda 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -27,7 +27,7 @@ from .idtest_mixin import IdTest from .models.monster_model import MonsterModel from .models.monster_stats import monster_stats, MonsterStatModifierInput -from .common.enums import DEFAULT_SERVER, SERVERS +from .models.enum_types import DEFAULT_SERVER, SERVERS from .monster_index import MonsterIndex logger = logging.getLogger('red.padbot-cogs.dadguide') diff --git a/dadguide/database_context.py b/dadguide/database_context.py index 49238195..a0246cf6 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -8,7 +8,7 @@ from .models.scheduled_event_model import ScheduledEventModel from .models.series_model import SeriesModel from .monster_graph import MonsterGraph -from .common.enums import DEFAULT_SERVER +from .models.enum_types import DEFAULT_SERVER SCHEDULED_EVENT_QUERY = """SELECT schedule.*, diff --git a/dadguide/find_monster_tokens.py b/dadguide/find_monster_tokens.py index 6e5d9ddb..b7869f44 100644 --- a/dadguide/find_monster_tokens.py +++ b/dadguide/find_monster_tokens.py @@ -2,7 +2,7 @@ from dadguide.models.monster_model import MonsterModel from dadguide.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP -from dadguide.common.enums import AwokenSkills +from dadguide.models.enum_types import AwokenSkills def regexlist(tokens): diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index 22f50f19..4ea26bf1 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -7,7 +7,7 @@ from redbot.core import commands, Config, checks from redbot.core.utils.chat_formatting import box, pagify -from dadguide.common.enums import DEFAULT_SERVER +from dadguide.models.enum_types import DEFAULT_SERVER from dadguide.monster_index import MonsterIndex diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 525e773c..82883811 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -1,5 +1,7 @@ from enum import Enum +from tsutils.enums import Server as TsutilsServer + class Attribute(Enum): """Standard 5 PAD colors in enum form. Values correspond to DadGuide values.""" @@ -56,3 +58,88 @@ def enum_or_none(enum, value, default=None): return enum(value) else: return default + + +DEFAULT_SERVER = TsutilsServer.COMBINED +SERVERS = [TsutilsServer.NA, TsutilsServer.COMBINED] + + +class AwokenSkills(Enum): + ENHANCEDHP = 1 + ENHANCEDATK = 2 + ENHANCEDRCV = 3 + REDUCERED = 4 + REDUCEBLUE = 5 + REDUCEGREEN = 6 + REDUCELIGHT = 7 + REDUCEDARK = 8 + AUTOHEAL = 9 + BINDRES = 10 + BLINDRES = 11 + JAMMERRES = 12 + POISONRES = 13 + ENHANCEDRED = 14 + ENHANCEDBLUE = 15 + ENHANCEDGREEN = 16 + ENHANCEDLIGHT = 17 + ENHANCEDDARK = 18 + EXTMOVE = 19 + BINDRECOVERY = 20 + SKILLBOOST = 21 + REDROW = 22 + BLUEROW = 23 + GREENROW = 24 + LIGHTROW = 25 + DARKROW = 26 + TPA = 27 + SKILLBINDRES = 28 + ENHANCEDHEAL = 29 + MULTIBOOST = 30 + DRAGONKILLER = 31 + GODKILLER = 32 + DEVILKILLER = 33 + MACHINEKILLER = 34 + BALANCEDKILLER = 35 + ATTACKERKILLER = 36 + PHYSICALKILLER = 37 + HEALERKILLER = 38 + EVOMATKILLER = 39 + AWOKENKILLER = 40 + FODDERKILLER = 41 + REDEEMKILLER = 42 + ENHCOMBO7C = 43 + GUARDBREAK = 44 + FUA = 45 + ENHTEAMHP = 46 + ENHTEAMRCV = 47 + VDP = 48 + EQUIP = 49 + SUPERFUA = 50 + SKILLCHARGE = 51 + UNBINDABLE = 52 + EXTMOVEPLUS = 53 + CLOUDRESIST = 54 + TAPERESIST = 55 + SKILLBOOSTPLUS = 56 + HP80ORMORE = 57 + HP50ORLESS = 58 + ELSHIELD = 59 + ELATTACK = 60 + ENHCOMBO10C = 61 + COMBOORB = 62 + VOICE = 63 + DUNGEONBONUS = 64 + REDUCEDHP = 65 + REDUCEDATK = 66 + REDUCEDRCV = 67 + UNBLINDABLE = 68 + UNJAMMABLE = 69 + UNPOISONABLE = 70 + JAMMERBLESSING = 71 + POISONBLESSING = 72 + REDCOMBOCOUNT = 73 + BLUECOMBOCOUNT = 74 + GREENCOMBOCOUNT = 75 + LIGHTCOMBOCOUNT = 76 + DARKCOMBOCOUNT = 77 + CROSSATTACK = 78 diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 02b2734f..6e54e689 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -1,5 +1,5 @@ from typing import Literal -from dadguide.common.enums import AwokenSkills +from dadguide.models.enum_types import AwokenSkills StatType = Literal['hp', 'atk', 'rcv'] diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index ba3efbe3..ac13641e 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -6,13 +6,12 @@ from networkx import MultiDiGraph from tsutils.enums import Server -from .common.enums import DEFAULT_SERVER, SERVERS from .database_manager import DadguideDatabase from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel from .models.base_model import BaseModel -from .models.enum_types import InternalEvoType +from .models.enum_types import InternalEvoType, DEFAULT_SERVER, SERVERS from .models.evolution_model import EvolutionModel from .models.leader_skill_model import LeaderSkillModel from .models.monster_model import MonsterModel diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index be889bae..04d0981e 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -11,7 +11,7 @@ from .models.monster_model import MonsterModel from .monster_graph import MonsterGraph -from .common.enums import DEFAULT_SERVER +from .models.enum_types import DEFAULT_SERVER from .token_mappings import * SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY' \ diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 2726175c..7059eba0 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -2,8 +2,7 @@ from enum import Enum from typing import Mapping, Tuple, TypeVar -from .common.enums import AwokenSkills -from .models.enum_types import Attribute, MonsterType +from .models.enum_types import Attribute, MonsterType, AwokenSkills K = TypeVar('K') V = TypeVar('V') From d1756e69f699382609748bdc6a0be9fe4ff62074 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 05:01:05 -0500 Subject: [PATCH 0816/1220] Remove unused Server enum (#1171) --- dadguide/models/enum_types.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 82883811..16be00d3 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -1,6 +1,6 @@ from enum import Enum -from tsutils.enums import Server as TsutilsServer +from tsutils.enums import Server class Attribute(Enum): @@ -40,12 +40,6 @@ class InternalEvoType(Enum): SuperReincarnated = "Super Reincarnated" -class Server(Enum): - JP = 0 - NA = 1 - KR = 2 - - class AwakeningRestrictedLatent(Enum): """Latent awakenings with availability gated by having an awakening""" UnmatchableClear = 606 @@ -60,8 +54,8 @@ def enum_or_none(enum, value, default=None): return default -DEFAULT_SERVER = TsutilsServer.COMBINED -SERVERS = [TsutilsServer.NA, TsutilsServer.COMBINED] +DEFAULT_SERVER = Server.COMBINED +SERVERS = [Server.NA, Server.COMBINED] class AwokenSkills(Enum): From 2663fcee8a453b9da06c2ae6843d09ad19fe08dd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 07:54:34 -0500 Subject: [PATCH 0817/1220] Remove id2 (#1172) --- padinfo/padinfo.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c31ee7b9..9aac30a1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -326,12 +326,6 @@ async def checkbadness(self, ctx): bad = await self.config.bad() await ctx.send(f"{bad}/{good + bad} ({int(round(bad / (good + bad) * 100)) if good or bad else 'NaN'}%)") - @commands.command() - @checks.bot_has_permissions() - async def id2(self, ctx): - """Monster info (main tab)""" - await ctx.send("id2 has been discontinued!".format(ctx.prefix)) - @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): From f3f440401d22c454641cce62980e65645fcbe913 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 21:34:32 -0500 Subject: [PATCH 0818/1220] Write nadiff (#1176) --- padinfo/menu/menu_map.py | 2 ++ padinfo/menu/na_diff.py | 61 ++++++++++++++++++++++++++++++++++++++++ padinfo/padinfo.py | 38 +++++++++++++++++++++++++ padinfo/view/id.py | 11 ++++++++ 4 files changed, 112 insertions(+) create mode 100644 padinfo/menu/na_diff.py diff --git a/padinfo/menu/menu_map.py b/padinfo/menu/menu_map.py index 23ea164c..6739ce3c 100644 --- a/padinfo/menu/menu_map.py +++ b/padinfo/menu/menu_map.py @@ -4,6 +4,7 @@ from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes +from padinfo.menu.na_diff import NaDiffMenu, NaDiffMenuPanes from padinfo.menu.series_scroll import SeriesScrollMenu, SeriesScrollMenuPanes from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes @@ -24,4 +25,5 @@ SeriesScrollMenu.MENU_TYPE: (SeriesScrollMenu, SeriesScrollMenuPanes), SimpleTextMenu.MENU_TYPE: (SimpleTextMenu, SimpleTextMenuPanes), TransformInfoMenu.MENU_TYPE: (TransformInfoMenu, TransformInfoMenuPanes), + NaDiffMenu.MENU_TYPE: (NaDiffMenu, NaDiffMenuPanes), } diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py new file mode 100644 index 00000000..18ddcf73 --- /dev/null +++ b/padinfo/menu/na_diff.py @@ -0,0 +1,61 @@ +from typing import Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from discordmenu.emoji.emoji_cache import emoji_cache +from tsutils.enums import Server +from tsutils.menu.panes import MenuPanes + +from padinfo.view.id import IdViewState, IdView + + +class NaDiffEmoji: + na = '\N{EARTH GLOBE AMERICAS}' + jp = '\N{EARTH GLOBE ASIA-AUSTRALIA}' + + +class NaDiffMenu: + MENU_TYPE = "NaDiffMenu" + + @staticmethod + def menu(initial_control=None): + if initial_control is None: + initial_control = NaDiffMenu.id_control + embed = EmbedMenu(NaDiffMenuPanes.transitions(), initial_control) + return embed + + @staticmethod + async def respond_with_na(message: Optional[Message], ims, **data): + return await NaDiffMenu.respond_with_id(message, ims, Server.NA, **data) + + @staticmethod + async def respond_with_jp(message: Optional[Message], ims, **data): + return await NaDiffMenu.respond_with_id(message, ims, Server.COMBINED, **data) + + @staticmethod + async def respond_with_id(message: Optional[Message], ims, server: Server, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + + view_state = await IdViewState.deserialize(dgcog, user_config, ims) + await view_state.set_server(dgcog, server) + control = NaDiffMenu.id_control(view_state) + return control + + @staticmethod + def id_control(state: IdViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [IdView.embed(state)], + reaction_list or [emoji_cache.get_by_name(e) for e in NaDiffMenuPanes.emoji_names()] + ) + + +class NaDiffMenuPanes(MenuPanes): + DATA = { + NaDiffEmoji.na: (NaDiffMenu.respond_with_na, IdView.VIEW_TYPE), + NaDiffEmoji.jp: (NaDiffMenu.respond_with_jp, IdView.VIEW_TYPE), + } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9aac30a1..5642df3a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -33,6 +33,7 @@ from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu from padinfo.menu.menu_map import padinfo_menu_map from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji +from padinfo.menu.na_diff import NaDiffMenu from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji from padinfo.menu.simple_text import SimpleTextMenu from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes @@ -326,6 +327,43 @@ async def checkbadness(self, ctx): bad = await self.config.bad() await ctx.send(f"{bad}/{good + bad} ({int(round(bad / (good + bad) * 100)) if good or bad else 'NaN'}%)") + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def nadiff(self, ctx, *, query: str): + """Show differences between NA & JP versions of a card""" + dgcog = await self.get_dgcog() + raw_query = query + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + + monster = await dgcog.find_monster(raw_query + ' --na', ctx.author.id) + + if monster is None: + await self.send_id_failure_message(ctx, query) + return + + await self.log_id_result(ctx, monster.monster_id) + + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + + if not is_jp_buffed: + await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, + monster.name_en) + 'which is the same in NA & JP.') + return + + alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) + transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ + await IdViewState.query(dgcog, monster) + + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + + state = IdViewState(original_author_id, NaDiffMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, is_jp_buffed, query_settings, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, + ) + menu = NaDiffMenu.menu() + await menu.create(ctx, state) + @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 32a9a352..c8b688c7 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -5,6 +5,7 @@ from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.enums import Server from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -90,6 +91,16 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): is_child=is_child, extra_state=ims) + async def set_server(self, dgcog, server: Server): + self.query_settings.server = server + self.monster = dgcog.database.graph.get_monster(self.monster.monster_id, server=server) + self.alt_monsters = self.get_alt_monsters_and_evos(dgcog, self.monster) + transform_base, true_evo_type_raw, acquire_raw, base_rarity = await IdViewState.query(dgcog, self.monster) + self.transform_base = transform_base + self.true_evo_type_raw = true_evo_type_raw + self.acquire_raw = acquire_raw + self.base_rarity = base_rarity + @classmethod async def query(cls, dgcog, monster): db_context = dgcog.database From 8b93610b4ee52548e59c4ebe197b816ac19d7743 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 21:55:31 -0500 Subject: [PATCH 0819/1220] Use flags & don't fail for jp-only cards (#1178) --- padinfo/menu/na_diff.py | 4 ++-- padinfo/padinfo.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index 18ddcf73..f608422c 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -11,8 +11,8 @@ class NaDiffEmoji: - na = '\N{EARTH GLOBE AMERICAS}' - jp = '\N{EARTH GLOBE ASIA-AUSTRALIA}' + na = '\N{REGIONAL INDICATOR SYMBOL LETTER U}\N{REGIONAL INDICATOR SYMBOL LETTER S}' + jp = '\N{REGIONAL INDICATOR SYMBOL LETTER J}\N{REGIONAL INDICATOR SYMBOL LETTER P}' class NaDiffMenu: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5642df3a..110e2a30 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -336,16 +336,20 @@ async def nadiff(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query + ' --na', ctx.author.id) + original_monster = await dgcog.find_monster(raw_query, ctx.author.id) + if original_monster is None: + await self.send_id_failure_message(ctx, query) + return + monster = await dgcog.find_monster(raw_query + ' --na', ctx.author.id) if monster is None: - await self.send_id_failure_message(ctx, query) + await ctx.send('Your query `{}` found [{}] {}, '.format(query, original_monster.monster_id, + original_monster.name_en) + 'which is only in JP.') return await self.log_id_result(ctx, monster.monster_id) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) - if not is_jp_buffed: await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, monster.name_en) + 'which is the same in NA & JP.') From 6570b4090a903721cbe2db16164536299e846671 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 22:38:42 -0500 Subject: [PATCH 0820/1220] Pull out invalid_monster_message (#1179) --- padinfo/padinfo.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 110e2a30..6b90896a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -327,6 +327,11 @@ async def checkbadness(self, ctx): bad = await self.config.bad() await ctx.send(f"{bad}/{good + bad} ({int(round(bad / (good + bad) * 100)) if good or bad else 'NaN'}%)") + @staticmethod + async def send_invalid_monster_message(ctx, query: str, monster: "MonsterModel", append_text: str): + base_text = 'Your query `{}` found {}{}.' + await ctx.send(base_text.format(query, MonsterHeader.short_with_emoji(monster, link=False).to_markdown(), append_text)) + @commands.command() @checks.bot_has_permissions(embed_links=True) async def nadiff(self, ctx, *, query: str): @@ -337,22 +342,23 @@ async def nadiff(self, ctx, *, query: str): original_author_id = ctx.message.author.id original_monster = await dgcog.find_monster(raw_query, ctx.author.id) + await self.log_id_result(ctx, original_monster.monster_id) if original_monster is None: await self.send_id_failure_message(ctx, query) return + if original_monster.on_na and not original_monster.on_jp: + await self.send_invalid_monster_message(ctx, query, original_monster, ', which is only in NA') + return + monster = await dgcog.find_monster(raw_query + ' --na', ctx.author.id) if monster is None: - await ctx.send('Your query `{}` found [{}] {}, '.format(query, original_monster.monster_id, - original_monster.name_en) + 'which is only in JP.') + await self.send_invalid_monster_message(ctx, query, original_monster, ', which is only in JP') return - await self.log_id_result(ctx, monster.monster_id) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) if not is_jp_buffed: - await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, - monster.name_en) + 'which is the same in NA & JP.') + await self.send_invalid_monster_message(ctx, query, monster, ', which is the same in NA & JP') return alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) @@ -391,8 +397,7 @@ async def evos(self, ctx, *, query: str): query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) if alt_versions is None: - await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, - monster.name_en) + 'which has no alt evos or gems.') + await self.send_invalid_monster_message(ctx, query, monster, ', which has no alt evos or gems') return full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] @@ -623,8 +628,7 @@ async def evolist(self, ctx, *, query): monster_list, _ = await EvosViewState.query(dgcog, monster) if monster_list is None: - await ctx.send('Your query `{}` found [{}] {}, '.format(query, monster.monster_id, - monster.name_en) + 'which has no alt evos.') + await self.send_invalid_monster_message(ctx, query, monster, ', which has no alt evos') return await self._do_monster_list(ctx, dgcog, query, monster_list, 'Evolution List', StaticMonsterListViewState) @@ -805,9 +809,7 @@ async def transforminfo(self, ctx, *, query): return if not transformed_mon: - await ctx.send('Your query `{}` '.format(query) - + 'found [{}] {}, '.format(base_mon.monster_id, base_mon.name_en) - + 'which has no evos that transform.') + await self.send_invalid_monster_message(ctx, query, base_mon, ', which has no evos that transform') return color = await self.get_user_embed_color(ctx) From 7a5ff22bf746fd382a45a136d1f9fa0cf1a13147 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Jun 2021 23:41:36 -0500 Subject: [PATCH 0821/1220] Rename query to do_query in ViewStates (#1180) This refactor is needed because otherwise there's a conflict between the method `query` and the property `query`, which is the user's query. Oops lol. --- padinfo/padinfo.py | 26 +++++++++---------- padinfo/reaction_list.py | 6 ++--- padinfo/view/awakening_list.py | 4 +-- padinfo/view/evos.py | 4 +-- padinfo/view/id.py | 6 ++--- padinfo/view/materials.py | 6 ++--- padinfo/view/monster_list/all_mats.py | 6 ++--- padinfo/view/monster_list/id_search.py | 4 +-- .../view/monster_list/static_monster_list.py | 4 +-- padinfo/view/pantheon.py | 4 +-- padinfo/view/series_scroll.py | 6 ++--- padinfo/view/transforminfo.py | 4 +-- 12 files changed, 40 insertions(+), 40 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6b90896a..9a766b58 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -262,7 +262,7 @@ async def send_message(): alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ - await IdViewState.query(dgcog, monster) + await IdViewState.do_query(dgcog, monster) full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) @@ -363,7 +363,7 @@ async def nadiff(self, ctx, *, query: str): alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ - await IdViewState.query(dgcog, monster) + await IdViewState.do_query(dgcog, monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) @@ -392,7 +392,7 @@ async def evos(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) alt_monsters = EvosViewState.get_alt_monsters_and_evos(dgcog, monster) - alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + alt_versions, gem_versions = await EvosViewState.do_query(dgcog, monster) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) @@ -428,7 +428,7 @@ async def evomats(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ - await MaterialsViewState.query(dgcog, monster) + await MaterialsViewState.do_query(dgcog, monster) if mats is None: await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) @@ -465,7 +465,7 @@ async def pantheon(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) - pantheon_list, series_name, base_monster = await PantheonViewState.query(dgcog, monster) + pantheon_list, series_name, base_monster = await PantheonViewState.do_query(dgcog, monster) if pantheon_list is None: await ctx.send('Unable to find a pantheon for the result of your query,' + ' [{}] {}.'.format(monster.monster_id, monster.name_en)) @@ -606,12 +606,12 @@ async def allmats(self, ctx, *, query: str): if not monster: await self.send_id_failure_message(ctx, query) return - monster_list = await AllMatsViewState.query(dgcog, monster) + monster_list = await AllMatsViewState.do_query(dgcog, monster) if monster_list is None: await ctx.send(inline("This monster is not a mat for anything nor does it have a gem")) return - _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.query(dgcog, monster) + _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.do_query(dgcog, monster) title = 'Material For' if usedin else 'Gem is Material For' await self._do_monster_list(ctx, dgcog, query, monster_list, title, AllMatsViewState) @@ -626,7 +626,7 @@ async def evolist(self, ctx, *, query): await self.send_id_failure_message(ctx, query) return - monster_list, _ = await EvosViewState.query(dgcog, monster) + monster_list, _ = await EvosViewState.do_query(dgcog, monster) if monster_list is None: await self.send_invalid_monster_message(ctx, query, monster, ', which has no alt evos') return @@ -686,8 +686,8 @@ async def collabscroll(self, ctx, *, query): query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) rarity = None for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: - paginated_monsters = await SeriesScrollViewState.query(dgcog, monster.series_id, - rarity, monster.server_priority) + paginated_monsters = await SeriesScrollViewState.do_query(dgcog, monster.series_id, + rarity, monster.server_priority) if paginated_monsters: break all_rarities = SeriesScrollViewState.query_all_rarities(dgcog, series_id, monster.server_priority) @@ -814,7 +814,7 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + acquire_raw = await TransformInfoViewState.do_query(dgcog, base_mon, transformed_mon) reaction_list = TransformInfoMenuPanes.get_reaction_list(len(monster_ids)) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) @@ -837,7 +837,7 @@ async def awakenings(self, ctx, *, query=None): if not query: sort_type = AwakeningListSortTypes.numerical - paginated_skills = await AwakeningListViewState.query(dgcog, sort_type) + paginated_skills = await AwakeningListViewState.do_query(dgcog, sort_type) menu = AwakeningListMenu.menu() state = AwakeningListViewState(ctx.message.author.id, AwakeningListMenu.MENU_TYPE, color, sort_type, paginated_skills, 0, @@ -1303,7 +1303,7 @@ async def idtraceback(self, ctx, *, query): async def idsearch(self, ctx, *, query): dgcog = self.bot.get_cog("Dadguide") - monster_list = await IdSearchViewState.query(dgcog, query, ctx.author.id) + monster_list = await IdSearchViewState.do_query(dgcog, query, ctx.author.id) if monster_list is None: await ctx.send("No monster matched.") diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index ae5af293..4f764323 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -19,15 +19,15 @@ async def get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] if not force_evoscroll and not settings.checkEvoID(ctx.author.id): return full_reaction_list - alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + alt_versions, gem_versions = await EvosViewState.do_query(dgcog, monster) if alt_versions is None and gem_versions is None: full_reaction_list.remove(IdMenuEmoji.left) full_reaction_list.remove(IdMenuEmoji.right) full_reaction_list.remove(IdMenuEmoji.evos) - pantheon_list, _, _ = await PantheonViewState.query(dgcog, monster) + pantheon_list, _, _ = await PantheonViewState.do_query(dgcog, monster) if pantheon_list is None: full_reaction_list.remove(IdMenuEmoji.pantheon) - mats, usedin, gemid, _, skillups, _, _, _ = await MaterialsViewState.query(dgcog, monster) + mats, usedin, gemid, _, skillups, _, _, _ = await MaterialsViewState.do_query(dgcog, monster) if mats is None and usedin is None and gemid is None and skillups is None: full_reaction_list.remove(IdMenuEmoji.mats) return full_reaction_list diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index a86dcd67..04258867 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -40,7 +40,7 @@ def serialize(self): @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): sort_type = ims['sort_type'] - paginated_skills = await cls.query(dgcog, sort_type) + paginated_skills = await cls.do_query(dgcog, sort_type) original_author_id = ims['original_author_id'] current_page = ims['current_page'] menu_type = ims['menu_type'] @@ -49,7 +49,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): current_page, reaction_list=reaction_list) @classmethod - async def query(cls, dgcog, sort_type): + async def do_query(cls, dgcog, sort_type): awoken_skills = dgcog.database.get_all_awoken_skills() if sort_type == AwakeningListSortTypes.alphabetical: awoken_skills = sorted(awoken_skills, key=lambda awo: awo.name_en) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index e0cac0c2..3974c21b 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -45,7 +45,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None monster = await get_monster_from_ims(dgcog, ims) - alt_versions, gem_versions = await EvosViewState.query(dgcog, monster) + alt_versions, gem_versions = await EvosViewState.do_query(dgcog, monster) if alt_versions is None: return None @@ -67,7 +67,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): extra_state=ims) @staticmethod - async def query(dgcog, monster): + async def do_query(dgcog, monster): db_context = dgcog.database alt_versions = sorted(db_context.graph.get_alt_monsters(monster), key=lambda x: x.monster_id) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index c8b688c7..c8e32bff 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -68,7 +68,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): monster = await get_monster_from_ims(dgcog, ims) alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ - await IdViewState.query(dgcog, monster) + await IdViewState.do_query(dgcog, monster) raw_query = ims['raw_query'] # This is to support the 2 vs 1 monster query difference between ^ls and ^id @@ -95,14 +95,14 @@ async def set_server(self, dgcog, server: Server): self.query_settings.server = server self.monster = dgcog.database.graph.get_monster(self.monster.monster_id, server=server) self.alt_monsters = self.get_alt_monsters_and_evos(dgcog, self.monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity = await IdViewState.query(dgcog, self.monster) + transform_base, true_evo_type_raw, acquire_raw, base_rarity = await self.do_query(dgcog, self.monster) self.transform_base = transform_base self.true_evo_type_raw = true_evo_type_raw self.acquire_raw = acquire_raw self.base_rarity = base_rarity @classmethod - async def query(cls, dgcog, monster): + async def do_query(cls, dgcog, monster): db_context = dgcog.database acquire_raw, base_rarity, transform_base, true_evo_type_raw = \ await IdViewState._get_monster_misc_info(db_context, monster) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 7dd74c5c..2d2547e7 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -60,7 +60,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): return None monster = await get_monster_from_ims(dgcog, ims) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ - await MaterialsViewState.query(dgcog, monster) + await MaterialsViewState.do_query(dgcog, monster) if mats is None: return None @@ -83,7 +83,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): extra_state=ims) @staticmethod - async def query(dgcog, monster): + async def do_query(dgcog, monster): db_context = dgcog.database mats = db_context.graph.evo_mats(monster) usedin = db_context.graph.material_of_monsters(monster) @@ -114,7 +114,7 @@ async def query(dgcog, monster): return None, None, None, None, None, None, None, None if not any([mats, usedin, skillups and not monster.is_stackable]): mats, gemusedin, _, usedin, skillups, skillup_evo_count, link, _ \ - = await MaterialsViewState.query(dgcog, evo_gem) + = await MaterialsViewState.do_query(dgcog, evo_gem) gem_override = True return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override diff --git a/padinfo/view/monster_list/all_mats.py b/padinfo/view/monster_list/all_mats.py index 5554b879..a380a0bc 100644 --- a/padinfo/view/monster_list/all_mats.py +++ b/padinfo/view/monster_list/all_mats.py @@ -11,8 +11,8 @@ class AllMatsViewState(MonsterListViewState): VIEW_STATE_TYPE = "AllMats" @classmethod - async def query(cls, dgcog, monster: "MonsterModel") -> Optional[List["MonsterModel"]]: - _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.query(dgcog, monster) + async def do_query(cls, dgcog, monster: "MonsterModel") -> Optional[List["MonsterModel"]]: + _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.do_query(dgcog, monster) if usedin is None and gemusedin is None: return None monster_list = usedin or gemusedin @@ -21,5 +21,5 @@ async def query(cls, dgcog, monster: "MonsterModel") -> Optional[List["MonsterMo @classmethod async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: monster = await dgcog.find_monster(ims['raw_query'], ims['original_author_id']) - monster_list = await cls.query(dgcog, monster) + monster_list = await cls.do_query(dgcog, monster) return monster_list diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py index 5526e8cd..3e3db6e8 100644 --- a/padinfo/view/monster_list/id_search.py +++ b/padinfo/view/monster_list/id_search.py @@ -10,7 +10,7 @@ class IdSearchViewState(MonsterListViewState): VIEW_STATE_TYPE = "IdSearch" @classmethod - async def query(cls, dgcog, query, original_author_id) -> Optional[List["MonsterModel"]]: + async def do_query(cls, dgcog, query, original_author_id) -> Optional[List["MonsterModel"]]: found_monsters = await dgcog.find_monsters(query, original_author_id) if not found_monsters: @@ -28,5 +28,5 @@ async def query(cls, dgcog, query, original_author_id) -> Optional[List["Monster @classmethod async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: - monster_list = await cls.query(dgcog, ims['raw_query'], ims['original_author_id']) + monster_list = await cls.do_query(dgcog, ims['raw_query'], ims['original_author_id']) return monster_list diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index 35875b87..57d761e2 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -21,7 +21,7 @@ def serialize(self): return ret @classmethod - async def query(cls, dgcog, monster_list, server) -> List["MonsterModel"]: + async def do_query(cls, dgcog, monster_list, server) -> List["MonsterModel"]: db_context: "DbContext" = dgcog.database monster_list = [db_context.graph.get_monster(int(m), server=server) for m in monster_list] return monster_list @@ -30,5 +30,5 @@ async def query(cls, dgcog, monster_list, server) -> List["MonsterModel"]: async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: monster_ids = ims['full_monster_list'] query_settings = QuerySettings.deserialize(ims['query_settings']) - monster_list = await cls.query(dgcog, monster_ids, query_settings.server) + monster_list = await cls.do_query(dgcog, monster_ids, query_settings.server) return monster_list diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 628dff92..8c9e673c 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -53,7 +53,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None monster = await get_monster_from_ims(dgcog, ims) - pantheon_list, series_name, base_monster = await PantheonViewState.query(dgcog, monster) + pantheon_list, series_name, base_monster = await PantheonViewState.do_query(dgcog, monster) if pantheon_list is None: return None @@ -80,7 +80,7 @@ def make_series_name(cls, series_name, filter_strings): return '{} [Filters: {}]'.format(series_name, ' '.join(filter_strings)) @classmethod - async def query(cls, dgcog, monster): + async def do_query(cls, dgcog, monster): # filtering rules: # 1. don't show monsters that only have mat types (or show only mats if monster is a mat) # 2. if still too many, show only monsters with the same base rarity diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 048c96ff..35b60af4 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -73,7 +73,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): rarity = ims['rarity'] all_rarities = ims['all_rarities'] query_settings = QuerySettings.deserialize(ims.get('query_settings')) - paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, query_settings.server) + paginated_monsters = await SeriesScrollViewState.do_query(dgcog, series_id, rarity, query_settings.server) current_page = ims['current_page'] monster_list = paginated_monsters[current_page] title = ims['title'] @@ -100,7 +100,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): child_message_id=child_message_id) @staticmethod - async def query(dgcog, series_id, rarity, server): + async def do_query(dgcog, series_id, rarity, server): db_context: "DbContext" = dgcog.database all_series_monsters = db_context.get_monsters_by_series(series_id, server=server) base_monsters_of_rarity = list(filter( @@ -121,7 +121,7 @@ async def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: series_id = ims['series_id'] rarity = ims['rarity'] query_settings = QuerySettings.deserialize(ims['query_settings']) - paginated_monsters = await SeriesScrollViewState.query(dgcog, series_id, rarity, query_settings.server) + paginated_monsters = await SeriesScrollViewState.do_query(dgcog, series_id, rarity, query_settings.server) return paginated_monsters diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 3b816331..0493c27c 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -57,7 +57,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): base_mon = dgcog.get_monster(base_mon_id, server=query_settings.server) transformed_mon = dgcog.get_monster(transformed_mon_id, server=query_settings.server) - acquire_raw = await TransformInfoViewState.query(dgcog, base_mon, transformed_mon) + acquire_raw = await TransformInfoViewState.do_query(dgcog, base_mon, transformed_mon) reaction_list = ims['reaction_list'] is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) \ or dgcog.database.graph.monster_is_discrepant(transformed_mon) @@ -68,7 +68,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): reaction_list=reaction_list) @staticmethod - async def query(dgcog, base_mon, transformed_mon): + async def do_query(dgcog, base_mon, transformed_mon): db_context = dgcog.database acquire_raw = db_context.graph.monster_acquisition(transformed_mon) return acquire_raw From 22c809d45e7ee384638d8a16cc3f6a9e33e61d2a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 5 Jun 2021 00:45:07 -0500 Subject: [PATCH 0822/1220] Refactor MonsterListMenu to get child ims from ViewState (#1181) Move child view_state creation down This may save an expensive query if we aren't actually changing the child view_state, so it's a performance benefit not just a code organization improvement --- padinfo/menu/monster_list.py | 26 ++++++------------- padinfo/view/monster_list/monster_list.py | 31 +++++++++++++++-------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 70de210c..5dd29fc6 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -165,20 +165,15 @@ def monster_list_control(state: MonsterListViewState): @classmethod async def click_child_number(cls, ims, emoji_clicked, **data): - view_state = await cls._get_view_state(ims, **data) emoji_response = IdMenuEmoji.refresh \ if MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} n = MonsterListMenuPanes.emoji_names().index(emoji_clicked) - extra_ims = { - 'is_child': True, - 'reaction_list': IdMenuPanes.emoji_names(), - 'menu_type': IdMenu.MENU_TYPE, - 'resolved_monster_id': view_state.monster_list[ - MonsterListMenuPanes.get_monster_index(n)].monster_id, - 'query_settings': view_state.query_settings.serialize(), - } + view_state = await cls._get_view_state(ims, **data) + view_state.set_index(MonsterListMenuPanes.get_monster_index(n)) + extra_ims = view_state.get_serialized_child_extra_ims(IdMenuPanes.emoji_names(), + IdMenu.MENU_TYPE) return emoji_response, extra_ims @classmethod @@ -193,16 +188,11 @@ async def auto_scroll_child_right(cls, ims, _emoji_clicked, **data): @staticmethod async def _scroll_child(view_state: MonsterListViewState, update_function): - # when the parent ims updated, it was a deepcopy of the ims / view_state we have now, so we have to - # redo the update function that we did before + # when the parent ims updated, it was a deepcopy of the ims / view_state we have now, + # so we have to redo the update function that we did before update_function() - extra_ims = { - 'is_child': True, - 'reaction_list': IdMenuPanes.emoji_names(), - 'menu_type': IdMenu.MENU_TYPE, - 'resolved_monster_id': view_state.monster_list[view_state.current_index].monster_id, - 'query_settings': view_state.query_settings.serialize(), - } + extra_ims = view_state.get_serialized_child_extra_ims(IdMenuPanes.emoji_names(), + IdMenu.MENU_TYPE) return IdMenuEmoji.refresh, extra_ims diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 85e14aae..5d7832e6 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -39,15 +39,18 @@ def __init__(self, original_author_id, menu_type, query, color, self.child_message_id = child_message_id self.title = title self.paginated_monsters = paginated_monsters - self.monster_list = None - self._update_monster_list() self.reaction_list = reaction_list self.color = color self.query = query self.query_settings = query_settings - def _update_monster_list(self): - self.monster_list = self.paginated_monsters[self.current_page] + @property + def current_monster_id(self) -> int: + return self.monster_list[self.current_index].monster_id + + @property + def monster_list(self) -> List["MonsterModel"]: + return self.paginated_monsters[self.current_page] def serialize(self): ret = super().serialize() @@ -64,6 +67,16 @@ def serialize(self): }) return ret + def get_serialized_child_extra_ims(self, emoji_names, menu_type): + extra_ims = { + 'is_child': True, + 'reaction_list': emoji_names, + 'menu_type': menu_type, + 'resolved_monster_id': self.current_monster_id, + 'query_settings': self.query_settings.serialize(), + } + return extra_ims + @classmethod async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): @@ -109,7 +122,6 @@ async def query_paginated_from_ims(cls, dgcog, ims) -> List[List["MonsterModel"] def increment_page(self): if self.current_page < len(self.paginated_monsters) - 1: self.current_page = self.current_page + 1 - self._update_monster_list() self.current_index = None return self.current_page = 0 @@ -119,11 +131,9 @@ def increment_page(self): def decrement_page(self): if self.current_page > 0: self.current_page = self.current_page - 1 - self._update_monster_list() self.current_index = None return self.current_page = len(self.paginated_monsters) - 1 - self._update_monster_list() if len(self.paginated_monsters) > 1: self.current_index = None @@ -137,12 +147,10 @@ def increment_index(self): return if self.current_index == max_index and self.current_page < len(self.paginated_monsters) - 1: self.current_page = self.current_page + 1 - self._update_monster_list() self.current_index = 0 return if self.current_index == max_index: self.current_page = 0 - self._update_monster_list() self.current_index = 0 def decrement_index(self): @@ -155,15 +163,16 @@ def decrement_index(self): return if self.current_index == 0 and self.current_page > 0: self.current_page = self.current_page - 1 - self._update_monster_list() self.current_index = MonsterListViewState.MAX_ITEMS_PER_PANE - 1 return if self.current_index == 0: # respond with left but then set the current index to the max thing possible self.current_page = len(self.paginated_monsters) - 1 - self._update_monster_list() self.current_index = len(self.paginated_monsters[-1]) - 1 + def set_index(self, new_index): + self.current_index = new_index + def _monster_list(monsters): if not len(monsters): From e7a8b84eb812257d2796fe057920556169fc8f74 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 5 Jun 2021 05:34:08 -0500 Subject: [PATCH 0823/1220] MVP version of ^nadiffs (#1183) --- padinfo/menu/monster_list.py | 25 ++++++----- padinfo/menu/na_diff.py | 26 +++++++++++ padinfo/padinfo.py | 53 +++++++++++++---------- padinfo/view/common.py | 7 +++ padinfo/view/id.py | 25 ++++++++++- padinfo/view/monster_list/monster_list.py | 18 ++++++-- 6 files changed, 115 insertions(+), 39 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 5dd29fc6..0ab7fd72 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -6,6 +6,7 @@ from tsutils.menu.panes import emoji_buttons, MenuPanes from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji +from padinfo.menu.na_diff import NaDiffMenu, NaDiffEmoji from padinfo.view.id import IdView from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState @@ -37,7 +38,6 @@ class MonsterListEmoji: class MonsterListMenu: MENU_TYPE = 'MonsterListMenu' - CHILD_MENU_TYPE = 'IdMenu' view_state_types = { AllMatsViewState.VIEW_STATE_TYPE: AllMatsViewState, @@ -45,6 +45,11 @@ class MonsterListMenu: StaticMonsterListViewState.VIEW_STATE_TYPE: StaticMonsterListViewState, } + child_menu_type_to_emoji_response_map = { + IdMenu.MENU_TYPE: IdMenuEmoji.refresh, + NaDiffMenu.MENU_TYPE: NaDiffEmoji.home + } + @staticmethod def menu(initial_control=None): if initial_control is None: @@ -165,15 +170,13 @@ def monster_list_control(state: MonsterListViewState): @classmethod async def click_child_number(cls, ims, emoji_clicked, **data): - emoji_response = IdMenuEmoji.refresh \ - if MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None - if emoji_response is None: + if not MonsterListMenuPanes.respond_to_emoji_with_child(emoji_clicked): return None, {} n = MonsterListMenuPanes.emoji_names().index(emoji_clicked) view_state = await cls._get_view_state(ims, **data) view_state.set_index(MonsterListMenuPanes.get_monster_index(n)) - extra_ims = view_state.get_serialized_child_extra_ims(IdMenuPanes.emoji_names(), - IdMenu.MENU_TYPE) + extra_ims = view_state.get_serialized_child_extra_ims() + emoji_response = cls.child_menu_type_to_emoji_response_map[view_state.child_menu_type] return emoji_response, extra_ims @classmethod @@ -186,14 +189,14 @@ async def auto_scroll_child_right(cls, ims, _emoji_clicked, **data): view_state = await cls._get_view_state(ims, **data) return await MonsterListMenu._scroll_child(view_state, view_state.increment_index) - @staticmethod - async def _scroll_child(view_state: MonsterListViewState, update_function): + @classmethod + async def _scroll_child(cls, view_state: MonsterListViewState, update_function): # when the parent ims updated, it was a deepcopy of the ims / view_state we have now, # so we have to redo the update function that we did before update_function() - extra_ims = view_state.get_serialized_child_extra_ims(IdMenuPanes.emoji_names(), - IdMenu.MENU_TYPE) - return IdMenuEmoji.refresh, extra_ims + extra_ims = view_state.get_serialized_child_extra_ims() + emoji_response = cls.child_menu_type_to_emoji_response_map[view_state.child_menu_type] + return emoji_response, extra_ims class MonsterListMenuPanes(MenuPanes): diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index f608422c..f9241b7e 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -8,9 +8,11 @@ from tsutils.menu.panes import MenuPanes from padinfo.view.id import IdViewState, IdView +from padinfo.view.simple_text import SimpleTextViewState, SimpleTextView class NaDiffEmoji: + home = '\N{HOUSE BUILDING}' na = '\N{REGIONAL INDICATOR SYMBOL LETTER U}\N{REGIONAL INDICATOR SYMBOL LETTER S}' jp = '\N{REGIONAL INDICATOR SYMBOL LETTER J}\N{REGIONAL INDICATOR SYMBOL LETTER P}' @@ -25,6 +27,16 @@ def menu(initial_control=None): embed = EmbedMenu(NaDiffMenuPanes.transitions(), initial_control) return embed + @staticmethod + async def respond_with_home(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + user_config = data['user_config'] + view_state = await IdViewState.deserialize(dgcog, user_config, ims) + if view_state.set_na_diff_invalid_message(ims): + new_view_state = await SimpleTextViewState.deserialize(dgcog, user_config, ims) + return NaDiffMenu.message_control(new_view_state) + return await NaDiffMenu.respond_with_na(message, ims, **data) + @staticmethod async def respond_with_na(message: Optional[Message], ims, **data): return await NaDiffMenu.respond_with_id(message, ims, Server.NA, **data) @@ -53,9 +65,23 @@ def id_control(state: IdViewState): reaction_list or [emoji_cache.get_by_name(e) for e in NaDiffMenuPanes.emoji_names()] ) + @staticmethod + def message_control(state: SimpleTextViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [SimpleTextView.embed(state)], + reaction_list + ) + class NaDiffMenuPanes(MenuPanes): DATA = { + NaDiffEmoji.home: (NaDiffMenu.respond_with_home, IdView.VIEW_TYPE), NaDiffEmoji.na: (NaDiffMenu.respond_with_na, IdView.VIEW_TYPE), NaDiffEmoji.jp: (NaDiffMenu.respond_with_jp, IdView.VIEW_TYPE), } + HIDDEN_EMOJIS = [ + NaDiffEmoji.home, + ] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9a766b58..b0116cca 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -33,7 +33,7 @@ from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu from padinfo.menu.menu_map import padinfo_menu_map from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji -from padinfo.menu.na_diff import NaDiffMenu +from padinfo.menu.na_diff import NaDiffMenu, NaDiffMenuPanes from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji from padinfo.menu.simple_text import SimpleTextMenu from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes @@ -41,6 +41,7 @@ from padinfo.view.awakening_help import AwakeningHelpView, AwakeningHelpViewProps from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListSortTypes from padinfo.view.closable_embed import ClosableEmbedViewState +from padinfo.view.common import invalid_monster_text from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.evos import EvosViewState from padinfo.view.id import IdViewState @@ -329,8 +330,7 @@ async def checkbadness(self, ctx): @staticmethod async def send_invalid_monster_message(ctx, query: str, monster: "MonsterModel", append_text: str): - base_text = 'Your query `{}` found {}{}.' - await ctx.send(base_text.format(query, MonsterHeader.short_with_emoji(monster, link=False).to_markdown(), append_text)) + await ctx.send(invalid_monster_text(query, monster, append_text)) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -341,30 +341,17 @@ async def nadiff(self, ctx, *, query: str): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - original_monster = await dgcog.find_monster(raw_query, ctx.author.id) - await self.log_id_result(ctx, original_monster.monster_id) - if original_monster is None: - await self.send_id_failure_message(ctx, query) - return - - if original_monster.on_na and not original_monster.on_jp: - await self.send_invalid_monster_message(ctx, query, original_monster, ', which is only in NA') - return - - monster = await dgcog.find_monster(raw_query + ' --na', ctx.author.id) + monster = await dgcog.find_monster(raw_query, ctx.author.id) + await self.log_id_result(ctx, monster.monster_id) if monster is None: - await self.send_invalid_monster_message(ctx, query, original_monster, ', which is only in JP') - return - - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) - if not is_jp_buffed: - await self.send_invalid_monster_message(ctx, query, monster, ', which is the same in NA & JP') + await self.send_id_failure_message(ctx, query) return alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ await IdViewState.do_query(dgcog, monster) + is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = IdViewState(original_author_id, NaDiffMenu.MENU_TYPE, raw_query, query, color, monster, @@ -372,6 +359,11 @@ async def nadiff(self, ctx, *, query: str): transform_base, true_evo_type_raw, acquire_raw, base_rarity, ) menu = NaDiffMenu.menu() + message = state.get_na_diff_invalid_message() + if message: + state = SimpleTextViewState(original_author_id, NaDiffMenu.MENU_TYPE, + raw_query, color, message) + menu = NaDiffMenu.menu(initial_control=NaDiffMenu.message_control) await menu.create(ctx, state) @commands.command(name="evos") @@ -633,7 +625,9 @@ async def evolist(self, ctx, *, query): await self._do_monster_list(ctx, dgcog, query, monster_list, 'Evolution List', StaticMonsterListViewState) async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], - title, view_state_type: Type[MonsterListViewState]): + title, view_state_type: Type[MonsterListViewState], + child_menu_type: str = IdMenu.MENU_TYPE, + child_reaction_list: List = IdMenuPanes.emoji_names()): raw_query = query original_author_id = ctx.message.author.id color = await self.get_user_embed_color(ctx) @@ -644,6 +638,8 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM state = view_state_type(original_author_id, view_state_type.VIEW_STATE_TYPE, query, color, monster_list, query_settings, title, instruction_message, + child_menu_type=child_menu_type, + child_reaction_list=child_reaction_list, reaction_list=initial_reaction_list ) parent_menu = MonsterListMenu.menu() @@ -1301,6 +1297,16 @@ async def idtraceback(self, ctx, *, query): @commands.command(aliases=["ids"]) @checks.bot_has_permissions(embed_links=True) async def idsearch(self, ctx, *, query): + await self._do_idsearch(ctx, query, child_menu_type=IdMenu.MENU_TYPE) + + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def nadiffs(self, ctx, *, query): + await self._do_idsearch(ctx, query, child_menu_type=NaDiffMenu.MENU_TYPE, + child_reaction_list=NaDiffMenuPanes.emoji_names()) + + async def _do_idsearch(self, ctx, query, child_menu_type=IdMenu.MENU_TYPE, + child_reaction_list=IdMenuPanes.emoji_names()): dgcog = self.bot.get_cog("Dadguide") monster_list = await IdSearchViewState.do_query(dgcog, query, ctx.author.id) @@ -1309,7 +1315,10 @@ async def idsearch(self, ctx, *, query): await ctx.send("No monster matched.") return - await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results', IdSearchViewState) + await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results', + IdSearchViewState, + child_menu_type=child_menu_type, + child_reaction_list=child_reaction_list) async def get_fm_flags(self, user: discord.User): return await self.bot.get_cog("Dadguide").config.user(user).fm_flags() diff --git a/padinfo/view/common.py b/padinfo/view/common.py index cf449818..9b0e1fe2 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -5,8 +5,10 @@ from tsutils.query_settings import QuerySettings from padinfo.common.emoji_map import get_awakening_emoji +from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel from dadguide.models.awakening_model import AwokenSkillModel @@ -31,3 +33,8 @@ def get_awoken_skill_description(awoken_skill: "AwokenSkillModel"): Text(desc), delimiter=' ' ) + + +def invalid_monster_text(query: str, monster: "MonsterModel", append_text: str, link=False): + base_text = 'Your query `{}` found {}{}.' + return base_text.format(query, MonsterHeader.short_with_emoji(monster, link=link).to_markdown(), append_text) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index c8e32bff..bae6e5f8 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField @@ -13,7 +13,7 @@ from padinfo.common.external_links import puzzledragonx from padinfo.core.leader_skills import createMultiplierText from padinfo.view.base import BaseIdView -from padinfo.view.common import get_monster_from_ims +from padinfo.view.common import get_monster_from_ims, invalid_monster_text from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution @@ -34,6 +34,10 @@ def alt_fmt(monsterevo, state): class IdViewState(ViewStateBaseId): + nadiff_na_only_text = ', which is only in NA' + nadiff_jp_only_text = ', which is only in JP' + nadiff_identical_text = ', which is the same in NA & JP' + def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, transform_base, true_evo_type_raw, acquire_raw, base_rarity, @@ -117,6 +121,23 @@ async def _get_monster_misc_info(cls, db_context, monster): base_rarity = db_context.graph.get_base_monster(monster).rarity return acquire_raw, base_rarity, transform_base, true_evo_type_raw + def set_na_diff_invalid_message(self, ims): + message = self.get_na_diff_invalid_message() + if message is not None: + ims['message'] = message + return True + return False + + def get_na_diff_invalid_message(self) -> Optional[str]: + monster: "MonsterModel" = self.monster + if monster.on_na and not monster.on_jp: + return invalid_monster_text(self.query, monster, self.nadiff_na_only_text, link=True) + if monster.on_jp and not monster.on_na: + return invalid_monster_text(self.query, monster, self.nadiff_jp_only_text, link=True) + if not self.is_jp_buffed: + return invalid_monster_text(self.query, monster, self.nadiff_identical_text, link=True) + return None + def _get_awakening_text(awakening: "AwakeningModel"): return get_awakening_emoji(awakening.awoken_skill_id, awakening.name) diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 5d7832e6..2d0fda68 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField @@ -25,6 +25,8 @@ def __init__(self, original_author_id, menu_type, query, color, *, current_page: int = 0, current_index: int = None, + child_menu_type: str = None, + child_reaction_list: Optional[List] = None, reaction_list=None, extra_state=None, child_message_id=None @@ -37,6 +39,8 @@ def __init__(self, original_author_id, menu_type, query, color, self.page_count = len(paginated_monsters) self.message = message self.child_message_id = child_message_id + self.child_menu_type = child_menu_type + self.child_reaction_list = child_reaction_list self.title = title self.paginated_monsters = paginated_monsters self.reaction_list = reaction_list @@ -64,14 +68,16 @@ def serialize(self): 'child_message_id': self.child_message_id, 'message': self.message, 'current_index': self.current_index, + 'child_menu_type': self.child_menu_type, + 'child_reaction_list': self.child_reaction_list, }) return ret - def get_serialized_child_extra_ims(self, emoji_names, menu_type): + def get_serialized_child_extra_ims(self): extra_ims = { 'is_child': True, - 'reaction_list': emoji_names, - 'menu_type': menu_type, + 'reaction_list': self.child_reaction_list, + 'menu_type': self.child_menu_type, 'resolved_monster_id': self.current_monster_id, 'query_settings': self.query_settings.serialize(), } @@ -94,12 +100,16 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') child_message_id = ims.get('child_message_id') + child_menu_type = ims.get('child_menu_type') + child_reaction_list = ims.get('child_reaction_list') message = ims.get('message') return MonsterListViewState(original_author_id, menu_type, query, user_config.color, monster_list, query_settings, title, message, current_page=current_page, current_index=current_index, + child_menu_type=child_menu_type, + child_reaction_list=child_reaction_list, reaction_list=reaction_list, extra_state=ims, child_message_id=child_message_id From 4a7320631310325e4dadf2954056beb8dfac414f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 5 Jun 2021 06:43:08 -0500 Subject: [PATCH 0824/1220] Fix nadiffs child message delete function (#1184) Additionally: * Adds extra_state to SimpleTextViewState * Refactors respond_with_delete in IdMenu --- padinfo/menu/id.py | 13 +++++++------ padinfo/menu/na_diff.py | 14 +++++++++++++- padinfo/view/id.py | 2 +- padinfo/view/monster_list/monster_list.py | 9 +++++---- padinfo/view/simple_text.py | 10 ++++++---- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index a68e6cf9..995647a2 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -106,12 +106,13 @@ async def respond_with_refresh(message: Optional[Message], ims, **data): @staticmethod async def respond_with_delete(message: Optional[Message], ims, **data): - if ims.get('is_child'): - if ims.get('message'): - ims['menu_type'] = SimpleTextMenu.MENU_TYPE - return await SimpleTextMenu.respond_with_message(message, ims, **data) - return await message.edit(embed=None) - return await message.delete() + if not ims.get('is_child'): + return await message.delete() + if ims.get('idle_message'): + ims['menu_type'] = SimpleTextMenu.MENU_TYPE + ims['message'] = ims['idle_message'] + return await SimpleTextMenu.respond_with_message(message, ims, **data) + return await message.edit(embed=None) @staticmethod async def respond_with_current_id(message: Optional[Message], ims, **data): diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index f9241b7e..e7e8342d 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -7,6 +7,7 @@ from tsutils.enums import Server from tsutils.menu.panes import MenuPanes +from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.id import IdViewState, IdView from padinfo.view.simple_text import SimpleTextViewState, SimpleTextView @@ -24,7 +25,8 @@ class NaDiffMenu: def menu(initial_control=None): if initial_control is None: initial_control = NaDiffMenu.id_control - embed = EmbedMenu(NaDiffMenuPanes.transitions(), initial_control) + embed = EmbedMenu(NaDiffMenuPanes.transitions(), initial_control, + delete_func=NaDiffMenu.respond_with_delete) return embed @staticmethod @@ -55,6 +57,16 @@ async def respond_with_id(message: Optional[Message], ims, server: Server, **dat control = NaDiffMenu.id_control(view_state) return control + @staticmethod + async def respond_with_delete(message: Optional[Message], ims, **data): + if not ims.get('is_child'): + return await message.delete() + if ims.get('idle_message'): + ims['menu_type'] = SimpleTextMenu.MENU_TYPE + ims['message'] = ims['idle_message'] + return await SimpleTextMenu.respond_with_message(message, ims, **data) + return await message.edit(embed=None) + @staticmethod def id_control(state: IdViewState): if state is None: diff --git a/padinfo/view/id.py b/padinfo/view/id.py index bae6e5f8..795a2932 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -121,7 +121,7 @@ async def _get_monster_misc_info(cls, db_context, monster): base_rarity = db_context.graph.get_base_monster(monster).rarity return acquire_raw, base_rarity, transform_base, true_evo_type_raw - def set_na_diff_invalid_message(self, ims): + def set_na_diff_invalid_message(self, ims: dict) -> bool: message = self.get_na_diff_invalid_message() if message is not None: ims['message'] = message diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 2d0fda68..7a68d878 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -37,7 +37,7 @@ def __init__(self, original_author_id, menu_type, query, color, self.current_index = current_index self.current_page = current_page self.page_count = len(paginated_monsters) - self.message = message + self.idle_message = message self.child_message_id = child_message_id self.child_menu_type = child_menu_type self.child_reaction_list = child_reaction_list @@ -66,7 +66,7 @@ def serialize(self): 'current_page': self.current_page, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, - 'message': self.message, + 'idle_message': self.idle_message, 'current_index': self.current_index, 'child_menu_type': self.child_menu_type, 'child_reaction_list': self.child_reaction_list, @@ -80,6 +80,7 @@ def get_serialized_child_extra_ims(self): 'menu_type': self.child_menu_type, 'resolved_monster_id': self.current_monster_id, 'query_settings': self.query_settings.serialize(), + 'idle_message': self.idle_message, } return extra_ims @@ -102,10 +103,10 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): child_message_id = ims.get('child_message_id') child_menu_type = ims.get('child_menu_type') child_reaction_list = ims.get('child_reaction_list') - message = ims.get('message') + idle_message = ims.get('idle_message') return MonsterListViewState(original_author_id, menu_type, query, user_config.color, monster_list, query_settings, - title, message, + title, idle_message, current_page=current_page, current_index=current_index, child_menu_type=child_menu_type, diff --git a/padinfo/view/simple_text.py b/padinfo/view/simple_text.py index 9e49f1c9..4b5d00ec 100644 --- a/padinfo/view/simple_text.py +++ b/padinfo/view/simple_text.py @@ -11,8 +11,10 @@ class SimpleTextViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, message, - reaction_list: List[str] = None): - super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) + reaction_list: List[str] = None, + extra_state=None): + super().__init__(original_author_id, menu_type, raw_query, + reaction_list=reaction_list, extra_state=extra_state) self.message = message self.color = color @@ -24,12 +26,12 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, _dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] raw_query = ims.get('raw_query') return cls(original_author_id, menu_type, raw_query, user_config.color, ims.get('message'), - reaction_list=ims.get('reaction_list')) + reaction_list=ims.get('reaction_list'), extra_state=ims) class SimpleTextView: From 79de9ae25979db25d682698e3246d9c8417b6550 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 5 Jun 2021 06:04:56 -0700 Subject: [PATCH 0825/1220] [PDI] Update info.json (#1185) --- padinfo/info.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/padinfo/info.json b/padinfo/info.json index 830eea28..950db0e8 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,10 +20,11 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.3.4", + "tsutils>=3.3.8", "prettytable", "python-levenshtein", - "discord-menu>=0.16.0" + "discord-menu>=0.16.0", + "tabulate" ], "tags": [ "PAD" From 59bb6c05c6fe6f19515269e5462f313a09892507 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 5 Jun 2021 08:59:39 -0500 Subject: [PATCH 0826/1220] Support --nadiff in any MonsterList query (#1186) --- padinfo/menu/awakening_list.py | 3 ++- padinfo/menu/id.py | 3 ++- padinfo/menu/na_diff.py | 4 ++-- padinfo/padinfo.py | 17 +++++++++++------ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index 359129ea..116d5987 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -3,6 +3,7 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu +from tsutils.enums import ChildMenuSelector from tsutils.menu.panes import emoji_buttons, MenuPanes from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListView, AwakeningListSortTypes @@ -19,7 +20,7 @@ class AwakeningListEmoji: class AwakeningListMenu: - MENU_TYPE = 'AwakeningList' + MENU_TYPE = ChildMenuSelector.AwakeningList.name @staticmethod def menu(initial_control=None): diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 995647a2..e5fa614d 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -4,6 +4,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji +from tsutils.enums import ChildMenuSelector from tsutils.menu.panes import MenuPanes from tsutils.query_settings import QuerySettings @@ -22,7 +23,7 @@ class IdMenu: - MENU_TYPE = 'IdMenu' + MENU_TYPE = ChildMenuSelector.IdMenu.name @staticmethod def menu(initial_control=None): diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index e7e8342d..c7bbd390 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -4,7 +4,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu from discordmenu.emoji.emoji_cache import emoji_cache -from tsutils.enums import Server +from tsutils.enums import Server, ChildMenuSelector from tsutils.menu.panes import MenuPanes from padinfo.menu.simple_text import SimpleTextMenu @@ -19,7 +19,7 @@ class NaDiffEmoji: class NaDiffMenu: - MENU_TYPE = "NaDiffMenu" + MENU_TYPE = ChildMenuSelector.NaDiffMenu.name @staticmethod def menu(initial_control=None): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b0116cca..6012024a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -626,8 +626,9 @@ async def evolist(self, ctx, *, query): async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], title, view_state_type: Type[MonsterListViewState], - child_menu_type: str = IdMenu.MENU_TYPE, - child_reaction_list: List = IdMenuPanes.emoji_names()): + child_menu_type: Optional[str] = None, + child_reaction_list: Optional[List] = None + ): raw_query = query original_author_id = ctx.message.author.id color = await self.get_user_embed_color(ctx) @@ -635,6 +636,11 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' + if child_menu_type is None: + child_menu_type = query_settings.menuselect.name + _, child_panes_class = padinfo_menu_map[child_menu_type] + child_reaction_list = child_panes_class.emoji_names() + state = view_state_type(original_author_id, view_state_type.VIEW_STATE_TYPE, query, color, monster_list, query_settings, title, instruction_message, @@ -1302,11 +1308,10 @@ async def idsearch(self, ctx, *, query): @commands.command() @checks.bot_has_permissions(embed_links=True) async def nadiffs(self, ctx, *, query): - await self._do_idsearch(ctx, query, child_menu_type=NaDiffMenu.MENU_TYPE, - child_reaction_list=NaDiffMenuPanes.emoji_names()) + await self._do_idsearch(ctx, query) - async def _do_idsearch(self, ctx, query, child_menu_type=IdMenu.MENU_TYPE, - child_reaction_list=IdMenuPanes.emoji_names()): + async def _do_idsearch(self, ctx, query, child_menu_type=None, + child_reaction_list=None): dgcog = self.bot.get_cog("Dadguide") monster_list = await IdSearchViewState.do_query(dgcog, query, ctx.author.id) From fd6ef120b20955b26e7aab6d8c828cc59830fc36 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 5 Jun 2021 09:20:46 -0500 Subject: [PATCH 0827/1220] Support --dfs & --numerical query flags (#1187) does NOT yet support saving actual preferences --- padinfo/view/components/view_state_base_id.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 0b4356f3..e0376411 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, List, NamedTuple, Optional from discordmenu.embed.view_state import ViewState +from tsutils.enums import AltEvoSort from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -24,7 +25,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst extra_state=None): super().__init__(original_author_id=original_author_id, menu_type=menu_type, raw_query=raw_query, extra_state=extra_state) - self.alt_monsters = alt_monsters + self.dfs_alt_monsters = alt_monsters self.reaction_list = reaction_list self.color = color self.monster = monster @@ -33,6 +34,11 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.query_settings = query_settings self.use_evo_scroll = use_evo_scroll + if self.query_settings.evosort == AltEvoSort.dfs: + self.alt_monsters = self.dfs_alt_monsters + else: + self.alt_monsters = sorted(self.dfs_alt_monsters, key=lambda m: m.monster.monster_id) + def serialize(self): ret = super().serialize() ret.update({ From 45b367e662dee8019f0bbf9ca95a57e8f3387eed Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 5 Jun 2021 09:31:38 -0500 Subject: [PATCH 0828/1220] Fix nadiffs & ids (#1188) --- padinfo/padinfo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6012024a..e85af72e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1303,12 +1303,13 @@ async def idtraceback(self, ctx, *, query): @commands.command(aliases=["ids"]) @checks.bot_has_permissions(embed_links=True) async def idsearch(self, ctx, *, query): - await self._do_idsearch(ctx, query, child_menu_type=IdMenu.MENU_TYPE) + await self._do_idsearch(ctx, query) @commands.command() @checks.bot_has_permissions(embed_links=True) async def nadiffs(self, ctx, *, query): - await self._do_idsearch(ctx, query) + await self._do_idsearch(ctx, query, child_menu_type=NaDiffMenu.MENU_TYPE, + child_reaction_list=NaDiffMenuPanes.emoji_names()) async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): From 1eee0ae79b3a6618f98d2e41816c4d06f659c6b5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 6 Jun 2021 00:40:56 -0500 Subject: [PATCH 0829/1220] Add idset evosort preference (#1189) --- padinfo/padinfo.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e85af72e..78e8e240 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -16,7 +16,7 @@ from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate from tsutils import char_to_emoji, is_donor, safe_read_json -from tsutils.enums import Server +from tsutils.enums import Server, AltEvoSort from tsutils.query_settings import QuerySettings from padinfo.common.config import BotConfig @@ -998,6 +998,25 @@ async def server(self, ctx, server: str): return await ctx.tick() + @idset.command() + async def evosort(self, ctx, value: str): + """Change the order for scrolling alt evos in your your `[p]id` queries + + `[p]idset evosort numerical`: Show alt evos sorted by card ID number. + `[p]idset evosort dfs`: Show alt evos in a depth-first-sort order, starting with the base of the tree. + """ + dgcog = await self.get_dgcog() + async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + value = value.lower() + if value == "dfs": + fm_flags['evosort'] = AltEvoSort.dfs.value + elif value == "numerical": + fm_flags['evosort'] = AltEvoSort.numerical.value + else: + await ctx.send(f'Please input an allowed value, either `{AltEvoSort.dfs.name}` or `{AltEvoSort.numerical.name}`.') + return + await ctx.send(f"Your `{ctx.prefix}id` evo sort preference has been set to **{value}**.") + @commands.group() @checks.is_owner() async def padinfo(self, ctx): From 0793edd81830b7bb0561853f150a6e8c2ace90f5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 6 Jun 2021 01:13:40 -0500 Subject: [PATCH 0830/1220] Add tradeable modifier (#1190) --- dadguide/monster_index.py | 3 +++ dadguide/token_mappings.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 04d0981e..1bce0abc 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -427,6 +427,9 @@ def is_story(m, do_transform=True): if self.graph.monster_is_mp_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.MP]) + if monster.sell_mp < 100: + modifiers.update(MISC_MAP[MiscModifiers.TRADEABLE]) + # Art if monster.orb_skin_id: modifiers.update(MISC_MAP[MiscModifiers.ORBSKIN]) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 7059eba0..870a5417 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -173,6 +173,7 @@ class MiscModifiers(Enum): CHIBI = 'Chibi' STORY = 'Story' FARMABLE = 'Farmable' + TRADEABLE = 'Tradeable' REM = 'REM' MP = 'MP' INJP = 'In JP Server' @@ -193,6 +194,7 @@ class MiscModifiers(Enum): MiscModifiers.CHIBI: ('chibi', 'mini'), MiscModifiers.STORY: ('story',), MiscModifiers.FARMABLE: ('farmable',), + MiscModifiers.TRADEABLE: ('tradeable',), MiscModifiers.REM: ('rem',), MiscModifiers.MP: ('mp',), MiscModifiers.INJP: ('injp',), From 30448a175964d4af7a400c9b37b41377a5fd48e1 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 6 Jun 2021 01:23:18 -0500 Subject: [PATCH 0831/1220] pdchu - Support 120 slb (#1191) --- padbuildimg/padbuildimg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 5970cb82..db75b966 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -44,7 +44,7 @@ Stats Format: | LV### SLV## AW# SA# +H## +A## +R## +(0 or 297) | indicates end of card name and start of stats - LV: level, 1 to 110 + LV: level, 1 to 120 SLV: skill level, 1 to 99 or MAX AW: awakenings, 0 to 9 SA: super awakening, 0 to 9 @@ -55,17 +55,17 @@ Case insensitive, order does not matter """ EXAMPLE_MSG = "Examples:\n1P{}\n2P{}\n3P{}\nLatent Validation{}\nStats Validation{}".format( - box("bj(weld)lv110/baldin[gok *3](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian)"), + box("bj(weld)lv120/baldin[gok *3](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian)"), box("amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf)"), box("zela(assist amen) *3/base raizer * 2/zela; zela(assist amen) *4/base valeria/zela; zela * 6"), box("eir[drk,drk,sdr]/eir[bak,bak,sdr]/eir[sdr *4, dek]/eir[sdr *8, dek]"), - box("dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15") + box("dmeta(uruka|lv120+297slvmax)|+h33+a66+r99lv120slv15/ hmyne(buruka|lv120+297slv1)|+h99+a99+r99lv120slv15") ) """ Examples: 1P: - bj(weld)lv110/baldin[gok, gok, gok](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian) + bj(weld)lv120/baldin[gok, gok, gok](gilgamesh)/youyu(assist reeche)/mel(chocolate)/isis(koenma)/bj(rathian) 2P: amen/dios(sdr) * 3/whaledor; mnoah(assist jack frost) *3/tengu/tengu[sdr,sdr,sdr,sdr,sdr,sdr](durandalf) 3P: @@ -73,7 +73,7 @@ Latent Validation: eir[drk,drk,sdr]/eir[bak,bak,sdr] Stats Validation: - dmeta(uruka|lv110+297slvmax)|+h33+a66+r99lv110slv15/ hmyne(buruka|lv110+297slv1)|+h99+a99+r99lv110slv15 + dmeta(uruka|lv120+297slvmax)|+h33+a66+r99lv120slv15/ hmyne(buruka|lv120+297slv1)|+h99+a99+r99lv120slv15 """ @@ -555,7 +555,7 @@ async def process_card(self, card_str, is_assist=False): ) result_card['LV'] = min( result_card['LV'], - 110 if card.limit_mult is not None and card.limit_mult > 1 else card.level + 120 if card.limit_mult is not None and card.limit_mult > 1 else card.level ) if card.active_skill: result_card['MAX_SLV'] = card.active_skill.turn_max - card.active_skill.turn_min + 1 From 820e14fd794eaec29586c63746df17e5c12c1551 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 9 Jun 2021 08:04:34 -0500 Subject: [PATCH 0832/1220] Show series on front page of ^id (#1200) --- padinfo/view/id.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 795a2932..ffe9306b 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -248,10 +248,11 @@ def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_ cost = LabeledText('Cost', str(m.cost)) acquire = BoldText(acquire_raw) if acquire_raw else None + series = BoldText(m.series.name_en) if m.series else None valid_true_evo_types = ("Reincarnated", "Assist", "Pixel", "Super Reincarnated") true_evo_type = BoldText(true_evo_type_raw) if true_evo_type_raw in valid_true_evo_types else None - return Box(rarity, cost, acquire, true_evo_type) + return Box(rarity, cost, series, acquire, true_evo_type) @staticmethod def stats(m: "MonsterModel"): From d66670b4c497b6eb4606d5fc9cdbb760b4fef08b Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Thu, 10 Jun 2021 01:45:37 -0400 Subject: [PATCH 0833/1220] Update simple tree checking in padglobal which (#1201) --- padglobal/padglobal.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index b3500156..397dfa01 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1395,13 +1395,16 @@ def emojify(self, message): def check_simple_tree(monster, db_context): attr1 = monster.attr1 active_skill = monster.active_skill - for m in db_context.graph.get_alt_monsters(monster): + for m in (evo_tree := db_context.graph.get_evo_tree(monster)): if m.attr1 != attr1 or m.active_skill.active_skill_id != active_skill.active_skill_id: return False if m.is_equip: return False - if 'awoken' in m.name_en.lower(): + if len(db_context.graph.get_next_evolutions(m)) > 1: return False + # every evo has super awakenings (can limit break) + if len(evo_tree) > 1 and all(m.superawakening_count for m in evo_tree): + return False return True From 2c6c1450e831fa8f880a004bffffa2cdfc5e289c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 10 Jun 2021 00:46:26 -0500 Subject: [PATCH 0834/1220] Make stats +297 on overview page (#1198) --- padinfo/view/id.py | 10 ++++++---- padinfo/view/otherinfo.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index ffe9306b..947a5aa9 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -256,8 +256,8 @@ def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_ @staticmethod def stats(m: "MonsterModel"): - hp, atk, rcv, weighted = m.stats() - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(lv=110) if m.limit_mult > 0 else (None, None, None, None) + hp, atk, rcv, weighted = m.stats(plus=297) + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(plus=297, lv=110) if m.limit_mult > 0 else (None, None, None, None) return Box( LabeledText('HP', _get_stat_text(hp, lb_hp, _get_awakening_emoji_for_stats(m, 1))), LabeledText('ATK', _get_stat_text(atk, lb_atk, _get_awakening_emoji_for_stats(m, 2))), @@ -267,9 +267,11 @@ def stats(m: "MonsterModel"): @staticmethod def stats_header(m: "MonsterModel"): - voice = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' + voice_emoji = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' + plus_297_emoji = get_emoji('plus_297') header = Box( - Text(voice), + Text(voice_emoji), + Text(plus_297_emoji), Text('Stats'), Text('(LB, +{}%)'.format(m.limit_mult)) if m.limit_mult else None, delimiter=' ' diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 69f15709..6acfb62b 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -28,16 +28,17 @@ def serialize(self): return ret -def statsbox(m): +def statsbox(m, plus: int): stat_cols = ['', 'HP', 'ATK', 'RCV'] tbl = prettytable.PrettyTable(stat_cols) tbl.hrules = prettytable.NONE tbl.vrules = prettytable.NONE tbl.align = "l" levels = (m.level, 110, 120) if m.limit_mult > 0 else (m.level,) + inh_tuple = (False, True) if plus == 297 else (False,) for lv in levels: - for inh in (False, True): - hp, atk, rcv, _ = m.stats(lv, plus=297, inherit=inh) + for inh in inh_tuple: + hp, atk, rcv, _ = m.stats(lv, plus=plus, inherit=inh) row_name = '(Inh)' if inh else 'Lv{}'.format(lv) tbl.add_row([row_name, hp, atk, rcv]) return box(tbl.get_string()) @@ -61,7 +62,10 @@ def embed(cls, state: OtherInfoViewState): EmbedField( "Stats at +297:", Box( - Text(statsbox(m)), + # need to put these on the same line to get around discord's insane + # whitespace margins around code blocks + Text(statsbox(m, plus=297) + 'Stats at +0:'), + Text(statsbox(m, plus=0)), LabeledText("JP Name", m.name_ja), LinksView.linksbox(m), LabeledText("JP Added", str(m.reg_date)) if m.reg_date else None, From 24051c7f2493bbe0b3709157f3d0540f81914362 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 10 Jun 2021 07:25:13 -0500 Subject: [PATCH 0835/1220] Support settings & flags for lsmultiplier & cardplus (#1203) --- padinfo/core/leader_skills.py | 22 +++++------ padinfo/padinfo.py | 57 +++++++++++++++++++++++++++-- padinfo/view/id.py | 27 ++++++++------ padinfo/view/leader_skill.py | 8 ++-- padinfo/view/leader_skill_single.py | 4 +- 5 files changed, 85 insertions(+), 33 deletions(-) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 24c4631c..0043bcd1 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,7 +1,7 @@ import re -def humanize_number(number, sigfigs=2): +def _humanize_number(number, sigfigs=2): n = float("{0:.{1}g}".format(number, sigfigs)) if n >= 1e9: return str(int(n // 1e9)) + "B" @@ -13,7 +13,7 @@ def humanize_number(number, sigfigs=2): return str(int(n)) -def createMultiplierText(ls1, ls2=False): +def ls_multiplier_text(ls1, ls2=False): if ls2 and not ls1: ls1, ls2 = ls2, ls1 @@ -31,7 +31,7 @@ def createMultiplierText(ls1, ls2=False): else: raise ValueError(f"ls2 must be LeaderSkillModel or None, not {type(ls2)}") - return format_ls_text( + return _format_ls_text( hp1 * hp2, atk1 * atk2, rcv1 * rcv2, @@ -43,33 +43,33 @@ def createMultiplierText(ls1, ls2=False): ) -def createSingleMultiplierText(ls=None): +def ls_single_multiplier_text(ls=None): if ls: hp, atk, rcv, resist, combo, fua, mfua, te = ls.data else: hp, atk, rcv, resist, combo, fua, mfua, te = 1, 1, 1, 0, 0, 0, 0, 0 - return format_ls_text(hp, atk, rcv, resist, combo, fua, mfua, te) + return _format_ls_text(hp, atk, rcv, resist, combo, fua, mfua, te) -def format_number(val): +def _format_number(val): return '{:.2f}'.format(val).strip('0').rstrip('.') -def format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): - resist = ' Resist {}%'.format(format_number(100 * resist)) if resist else '' +def _format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): + resist = ' Resist {}%'.format(_format_number(100 * resist)) if resist else '' combos = '+{}c'.format(combo) if combo else '' - true_damage = '{}'.format(humanize_number(fua, 2)) if fua else '' + true_damage = '{}'.format(_humanize_number(fua, 2)) if fua else '' any_fua = 'fua' if fua or mfua else '' joined = ' '.join((a for a in [combos, true_damage, any_fua] if a)) extras = f"[{joined}]" if joined else '' - return f"[{format_number(hp)}/{format_number(atk)}/{format_number(rcv)}{resist}] {extras}" + return f"[{_format_number(hp)}/{_format_number(atk)}/{_format_number(rcv)}{resist}] {extras}" -async def perform_leaderskill_query(dgcog, raw_query, author_id): +async def leaderskill_query(dgcog, raw_query, author_id): # Remove unicode quotation marks query = re.sub("[\u201c\u201d]", '"', raw_query) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 78e8e240..ed6724f4 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -16,14 +16,14 @@ from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file from tabulate import tabulate from tsutils import char_to_emoji, is_donor, safe_read_json -from tsutils.enums import Server, AltEvoSort +from tsutils.enums import Server, AltEvoSort, LsMultiplier, CardPlusModifier from tsutils.query_settings import QuerySettings from padinfo.common.config import BotConfig from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ get_attribute_emoji_by_monster, AWAKENING_ID_TO_EMOJI_NAME_MAP from padinfo.core.button_info import button_info -from padinfo.core.leader_skills import perform_leaderskill_query +from padinfo.core.leader_skills import leaderskill_query from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.menu.awakening_list import AwakeningListMenu, AwakeningListMenuPanes @@ -742,7 +742,7 @@ async def leaderskill(self, ctx, *, raw_query): [p]ls sonia lubu """ dgcog = await self.get_dgcog() - l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, ctx.author.id) + l_mon, l_query, r_mon, r_query = await leaderskill_query(dgcog, raw_query, ctx.author.id) err_msg = ('{} query failed to match a monster: [ {} ]. If your query is multiple words,' ' try separating the queries with / or wrap with quotes.') @@ -1013,10 +1013,59 @@ async def evosort(self, ctx, value: str): elif value == "numerical": fm_flags['evosort'] = AltEvoSort.numerical.value else: - await ctx.send(f'Please input an allowed value, either `{AltEvoSort.dfs.name}` or `{AltEvoSort.numerical.name}`.') + await ctx.send( + f'Please input an allowed value, either `{AltEvoSort.dfs.name}` or `{AltEvoSort.numerical.name}`.') return await ctx.send(f"Your `{ctx.prefix}id` evo sort preference has been set to **{value}**.") + @idset.command() + async def lsmultiplier(self, ctx, value: str): + """Change the display of leader skill multipliers in your `[p]id` queries + + `[p]idset lsmultiplier double`: [Default] Show leader skill multipliers assuming the card is paired with itself. + `[p]idset lsmultiplier single`: Show leader skill multipliers of just the single card. + """ + dgcog = await self.get_dgcog() + async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + value = value.lower() + if value == "double": + fm_flags['lsmultiplier'] = LsMultiplier.lsdouble.value + not_value = 'single' + not_value_flag = 'lssingle' + elif value == "single": + fm_flags['lsmultiplier'] = LsMultiplier.lssingle.value + not_value = 'double' + not_value_flag = 'lsdouble' + else: + await ctx.send( + f'Please input an allowed value, either `double` or `single`.') + return + await ctx.send(f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + + @idset.command() + async def cardplus(self, ctx, value: str): + """Change the monster stat plus points amount in your your `[p]id` queries + + `[p]idset cardplus 297`: [Default] Show cards with +297 stats. + `[p]idset cardplus 0`: Show cards with +0 stats. + """ + dgcog = await self.get_dgcog() + async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + value = value.lower() + if value == "297": + fm_flags['cardplus'] = CardPlusModifier.plus297.value + not_value = '0' + not_value_flag = 'plus0' + elif value == "0": + fm_flags['cardplus'] = CardPlusModifier.plus0.value + not_value = '297' + not_value_flag = 'plus297' + else: + await ctx.send( + f'Please input an allowed value, either `297` or `0`.') + return + await ctx.send(f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + @commands.group() @checks.is_owner() async def padinfo(self, ctx): diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 947a5aa9..de710913 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -4,14 +4,14 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state -from tsutils.enums import Server +from tsutils import embed_footer_with_state, char_to_emoji +from tsutils.enums import Server, LsMultiplier, CardPlusModifier from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx -from padinfo.core.leader_skills import createMultiplierText +from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims, invalid_monster_text from padinfo.view.components.monster.header import MonsterHeader @@ -255,8 +255,9 @@ def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_ return Box(rarity, cost, series, acquire, true_evo_type) @staticmethod - def stats(m: "MonsterModel"): - hp, atk, rcv, weighted = m.stats(plus=297) + def stats(m: "MonsterModel", cardplus: CardPlusModifier): + plus = 297 if cardplus == CardPlusModifier.plus297 else 0 + hp, atk, rcv, weighted = m.stats(plus=plus) lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(plus=297, lv=110) if m.limit_mult > 0 else (None, None, None, None) return Box( LabeledText('HP', _get_stat_text(hp, lb_hp, _get_awakening_emoji_for_stats(m, 1))), @@ -266,9 +267,10 @@ def stats(m: "MonsterModel"): ) @staticmethod - def stats_header(m: "MonsterModel"): + def stats_header(m: "MonsterModel", cardplus: CardPlusModifier): voice_emoji = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' - plus_297_emoji = get_emoji('plus_297') + # TODO: Get a +0 emoji for the +0 setting + plus_297_emoji = get_emoji('plus_297') if cardplus == CardPlusModifier.plus297 else '' header = Box( Text(voice_emoji), Text(plus_297_emoji), @@ -299,10 +301,11 @@ def active_skill_header(m: "MonsterModel", transform_base): ) @staticmethod - def leader_skill_header(m: "MonsterModel"): + def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier): return Box( BoldText('Leader Skill'), - BoldText(createMultiplierText(m.leader_skill)), + BoldText(ls_multiplier_text(m.leader_skill) if lsmultiplier == LsMultiplier.lsdouble + else char_to_emoji(1) + ' ' + ls_single_multiplier_text(m.leader_skill)), delimiter=' ' ) @@ -323,8 +326,8 @@ def embed(cls, state: IdViewState): inline=True ), EmbedField( - IdView.stats_header(m).to_markdown(), - IdView.stats(m), + IdView.stats_header(m, state.query_settings.cardplus).to_markdown(), + IdView.stats(m, state.query_settings.cardplus), inline=True ), EmbedField( @@ -332,7 +335,7 @@ def embed(cls, state: IdViewState): Text(m.active_skill.desc if m.active_skill else 'None') ), EmbedField( - IdView.leader_skill_header(m).to_markdown(), + IdView.leader_skill_header(m, state.query_settings.lsmultiplier).to_markdown(), Text(m.leader_skill.desc if m.leader_skill else 'None') ), evos_embed_field(state) diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 44c1af04..0c043ba5 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -8,8 +8,8 @@ from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig -from padinfo.core.leader_skills import createMultiplierText -from padinfo.core.leader_skills import perform_leaderskill_query +from padinfo.core.leader_skills import ls_multiplier_text +from padinfo.core.leader_skills import leaderskill_query from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base import ViewStateBase @@ -47,7 +47,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): l_query_settings = QuerySettings.deserialize(ims['l_query_settings']) r_query_settings = QuerySettings.deserialize(ims['r_query_settings']) - l_mon, l_query, r_mon, r_query = await perform_leaderskill_query(dgcog, raw_query, ims['original_author_id']) + l_mon, l_query, r_mon, r_query = await leaderskill_query(dgcog, raw_query, ims['original_author_id']) return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, r_query, l_query_settings, r_query_settings) @@ -61,7 +61,7 @@ def embed(state: LeaderSkillViewState): rls = state.r_mon.leader_skill return EmbedView( embed_main=EmbedMain( - title=createMultiplierText(lls, rls), + title=ls_multiplier_text(lls, rls), description=Box( BoldText(MonsterHeader.name(state.l_mon, link=True, show_jp=True)), Text(lls.desc if lls else 'None'), diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index c05b93a3..777e80ec 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -8,7 +8,7 @@ from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig -from padinfo.core.leader_skills import createSingleMultiplierText +from padinfo.core.leader_skills import ls_single_multiplier_text from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base import ViewStateBase from padinfo.view.common import get_monster_from_ims @@ -50,7 +50,7 @@ def embed(state: LeaderSkillSingleViewState): ls = state.mon.leader_skill return EmbedView( embed_main=EmbedMain( - title=createSingleMultiplierText(ls), + title=ls_single_multiplier_text(ls), description=Box( BoldText(MonsterHeader.name(state.mon, link=True, show_jp=True)), Text(ls.desc if ls else 'None')), From 6cb4c71e43beb0ac8bd9e2d7abefd9fdd14f603d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 10 Jun 2021 07:37:24 -0500 Subject: [PATCH 0836/1220] Add "tradable" as alias of "tradeable" (#1206) --- dadguide/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 870a5417..9917fe22 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -194,7 +194,7 @@ class MiscModifiers(Enum): MiscModifiers.CHIBI: ('chibi', 'mini'), MiscModifiers.STORY: ('story',), MiscModifiers.FARMABLE: ('farmable',), - MiscModifiers.TRADEABLE: ('tradeable',), + MiscModifiers.TRADEABLE: ('tradeable', 'tradable'), MiscModifiers.REM: ('rem',), MiscModifiers.MP: ('mp',), MiscModifiers.INJP: ('injp',), From f6a4eb6e29a4ebcad4f82aeb811cb008e4fa5416 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 10 Jun 2021 08:01:32 -0500 Subject: [PATCH 0837/1220] Use ChildMenuType, not ChildMenuSelector (#1208) --- padinfo/info.json | 2 +- padinfo/menu/awakening_list.py | 4 ++-- padinfo/menu/id.py | 4 ++-- padinfo/menu/na_diff.py | 4 ++-- padinfo/padinfo.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/padinfo/info.json b/padinfo/info.json index 950db0e8..e75a0096 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.3.8", + "tsutils>=3.4.0", "prettytable", "python-levenshtein", "discord-menu>=0.16.0", diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index 116d5987..95aeae2d 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -3,7 +3,7 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from tsutils.enums import ChildMenuSelector +from tsutils.enums import ChildMenuType from tsutils.menu.panes import emoji_buttons, MenuPanes from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListView, AwakeningListSortTypes @@ -20,7 +20,7 @@ class AwakeningListEmoji: class AwakeningListMenu: - MENU_TYPE = ChildMenuSelector.AwakeningList.name + MENU_TYPE = ChildMenuType.AwakeningList.name @staticmethod def menu(initial_control=None): diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index e5fa614d..01fb97d0 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -4,7 +4,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from tsutils.enums import ChildMenuSelector +from tsutils.enums import ChildMenuType from tsutils.menu.panes import MenuPanes from tsutils.query_settings import QuerySettings @@ -23,7 +23,7 @@ class IdMenu: - MENU_TYPE = ChildMenuSelector.IdMenu.name + MENU_TYPE = ChildMenuType.IdMenu.name @staticmethod def menu(initial_control=None): diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index c7bbd390..65f3479f 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -4,7 +4,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu from discordmenu.emoji.emoji_cache import emoji_cache -from tsutils.enums import Server, ChildMenuSelector +from tsutils.enums import Server, ChildMenuType from tsutils.menu.panes import MenuPanes from padinfo.menu.simple_text import SimpleTextMenu @@ -19,7 +19,7 @@ class NaDiffEmoji: class NaDiffMenu: - MENU_TYPE = ChildMenuSelector.NaDiffMenu.name + MENU_TYPE = ChildMenuType.NaDiffMenu.name @staticmethod def menu(initial_control=None): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ed6724f4..e22c35b7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -637,7 +637,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM instruction_message = 'Click a reaction to see monster details!' if child_menu_type is None: - child_menu_type = query_settings.menuselect.name + child_menu_type = query_settings.child_menu_type.name _, child_panes_class = padinfo_menu_map[child_menu_type] child_reaction_list = child_panes_class.emoji_names() From 50a3a0f6b9a13e2f722cc213558a9f5c098f6aa1 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Fri, 11 Jun 2021 01:37:46 -0400 Subject: [PATCH 0838/1220] Add edit ops for pguide, pboss, etc (#1210) --- padglobal/padglobal.py | 73 ++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 397dfa01..6f5ce196 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -538,16 +538,23 @@ async def pglossary_add(self, ctx, term, *, definition): e.x. [p]pglossary add alb Awoken Liu Bei e.x. [p]pglossary add "never dathena" NA will never get dathena """ + await self._pglossary_add(ctx, term, definition, True) + + @pglossary.command(name='edit') + async def pglossary_edit(self, ctx, term, *, definition): + """Edit a term from the glossary.""" + await self._pglossary_add(ctx, term, definition, False) + + async def _pglossary_add(self, ctx, term, definition, need_confirm=True): term = term.lower() definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') definition = replace_emoji_names_with_code(self._get_emojis(), definition) op = 'edited' if term in self.settings.glossary() else 'added' - if op == 'edited' and not await confirm_message(ctx, - "Are you sure you want to edit the glossary info for {}?".format( - term)): - return + if op == 'edited' and need_confirm: + if not await confirm_message(ctx, "Are you sure you want to edit the glossary info for {}?".format(term)): + return self.settings.addGlossary(term, definition) await ctx.send("PAD glossary term successfully {}.".format(bold(op))) @@ -633,6 +640,14 @@ async def pboss_add(self, ctx, term, *, definition): If you want to use a multiple word boss name, enclose it in quotes. """ + await self._pboss_add(ctx, term, definition, True) + + @pboss.command(name='edit') + async def pboss_edit(self, ctx, term, *, definition): + """Edit a set of boss mechanics.""" + await self._pboss_add(ctx, term, definition, False) + + async def _pboss_add(self, ctx, term, definition, need_confirm=True): dgcog = self.bot.get_cog('Dadguide') pdicog = self.bot.get_cog("PadInfo") @@ -645,10 +660,10 @@ async def pboss_add(self, ctx, term, *, definition): base = dgcog.database.graph.get_base_monster(m) op = 'edited' if base.monster_id in self.settings.boss() else 'added' - if op == 'edited' and not await confirm_message(ctx, - "Are you sure you want to edit the boss info for {}?".format( - base.name_en)): - return + if op == 'edited' and need_confirm: + if not await confirm_message(ctx, + "Are you sure you want to edit the boss info for {}?".format(base.name_en)): + return definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') definition = replace_emoji_names_with_code(self._get_emojis(), definition) @@ -801,6 +816,14 @@ async def pwhich_add(self, ctx, term, *, definition): Accepts queries. The which text will be entered for the resulting monster's tree. e.x. [p]pwhich add 3818 take the pixel one """ + await self._pwhich_add(ctx, term, definition, True) + + @pwhich.command(name='edit') + async def pwhich_edit(self, ctx, term, *, definition): + """Edit an entry from the which monster evo list.""" + await self._pwhich_add(ctx, term, definition, False) + + async def _pwhich_add(self, ctx, term, definition, need_confirm=True): dgcog = self.bot.get_cog("Dadguide") pdicog = self.bot.get_cog("PadInfo") @@ -819,7 +842,7 @@ async def pwhich_add(self, ctx, term, *, definition): is_int = re.fullmatch(r'\d+', term) op = 'edited' if name in self.settings.which() else 'added' - if op == 'added' and not is_int or op == 'edited': + if not is_int or op == 'edited' and need_confirm: if not await confirm_message(ctx, "Are you sure you want to {} which info for {} [{}] {}?".format( 'edit the' if op == 'edited' else 'add new', pdicog.get_attribute_emoji_by_monster(m), @@ -1256,12 +1279,20 @@ async def dungeon(self, ctx): @dungeon.command(name='add') async def dungeon_add(self, ctx, term: str, *, definition: str): """Adds a dungeon guide to the [p]guide command""" + await self._dungeon_add(ctx, term, definition, True) + + @dungeon.command(name='edit') + async def dungeon_edit(self, ctx, term: str, *, definition: str): + """Edit a dungeon guide from the [p]guide command.""" + await self._dungeon_add(ctx, term, definition, False) + + async def _dungeon_add(self, ctx, term: str, definition: str, need_confirm=True): term = term.lower() op = 'edited' if term in self.settings.dungeonGuide() else 'added' - if op == 'edited' and not await confirm_message(ctx, - "Are you sure you want to edit the dungeon guide info for {}?".format( - term)): - return + if op == 'edited' and need_confirm: + if not await confirm_message(ctx, + "Are you sure you want to edit the dungeon guide info for {}?".format(term)): + return self.settings.addDungeonGuide(term, definition) await ctx.send("PAD dungeon guide successfully {}.".format(bold(op))) @@ -1286,6 +1317,14 @@ async def leader(self, ctx): @leader.command(name='add') async def leader_add(self, ctx, monster_id: int, *, definition: str): """Adds a leader guide to the [p]guide command""" + await self._leader_add(ctx, monster_id, definition, True) + + @leader.command(name='edit') + async def leader_edit(self, ctx, monster_id: int, *, definition: str): + """Edit a leader guide from the [p]guide command.""" + await self._leader_add(ctx, monster_id, definition, False) + + async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm=True): m = self.bot.get_cog("Dadguide").get_monster(monster_id) base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: @@ -1294,10 +1333,10 @@ async def leader_add(self, ctx, monster_id: int, *, definition: str): name = m.monster_id op = 'edited' if name in self.settings.leaderGuide() else 'added' - if op == 'edited' and not await confirm_message(ctx, - "Are you sure you want to edit the leader guide for {}?".format( - m.name_en)): - return + if op == 'edited' and need_confirm: + if not await confirm_message(ctx, + "Are you sure you want to edit the leader guide for {}?".format(m.name_en)): + return self.settings.addLeaderGuide(name, definition) await ctx.send("PAD leader guide info successfully {}.".format(bold(op))) From 47612a1aacdd694c5e329d51de6ee0ebefbb61d1 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 03:58:22 -0500 Subject: [PATCH 0839/1220] Update readme (#1211) --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 62e18737..1901d2ac 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Code should be PEP 8 formatted with a 120 character line limit. ## Check out the repo Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/padbot-cogs). + +> Note: If you are developing a cog that is' in the [misc-cogs repo](https://github.com/TsubakiBotPad/misc-cogs), you may need to complete some steps for both pad-cogs AND misc-cogs. + You will need a GitHub account to do this. Click the button that says "Fork" in the upper-right-hand corner of the page. I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) for development. Change into the @@ -32,6 +35,8 @@ The installation instructions in Red's official documentation are pretty good. Y * Install the Red library * Create a Discord API key, make a bot, and associate it to your installation +> You may want to perform the next step, "Installing dependencies," partway through the RED installation. The ideal time is immediately after you have created your venv, but before doing anything else. + Installation instruction links: * [Windows install instructions](https://docs.discord.red/en/stable/install_windows.html) * [Linux/Mac install instructions](https://docs.discord.red/en/stable/install_linux_mac.html) @@ -51,12 +56,15 @@ deactivate ``` The rest of the guide takes place from inside Discord. Replace `^` with your prefix. -## Configuring development version +## Telling your bot where to find your cogs Once the bot is launched, set it to use your repo directory as a cog path. Type this in Discord where the bot is: ``` ^addpath path/to/repo/root ``` + +> Note: you may have to do this for both `pad-cogs` and `misc-cogs`. + ## Loading cogs * Some cogs will have cross dependencies on each other. Use `^traceback 1` if you encounter any errors to see your traceback. @@ -65,11 +73,10 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type * padinfo ### Emoji -* You probably don't actually need emoji to be working in order to do development for Tsubaki. -* There are over seven emoji servers. River has access to these, talk to her if you think you need access to them. In-depth development of the `^id` command may be reason to get access to them. +* If you are doing in-depth development of the `^id` command, you may need your bot invited to the emoji servers. River has access to these, talk to her. * You will use `^padinfo emojiserver add` with the IDs she gives you. -# Puzzle and Dragons +## Cog list Most cogs here relate to the mobile game 'Puzzle & Dragons'. From a7c90fa9e47f6d7001ac719ec1a2c8a7feae0414 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 03:59:54 -0500 Subject: [PATCH 0840/1220] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1901d2ac..a7f6f140 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Code should be PEP 8 formatted with a 120 character line limit. Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/padbot-cogs). -> Note: If you are developing a cog that is' in the [misc-cogs repo](https://github.com/TsubakiBotPad/misc-cogs), you may need to complete some steps for both pad-cogs AND misc-cogs. +> Note: If you are developing a cog that is in the [misc-cogs repo](https://github.com/TsubakiBotPad/misc-cogs), you may need to complete some steps for both pad-cogs AND misc-cogs. You will need a GitHub account to do this. Click the button that says "Fork" in the upper-right-hand corner of the page. From 58ab9117f49c89f16cd55fbac522c901aa0c2125 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 08:51:37 -0500 Subject: [PATCH 0841/1220] Error message reporting monster id for invalid graph state (#1212) --- dadguide/errors.py | 14 ++++++++++++++ dadguide/monster_graph.py | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 dadguide/errors.py diff --git a/dadguide/errors.py b/dadguide/errors.py new file mode 100644 index 00000000..d56d6810 --- /dev/null +++ b/dadguide/errors.py @@ -0,0 +1,14 @@ +class InvalidGraphState(KeyError): + def __init__(self, monster_id: int): + self.monster_id = monster_id + + def __str__(self): + return 'Invalid graph state, no model found for node ' + str(self.monster_id) + + +class InvalidMonsterId(KeyError): + def __init__(self, monster_id: int): + self.monster_id = monster_id + + def __str__(self): + return '{} is not found in the graph'.format(str(self.monster_id)) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index ac13641e..2c138bed 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -7,6 +7,7 @@ from tsutils.enums import Server from .database_manager import DadguideDatabase +from .errors import InvalidGraphState from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel @@ -306,7 +307,11 @@ def _get_edge_model(self, monster: MonsterModel, etype) -> Optional[BaseModel]: def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[MonsterModel]: if monster_id not in self.graph_dict[server].nodes: return None - return self.graph_dict[server].nodes[monster_id]['model'] + try: + server_graph = self.graph_dict[server] + return server_graph.nodes[monster_id]['model'] + except KeyError: + raise InvalidGraphState(monster_id) def get_all_monsters(self, server: Server) -> Set[MonsterModel]: return {mdata['model'] for mdata in self.graph_dict[server].nodes.values()} From 4c38807303de5c2141379451ee2e8ce16c6d8856 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 09:03:14 -0500 Subject: [PATCH 0842/1220] Don't fail if an evo errors (#1213) --- dadguide/monster_graph.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 2c138bed..dfd729ce 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -282,8 +282,12 @@ def build_graph(self, server: Server) -> MultiDiGraph: def _cache_graphs(self) -> None: for server in self.graph_dict: for mid in self.graph_dict[server].nodes: - self.graph_dict[server].nodes[mid]['alt_versions'] = self.process_alt_ids( - self.get_monster(mid, server=server)) + try: + self.graph_dict[server].nodes[mid]['alt_versions'] = self.process_alt_ids( + self.get_monster(mid, server=server)) + except InvalidGraphState: + # TODO: Actually log something to a channel + pass def _get_edges(self, monster: MonsterModel, etype) -> Set[int]: return {mid for mid, atlas in self.graph_dict[monster.server_priority][monster.monster_id].items() From 00151a8e37f1a357006eb5f8a9aedfe5a0e2b2e7 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 09:09:15 -0500 Subject: [PATCH 0843/1220] Don't fail in get_all_monsters (#1214) --- dadguide/monster_graph.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index dfd729ce..f837afe8 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -318,7 +318,9 @@ def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> Op raise InvalidGraphState(monster_id) def get_all_monsters(self, server: Server) -> Set[MonsterModel]: - return {mdata['model'] for mdata in self.graph_dict[server].nodes.values()} + # Fail gracefully if one of the nodes doesn't exist + # TODO: log which node doesn't exist? Or unneeded bc we will do that at startup + return {mdata['model'] for mdata in self.graph_dict[server].nodes.values() if mdata.get('model')} def get_evo_tree(self, monster: MonsterModel) -> List[MonsterModel]: while (prev := self.get_prev_evolution(monster)): From 35ffa1c2e2b410d5f4b873a41cec596b3b302d99 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 09:15:17 -0500 Subject: [PATCH 0844/1220] Fail gracefully on invalid graph state in index (#1215) --- dadguide/monster_index.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 1bce0abc..a202d46b 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -9,6 +9,7 @@ from redbot.core.utils import AsyncIter from tsutils.enums import Server +from .errors import InvalidGraphState from .models.monster_model import MonsterModel from .monster_graph import MonsterGraph from .models.enum_types import DEFAULT_SERVER @@ -63,7 +64,13 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): ) for m_id, name, lp, ov, i in nickname_data: - if m_id.isdigit() and not i and graph.get_monster(int(m_id), server=server): + if m_id.isdigit() and not i: + try: + monster = graph.get_monster(int(m_id), server=server) + except InvalidGraphState: + continue + if not monster: + continue name = name.strip().lower() mid = int(m_id) if lp: From 79d4df5db3c6f044af14ef3d0dff0f4dbae8a1d4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 09:23:22 -0500 Subject: [PATCH 0845/1220] Catch more cases in index (#1216) --- dadguide/monster_index.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index a202d46b..513c1e58 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -84,7 +84,13 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.monster_id_to_nickname[mid].add(name.lower().replace(" ", "")) for m_id, name, mp, ov, i in treenames_data: - if m_id.isdigit() and not i and graph.get_monster(int(m_id), server=server): + if m_id.isdigit() and not i: + try: + monster = graph.get_monster(int(m_id), server=server) + except InvalidGraphState: + continue + if not monster: + continue name = name.strip().lower() mid = int(m_id) if ov: @@ -112,7 +118,13 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.manual_prefixes = defaultdict(set) for mid, mods, rmv in mod_data: - if mid.isdigit() and graph.get_monster(int(mid), server=server): + if mid.isdigit(): + try: + monster = graph.get_monster(int(mid), server=server) + except InvalidGraphState: + continue + if not monster: + continue mid = int(mid) for mod in mods.split(","): mod = mod.strip().lower() From 52dfd9c3444fdff59cb8624c4dc83e9b5bb9a1d1 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 10:05:23 -0500 Subject: [PATCH 0846/1220] Last try to fix broken graph state (#1217) --- dadguide/monster_graph.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index f837afe8..778b5639 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -241,7 +241,8 @@ def build_graph(self, server: Server) -> MultiDiGraph: has_hqimage=m.has_hqimage == 1, server_priority=server, ) - + if not m_model: + continue graph.add_node(m.monster_id, model=m_model) if m.linked_monster_id: graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') From 157381bf13d3422f80df2704dc20c26eef2c4224 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 10:41:39 -0500 Subject: [PATCH 0847/1220] Check nodes are valid before adding edges (#1218) --- dadguide/monster_graph.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 778b5639..024b4fb0 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -256,6 +256,15 @@ def build_graph(self, server: Server) -> MultiDiGraph: for e in es: evo_model = EvolutionModel(**e) + # TODO: add logging in these error cases + if evo_model.from_id not in graph.nodes: + continue + if evo_model.to_id not in graph.nodes: + continue + if graph.nodes[evo_model.from_id].get('model') is None: + continue + if graph.nodes[evo_model.to_id].get('model') is None: + continue graph.add_edge( evo_model.from_id, evo_model.to_id, type='evolution', model=evo_model) From a16d0c9616a4ff3d91a668c1f248f1311a45c048 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 10:46:06 -0500 Subject: [PATCH 0848/1220] Ignore InvalidGraphState again (#1219) --- dadguide/monster_index.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 513c1e58..890bafd7 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -441,8 +441,11 @@ def is_story(m, do_transform=True): if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) else: - if self.graph.monster_is_vendor_exchange(monster): - modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) + try: + if self.graph.monster_is_vendor_exchange(monster): + modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) + except InvalidGraphState: + pass if self.graph.monster_is_mp_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.MP]) From a35f8394bf81f619390ac8c018d8230deb526836 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 13:24:07 -0500 Subject: [PATCH 0849/1220] Fix typo in evosort docstring (#1220) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e22c35b7..ecee002e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1000,7 +1000,7 @@ async def server(self, ctx, server: str): @idset.command() async def evosort(self, ctx, value: str): - """Change the order for scrolling alt evos in your your `[p]id` queries + """Change the order for scrolling alt evos in your `[p]id` queries `[p]idset evosort numerical`: Show alt evos sorted by card ID number. `[p]idset evosort dfs`: Show alt evos in a depth-first-sort order, starting with the base of the tree. From b3365de05bc3b1a3ae07f5179ff98b05a59177c0 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 11 Jun 2021 13:40:09 -0500 Subject: [PATCH 0850/1220] Fix evosort preference (#1221) This fix is sort of a hack and should be improved in a later refactor --- padinfo/menu/id.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 01fb97d0..1498b85f 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -4,7 +4,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from tsutils.enums import ChildMenuType +from tsutils.enums import ChildMenuType, AltEvoSort from tsutils.menu.panes import MenuPanes from tsutils.query_settings import QuerySettings @@ -43,7 +43,8 @@ async def respond_with_left(message: Optional[Message], ims, **data): m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=query_settings.server) use_evo_scroll = ims.get('use_evo_scroll') != 'False' - new_monster_id = IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll) + evosort = query_settings.evosort + new_monster_id = IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll, evosort) if new_monster_id is None: ims['unsupported_transition'] = True ims['resolved_monster_id'] = str(new_monster_id) if new_monster_id else None @@ -53,9 +54,11 @@ async def respond_with_left(message: Optional[Message], ims, **data): return await response_func(message, ims, **data) @staticmethod - def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): + def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll, evosort): if use_evo_scroll: evos = db_context.graph.get_alt_ids(monster) + if evosort != AltEvoSort.dfs: + evos = sorted(evos) index = evos.index(monster.monster_id) new_id = evos[index - 1] return new_id @@ -72,7 +75,8 @@ async def respond_with_right(message: Optional[Message], ims, **data): m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=query_settings.server) use_evo_scroll = ims.get('use_evo_scroll') != 'False' - new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll) or '') + evosort = query_settings.evosort + new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll, evosort) or '') if new_monster_id is None: ims['unsupported_transition'] = True ims['resolved_monster_id'] = new_monster_id @@ -82,9 +86,11 @@ async def respond_with_right(message: Optional[Message], ims, **data): return await response_func(message, ims, **data) @staticmethod - def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll): + def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll, evosort): if use_evo_scroll: evos = db_context.graph.get_alt_ids(monster) + if evosort != AltEvoSort.dfs: + evos = sorted(evos) index = evos.index(monster.monster_id) if index == len(evos) - 1: # cycle back to the beginning of the evos list From c00f0393c3cda8e7750b119846e11ef58264e15f Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Jun 2021 04:16:53 -0700 Subject: [PATCH 0851/1220] Log invalid graph warnings (#1225) * Log on invalid graph * whoops * Log better --- dadguide/dadguide.py | 21 +++++++++++++-------- dadguide/errors.py | 7 +------ dadguide/models/enum_types.py | 2 +- dadguide/monster_graph.py | 29 ++++++++++------------------- 4 files changed, 25 insertions(+), 34 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 5e14beda..1e3fbf0d 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -143,20 +143,25 @@ async def forceindexreload3(self, ctx): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" for server in SERVERS: - self.indexes[server] = await MonsterIndex(self.database.graph, server) # noqa + index = await MonsterIndex(self.database.graph, server) # noqa + self.indexes[server] = index + self.mon_finder = FindMonster(self, self.fm_flags_default) asyncio.create_task(self.check_index()) async def check_index(self): - if not await self.config.indexlog(): + channel = self.bot.get_channel(await self.config.indexlog()) + if not channel: return - index = self.indexes[Server.COMBINED] - index.issues.extend((await self.run_tests())[:25]) + issues = [] + issues.extend(self.database.graph.issues) + for index in self.indexes.values(): + issues.extend(index.issues) + issues.extend((await self.run_tests())[:25]) - channel = self.bot.get_channel(await self.config.indexlog()) - if index.issues: - for page in pagify(f"Index Load Warnings:\n" + "\n".join(index.issues[:100])): + if issues: + for page in pagify(f"Load Warnings:\n" + "\n".join(issues[:100])): await channel.send(box(page)) def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> MonsterModel: @@ -194,7 +199,7 @@ async def reload_data_task(self): await asyncio.sleep(wait_time) except Exception as ex: logger.exception("dadguide data wait loop failed: %s", ex) - raise ex + raise async def download_and_refresh_nicknames(self): if await self.config.datafile(): diff --git a/dadguide/errors.py b/dadguide/errors.py index d56d6810..93fe7422 100644 --- a/dadguide/errors.py +++ b/dadguide/errors.py @@ -1,10 +1,5 @@ class InvalidGraphState(KeyError): - def __init__(self, monster_id: int): - self.monster_id = monster_id - - def __str__(self): - return 'Invalid graph state, no model found for node ' + str(self.monster_id) - + pass class InvalidMonsterId(KeyError): def __init__(self, monster_id: int): diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 16be00d3..7f739ace 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -55,7 +55,7 @@ def enum_or_none(enum, value, default=None): DEFAULT_SERVER = Server.COMBINED -SERVERS = [Server.NA, Server.COMBINED] +SERVERS = [Server.COMBINED, Server.NA] class AwokenSkills(Enum): diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 024b4fb0..2b1d66a4 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -111,6 +111,8 @@ class MonsterGraph(object): def __init__(self, database: DadguideDatabase): + self.issues = [] + self.database = database self.max_monster_id = -1 self.graph_dict: Dict[Server, MultiDiGraph] = { # noqa @@ -256,15 +258,6 @@ def build_graph(self, server: Server) -> MultiDiGraph: for e in es: evo_model = EvolutionModel(**e) - # TODO: add logging in these error cases - if evo_model.from_id not in graph.nodes: - continue - if evo_model.to_id not in graph.nodes: - continue - if graph.nodes[evo_model.from_id].get('model') is None: - continue - if graph.nodes[evo_model.to_id].get('model') is None: - continue graph.add_edge( evo_model.from_id, evo_model.to_id, type='evolution', model=evo_model) @@ -292,12 +285,11 @@ def build_graph(self, server: Server) -> MultiDiGraph: def _cache_graphs(self) -> None: for server in self.graph_dict: for mid in self.graph_dict[server].nodes: - try: + if 'model' in self.graph_dict[server].nodes[mid]: self.graph_dict[server].nodes[mid]['alt_versions'] = self.process_alt_ids( self.get_monster(mid, server=server)) - except InvalidGraphState: - # TODO: Actually log something to a channel - pass + else: + self.issues.append(f"{mid} has no model in the {server.name} graph.") def _get_edges(self, monster: MonsterModel, etype) -> Set[int]: return {mid for mid, atlas in self.graph_dict[monster.server_priority][monster.monster_id].items() @@ -318,14 +310,13 @@ def _get_edge_model(self, monster: MonsterModel, etype) -> Optional[BaseModel]: return None return sorted(possible_results, key=lambda x: x.tstamp)[-1] - def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[MonsterModel]: + def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER, do_logging: bool = False) \ + -> Optional[MonsterModel]: if monster_id not in self.graph_dict[server].nodes: return None - try: - server_graph = self.graph_dict[server] - return server_graph.nodes[monster_id]['model'] - except KeyError: - raise InvalidGraphState(monster_id) + if 'model' not in self.graph_dict[server].nodes[monster_id]: + raise InvalidGraphState(f"{monster_id} has no model in the {server.name} graph.") + return self.graph_dict[server].nodes[monster_id]['model'] def get_all_monsters(self, server: Server) -> Set[MonsterModel]: # Fail gracefully if one of the nodes doesn't exist From 6836078bc07236f95b9fd963df42c75958780dab Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Jun 2021 04:45:40 -0700 Subject: [PATCH 0852/1220] Load exchange data later (#1224) --- dadguide/dadguide.py | 18 +++++++++++------- dadguide/models/monster_model.py | 2 +- dadguide/monster_graph.py | 11 +++++++++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 1e3fbf0d..b0afe268 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -150,19 +150,23 @@ async def create_index(self): asyncio.create_task(self.check_index()) async def check_index(self): - channel = self.bot.get_channel(await self.config.indexlog()) - if not channel: - return - issues = [] issues.extend(self.database.graph.issues) for index in self.indexes.values(): issues.extend(index.issues) - issues.extend((await self.run_tests())[:25]) + issues.extend(await self.run_tests()) if issues: + channels = [self.bot.get_channel(await self.config.indexlog())] + if not any(channels): + channels = [owner for oid in self.bot.owner_ids if (owner := self.bot.get_user(owner))] + for channel in channels: + await channel.send("Use `{}dadguide setfailurechannel `" + " to move these out of your DMs!" + "".format((await self.bot.get_valid_prefixes())[0])) for page in pagify(f"Load Warnings:\n" + "\n".join(issues[:100])): - await channel.send(box(page)) + for channel in channels: + await channel.send(box(page)) def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> MonsterModel: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" @@ -234,7 +238,7 @@ async def setdatafile(self, ctx, data_file): @dadguide.command() @checks.is_owner() - async def setindexlog(self, ctx, channel: discord.TextChannel): + async def setfailurechannel(self, ctx, channel: discord.TextChannel): await self.config.indexlog.set(channel.id) await ctx.tick() diff --git a/dadguide/models/monster_model.py b/dadguide/models/monster_model.py index 1238de98..f95e040b 100644 --- a/dadguide/models/monster_model.py +++ b/dadguide/models/monster_model.py @@ -173,7 +173,7 @@ def get_difference(self, other): return MonsterDifference.from_monsters(self, other) def __repr__(self): - server_prefix = self.server_priority + " " if self.server_priority != Server.COMBINED else "" + server_prefix = self.server_priority.name + " " if self.server_priority != Server.COMBINED else "" return "Monster<{}{} ({})>".format(server_prefix, self.name_en, self.monster_id) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 2b1d66a4..fad23aa5 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -1,4 +1,5 @@ import json +import logging import re from collections import defaultdict from typing import Optional, List, Set, Dict @@ -19,6 +20,8 @@ from .models.monster.monster_difference import MonsterDifference from .models.series_model import SeriesModel +logger = logging.getLogger('red.padbot-cogs.dadguide') + MONSTER_QUERY = """SELECT monsters{0}.*, COALESCE(monster_name_overrides.name_en, monsters{0}.name_en_override) AS name_en_override, @@ -98,15 +101,19 @@ FROM egg_machines JOIN d_egg_machine_types ON d_egg_machine_types.egg_machine_type_id = egg_machines.egg_machine_type_id +WHERE + start_timestamp < strftime('%s', 'now') """ EXCHANGE_QUERY = """SELECT * FROM exchanges +WHERE + start_timestamp < strftime('%s', 'now') """ -SERVER_ID_WHERE_CONDITION = " WHERE server_id = {}" +SERVER_ID_WHERE_CONDITION = " AND server_id = {}" class MonsterGraph(object): @@ -315,7 +322,7 @@ def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER, do_lo if monster_id not in self.graph_dict[server].nodes: return None if 'model' not in self.graph_dict[server].nodes[monster_id]: - raise InvalidGraphState(f"{monster_id} has no model in the {server.name} graph.") + return None return self.graph_dict[server].nodes[monster_id]['model'] def get_all_monsters(self, server: Server) -> Set[MonsterModel]: From 35b8505ce198744ac4958b86b47431c0fccd8c39 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 12 Jun 2021 05:27:41 -0700 Subject: [PATCH 0853/1220] Add is_7x6 property (#1226) --- dadguide/models/leader_skill_model.py | 7 +++++++ dadguide/monster_graph.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/dadguide/models/leader_skill_model.py b/dadguide/models/leader_skill_model.py index b7153e2b..804712af 100644 --- a/dadguide/models/leader_skill_model.py +++ b/dadguide/models/leader_skill_model.py @@ -1,3 +1,5 @@ +import re + from .base_model import BaseModel @@ -15,6 +17,7 @@ def __init__(self, **kwargs): self.bonus_damage = kwargs['bonus_damage'] self.mult_bonus_damage = kwargs['mult_bonus_damage'] self.extra_time = kwargs['extra_time'] + self.tags = [int(tag) for tag in re.findall(r'\((\d+)\)', kwargs['tags'])] self.desc_en = kwargs['desc_en'] self.desc_ja = kwargs['desc_ja'] self.desc_ko = kwargs['desc_ko'] @@ -38,6 +41,10 @@ def desc(self): def name(self): return self.name_en or self.name_ja + @property + def is_7x6(self): + return 200 in self.tags + def to_dict(self): return { 'leader_skill_id': self.leader_skill_id, diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index fad23aa5..8f5dcc2b 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -40,6 +40,7 @@ leader_skills{0}.bonus_damage, leader_skills{0}.mult_bonus_damage, leader_skills{0}.extra_time, + leader_skills{0}.tags, active_skills{0}.name_ja AS as_name_ja, active_skills{0}.name_en AS as_name_en, active_skills{0}.name_ko AS as_name_ko, @@ -174,6 +175,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: bonus_damage=m.bonus_damage, mult_bonus_damage=m.mult_bonus_damage, extra_time=m.extra_time, + tags=m.tags, ) if m.leader_skill_id != 0 else None as_model = ActiveSkillModel(active_skill_id=m.active_skill_id, From 59efbe8b488d75d5f5f90ac45698d68c6d376d65 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 12 Jun 2021 08:20:32 -0500 Subject: [PATCH 0854/1220] Show 7x6 base in leader skill (#1227) --- padinfo/view/id.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index de710913..1c494d01 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -52,7 +52,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.is_child = is_child self.acquire_raw = acquire_raw self.base_rarity = base_rarity - self.transform_base = transform_base + self.transform_base: "MonsterModel" = transform_base self.true_evo_type_raw = true_evo_type_raw def serialize(self): @@ -301,11 +301,12 @@ def active_skill_header(m: "MonsterModel", transform_base): ) @staticmethod - def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier): + def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): return Box( BoldText('Leader Skill'), BoldText(ls_multiplier_text(m.leader_skill) if lsmultiplier == LsMultiplier.lsdouble else char_to_emoji(1) + ' ' + ls_single_multiplier_text(m.leader_skill)), + BoldText('(' + get_emoji('\N{DOWN-POINTING RED TRIANGLE}') + '7x6)') if m != transform_base and transform_base.leader_skill.is_7x6 else None, delimiter=' ' ) @@ -335,7 +336,7 @@ def embed(cls, state: IdViewState): Text(m.active_skill.desc if m.active_skill else 'None') ), EmbedField( - IdView.leader_skill_header(m, state.query_settings.lsmultiplier).to_markdown(), + IdView.leader_skill_header(m, state.query_settings.lsmultiplier, state.transform_base).to_markdown(), Text(m.leader_skill.desc if m.leader_skill else 'None') ), evos_embed_field(state) From 30b1d6a09306c061d2ce35a56d3dd2c617b8130c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 13 Jun 2021 05:50:31 -0500 Subject: [PATCH 0855/1220] Refactor respond with left/right in IdMenu (#1228) --- padinfo/menu/id.py | 59 +++---------------- padinfo/view/components/view_state_base_id.py | 31 ++++++++++ 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 1498b85f..82af1f23 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -10,6 +10,7 @@ from tsutils.query_settings import QuerySettings from padinfo.menu.simple_text import SimpleTextMenu +from padinfo.view.components.view_state_base_id import ViewStateBaseId from padinfo.view.evos import EvosView, EvosViewState from padinfo.view.id import IdView, IdViewState from padinfo.view.materials import MaterialsView, MaterialsViewState @@ -37,71 +38,29 @@ def menu(initial_control=None): @staticmethod async def respond_with_left(message: Optional[Message], ims, **data): dgcog = data['dgcog'] - db_context: "DbContext" = dgcog.database - query_settings = QuerySettings.deserialize(ims.get('query_settings')) - - m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=query_settings.server) + user_config = data['user_config'] + # Figure out the new monster before doing all of the queries necessary for + # The specific pane type. For now just deserialize as the base Id ViewState. + view_state = await ViewStateBaseId.deserialize(dgcog, user_config, ims) + view_state.decrement_monster(dgcog, ims) - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - evosort = query_settings.evosort - new_monster_id = IdMenu.get_prev_monster_id(db_context, m, use_evo_scroll, evosort) - if new_monster_id is None: - ims['unsupported_transition'] = True - ims['resolved_monster_id'] = str(new_monster_id) if new_monster_id else None pane_type = ims['pane_type'] pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] return await response_func(message, ims, **data) - @staticmethod - def get_prev_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll, evosort): - if use_evo_scroll: - evos = db_context.graph.get_alt_ids(monster) - if evosort != AltEvoSort.dfs: - evos = sorted(evos) - index = evos.index(monster.monster_id) - new_id = evos[index - 1] - return new_id - else: - prev_monster = db_context.graph.numeric_prev_monster(monster) - return prev_monster.monster_id if prev_monster else None - @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): dgcog = data['dgcog'] - db_context: "DbContext" = dgcog.database - query_settings = QuerySettings.deserialize(ims.get('query_settings')) - - m = db_context.graph.get_monster(int(ims['resolved_monster_id']), server=query_settings.server) + user_config = data['user_config'] + view_state = await ViewStateBaseId.deserialize(dgcog, user_config, ims) + view_state.increment_monster(dgcog, ims) - use_evo_scroll = ims.get('use_evo_scroll') != 'False' - evosort = query_settings.evosort - new_monster_id = str(IdMenu.get_next_monster_id(db_context, m, use_evo_scroll, evosort) or '') - if new_monster_id is None: - ims['unsupported_transition'] = True - ims['resolved_monster_id'] = new_monster_id pane_type = ims.get('pane_type') pane_type_to_func_map = IdMenuPanes.pane_types() response_func = pane_type_to_func_map[pane_type] return await response_func(message, ims, **data) - @staticmethod - def get_next_monster_id(db_context: "DbContext", monster: "MonsterModel", use_evo_scroll, evosort): - if use_evo_scroll: - evos = db_context.graph.get_alt_ids(monster) - if evosort != AltEvoSort.dfs: - evos = sorted(evos) - index = evos.index(monster.monster_id) - if index == len(evos) - 1: - # cycle back to the beginning of the evos list - new_id = evos[0] - else: - new_id = evos[index + 1] - return new_id - else: - next_monster = db_context.graph.numeric_next_monster(monster) - return next_monster.monster_id if next_monster else None - @staticmethod async def respond_with_refresh(message: Optional[Message], ims, **data): # This is used by disambig screen & other multi-message embeds, where we need to deserialize & then diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index e0376411..7ab96df3 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from dadguide.models.monster_model import MonsterModel from dadguide.models.evolution_model import EvolutionModel + from dadguide.database_context import DbContext class MonsterEvolution(NamedTuple): @@ -39,6 +40,8 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst else: self.alt_monsters = sorted(self.dfs_alt_monsters, key=lambda m: m.monster.monster_id) + self.alt_monster_ids = [m.monster.monster_id for m in self.alt_monsters] + def serialize(self): ret = super().serialize() ret.update({ @@ -78,3 +81,31 @@ def get_alt_monsters_and_evos(cls, dgcog, monster) -> List[MonsterEvolution]: graph = dgcog.database.graph alt_monsters = graph.get_alt_monsters(monster) return [MonsterEvolution(m, graph.get_evolution(m)) for m in alt_monsters] + + def decrement_monster(self, dgcog, ims: dict): + db_context: "DbContext" = dgcog.database + if self.use_evo_scroll: + index = self.alt_monster_ids.index(self.monster.monster_id) + prev_monster_id = self.alt_monster_ids[index - 1] + else: + prev_monster = db_context.graph.numeric_prev_monster(self.monster) + prev_monster_id = prev_monster.monster_id if prev_monster else None + if prev_monster_id is None: + ims['unsupported_transition'] = True + ims['resolved_monster_id'] = str(prev_monster_id) + + def increment_monster(self, dgcog, ims: dict): + db_context: "DbContext" = dgcog.database + if self.use_evo_scroll: + index = self.alt_monster_ids.index(self.monster.monster_id) + if index == len(self.alt_monster_ids) - 1: + # cycle back to the beginning of the evos list + next_monster_id = self.alt_monster_ids[0] + else: + next_monster_id = self.alt_monster_ids[index + 1] + else: + next_monster = db_context.graph.numeric_next_monster(self.monster) + next_monster_id = next_monster.monster_id if next_monster else None + if next_monster_id is None: + ims['unsupported_transition'] = True + ims['resolved_monster_id'] = str(next_monster_id) From 67584cbae935073fa29c57479e9687f73823a646 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 13 Jun 2021 05:57:32 -0500 Subject: [PATCH 0856/1220] Remove unused imports (#1229) --- padinfo/menu/id.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 82af1f23..5f1a56d1 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -1,13 +1,11 @@ -from typing import TYPE_CHECKING, Optional +from typing import Optional from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji -from tsutils.enums import ChildMenuType, AltEvoSort - +from tsutils.enums import ChildMenuType from tsutils.menu.panes import MenuPanes -from tsutils.query_settings import QuerySettings from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -18,10 +16,6 @@ from padinfo.view.pantheon import PantheonView, PantheonViewState from padinfo.view.pic import PicView, PicViewState -if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.database_context import DbContext - class IdMenu: MENU_TYPE = ChildMenuType.IdMenu.name From 74511747d040b553394fb182c6f61a21e5854909 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Jun 2021 04:02:59 -0700 Subject: [PATCH 0857/1220] Log graph issues to owners correctly (#1233) --- dadguide/dadguide.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index b0afe268..054860ad 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -159,7 +159,7 @@ async def check_index(self): if issues: channels = [self.bot.get_channel(await self.config.indexlog())] if not any(channels): - channels = [owner for oid in self.bot.owner_ids if (owner := self.bot.get_user(owner))] + channels = [owner for oid in self.bot.owner_ids if (owner := self.bot.get_user(oid))] for channel in channels: await channel.send("Use `{}dadguide setfailurechannel `" " to move these out of your DMs!" From 53274e605d2b3bf8329a9314fb76b98201033429 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Jun 2021 04:49:13 -0700 Subject: [PATCH 0858/1220] [PDE] Save event loops as variables for easy deletion (#1234) --- padevents/__init__.py | 5 +- padevents/padevents.py | 204 +++++++++++++++++++++-------------------- 2 files changed, 107 insertions(+), 102 deletions(-) diff --git a/padevents/__init__.py b/padevents/__init__.py index d6ef7327..fae15e3d 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -4,7 +4,4 @@ def setup(bot): - n = PadEvents(bot) - bot.add_cog(n) - bot.loop.create_task(n.reload_padevents()) - bot.loop.create_task(n.check_started()) + bot.add_cog(PadEvents(bot)) diff --git a/padevents/padevents.py b/padevents/padevents.py index db760e14..12a609f6 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -55,6 +55,9 @@ def __init__(self, bot, *args, **kwargs): self.fake_uid = -999 + self._event_loop = bot.loop.create_task(self.reload_padevents()) + self._refresh_loop = bot.loop.create_task(self.event_check_loop()) + async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) @@ -71,6 +74,8 @@ def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty self.events = list() self.started_events = set() + self._event_loop.cancel() + self._refresh_loop.cancel() async def reload_padevents(self): await self.bot.wait_until_ready() @@ -100,111 +105,114 @@ async def refresh_data(self): self.started_events = {ev.key for ev in new_events if ev.is_started()} self.rolepinged_events = set() - async def check_started(self): + async def event_check_loop(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadEvents'): try: - events = filter(lambda e: e.key not in self.started_events, self.events) - for event in events: - for gid, data in (await self.config.all_guilds()).items(): - guild = self.bot.get_guild(gid) - if guild is None: - continue - for key, aep in data.get('pingroles', {}).items(): - if event.start_from_now_sec() > aep['offset'] * 60 \ - or not aep['enabled'] \ - or event.server != aep['server'] \ - or (key, event.key) in self.rolepinged_events: - continue - elif aep['regex']: - matches = re.search(aep['searchstr'], event.clean_dungeon_name) - else: - matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() - - self.rolepinged_events.add((key, event.key)) - if matches: - index = GROUPS.index(event.group) - channel = guild.get_channel(aep['channels'][index]) - if channel is not None: - role = guild.get_role(aep['roles'][index]) - ment = role.mention if role else "" - offsetstr = "" - if aep['offset']: - offsetstr = " in {} minute(s)".format(aep['offset']) - try: - await channel.send("{}{} {}".format(event.name_and_modifier, offsetstr, ment), - allowed_mentions=discord.AllowedMentions(roles=True)) - except Exception: - logger.exception("Failed to send AEP in channel {}".format(channel.id)) - - for uid, data in (await self.config.all_users()).items(): - user = self.bot.get_user(uid) - if user is None: - continue - for aed in data.get('dmevents', []): - if event.start_from_now_sec() > aed['offset'] * 60 \ - or (event.group not in (aed['group'], None)) \ - or event.server != aed['server'] \ - or (aed['key'], event.key) in self.rolepinged_events: - continue - self.rolepinged_events.add((aed['key'], event.key)) - if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): - offsetstr = " starts now!" - if aed['offset']: - offsetstr = " starts in {} minute(s)!".format(aed['offset']) - try: - await user.send(event.clean_dungeon_name + offsetstr) - except Exception: - logger.exception("Failed to send AED to user {}".format(user.id)) - - events = filter(lambda e: e.is_started() and e.key not in self.started_events, self.events) - daily_refresh_servers = set() - for event in events: - self.started_events.add(event.key) - if event.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, - EventType.Week]: - for gr in list(self.settings.list_guerrilla_reg()): - if event.server == gr['server']: - try: - channel = self.bot.get_channel(int(gr['channel_id'])) - if channel is None: - continue - - role_name = '{}_group_{}'.format(event.server, event.group_long_name()) - role = channel.guild.get_role(role_name) - if role and role.mentionable: - message = "{} `: {} is starting`".format(role.mention, event.name_and_modifier) - else: - message = box( - "Server " + event.server + ", group " + event.group_long_name() + - " : " + event.name_and_modifier - ) - - await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) - except Exception as ex: - # self.settings.remove_guerrilla_reg(gr['channel_id'], gr['server']) - logger.exception("caught exception while sending guerrilla msg:") - - else: - if event.dungeon_type not in [DungeonType.Normal]: - msg = self.make_active_text(event.server) - for daily_registration in list(self.settings.list_daily_reg()): - try: - if event.server == daily_registration['server']: - await self.page_output(self.bot.get_channel(daily_registration['channel_id']), - msg, channel_id=daily_registration['channel_id']) - logger.info("daily_reg server") - except Exception as ex: - # self.settings.remove_daily_reg( - # daily_registration['channel_id'], daily_registration['server']) - logger.exception("caught exception while sending daily msg:") - - except Exception as ex: + await self.check_started() + except asyncio.CancelledError: + break + except Exception: logger.exception("caught exception while checking guerrillas:") - await asyncio.sleep(10) logger.info("done check_started (cog probably unloaded)") + async def check_started(self): + events = filter(lambda e: e.key not in self.started_events, self.events) + for event in events: + for gid, data in (await self.config.all_guilds()).items(): + guild = self.bot.get_guild(gid) + if guild is None: + continue + for key, aep in data.get('pingroles', {}).items(): + if event.start_from_now_sec() > aep['offset'] * 60 \ + or not aep['enabled'] \ + or event.server != aep['server'] \ + or (key, event.key) in self.rolepinged_events: + continue + elif aep['regex']: + matches = re.search(aep['searchstr'], event.clean_dungeon_name) + else: + matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() + + self.rolepinged_events.add((key, event.key)) + if matches: + index = GROUPS.index(event.group) + channel = guild.get_channel(aep['channels'][index]) + if channel is not None: + role = guild.get_role(aep['roles'][index]) + ment = role.mention if role else "" + offsetstr = "" + if aep['offset']: + offsetstr = " in {} minute(s)".format(aep['offset']) + try: + await channel.send("{}{} {}".format(event.name_and_modifier, offsetstr, ment), + allowed_mentions=discord.AllowedMentions(roles=True)) + except Exception: + logger.exception("Failed to send AEP in channel {}".format(channel.id)) + + for uid, data in (await self.config.all_users()).items(): + user = self.bot.get_user(uid) + if user is None: + continue + for aed in data.get('dmevents', []): + if event.start_from_now_sec() > aed['offset'] * 60 \ + or (event.group not in (aed['group'], None)) \ + or event.server != aed['server'] \ + or (aed['key'], event.key) in self.rolepinged_events: + continue + self.rolepinged_events.add((aed['key'], event.key)) + if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): + offsetstr = " starts now!" + if aed['offset']: + offsetstr = " starts in {} minute(s)!".format(aed['offset']) + try: + await user.send(event.clean_dungeon_name + offsetstr) + except Exception: + logger.exception("Failed to send AED to user {}".format(user.id)) + + events = filter(lambda e: e.is_started() and e.key not in self.started_events, self.events) + daily_refresh_servers = set() + for event in events: + self.started_events.add(event.key) + if event.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, + EventType.Week]: + for gr in list(self.settings.list_guerrilla_reg()): + if event.server == gr['server']: + try: + channel = self.bot.get_channel(int(gr['channel_id'])) + if channel is None: + continue + + role_name = '{}_group_{}'.format(event.server, event.group_long_name()) + role = channel.guild.get_role(role_name) + if role and role.mentionable: + message = "{} `: {} is starting`".format(role.mention, event.name_and_modifier) + else: + message = box( + "Server " + event.server + ", group " + event.group_long_name() + + " : " + event.name_and_modifier + ) + + await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) + except Exception as ex: + # self.settings.remove_guerrilla_reg(gr['channel_id'], gr['server']) + logger.exception("caught exception while sending guerrilla msg:") + + else: + if event.dungeon_type not in [DungeonType.Normal]: + msg = self.make_active_text(event.server) + for daily_registration in list(self.settings.list_daily_reg()): + try: + if event.server == daily_registration['server']: + await self.page_output(self.bot.get_channel(daily_registration['channel_id']), + msg, channel_id=daily_registration['channel_id']) + logger.info("daily_reg server") + except Exception as ex: + # self.settings.remove_daily_reg( + # daily_registration['channel_id'], daily_registration['server']) + logger.exception("caught exception while sending daily msg:") + @commands.group(aliases=['pde']) @checks.mod_or_permissions(manage_guild=True) async def padevents(self, ctx): From d9a202d7c9c23931f4970a7587c4014ca1b9888e Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Jun 2021 05:43:04 -0700 Subject: [PATCH 0859/1220] Always wait for index load (#1235) --- padglobal/padglobal.py | 45 ++++++++++++++++++++++++------------------ padinfo/padinfo.py | 43 ++++++++++++++++++---------------------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 6f5ce196..60bc486d 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -8,7 +8,6 @@ from collections import defaultdict from io import BytesIO -import aiohttp import discord import prettytable import pytz @@ -312,6 +311,7 @@ async def append(self, ctx, command: str, *, addition): return result = self.c_commands.get(corrected_cmd, None) # go a level deeper if trying to append to an alias + source_cmd = None if result in self.c_commands: alias = True source_cmd = result @@ -411,7 +411,7 @@ async def boards(self, ctx): cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} await self.send_cmdlist(ctx, cmdlist) - async def send_cmdlist(self, ctx, cmdlist, inline=False): + async def send_cmdlist(self, ctx, cmdlist, write_inline=False): if not cmdlist: await ctx.send("There are no padglobal commands yet") return @@ -595,7 +595,7 @@ async def bosslist(self, ctx): await ctx.author.send(page) async def lookup_boss(self, term, ctx): - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') @@ -612,7 +612,7 @@ async def lookup_boss(self, term, ctx): async def boss_to_text(self, ctx): bosses = self.settings.boss() - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( ctx.prefix) for term in sorted(bosses.keys()): @@ -648,7 +648,7 @@ async def pboss_edit(self, ctx, term, *, definition): await self._pboss_add(ctx, term, definition, False) async def _pboss_add(self, ctx, term, definition, need_confirm=True): - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -673,7 +673,7 @@ async def _pboss_add(self, ctx, term, definition, need_confirm=True): @pboss.command(name='remove', aliases=['rm', 'delete', 'del']) async def pboss_remove(self, ctx, *, term): """Removes a set of boss mechanics.""" - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -722,7 +722,7 @@ async def which(self, ctx, *, term: str = None): await menu.create(ctx, state) async def _resolve_which(self, ctx, term): - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() db_context = dgcog.database padinfo = self.bot.get_cog("PadInfo") @@ -777,7 +777,7 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): def which_to_text(self): monsters = defaultdict(list) for monster_id in self.settings.which(): - m = self.bot.get_cog("Dadguide").get_monster(monster_id) + m = (await self.get_dgcog()).get_monster(monster_id) if m is None: continue name = m.name_en.split(", ")[-1] @@ -824,7 +824,7 @@ async def pwhich_edit(self, ctx, term, *, definition): await self._pwhich_add(ctx, term, definition, False) async def _pwhich_add(self, ctx, term, definition, need_confirm=True): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -859,8 +859,9 @@ async def _pwhich_add(self, ctx, term, definition, need_confirm=True): @pwhich.command(name='remove', aliases=['rm', 'delete', 'del']) async def pwhich_remove(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + dgcog = await self.get_dgcog() + m = dgcog.get_monster(monster_id) + base_monster = dgcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -887,7 +888,7 @@ async def pwhich_append(self, ctx, term: str, *, addition): await self._concatenate_which(ctx, term, 'append', addition) async def _concatenate_which(self, ctx, term: str, operation: str, addition): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -969,7 +970,7 @@ async def pwhich_list(self, ctx): for w in self.settings.which(): w %= 10000 - m = self.bot.get_cog("Dadguide").get_monster(w) + m = (await self.get_dgcog()).get_monster(w) name = m.name_en.split(', ')[-1] result = self.settings.which()[w] @@ -1209,7 +1210,7 @@ async def guide(self, ctx, *, term: str = None): await ctx.send(self.emojify(text)) async def get_guide_text(self, term: str, ctx): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() term = term.lower() if term in self.settings.dungeonGuide(): @@ -1240,7 +1241,7 @@ def guide_to_text(self): msg += '\n\n__**Leader Guides**__' for monster_id, definition in self.settings.leaderGuide().items(): - m = self.bot.get_cog("Dadguide").get_monster(monster_id) + m = (await self.get_dgcog()).get_monster(monster_id) if m is None: continue name = m.name_en.split(', ')[-1].title() @@ -1325,8 +1326,8 @@ async def leader_edit(self, ctx, monster_id: int, *, definition: str): await self._leader_add(ctx, monster_id, definition, False) async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm=True): - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + m = (await self.get_dgcog()).get_monster(monster_id) + base_monster = (await self.get_dgcog()).database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1343,8 +1344,9 @@ async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm= @leader.command(name='remove', aliases=['rm', 'delete', 'del']) async def leader_remove(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + dgcog = await self.get_dgcog() + m = dgcog.get_monster(monster_id) + base_monster = dgcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1417,6 +1419,11 @@ async def daychange(self, ctx): await ctx.send(msg) + async def get_dgcog(self): + dgcog = self.bot.get_cog("Dadguide") + await dgcog.wait_until_ready() + return dgcog + def emojify(self, message): emojis = list() emoteservers = self.settings.emojiServers() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ecee002e..e8ff6cab 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -149,8 +149,7 @@ async def reload_nicknames(self): try: emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) - dgcog = self.bot.get_cog('Dadguide') - await dgcog.wait_until_ready() + dgcog = await self.get_dgcog() except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) @@ -171,10 +170,6 @@ async def get_menu_default_data(self, ims): } return data - def get_monster(self, monster_id: int): - dgcog = self.bot.get_cog('Dadguide') - return dgcog.get_monster(monster_id) - @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" @@ -217,10 +212,11 @@ async def _do_id(self, ctx, query: str): original_author_id = ctx.message.author.id goodquery = None - if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: + if query[0] in dgcog.token_maps.ID1_SUPPORTED \ + and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes[ - Server.COMBINED].all_name_tokens: + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED \ + and query[2:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: @@ -582,9 +578,9 @@ async def buttoninfo(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) return - DGCOG = self.bot.get_cog("Dadguide") - info = button_info.get_info(DGCOG, monster) - info_str = button_info.to_string(DGCOG, monster, info) + + info = button_info.get_info(dgcog, monster) + info_str = button_info.to_string(dgcog, monster, info) for page in pagify(info_str): await ctx.send(box(page)) @@ -1040,7 +1036,8 @@ async def lsmultiplier(self, ctx, value: str): await ctx.send( f'Please input an allowed value, either `double` or `single`.') return - await ctx.send(f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await ctx.send( + f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() async def cardplus(self, ctx, value: str): @@ -1064,7 +1061,8 @@ async def cardplus(self, ctx, value: str): await ctx.send( f'Please input an allowed value, either `297` or `0`.') return - await ctx.send(f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await ctx.send( + f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @commands.group() @checks.is_owner() @@ -1126,7 +1124,7 @@ async def send_id_failure_message(ctx, query: str): @commands.command(aliases=["iddebug", "dbid", "iddb"]) async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, query): """Get helpful id information about a monster""" - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() mon = await dgcog.find_monster(query, ctx.author.id) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) @@ -1159,7 +1157,7 @@ async def debugiddist(self, ctx, s1, s2): For name tokens, the full word goes second as name token matching is not commutitive """ - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() dist = dgcog.mon_finder.calc_ratio_modifier(s1, s2) dist2 = dgcog.mon_finder.calc_ratio_name(s1, s2) @@ -1187,7 +1185,7 @@ async def idhelp(self, ctx, *, query=""): @commands.command() async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] = "COMBINED"): server = Server(server) - DGCOG = self.bot.get_cog("Dadguide") + DGCOG = await self.get_dgcog() maps = DGCOG.token_maps awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} @@ -1226,9 +1224,7 @@ async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] async def idmeaning(self, ctx, token, server: Optional[Server] = Server.COMBINED): """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") - DGCOG = self.bot.get_cog("Dadguide") - - await DGCOG.wait_until_ready() + DGCOG = await self.get_dgcog() tms = DGCOG.token_maps awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_SKILL_MAP.values() for aw in aws) + ")" @@ -1270,8 +1266,7 @@ def f(m, s): ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + str(DGCOG.mon_finder.get_most_eligable_monster( - DGCOG.indexes[server].all_name_tokens[''.join(t)], - DGCOG).monster_id) + DGCOG.indexes[server].all_name_tokens[''.join(t)]).monster_id) + ")\n") def additmods(ms, om): @@ -1325,7 +1320,7 @@ async def idtraceback(self, ctx, *, query): return selected_monster_id = int(selected_monster_id.strip()) - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() bestmatch, matches, _, _ = await dgcog.mon_finder.find_monster_debug(query) @@ -1381,7 +1376,7 @@ async def nadiffs(self, ctx, *, query): async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() monster_list = await IdSearchViewState.do_query(dgcog, query, ctx.author.id) From 50f6db8a02eff9f324bf2400537068154125d8bd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 14 Jun 2021 07:45:46 -0500 Subject: [PATCH 0860/1220] Revert "Always wait for index load" (#1237) This reverts commit d9a202d7c9c23931f4970a7587c4014ca1b9888e. --- padglobal/padglobal.py | 45 ++++++++++++++++++------------------------ padinfo/padinfo.py | 43 ++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 60bc486d..6f5ce196 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -8,6 +8,7 @@ from collections import defaultdict from io import BytesIO +import aiohttp import discord import prettytable import pytz @@ -311,7 +312,6 @@ async def append(self, ctx, command: str, *, addition): return result = self.c_commands.get(corrected_cmd, None) # go a level deeper if trying to append to an alias - source_cmd = None if result in self.c_commands: alias = True source_cmd = result @@ -411,7 +411,7 @@ async def boards(self, ctx): cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} await self.send_cmdlist(ctx, cmdlist) - async def send_cmdlist(self, ctx, cmdlist, write_inline=False): + async def send_cmdlist(self, ctx, cmdlist, inline=False): if not cmdlist: await ctx.send("There are no padglobal commands yet") return @@ -595,7 +595,7 @@ async def bosslist(self, ctx): await ctx.author.send(page) async def lookup_boss(self, term, ctx): - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog('Dadguide') pdicog = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') @@ -612,7 +612,7 @@ async def lookup_boss(self, term, ctx): async def boss_to_text(self, ctx): bosses = self.settings.boss() - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog('Dadguide') msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( ctx.prefix) for term in sorted(bosses.keys()): @@ -648,7 +648,7 @@ async def pboss_edit(self, ctx, term, *, definition): await self._pboss_add(ctx, term, definition, False) async def _pboss_add(self, ctx, term, definition, need_confirm=True): - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog('Dadguide') pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -673,7 +673,7 @@ async def _pboss_add(self, ctx, term, definition, need_confirm=True): @pboss.command(name='remove', aliases=['rm', 'delete', 'del']) async def pboss_remove(self, ctx, *, term): """Removes a set of boss mechanics.""" - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog('Dadguide') pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -722,7 +722,7 @@ async def which(self, ctx, *, term: str = None): await menu.create(ctx, state) async def _resolve_which(self, ctx, term): - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog('Dadguide') db_context = dgcog.database padinfo = self.bot.get_cog("PadInfo") @@ -777,7 +777,7 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): def which_to_text(self): monsters = defaultdict(list) for monster_id in self.settings.which(): - m = (await self.get_dgcog()).get_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) if m is None: continue name = m.name_en.split(", ")[-1] @@ -824,7 +824,7 @@ async def pwhich_edit(self, ctx, term, *, definition): await self._pwhich_add(ctx, term, definition, False) async def _pwhich_add(self, ctx, term, definition, need_confirm=True): - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog("Dadguide") pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -859,9 +859,8 @@ async def _pwhich_add(self, ctx, term, definition, need_confirm=True): @pwhich.command(name='remove', aliases=['rm', 'delete', 'del']) async def pwhich_remove(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" - dgcog = await self.get_dgcog() - m = dgcog.get_monster(monster_id) - base_monster = dgcog.database.graph.get_base_monster(m) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -888,7 +887,7 @@ async def pwhich_append(self, ctx, term: str, *, addition): await self._concatenate_which(ctx, term, 'append', addition) async def _concatenate_which(self, ctx, term: str, operation: str, addition): - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog("Dadguide") pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -970,7 +969,7 @@ async def pwhich_list(self, ctx): for w in self.settings.which(): w %= 10000 - m = (await self.get_dgcog()).get_monster(w) + m = self.bot.get_cog("Dadguide").get_monster(w) name = m.name_en.split(', ')[-1] result = self.settings.which()[w] @@ -1210,7 +1209,7 @@ async def guide(self, ctx, *, term: str = None): await ctx.send(self.emojify(text)) async def get_guide_text(self, term: str, ctx): - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog("Dadguide") term = term.lower() if term in self.settings.dungeonGuide(): @@ -1241,7 +1240,7 @@ def guide_to_text(self): msg += '\n\n__**Leader Guides**__' for monster_id, definition in self.settings.leaderGuide().items(): - m = (await self.get_dgcog()).get_monster(monster_id) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) if m is None: continue name = m.name_en.split(', ')[-1].title() @@ -1326,8 +1325,8 @@ async def leader_edit(self, ctx, monster_id: int, *, definition: str): await self._leader_add(ctx, monster_id, definition, False) async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm=True): - m = (await self.get_dgcog()).get_monster(monster_id) - base_monster = (await self.get_dgcog()).database.graph.get_base_monster(m) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1344,9 +1343,8 @@ async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm= @leader.command(name='remove', aliases=['rm', 'delete', 'del']) async def leader_remove(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" - dgcog = await self.get_dgcog() - m = dgcog.get_monster(monster_id) - base_monster = dgcog.database.graph.get_base_monster(m) + m = self.bot.get_cog("Dadguide").get_monster(monster_id) + base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1419,11 +1417,6 @@ async def daychange(self, ctx): await ctx.send(msg) - async def get_dgcog(self): - dgcog = self.bot.get_cog("Dadguide") - await dgcog.wait_until_ready() - return dgcog - def emojify(self, message): emojis = list() emoteservers = self.settings.emojiServers() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e8ff6cab..ecee002e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -149,7 +149,8 @@ async def reload_nicknames(self): try: emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog('Dadguide') + await dgcog.wait_until_ready() except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) @@ -170,6 +171,10 @@ async def get_menu_default_data(self, ims): } return data + def get_monster(self, monster_id: int): + dgcog = self.bot.get_cog('Dadguide') + return dgcog.get_monster(monster_id) + @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" @@ -212,11 +217,10 @@ async def _do_id(self, ctx, query: str): original_author_id = ctx.message.author.id goodquery = None - if query[0] in dgcog.token_maps.ID1_SUPPORTED \ - and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: + if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED \ - and query[2:] in dgcog.indexes[Server.COMBINED].all_name_tokens: + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes[ + Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: @@ -578,9 +582,9 @@ async def buttoninfo(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) return - - info = button_info.get_info(dgcog, monster) - info_str = button_info.to_string(dgcog, monster, info) + DGCOG = self.bot.get_cog("Dadguide") + info = button_info.get_info(DGCOG, monster) + info_str = button_info.to_string(DGCOG, monster, info) for page in pagify(info_str): await ctx.send(box(page)) @@ -1036,8 +1040,7 @@ async def lsmultiplier(self, ctx, value: str): await ctx.send( f'Please input an allowed value, either `double` or `single`.') return - await ctx.send( - f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await ctx.send(f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() async def cardplus(self, ctx, value: str): @@ -1061,8 +1064,7 @@ async def cardplus(self, ctx, value: str): await ctx.send( f'Please input an allowed value, either `297` or `0`.') return - await ctx.send( - f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await ctx.send(f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @commands.group() @checks.is_owner() @@ -1124,7 +1126,7 @@ async def send_id_failure_message(ctx, query: str): @commands.command(aliases=["iddebug", "dbid", "iddb"]) async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, query): """Get helpful id information about a monster""" - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog("Dadguide") mon = await dgcog.find_monster(query, ctx.author.id) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) @@ -1157,7 +1159,7 @@ async def debugiddist(self, ctx, s1, s2): For name tokens, the full word goes second as name token matching is not commutitive """ - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog("Dadguide") dist = dgcog.mon_finder.calc_ratio_modifier(s1, s2) dist2 = dgcog.mon_finder.calc_ratio_name(s1, s2) @@ -1185,7 +1187,7 @@ async def idhelp(self, ctx, *, query=""): @commands.command() async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] = "COMBINED"): server = Server(server) - DGCOG = await self.get_dgcog() + DGCOG = self.bot.get_cog("Dadguide") maps = DGCOG.token_maps awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} @@ -1224,7 +1226,9 @@ async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] async def idmeaning(self, ctx, token, server: Optional[Server] = Server.COMBINED): """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") - DGCOG = await self.get_dgcog() + DGCOG = self.bot.get_cog("Dadguide") + + await DGCOG.wait_until_ready() tms = DGCOG.token_maps awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_SKILL_MAP.values() for aw in aws) + ")" @@ -1266,7 +1270,8 @@ def f(m, s): ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + str(DGCOG.mon_finder.get_most_eligable_monster( - DGCOG.indexes[server].all_name_tokens[''.join(t)]).monster_id) + DGCOG.indexes[server].all_name_tokens[''.join(t)], + DGCOG).monster_id) + ")\n") def additmods(ms, om): @@ -1320,7 +1325,7 @@ async def idtraceback(self, ctx, *, query): return selected_monster_id = int(selected_monster_id.strip()) - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog("Dadguide") bestmatch, matches, _, _ = await dgcog.mon_finder.find_monster_debug(query) @@ -1376,7 +1381,7 @@ async def nadiffs(self, ctx, *, query): async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): - dgcog = await self.get_dgcog() + dgcog = self.bot.get_cog("Dadguide") monster_list = await IdSearchViewState.do_query(dgcog, query, ctx.author.id) From 1eee79b230f73f8374b74f5f83c300420105aa9b Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Jun 2021 06:36:33 -0700 Subject: [PATCH 0861/1220] Fix fluff import (#1239) --- dadguide/idtest_mixin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index 4ea26bf1..fdd24544 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -169,6 +169,7 @@ async def norf_import(self, ctx, queries): 'token': query, 'fluff': fluff == 'fluff', 'reason': reason, + 'server': "COMBINED", 'ts': datetime.now().timestamp()}) await ctx.tick() From a539eca619cae3b7254f291a6d254a69593d2049 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 14 Jun 2021 07:41:50 -0700 Subject: [PATCH 0862/1220] Always wait for index load (#1238) --- dadguide/dadguide.py | 19 +++++++++++++-- padglobal/padglobal.py | 53 ++++++++++++++++++++++++------------------ padinfo/padinfo.py | 43 +++++++++++++++------------------- 3 files changed, 66 insertions(+), 49 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 054860ad..58d06dc0 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -93,13 +93,28 @@ def __init__(self, bot, *args, **kwargs): if GADMIN_COG: GADMIN_COG.register_perm("contentadmin") - async def wait_until_ready(self): + async def wait_until_ready(self) -> None: """Wait until the Dadguide cog is ready. Call this from other cogs to wait until Dadguide finishes refreshing its database for the first time. """ - return await self._is_ready.wait() + await self._is_ready.wait() + + async def wait_until_ready_verbose(self, ctx) -> None: + """Wait until the Dadguide cog is ready but loudly. + + Call this from other cogs to wait until Dadguide finishes refreshing its database + for the first time. + """ + if self._is_ready.is_set(): + return + msg = await ctx.send("Index is still loading. Please wait...") + await self._is_ready.wait() + try: + await msg.delete() + except discord.Forbidden: + pass async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 6f5ce196..c6fba7a8 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -8,7 +8,6 @@ from collections import defaultdict from io import BytesIO -import aiohttp import discord import prettytable import pytz @@ -312,6 +311,7 @@ async def append(self, ctx, command: str, *, addition): return result = self.c_commands.get(corrected_cmd, None) # go a level deeper if trying to append to an alias + source_cmd = None if result in self.c_commands: alias = True source_cmd = result @@ -411,7 +411,7 @@ async def boards(self, ctx): cmdlist = {k: v for k, v in self.c_commands.items() if k in self.settings.boards()} await self.send_cmdlist(ctx, cmdlist) - async def send_cmdlist(self, ctx, cmdlist, inline=False): + async def send_cmdlist(self, ctx, cmdlist, write_inline=False): if not cmdlist: await ctx.send("There are no padglobal commands yet") return @@ -595,7 +595,7 @@ async def bosslist(self, ctx): await ctx.author.send(page) async def lookup_boss(self, term, ctx): - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') @@ -612,7 +612,7 @@ async def lookup_boss(self, term, ctx): async def boss_to_text(self, ctx): bosses = self.settings.boss() - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( ctx.prefix) for term in sorted(bosses.keys()): @@ -648,7 +648,7 @@ async def pboss_edit(self, ctx, term, *, definition): await self._pboss_add(ctx, term, definition, False) async def _pboss_add(self, ctx, term, definition, need_confirm=True): - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -673,7 +673,7 @@ async def _pboss_add(self, ctx, term, definition, need_confirm=True): @pboss.command(name='remove', aliases=['rm', 'delete', 'del']) async def pboss_remove(self, ctx, *, term): """Removes a set of boss mechanics.""" - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -704,7 +704,7 @@ async def which(self, ctx, *, term: str = None): await ctx.author.send( '__**PAD Which Monster**__ *(also check out {0}pad / {0}padfaq / {0}boards / {0}glossary)*'.format( ctx.prefix)) - msg = self.which_to_text() + msg = await self.which_to_text() for page in pagify(msg): await ctx.author.send(box(page)) return @@ -722,7 +722,7 @@ async def which(self, ctx, *, term: str = None): await menu.create(ctx, state) async def _resolve_which(self, ctx, term): - dgcog = self.bot.get_cog('Dadguide') + dgcog = await self.get_dgcog() db_context = dgcog.database padinfo = self.bot.get_cog("PadInfo") @@ -774,10 +774,10 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): return await self._do_send_which(ctx, to_user, name, definition, timestamp) - def which_to_text(self): + async def which_to_text(self): monsters = defaultdict(list) for monster_id in self.settings.which(): - m = self.bot.get_cog("Dadguide").get_monster(monster_id) + m = (await self.get_dgcog()).get_monster(monster_id) if m is None: continue name = m.name_en.split(", ")[-1] @@ -824,7 +824,7 @@ async def pwhich_edit(self, ctx, term, *, definition): await self._pwhich_add(ctx, term, definition, False) async def _pwhich_add(self, ctx, term, definition, need_confirm=True): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -859,8 +859,9 @@ async def _pwhich_add(self, ctx, term, definition, need_confirm=True): @pwhich.command(name='remove', aliases=['rm', 'delete', 'del']) async def pwhich_remove(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + dgcog = await self.get_dgcog() + m = dgcog.get_monster(monster_id) + base_monster = dgcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -887,7 +888,7 @@ async def pwhich_append(self, ctx, term: str, *, addition): await self._concatenate_which(ctx, term, 'append', addition) async def _concatenate_which(self, ctx, term: str, operation: str, addition): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() @@ -969,7 +970,7 @@ async def pwhich_list(self, ctx): for w in self.settings.which(): w %= 10000 - m = self.bot.get_cog("Dadguide").get_monster(w) + m = (await self.get_dgcog()).get_monster(w) name = m.name_en.split(', ')[-1] result = self.settings.which()[w] @@ -1209,7 +1210,7 @@ async def guide(self, ctx, *, term: str = None): await ctx.send(self.emojify(text)) async def get_guide_text(self, term: str, ctx): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() term = term.lower() if term in self.settings.dungeonGuide(): @@ -1228,11 +1229,11 @@ async def get_guide_text(self, term: str, ctx): return name, definition, None async def send_guide(self, ctx): - msg = self.guide_to_text() + msg = await self.guide_to_text() for page in pagify(msg): await ctx.author.send(page) - def guide_to_text(self): + async def guide_to_text(self): msg = '__**Dungeon Guides**__' dungeon_guide = self.settings.dungeonGuide() for term in sorted(dungeon_guide.keys()): @@ -1240,7 +1241,7 @@ def guide_to_text(self): msg += '\n\n__**Leader Guides**__' for monster_id, definition in self.settings.leaderGuide().items(): - m = self.bot.get_cog("Dadguide").get_monster(monster_id) + m = (await self.get_dgcog()).get_monster(monster_id) if m is None: continue name = m.name_en.split(', ')[-1].title() @@ -1325,8 +1326,8 @@ async def leader_edit(self, ctx, monster_id: int, *, definition: str): await self._leader_add(ctx, monster_id, definition, False) async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm=True): - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + m = (await self.get_dgcog()).get_monster(monster_id) + base_monster = (await self.get_dgcog()).database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1343,8 +1344,9 @@ async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm= @leader.command(name='remove', aliases=['rm', 'delete', 'del']) async def leader_remove(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" - m = self.bot.get_cog("Dadguide").get_monster(monster_id) - base_monster = self.bot.get_cog("Dadguide").database.graph.get_base_monster(m) + dgcog = await self.get_dgcog() + m = dgcog.get_monster(monster_id) + base_monster = dgcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1417,6 +1419,11 @@ async def daychange(self, ctx): await ctx.send(msg) + async def get_dgcog(self): + dgcog = self.bot.get_cog("Dadguide") + await dgcog.wait_until_ready() + return dgcog + def emojify(self, message): emojis = list() emoteservers = self.settings.emojiServers() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ecee002e..e8ff6cab 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -149,8 +149,7 @@ async def reload_nicknames(self): try: emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) - dgcog = self.bot.get_cog('Dadguide') - await dgcog.wait_until_ready() + dgcog = await self.get_dgcog() except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) @@ -171,10 +170,6 @@ async def get_menu_default_data(self, ims): } return data - def get_monster(self, monster_id: int): - dgcog = self.bot.get_cog('Dadguide') - return dgcog.get_monster(monster_id) - @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" @@ -217,10 +212,11 @@ async def _do_id(self, ctx, query: str): original_author_id = ctx.message.author.id goodquery = None - if query[0] in dgcog.token_maps.ID1_SUPPORTED and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: + if query[0] in dgcog.token_maps.ID1_SUPPORTED \ + and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED and query[2:] in dgcog.indexes[ - Server.COMBINED].all_name_tokens: + elif query[:2] in dgcog.token_maps.ID1_SUPPORTED \ + and query[2:] in dgcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: @@ -582,9 +578,9 @@ async def buttoninfo(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) return - DGCOG = self.bot.get_cog("Dadguide") - info = button_info.get_info(DGCOG, monster) - info_str = button_info.to_string(DGCOG, monster, info) + + info = button_info.get_info(dgcog, monster) + info_str = button_info.to_string(dgcog, monster, info) for page in pagify(info_str): await ctx.send(box(page)) @@ -1040,7 +1036,8 @@ async def lsmultiplier(self, ctx, value: str): await ctx.send( f'Please input an allowed value, either `double` or `single`.') return - await ctx.send(f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await ctx.send( + f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() async def cardplus(self, ctx, value: str): @@ -1064,7 +1061,8 @@ async def cardplus(self, ctx, value: str): await ctx.send( f'Please input an allowed value, either `297` or `0`.') return - await ctx.send(f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await ctx.send( + f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @commands.group() @checks.is_owner() @@ -1126,7 +1124,7 @@ async def send_id_failure_message(ctx, query: str): @commands.command(aliases=["iddebug", "dbid", "iddb"]) async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, query): """Get helpful id information about a monster""" - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() mon = await dgcog.find_monster(query, ctx.author.id) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) @@ -1159,7 +1157,7 @@ async def debugiddist(self, ctx, s1, s2): For name tokens, the full word goes second as name token matching is not commutitive """ - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() dist = dgcog.mon_finder.calc_ratio_modifier(s1, s2) dist2 = dgcog.mon_finder.calc_ratio_name(s1, s2) @@ -1187,7 +1185,7 @@ async def idhelp(self, ctx, *, query=""): @commands.command() async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] = "COMBINED"): server = Server(server) - DGCOG = self.bot.get_cog("Dadguide") + DGCOG = await self.get_dgcog() maps = DGCOG.token_maps awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} @@ -1226,9 +1224,7 @@ async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] async def idmeaning(self, ctx, token, server: Optional[Server] = Server.COMBINED): """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") - DGCOG = self.bot.get_cog("Dadguide") - - await DGCOG.wait_until_ready() + DGCOG = await self.get_dgcog() tms = DGCOG.token_maps awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_SKILL_MAP.values() for aw in aws) + ")" @@ -1270,8 +1266,7 @@ def f(m, s): ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + str(DGCOG.mon_finder.get_most_eligable_monster( - DGCOG.indexes[server].all_name_tokens[''.join(t)], - DGCOG).monster_id) + DGCOG.indexes[server].all_name_tokens[''.join(t)]).monster_id) + ")\n") def additmods(ms, om): @@ -1325,7 +1320,7 @@ async def idtraceback(self, ctx, *, query): return selected_monster_id = int(selected_monster_id.strip()) - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() bestmatch, matches, _, _ = await dgcog.mon_finder.find_monster_debug(query) @@ -1381,7 +1376,7 @@ async def nadiffs(self, ctx, *, query): async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): - dgcog = self.bot.get_cog("Dadguide") + dgcog = await self.get_dgcog() monster_list = await IdSearchViewState.do_query(dgcog, query, ctx.author.id) From 152780720e6cfbfab7a08dd8c9b519a268096c3d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 15 Jun 2021 18:46:59 -0500 Subject: [PATCH 0863/1220] Move leader_skill_header to common (#1243) --- padinfo/view/common.py | 16 +++++++++++++++- padinfo/view/id.py | 19 ++++--------------- padinfo/view/transforminfo.py | 17 +++++++++-------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 9b0e1fe2..faad331e 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -2,9 +2,12 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import Text, BoldText +from tsutils import char_to_emoji +from tsutils.enums import LsMultiplier from tsutils.query_settings import QuerySettings -from padinfo.common.emoji_map import get_awakening_emoji +from padinfo.common.emoji_map import get_awakening_emoji, get_emoji +from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: @@ -38,3 +41,14 @@ def get_awoken_skill_description(awoken_skill: "AwokenSkillModel"): def invalid_monster_text(query: str, monster: "MonsterModel", append_text: str, link=False): base_text = 'Your query `{}` found {}{}.' return base_text.format(query, MonsterHeader.short_with_emoji(monster, link=link).to_markdown(), append_text) + + +def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): + return Box( + BoldText('Leader Skill'), + BoldText(ls_multiplier_text(m.leader_skill) if lsmultiplier == LsMultiplier.lsdouble + else char_to_emoji(1) + ' ' + ls_single_multiplier_text(m.leader_skill)), + BoldText('(' + get_emoji( + '\N{DOWN-POINTING RED TRIANGLE}') + '7x6)') if m != transform_base and transform_base.leader_skill.is_7x6 else None, + delimiter=' ' + ) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 1c494d01..c9d11518 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -4,16 +4,15 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state, char_to_emoji -from tsutils.enums import Server, LsMultiplier, CardPlusModifier +from tsutils import embed_footer_with_state +from tsutils.enums import Server, CardPlusModifier from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx -from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text from padinfo.view.base import BaseIdView -from padinfo.view.common import get_monster_from_ims, invalid_monster_text +from padinfo.view.common import get_monster_from_ims, invalid_monster_text, leader_skill_header from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution @@ -300,16 +299,6 @@ def active_skill_header(m: "MonsterModel", transform_base): delimiter=' ' ) - @staticmethod - def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): - return Box( - BoldText('Leader Skill'), - BoldText(ls_multiplier_text(m.leader_skill) if lsmultiplier == LsMultiplier.lsdouble - else char_to_emoji(1) + ' ' + ls_single_multiplier_text(m.leader_skill)), - BoldText('(' + get_emoji('\N{DOWN-POINTING RED TRIANGLE}') + '7x6)') if m != transform_base and transform_base.leader_skill.is_7x6 else None, - delimiter=' ' - ) - @classmethod def embed(cls, state: IdViewState): m = state.monster @@ -336,7 +325,7 @@ def embed(cls, state: IdViewState): Text(m.active_skill.desc if m.active_skill else 'None') ), EmbedField( - IdView.leader_skill_header(m, state.query_settings.lsmultiplier, state.transform_base).to_markdown(), + leader_skill_header(m, state.query_settings.lsmultiplier, state.transform_base).to_markdown(), Text(m.leader_skill.desc if m.leader_skill else 'None') ), evos_embed_field(state) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 0493c27c..125b918c 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -5,10 +5,12 @@ from discordmenu.embed.text import Text, BoldText, LabeledText from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.enums import LsMultiplier from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx +from padinfo.view.common import leader_skill_header from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base import ViewStateBase @@ -32,7 +34,7 @@ def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, tr self.acquire_raw = acquire_raw self.monster_ids = monster_ids self.is_jp_buffed = is_jp_buffed - self.query_settings = query_settings + self.query_settings: QuerySettings = query_settings self.reaction_list = reaction_list def serialize(self): @@ -60,7 +62,7 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): acquire_raw = await TransformInfoViewState.do_query(dgcog, base_mon, transformed_mon) reaction_list = ims['reaction_list'] is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) \ - or dgcog.database.graph.monster_is_discrepant(transformed_mon) + or dgcog.database.graph.monster_is_discrepant(transformed_mon) return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, base_mon, transformed_mon, acquire_raw, monster_ids, is_jp_buffed, @@ -139,19 +141,18 @@ def base_active_header(m: "MonsterModel"): ) -def leader_header(m: "MonsterModel", is_base: bool): +def leader_header(m: "MonsterModel", is_base: bool, lsmultiplier: LsMultiplier, base_mon: "MonsterModel"): if is_base: emoji = BASE_EMOJI label = 'Base' else: emoji = TRANSFORM_EMOJI label = 'Transform' - leader_skill = m.leader_skill return Box( emoji, BoldText(label), - IdView.leader_skill_header(m).to_markdown(), + leader_skill_header(m, lsmultiplier, base_mon).to_markdown(), delimiter=' ' ) @@ -163,7 +164,7 @@ class TransformInfoView: def embed(state: TransformInfoViewState): base_mon = state.base_mon transformed_mon = state.transformed_mon - + lsmultiplier = state.query_settings.lsmultiplier fields = [ EmbedField( '/'.join(['{}'.format(t.name) for t in transformed_mon.types]), @@ -198,11 +199,11 @@ def embed(state: TransformInfoViewState): ) ), EmbedField( - transformat(leader_header(transformed_mon, False).to_markdown()), + transformat(leader_header(transformed_mon, False, lsmultiplier, base_mon).to_markdown()), Box( Text(transformed_mon.leader_skill.desc if transformed_mon.leader_skill else 'None'), - leader_header(base_mon, True).to_markdown(), + leader_header(base_mon, True, lsmultiplier, base_mon).to_markdown(), Text(base_mon.leader_skill.desc if base_mon.leader_skill else 'None') ) ) From 5906fe690508153bd0681e8cf28afda1850eca7c Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Thu, 17 Jun 2021 01:16:20 -0400 Subject: [PATCH 0864/1220] Refactor seriesscroll (#1241) --- padinfo/menu/series_scroll.py | 193 +++++++++++----------------------- padinfo/padinfo.py | 6 +- padinfo/view/series_scroll.py | 113 +++++++++++++++++--- 3 files changed, 163 insertions(+), 149 deletions(-) diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 8db23ae6..b9d1a944 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -1,11 +1,10 @@ -from copy import deepcopy from typing import Optional, List from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils import char_to_emoji - from tsutils.menu.panes import MenuPanes, emoji_buttons + from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView from padinfo.view.series_scroll import SeriesScrollView, SeriesScrollViewState @@ -47,6 +46,12 @@ def menu(initial_control=None): embed = EmbedMenu(SeriesScrollMenuPanes.transitions(), initial_control) return embed + @classmethod + async def _get_view_state(cls, ims: dict, **data) -> SeriesScrollViewState: + dgcog = data['dgcog'] + user_config = data['user_config'] + return await SeriesScrollViewState.deserialize(dgcog, user_config, ims) + @staticmethod async def respond_with_refresh(message: Optional[Message], ims, **data): # this is only called once on message load @@ -59,114 +64,50 @@ async def respond_with_reset(message: Optional[Message], ims, **data): # replace with the overview list after the child menu changes return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) - @staticmethod - async def respond_with_left(message: Optional[Message], ims, **data): - current_page = ims['current_page'] - if current_page > 0: - ims['current_page'] = current_page - 1 - ims['current_index'] = None - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) - current_rarity_index = ims['all_rarities'].index(ims['rarity']) - ims['rarity'] = ims['all_rarities'][current_rarity_index - 1] - paginated_monsters = await SeriesScrollViewState.query_from_ims(data['dgcog'], ims) - ims['current_page'] = len(paginated_monsters) - 1 - ims['current_index'] = None - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) - - @staticmethod - async def respond_with_right(message: Optional[Message], ims, **data): - paginated_monsters = await SeriesScrollViewState.query_from_ims(data['dgcog'], ims) - current_page = ims['current_page'] - if current_page < len(paginated_monsters) - 1: - ims['current_page'] = current_page + 1 - ims['current_index'] = None - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) - current_rarity_index = ims['all_rarities'].index(ims['rarity']) - if current_rarity_index == len(ims['all_rarities']) - 1: - ims['rarity'] = ims['all_rarities'][0] - else: - ims['rarity'] = ims['all_rarities'][current_rarity_index + 1] - ims['current_page'] = 0 - ims['current_index'] = None - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) - - @staticmethod - async def respond_with_monster_list(message: Optional[Message], ims, **data): + @classmethod + async def respond_with_left(cls, message: Optional[Message], ims, **data): dgcog = data['dgcog'] - user_config = data['user_config'] - view_state = await SeriesScrollViewState.deserialize(dgcog, user_config, ims) - control = SeriesScrollMenu.monster_list_control(view_state) - return control + view_state = await cls._get_view_state(ims, **data) + await view_state.decrement_page(dgcog) + return SeriesScrollMenu.monster_list_control(view_state) - @staticmethod - async def respond_with_previous_monster(message: Optional[Message], ims, **data): + @classmethod + async def respond_with_right(cls, message: Optional[Message], ims, **data): dgcog = data['dgcog'] - await SeriesScrollMenu._update_ims_prev(dgcog, ims) - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + view_state = await cls._get_view_state(ims, **data) + await view_state.increment_page(dgcog) + return SeriesScrollMenu.monster_list_control(view_state) - @staticmethod - async def _update_ims_prev(dgcog, ims): - current_index = ims['current_index'] - page = ims['current_page'] - paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, ims) - max_index = len(paginated_monsters[page]) - 1 - if current_index is None: - ims['current_index'] = max_index - return - if current_index > 0: - ims['current_index'] = current_index - 1 - return - if current_index == 0 and page > 0: - ims['current_page'] = page - 1 - ims['current_index'] = SeriesScrollViewState.MAX_ITEMS_PER_PANE - return - if current_index == 0: - # respond with left but then set the current index to the max thing possible - current_rarity_index = ims['all_rarities'].index(ims['rarity']) - ims['rarity'] = ims['all_rarities'][current_rarity_index - 1] - paginated_monsters_new = await SeriesScrollViewState.query_from_ims(dgcog, ims) - ims['current_page'] = len(paginated_monsters_new) - 1 - ims['current_index'] = len(paginated_monsters_new[-1]) - 1 + @classmethod + async def respond_with_monster_list(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + return SeriesScrollMenu.monster_list_control(view_state) - @staticmethod - async def respond_with_next_monster(message: Optional[Message], ims, **data): + @classmethod + async def respond_with_previous_monster(cls, message: Optional[Message], ims, **data): dgcog = data['dgcog'] - await SeriesScrollMenu._update_ims_next(dgcog, ims) - return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) + view_state = await cls._get_view_state(ims, **data) + await view_state.decrement_index(dgcog) + return SeriesScrollMenu.monster_list_control(view_state) - @staticmethod - async def _update_ims_next(dgcog, ims): - current_index = ims['current_index'] - page = ims['current_page'] - paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, ims) - max_index = len(paginated_monsters[page]) - 1 - if current_index is None: - ims['current_index'] = 0 - return - if current_index < max_index: - ims['current_index'] = current_index + 1 - return - if current_index == max_index and page < len(paginated_monsters) - 1: - ims['current_page'] = page + 1 - ims['current_index'] = 0 - return - if current_index == max_index: - # copied from respond with right - current_rarity_index = ims['all_rarities'].index(ims['rarity']) - if current_rarity_index == len(ims['all_rarities']) - 1: - ims['rarity'] = ims['all_rarities'][0] - else: - ims['rarity'] = ims['all_rarities'][current_rarity_index + 1] - ims['current_page'] = 0 - ims['current_index'] = 0 + @classmethod + async def respond_with_next_monster(cls, message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + view_state = await cls._get_view_state(ims, **data) + await view_state.increment_index(dgcog) + return SeriesScrollMenu.monster_list_control(view_state) @staticmethod async def respond_with_delete(message: Message, ims, **data): return await message.delete() - @staticmethod - async def respond_with_n(message: Optional[Message], ims, n, **data): - ims['current_index'] = n + @classmethod + async def respond_with_n(cls, message: Optional[Message], ims, n, **data): + view_state = await cls._get_view_state(ims, **data) + current_monster_list = view_state.paginated_monsters[view_state.current_page] + # ims will be immediately be deserialized and shown, so don't change if n is out of range + if n < len(current_monster_list): + ims['current_index'] = n return await SeriesScrollMenu.respond_with_monster_list(message, ims, **data) @staticmethod @@ -220,51 +161,35 @@ def monster_list_control(state: SeriesScrollViewState): SeriesScrollMenuPanes.get_initial_reaction_list(state.max_len_so_far) ) - @staticmethod - async def click_child_number(ims, emoji_clicked, **data): + @classmethod + async def click_child_number(cls, ims, emoji_clicked, **data): dgcog = data['dgcog'] emoji_response = IdMenuEmoji.refresh \ if SeriesScrollMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: return None, {} n = SeriesScrollMenuPanes.emoji_names().index(emoji_clicked) - paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, ims) - page = ims.get('current_page') or 0 - monster_list = paginated_monsters[page] - extra_ims = { - 'is_child': True, - 'reaction_list': IdMenuPanes.emoji_names(), - 'menu_type': IdMenu.MENU_TYPE, - 'resolved_monster_id': - monster_list[n - SeriesScrollMenuPanes.NON_MONSTER_EMOJI_COUNT].monster_id, - 'query_settings': ims['query_settings'] - } + view_state = await cls._get_view_state(ims, **data) + view_state.set_index(SeriesScrollMenuPanes.get_monster_index(n)) + extra_ims = view_state.get_serialized_child_extra_ims(IdMenuPanes.emoji_names(), IdMenu.MENU_TYPE) return emoji_response, extra_ims - @staticmethod - def auto_scroll_child_left(ims, _emoji_clicked, **data): - return SeriesScrollMenu._scroll_child(ims, SeriesScrollMenu._update_ims_prev, **data) + @classmethod + async def auto_scroll_child_left(cls, ims, _emoji_clicked, **data): + dgcog = data['dgcog'] + view_state = await cls._get_view_state(ims, **data) + return await SeriesScrollMenu._scroll_child(view_state, view_state.decrement_index, dgcog) - @staticmethod - def auto_scroll_child_right(ims, _emoji_clicked, **data): - return SeriesScrollMenu._scroll_child(ims, SeriesScrollMenu._update_ims_next, **data) + @classmethod + async def auto_scroll_child_right(cls, ims, _emoji_clicked, **data): + dgcog = data['dgcog'] + view_state = await cls._get_view_state(ims, **data) + return await SeriesScrollMenu._scroll_child(view_state, view_state.increment_index, dgcog) @staticmethod - async def _scroll_child(ims, update_fn, **data): - dgcog = data['dgcog'] - copy_ims = deepcopy(ims) - await update_fn(dgcog, copy_ims) - paginated_monsters = await SeriesScrollViewState.query_from_ims(dgcog, copy_ims) - page = copy_ims.get('current_page') or 0 - monster_list = paginated_monsters[page] - # after we figure out new rarity - extra_ims = { - 'is_child': True, - 'reaction_list': IdMenuPanes.emoji_names(), - 'menu_type': IdMenu.MENU_TYPE, - 'resolved_monster_id': monster_list[copy_ims['current_index']].monster_id, - 'query_settings': ims['query_settings'] - } + async def _scroll_child(view_state: SeriesScrollViewState, update_fn, dgcog): + await update_fn(dgcog) + extra_ims = view_state.get_serialized_child_extra_ims(IdMenuPanes.emoji_names(), IdMenu.MENU_TYPE) return IdMenuEmoji.refresh, extra_ims @@ -319,3 +244,7 @@ def get_initial_reaction_list(cls, number_of_evos: int): @classmethod def get_previous_reaction_list_num_monsters(cls, reaction_list: List): return len(reaction_list) - cls.NON_MONSTER_EMOJI_COUNT + + @classmethod + def get_monster_index(cls, n: int): + return n - cls.NON_MONSTER_EMOJI_COUNT diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e8ff6cab..0077fe45 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -668,9 +668,9 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM # The user could delete the menu before we can do this pass - @commands.command(aliases=['seriesscroll', 'ss']) + @commands.command(aliases=['collabscroll', 'ss']) @checks.bot_has_permissions(embed_links=True) - async def collabscroll(self, ctx, *, query): + async def seriesscroll(self, ctx, *, query): dgcog = await self.get_dgcog() monster = await dgcog.find_monster(query, ctx.author.id) @@ -697,7 +697,7 @@ async def collabscroll(self, ctx, *, query): instruction_message = 'Click a reaction to see monster details!' state = SeriesScrollViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, query, color, - series_id, 0, paginated_monsters[0], int(rarity), len(paginated_monsters), + series_id, paginated_monsters, 0, int(rarity), query_settings, all_rarities, title, instruction_message, diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 35b60af4..5c3ed8ad 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -5,7 +5,6 @@ from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils import char_to_emoji, embed_footer_with_state -from tsutils.enums import Server from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -20,8 +19,9 @@ class SeriesScrollViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 11 - def __init__(self, original_author_id, menu_type, raw_query, query, color, series_id, current_page, - monster_list: List["MonsterModel"], rarity: int, pages_in_rarity: int, query_settings: QuerySettings, + def __init__(self, original_author_id, menu_type, raw_query, query, color, series_id, + paginated_monsters: List[List["MonsterModel"]], current_page, rarity: int, + query_settings: QuerySettings, all_rarities: List[int], title, message, current_index: int = None, @@ -30,21 +30,37 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, serie child_message_id=None): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) - self.pages_in_rarity = pages_in_rarity self.current_index = current_index self.all_rarities = all_rarities + self.paginated_monsters = paginated_monsters self.current_page = current_page or 0 self.series_id = series_id self.rarity = rarity self.query_settings = query_settings - self.message = message + self.idle_message = message self.child_message_id = child_message_id self.title = title - self.monster_list = monster_list self.reaction_list = reaction_list self.color = color self.query = query - self.max_len_so_far = max(max_len_so_far or len(monster_list), len(self.monster_list)) + self._max_len_so_far = max(max_len_so_far or len(self.monster_list), len(self.monster_list)) + + @property + def monster_list(self) -> List["MonsterModel"]: + return self.paginated_monsters[self.current_page] + + @property + def max_len_so_far(self) -> int: + self._max_len_so_far = max(len(self.monster_list), self._max_len_so_far) + return self._max_len_so_far + + @property + def current_monster_id(self) -> int: + return self.monster_list[self.current_index].monster_id + + @property + def pages_in_rarity(self) -> int: + return len(self.paginated_monsters) def serialize(self): ret = super().serialize() @@ -59,12 +75,23 @@ def serialize(self): 'all_rarities': self.all_rarities, 'reaction_list': self.reaction_list, 'child_message_id': self.child_message_id, - 'message': self.message, + 'idle_message': self.idle_message, 'max_len_so_far': self.max_len_so_far, 'current_index': self.current_index, }) return ret + def get_serialized_child_extra_ims(self, emoji_names, menu_type): + extra_ims = { + 'is_child': True, + 'reaction_list': emoji_names, + 'menu_type': menu_type, + 'resolved_monster_id': self.current_monster_id, + 'query_settings': self.query_settings.serialize(), + 'idle_message': self.idle_message + } + return extra_ims + @staticmethod async def deserialize(dgcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): @@ -75,24 +102,23 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): query_settings = QuerySettings.deserialize(ims.get('query_settings')) paginated_monsters = await SeriesScrollViewState.do_query(dgcog, series_id, rarity, query_settings.server) current_page = ims['current_page'] - monster_list = paginated_monsters[current_page] title = ims['title'] - pages_in_rarity = len(paginated_monsters) raw_query = ims['raw_query'] query = ims.get('query') or raw_query original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') child_message_id = ims.get('child_message_id') - message = ims.get('message') - max_len_so_far = max(ims['max_len_so_far'] or len(monster_list), len(monster_list)) current_index = ims.get('current_index') + current_monster_list = paginated_monsters[current_page] + max_len_so_far = max(ims['max_len_so_far'] or len(current_monster_list), len(current_monster_list)) + idle_message = ims.get('idle_message') return SeriesScrollViewState(original_author_id, menu_type, raw_query, query, user_config.color, series_id, - current_page, monster_list, rarity, pages_in_rarity, query_settings, + paginated_monsters, current_page, rarity, query_settings, all_rarities, - title, message, + title, idle_message, current_index=current_index, max_len_so_far=max_len_so_far, reaction_list=reaction_list, @@ -124,6 +150,65 @@ async def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: paginated_monsters = await SeriesScrollViewState.do_query(dgcog, series_id, rarity, query_settings.server) return paginated_monsters + async def decrement_page(self, dgcog): + if self.current_page > 0: + self.current_page = self.current_page - 1 + self.current_index = None + else: + # if there are multiple rarities, decrementing first page will change rarity + if len(self.all_rarities) > 1: + rarity_index = self.all_rarities.index(self.rarity) + self.rarity = self.all_rarities[rarity_index - 1] + self.paginated_monsters = await SeriesScrollViewState.do_query(dgcog, self.series_id, self.rarity, + self.query_settings.server) + self.current_index = None + self.current_page = len(self.paginated_monsters) - 1 + + if len(self.paginated_monsters) > 1: + self.current_index = None + + async def increment_page(self, dgcog): + if self.current_page < len(self.paginated_monsters) - 1: + self.current_page = self.current_page + 1 + self.current_index = None + else: + # if there are multiple rarities, incrementing last page will change rarity + if len(self.all_rarities) > 1: + rarity_index = self.all_rarities.index(self.rarity) + self.rarity = self.all_rarities[(rarity_index + 1) % len(self.all_rarities)] + self.paginated_monsters = await SeriesScrollViewState.do_query(dgcog, self.series_id, self.rarity, + self.query_settings.server) + self.current_index = None + self.current_page = 0 + + if len(self.paginated_monsters) > 1: + self.current_index = None + + async def decrement_index(self, dgcog): + if self.current_index is None: + self.current_index = len(self.monster_list) - 1 + return + if self.current_index > 0: + self.current_index = self.current_index - 1 + return + await self.decrement_page(dgcog, ims) + self.current_index = len(self.monster_list) - 1 + + async def increment_index(self, dgcog): + if self.current_index is None: + self.current_index = 0 + return + if self.current_index < len(self.monster_list) - 1: + self.current_index = self.current_index + 1 + return + await self.increment_page(dgcog, ims) + self.current_index = 0 + + def set_index(self, new_index: int): + # don't want to go out of range, which will forget current index, break next, and break prev + if new_index < len(self.monster_list): + self.current_index = new_index + class SeriesScrollView: VIEW_TYPE = 'SeriesScroll' From 40e4911841d165caf615c7459eb7b0a20aa4cbe1 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Thu, 17 Jun 2021 01:39:57 -0400 Subject: [PATCH 0865/1220] Fix seriesscroll next and prev monster (#1244) --- padinfo/view/series_scroll.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 5c3ed8ad..6c84094c 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -191,7 +191,7 @@ async def decrement_index(self, dgcog): if self.current_index > 0: self.current_index = self.current_index - 1 return - await self.decrement_page(dgcog, ims) + await self.decrement_page(dgcog) self.current_index = len(self.monster_list) - 1 async def increment_index(self, dgcog): @@ -201,7 +201,7 @@ async def increment_index(self, dgcog): if self.current_index < len(self.monster_list) - 1: self.current_index = self.current_index + 1 return - await self.increment_page(dgcog, ims) + await self.increment_page(dgcog) self.current_index = 0 def set_index(self, new_index: int): From cca2d71006e82445135eed4f6194cdd5c0711dae Mon Sep 17 00:00:00 2001 From: Hans Xu Date: Fri, 18 Jun 2021 03:31:53 -0500 Subject: [PATCH 0866/1220] Dungeon Cog (now with Menu2) (#1230) --- dadguide/database_context.py | 6 +- dadguide/database_loader.py | 8 +- dadguide/dungeon_context.py | 265 ++++++++++ dadguide/models/dungeon_model.py | 6 +- dadguide/models/encounter_model.py | 33 ++ dadguide/models/enemy_data_model.py | 13 + dadguide/models/enemy_skill_model.py | 23 + dadguide/models/sub_dungeon_model.py | 24 + dungeon/__init__.py | 10 + dungeon/dungeon_monster.py | 31 ++ dungeon/dungeoncog.py | 199 ++++++++ dungeon/enemy_skill.py | 53 ++ dungeon/enemy_skills_pb2.py | 646 ++++++++++++++++++++++++ dungeon/grouped_skillls.py | 41 ++ dungeon/menu/__init__.py | 0 dungeon/menu/dungeon.py | 126 +++++ dungeon/menu/menu_map.py | 7 + dungeon/menu/simple.py | 71 +++ dungeon/models/EnemySkill.py | 26 + dungeon/models/__init__.py | 0 dungeon/processors.py | 271 ++++++++++ dungeon/safe_dict.py | 3 + dungeon/view/__init__.py | 0 dungeon/view/dungeon.py | 288 +++++++++++ dungeon/view/simple.py | 53 ++ dungeon/view/test_view.py | 0 requirements.txt | 1 + tests/dadguide/database_context_test.py | 5 +- 28 files changed, 2202 insertions(+), 7 deletions(-) create mode 100644 dadguide/dungeon_context.py create mode 100644 dadguide/models/encounter_model.py create mode 100644 dadguide/models/enemy_data_model.py create mode 100644 dadguide/models/enemy_skill_model.py create mode 100644 dadguide/models/sub_dungeon_model.py create mode 100644 dungeon/__init__.py create mode 100644 dungeon/dungeon_monster.py create mode 100644 dungeon/dungeoncog.py create mode 100644 dungeon/enemy_skill.py create mode 100644 dungeon/enemy_skills_pb2.py create mode 100644 dungeon/grouped_skillls.py create mode 100644 dungeon/menu/__init__.py create mode 100644 dungeon/menu/dungeon.py create mode 100644 dungeon/menu/menu_map.py create mode 100644 dungeon/menu/simple.py create mode 100644 dungeon/models/EnemySkill.py create mode 100644 dungeon/models/__init__.py create mode 100644 dungeon/processors.py create mode 100644 dungeon/safe_dict.py create mode 100644 dungeon/view/__init__.py create mode 100644 dungeon/view/dungeon.py create mode 100644 dungeon/view/simple.py create mode 100644 dungeon/view/test_view.py diff --git a/dadguide/database_context.py b/dadguide/database_context.py index a0246cf6..cbb0b275 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -3,12 +3,13 @@ from tsutils.enums import Server from .database_manager import DadguideDatabase +from .dungeon_context import DungeonContext from .models.awoken_skill_model import AwokenSkillModel from .models.dungeon_model import DungeonModel +from .models.enum_types import DEFAULT_SERVER from .models.scheduled_event_model import ScheduledEventModel from .models.series_model import SeriesModel from .monster_graph import MonsterGraph -from .models.enum_types import DEFAULT_SERVER SCHEDULED_EVENT_QUERY = """SELECT schedule.*, @@ -21,9 +22,10 @@ class DbContext(object): - def __init__(self, database: DadguideDatabase, graph: MonsterGraph): + def __init__(self, database: DadguideDatabase, graph: MonsterGraph, dungeon: DungeonContext): self.database = database self.graph = graph + self.dungeon = dungeon self.awoken_skill_map = {awsk.awoken_skill_id: awsk for awsk in self.get_all_awoken_skills()} diff --git a/dadguide/database_loader.py b/dadguide/database_loader.py index fbeb5a8c..32aac1d9 100644 --- a/dadguide/database_loader.py +++ b/dadguide/database_loader.py @@ -1,10 +1,11 @@ -import shutil import os +import shutil from redbot.core import data_manager -from .database_manager import DadguideDatabase from .database_context import DbContext +from .database_manager import DadguideDatabase +from .dungeon_context import DungeonContext from .monster_graph import MonsterGraph @@ -24,5 +25,6 @@ def load_database(existing_db): # Open the new working copy. database = DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) graph = MonsterGraph(database) - db_context = DbContext(database, graph) + dungeon = DungeonContext(database) + db_context = DbContext(database, graph, dungeon) return db_context diff --git a/dadguide/dungeon_context.py b/dadguide/dungeon_context.py new file mode 100644 index 00000000..c6d782ca --- /dev/null +++ b/dadguide/dungeon_context.py @@ -0,0 +1,265 @@ +from dadguide.database_manager import DadguideDatabase +from dadguide.models.dungeon_model import DungeonModel +from dadguide.models.encounter_model import EncounterModel +from dadguide.models.enemy_data_model import EnemyDataModel +from dadguide.models.enemy_skill_model import EnemySkillModel +from dadguide.models.sub_dungeon_model import SubDungeonModel + +nickname_query = ''' +SELECT +enemy_data.behavior, +encounters.encounter_id, +encounters.dungeon_id, +encounters.sub_dungeon_id, +encounters.enemy_id, +encounters.monster_id, +encounters.stage, +encounters.amount, +encounters.turns, +encounters.level, +encounters.hp, +encounters.atk, +encounters.defence, +sub_dungeons.name_ja AS sub_name_ja, +sub_dungeons.name_en AS sub_name_en, +sub_dungeons.name_ko AS sub_name_ko, +sub_dungeons.technical, +dungeons.name_ja, +dungeons.name_en, +dungeons.name_ko, +dungeons.dungeon_type +FROM +encounters +LEFT OUTER JOIN dungeons on encounters.dungeon_id = dungeons.dungeon_id +LEFT OUTER JOIN enemy_data on encounters.enemy_id = enemy_data.enemy_id +LEFT OUTER JOIN monsters on encounters.monster_id = monsters.monster_id +LEFT OUTER JOIN sub_dungeons on sub_dungeons.sub_dungeon_id = encounters.sub_dungeon_id +WHERE +encounters.sub_dungeon_id = {} +ORDER BY +encounters.sub_dungeon_id, +encounters.stage +''' + +sub_dungeon_query = ''' +SELECT +sub_dungeons.sub_dungeon_id, +sub_dungeons.dungeon_id, +sub_dungeons.name_ja, +sub_dungeons.name_en, +sub_dungeons.name_ko, +sub_dungeons.technical +FROM +sub_dungeons +WHERE +sub_dungeons.dungeon_id = {} +''' + +dungeon_query = ''' +SELECT +dungeons.dungeon_id, +dungeons.name_ja, +dungeons.name_en, +dungeons.name_ko, +dungeons.dungeon_type +FROM +dungeons +WHERE +dungeons.name_en LIKE "{}%" +''' + +skill_query = ''' +SELECT +enemy_skills.enemy_skill_id, +enemy_skills.name_en, +enemy_skills.desc_en, +enemy_skills.desc_en_emoji, +enemy_skills.min_hits, +enemy_skills.max_hits, +enemy_skills.atk_mult +FROM +enemy_skills +WHERE +enemy_skill_id = {} +''' + +sub_dungeons_query = ''' +SELECT +sub_dungeons.* +FROM +sub_dungeons +WHERE +sub_dungeons.dungeon_id = {} AND +sub_dungeons.name_en LIKE "%{}%" +ORDER BY +sub_dungeons.sub_dungeon_id +''' + +encounter_query = ''' +SELECT +encounters.encounter_id, +encounters.sub_dungeon_id, +encounters.enemy_id, +encounters.monster_id, +encounters.stage, +encounters.amount, +encounters.turns, +encounters.level, +encounters.hp, +encounters.atk, +encounters.defence +FROM +encounters +WHERE +encounters.sub_dungeon_id = {} +ORDER BY +encounters.stage +''' + +specific_floor_query = ''' +SELECT +encounters.encounter_id, +encounters.sub_dungeon_id, +encounters.enemy_id, +encounters.monster_id, +encounters.stage, +encounters.amount, +encounters.turns, +encounters.level, +encounters.hp, +encounters.atk, +encounters.defence +FROM +encounters +WHERE +encounters.sub_dungeon_id = {} +AND +encounters.stage = {} +''' + +enemy_data_query = ''' +SELECT +enemy_data.enemy_id, +enemy_data.behavior +FROM +enemy_data +WHERE +enemy_data.enemy_id = {} +''' + +# TODO: Move to gdoc +DUNGEON_NICKNAMES = { + 'a1': 1022001, + 'arena1': 102201, + 'bipolar goddess 1': 1022001, + 'bp1': 1022001, + 'a2': 1022002, + 'arena2': 102202, + 'bipolar goddess 2': 1022002, + 'bp2': 1022002, + 'a3': 1022003, + 'arena3': 102203, + 'bipolar goddess 3': 1022003, + 'bp3': 1022003, + 'a4': 1022004, + 'arena4': 1022004, + 'three hands of fate': 1022004, + 'thof': 1022004, + 'a5': 1022005, + 'arena5': 1022005, + 'incarnation of worlds': 1022006, + 'iow': 1022006, + 'aa1': 2660001, + 'aa2': 2660002, + 'aa3': 2660003, + 'aa4': 2660004, + 'shura1': 4400001, + 'shura2': 4400002, + 'shura3': 4400003, + 'iwoc': 4400001, + 'alt. iwoc': 4400001, +} + + +class DungeonContext(object): + def __init__(self, database: DadguideDatabase): + self.database = database + + def get_dungeons_from_name(self, name: str): + dungeons_result = self.database.query_many(dungeon_query.format(name), ()) + dungeons = [] + for d in dungeons_result: + dungeons.append(DungeonModel([], **d)) + + for dm in dungeons: + subs = self.database.query_many(sub_dungeon_query.format(dm.dungeon_id), ()) + for s in subs: + encounters = self.database.query_many(encounter_query.format(s['sub_dungeon_id']), ()) + ems = [] + for e in encounters: + data = self.database.query_one(enemy_data_query.format(e["enemy_id"]), ()) + if data is not None: + edm = EnemyDataModel(**data) + else: + edm = None + ems.append(EncounterModel(edm, **e)) + dm.sub_dungeons.append(SubDungeonModel(ems, **s)) + return dungeons + + def get_dungeons_from_nickname(self, name: str): + if name not in DUNGEON_NICKNAMES: + return None + sub_id = DUNGEON_NICKNAMES.get(name) + mega = self.database.query_many(nickname_query.format(sub_id), ()) + print(mega) + ems = [] + for enc in mega: + data = self.database.query_one(enemy_data_query.format(enc["enemy_id"]), ()) + if data is not None: + edm = EnemyDataModel(**data) + else: + edm = None + ems.append(EncounterModel(edm, **enc)) + sm = SubDungeonModel(ems, + sub_dungeon_id=mega[0]['sub_dungeon_id'], + dungeon_id=mega[0]['dungeon_id'], + name_ja=mega[0]['sub_name_ja'], + name_en=mega[0]['sub_name_en'], + name_ko=mega[0]['sub_name_ko'], + technical=mega[0]['technical']) + return [DungeonModel([sm], **mega[0])] + + def get_floor_from_sub_dungeon(self, sub_id, floor): + floor_query = self.database.query_many(specific_floor_query.format(sub_id, floor), ()) + invade_query = self.database.query_many(specific_floor_query.format(sub_id, -1), ()) + encounter_models = [] + floor_query.extend(invade_query) + for f in floor_query: + data = self.database.query_one(enemy_data_query.format(f['enemy_id']), ()) + if data is not None: + edm = EnemyDataModel(**data) + else: + edm = None + encounter_models.append(EncounterModel(edm, **f)) + return encounter_models + + def get_enemy_skill(self, enemy_skill_id): + enemy_skill_query = self.database.query_one(skill_query.format(enemy_skill_id), ()) + return EnemySkillModel(**enemy_skill_query) + + def get_sub_dungeon_id_from_name(self, dungeon_id, sub_name: str): + sub_dungeons = self.database.query_many(sub_dungeons_query.format(dungeon_id, sub_name), ()) + if len(sub_dungeons) == 0: + return None + elif len(sub_dungeons) > 1: + if 'plus' in sub_name.lower(): + for sd in sub_dungeons: + if 'plus' in sd['name_en'].lower(): + return sd['sub_dungeon_id'] + else: + for sd in sub_dungeons: + if 'plus' not in sd['name_en'].lower(): + return sd['sub_dungeon_id'] + elif len(sub_dungeons) > 2: + return 1 + return sub_dungeons[0]['sub_dungeon_id'] diff --git a/dadguide/models/dungeon_model.py b/dadguide/models/dungeon_model.py index 2ef1d301..3fac8b7b 100644 --- a/dadguide/models/dungeon_model.py +++ b/dadguide/models/dungeon_model.py @@ -1,8 +1,11 @@ +from typing import List + from .base_model import BaseModel +from .sub_dungeon_model import SubDungeonModel class DungeonModel(BaseModel): - def __init__(self, **kwargs): + def __init__(self, sub_dungeon_models: List[SubDungeonModel] = None, **kwargs): self.dungeon_id = kwargs['dungeon_id'] self.name_ja = kwargs['name_ja'] self.name_en = kwargs['name_en'] @@ -10,6 +13,7 @@ def __init__(self, **kwargs): self.name_ko = kwargs['name_ko'] self.dungeon_type = kwargs['dungeon_type'] + self.sub_dungeons = sub_dungeon_models @staticmethod def _make_clean_name_en(name): diff --git a/dadguide/models/encounter_model.py b/dadguide/models/encounter_model.py new file mode 100644 index 00000000..f3d20d5e --- /dev/null +++ b/dadguide/models/encounter_model.py @@ -0,0 +1,33 @@ +from .base_model import BaseModel +from .enemy_data_model import EnemyDataModel + + +class EncounterModel(BaseModel): + def __init__(self, enemy_data_model: EnemyDataModel = None, **kwargs): + self.encounter_id = kwargs['encounter_id'] + self.sub_dungeon_id = kwargs['sub_dungeon_id'] + self.enemy_id = kwargs['enemy_id'] + self.monster_id = kwargs['monster_id'] + self.stage = kwargs['stage'] + self.amount = kwargs['amount'] + self.turns = kwargs['turns'] + self.level = kwargs['level'] + self.hp = kwargs['hp'] + self.atk = kwargs['atk'] + self.defence = kwargs['defence'] + self.enemy_data = enemy_data_model + + def to_dict(self): + return { + 'encounter_id': self.encounter_id, + 'sub_dungeon_id': self.sub_dungeon_id, + 'enemy_id': self.enemy_id, + 'monster_id': self.monster_id, + 'stage': self.stage, + 'amount': self.amount, + 'turns': self.turns, + 'level': self.level, + 'hp': self.hp, + 'atk': self.atk, + 'defence': self.defence + } diff --git a/dadguide/models/enemy_data_model.py b/dadguide/models/enemy_data_model.py new file mode 100644 index 00000000..ad8d9695 --- /dev/null +++ b/dadguide/models/enemy_data_model.py @@ -0,0 +1,13 @@ +from .base_model import BaseModel + + +class EnemyDataModel(BaseModel): + def __init__(self, **kwargs): + self.enemy_id = kwargs['enemy_id'] + self.behavior = kwargs['behavior'] + + def to_dict(self): + return { + 'enemy_id': self.enemy_id, + 'behavior': self.behavior, + } diff --git a/dadguide/models/enemy_skill_model.py b/dadguide/models/enemy_skill_model.py new file mode 100644 index 00000000..60b3427c --- /dev/null +++ b/dadguide/models/enemy_skill_model.py @@ -0,0 +1,23 @@ +from .base_model import BaseModel + + +class EnemySkillModel(BaseModel): + def __init__(self, **kwargs): + self.enemy_skill_id = kwargs['enemy_skill_id'] + self.name_en = kwargs['name_en'] + self.desc_en = kwargs['desc_en'] + self.desc_en_emoji = kwargs['desc_en_emoji'] + self.min_hits = kwargs['min_hits'] + self.max_hits = kwargs['max_hits'] + self.atk_mult = kwargs['atk_mult'] + + def to_dict(self): + return { + 'enemy_skill_id': self.enemy_skill_id, + 'name_en': self.name_en, + 'desc_en': self.desc_en, + 'desc_en_emoji': self.desc_en_emoji, + 'min_hits': self.min_hits, + 'max_hits': self.max_hits, + 'atk_mult': self.atk_mult + } diff --git a/dadguide/models/sub_dungeon_model.py b/dadguide/models/sub_dungeon_model.py new file mode 100644 index 00000000..d4af404d --- /dev/null +++ b/dadguide/models/sub_dungeon_model.py @@ -0,0 +1,24 @@ +from typing import List + +from .base_model import BaseModel +from .encounter_model import EncounterModel + + +class SubDungeonModel(BaseModel): + def __init__(self, encounter_models: List[EncounterModel] = None, **kwargs): + self.sub_dungeon_id = kwargs['sub_dungeon_id'] + self.dungeon_id = kwargs['dungeon_id'] + self.name_ja = kwargs['name_ja'] + self.name_en = kwargs['name_en'] + self.name_ko = kwargs['name_ko'] + self.technical = kwargs['technical'] + self.encounter_models = encounter_models + + def to_dict(self): + return { + 'sub_dungeon_id': self.sub_dungeon_id, + 'dungeon_id': self.dungeon_id, + 'name_ja': self.name_ja, + 'name_en': self.name_en, + 'name_ko': self.name_ko + } diff --git a/dungeon/__init__.py b/dungeon/__init__.py new file mode 100644 index 00000000..e65b9960 --- /dev/null +++ b/dungeon/__init__.py @@ -0,0 +1,10 @@ +import asyncio + +from .dungeoncog import DungeonCog + + +def setup(bot): + n = DungeonCog(bot) + bot.add_cog(n) + bot.loop.create_task(n.register_menu()) + asyncio.create_task(n.load_emojis()) diff --git a/dungeon/dungeon_monster.py b/dungeon/dungeon_monster.py new file mode 100644 index 00000000..32d1c418 --- /dev/null +++ b/dungeon/dungeon_monster.py @@ -0,0 +1,31 @@ +from typing import List + +from dungeon.grouped_skillls import GroupedSkills + + +class DungeonMonster(object): + """ + A class that symbolizes an encounter in a dungeon. Why is it not a model? Good question. + """ + + def __init__(self, name: str, hp, atk, defense, turns, level, error=None): + self.name = name + self.hp = hp + self.atk = atk + self.defense = defense + self.defense = hp + self.turns = turns + self.level = level + self.groups: List[GroupedSkills] = [] + self.am_invade = False + + self.error = error + + def add_group(self, group: GroupedSkills): + self.groups.append(group) + + async def collect_skills(self): + skills = [] + for g in self.groups: + skills.extend(await g.collect_skills()) + return skills diff --git a/dungeon/dungeoncog.py b/dungeon/dungeoncog.py new file mode 100644 index 00000000..3f4a0cd8 --- /dev/null +++ b/dungeon/dungeoncog.py @@ -0,0 +1,199 @@ +import logging +import os + +import discord +from discord import Color +from discordmenu.emoji.emoji_cache import emoji_cache +from redbot.core import commands, data_manager + +from dungeon.enemy_skills_pb2 import MonsterBehavior +from dungeon.menu.dungeon import DungeonMenu, DungeonMenuPanes +from dungeon.menu.menu_map import dungeon_menu_map +from dungeon.view.dungeon import DungeonViewState + +logger = logging.getLogger('red.padbot-cogs.dungeon') +EMBED_NOT_GENERATED = -1 + + +def _data_file(file_name: str) -> str: + return os.path.join(str(data_manager.cog_data_path(raw_name='dungeon')), file_name) + + +RAW_ENEMY_SKILLS_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/ilmina/download_enemy_skill_data.json' +RAW_ENEMY_SKILLS_DUMP = _data_file('enemy_skills.json') + + +class DungeonCog(commands.Cog): + """ + Contains commands that are display information about dungeons. + Right now only one command, dungeon_info which displays information such as spawns in a dungeon. + """ + menu_map = dungeon_menu_map + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + async def load_emojis(self): + await self.bot.wait_until_ready() + logging.debug('load_emojis, dungeon') + emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) + emoji_cache.refresh_from_discord_bot(self.bot) + + async def register_menu(self): + await self.bot.wait_until_ready() + menulistener = self.bot.get_cog("MenuListener") + if menulistener is None: + logger.warning("MenuListener is not loaded.") + return + await menulistener.register(self) + + async def get_menu_default_data(self, ims): + data = { + 'dgcog': await self.get_dgcog(), + 'color': Color.default() + } + return data + + async def get_dgcog(self): + dgcog = self.bot.get_cog("Dadguide") + if dgcog is None: + raise ValueError("Dadguide cog is not loaded") + await dgcog.wait_until_ready() + return dgcog + + async def find_dungeon_from_name2(self, ctx, name: str, database: "DungeonContext", difficulty: str = None): + """ + Gets the sub_dungeon model given the name of a dungeon and its difficulty. + """ + dungeon = database.get_dungeons_from_nickname(name.lower()) + if dungeon is None: + dungeons = database.get_dungeons_from_name(name) + if len(dungeons) == 0: + await ctx.send("No dungeons found!") + return None + if len(dungeons) > 1: + message = "Multiple Dungeons Found, please be more specific:" + for d in dungeons: + message += "\n{}".format(d.name_en) + await ctx.send(message) + return None + dungeon = dungeons[0] + sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty) + sub_dungeon_model = None + if sub_id is None: + sub_id = 0 + for sd in dungeon.sub_dungeons: + if sd.sub_dungeon_id > sub_id: + sub_id = sd.sub_dungeon_id + sub_dungeon_model = sd + else: + for sd in dungeon.sub_dungeons: + if sd.sub_dungeon_id == sub_id: + sub_dungeon_model = sd + break + dungeon.sub_dungeons = [sub_dungeon_model] + else: + dungeon = dungeon[0] + return dungeon + + async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=60): + if starting_menu_emoji not in emoji_to_embed.emoji_dict: + # Selected menu wasn't generated for this monster + return EMBED_NOT_GENERATED + + emoji_to_embed.emoji_dict[self.menu.emoji.get("no")] = self.menu.reaction_delete_message + + try: + result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, + starting_menu_emoji, timeout=timeout) + if result_msg and result_embed: + # Message is finished but not deleted, clear the footer + result_embed.set_footer(text=discord.Embed.Empty) + await result_msg.edit(embed=result_embed) + await result_msg.e + except Exception as ex: + logger.error('Menu failure', exc_info=True) + + @commands.command() + async def dgid(self, ctx, name: str, difficulty: str = None): + ''' + Name: Name of Dungeon + Difficulty: Difficulty level/name of floor (eg. for A1, "Bipolar Goddess") + ''' + # load dadguide cog for database access + dgcog = self.bot.get_cog('Dadguide') + if not dgcog: + logger.warning("Cog 'Dadguide' not loaded") + return + logger.info('Waiting until DG is ready') + await dgcog.wait_until_ready() + dungeon = await self.find_dungeon_from_name2(ctx=ctx, name=name, database=dgcog.database.dungeon, + difficulty=difficulty) + + if dungeon is not None: + # await ctx.send(formatOverview(test_result)) + current_stage = 0 + pm_dungeon = [] + # check for invades: + invades = [] + + # print(dungeon.sub_dungeons[0].encounter_models) + for enc_model in dungeon.sub_dungeons[0].encounter_models: + behavior_test = MonsterBehavior() + if (enc_model.enemy_data is not None) and (enc_model.enemy_data.behavior is not None): + behavior_test.ParseFromString(enc_model.enemy_data.behavior) + else: + behavior_test = None + + # await ctx.send(formatOverview(test_result)) + # pm = process_monster(behavior_test, enc_model, dg_cog.database) + if enc_model.stage < 0: + # pm.am_invade = True + invades.append(enc_model) + elif enc_model.stage > current_stage: + current_stage = enc_model.stage + floor = [enc_model] + pm_dungeon.append(floor) + else: + pm_dungeon[current_stage - 1].append(enc_model) + for f in pm_dungeon: + if pm_dungeon.index(f) != (len(pm_dungeon) - 1): + f.extend(invades) + + menu = DungeonMenu.menu() + original_author_id = ctx.message.author.id + full_reaction_list = [emoji_cache.get_by_name(e) for e in DungeonMenuPanes.emoji_names()] + test_list = ['1', '2', '3', '4'] + # print(pm_dungeon[0]) + view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, Color.default(), pm_dungeon[0][0], + dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, + len(pm_dungeon[0]), 0, + int(dungeon.sub_dungeons[0].technical), dgcog.database, verbose=False, + reaction_list=full_reaction_list) + await ctx.send( + "EN: {}({})\nJP: {}({})".format(dungeon.name_en, dungeon.sub_dungeons[0].name_en, dungeon.name_ja, + dungeon.sub_dungeons[0].name_ja)) + message = await menu.create(ctx, view_state) + + +''' @commands.command() + async def spinner_help(self, ctx, spin_time, move_time): + """ + spin_time: The cycle the spinner is set at (typically changes once every second) + move_time: How much time you have to move orbs + """ + embed = discord.Embed(title="Spinner Helper {}s Rotation {}s Move Time".format(spin_time, move_time), + description="Assuming orbs are set and are NOT moved during movement:") + casino = ["🔥", "🌊", "🌿", "💡", "🌙", "🩹"] + cycles = float(move_time) / float(spin_time) + rounded = int(cycles) + casino_dict = OrderedDict() + for c in casino: + casino_dict.update({c: casino[(casino.index(c) + rounded) % 6]}) + casino_dict.update({"Other": casino[(rounded - 1) % 6]}) + output = "" + for k, v in casino_dict.items(): + output += "\n{} {} {}".format(k, "➡", v) + embed.add_field(name="Original -> Final", value=output) + await ctx.send(embed=embed)''' diff --git a/dungeon/enemy_skill.py b/dungeon/enemy_skill.py new file mode 100644 index 00000000..6e44c3fe --- /dev/null +++ b/dungeon/enemy_skill.py @@ -0,0 +1,53 @@ +from typing import List + +from dungeon.grouped_skillls import GroupedSkills + + +class ProcessedSkill(object): + """ + This is a representation of a processed enemy skill. It contains the name, effect, + processed (emoji) version of the effect, condition requriements, parents, type (passive, preempt) + The class also contains a function called give_string. When called it returns the necessary strings needed to + display it in dungeon_info2 + """ + + def __init__(self, name: str, effect: str, processed: List[str], condition: str = None, + parent: GroupedSkills = None): + self.name = name + self.effect = effect + self.processed = processed + self.condition = condition + self.parent = parent + self.type = self.find_type() + self.is_passive_preempt = len(self.process_type()) != 0 + + def find_type(self): + """Helper function to determine whether the skill is a preempt/passive from the parent group""" + parent = self.parent + while parent is not None: + if parent.type is not None: + return parent.type + parent = parent.parent + return None + + def process_type(self): + if "Passive" in self.type or "Preemptive" in self.type: + return "({})".format(self.type) + return "" + + def give_string(self, indent: str = "", verbose: bool = False): + """When called returns the strings of a skill in the following format: [emoji, effect, condition]""" + components = [[], None, None] # skills (condensed), effect, condition + # if no skills/not processed just return (N/A) as the skill + if len(self.processed) == 0: + components[0].append("(N/A)") + return components + # For the emoji text we append the skill type to the beginning eg. (Emoji Effect) -> (Passive) (Emoji Effect) + components[0] = [self.process_type()] + components[0].extend(self.processed) + # If verbose information is requested (ie display Effect text) we provide it + if verbose: + components[1] = "E: {}".format(self.effect) + if self.condition is not None: + components[2] = "Cond: {}".format(self.condition) + return components diff --git a/dungeon/enemy_skills_pb2.py b/dungeon/enemy_skills_pb2.py new file mode 100644 index 00000000..64a724a7 --- /dev/null +++ b/dungeon/enemy_skills_pb2.py @@ -0,0 +1,646 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: enemy_skills.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='enemy_skills.proto', + package='dadguide_proto', + syntax='proto3', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x12\x65nemy_skills.proto\x12\x0e\x64\x61\x64guide_proto\"\xbf\x02\n\x1cMonsterBehaviorWithOverrides\x12\x12\n\nmonster_id\x18\x01 \x01(\x05\x12-\n\x06levels\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x36\n\x0flevel_overrides\x18\x03 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x43\n\x06status\x18\x04 \x01(\x0e\x32\x33.dadguide_proto.MonsterBehaviorWithOverrides.Status\"_\n\x06Status\x12\x10\n\x0cNOT_APPROVED\x10\x00\x12\x12\n\x0e\x41PPROVED_AS_IS\x10\x01\x12\x14\n\x10NEEDS_REAPPROVAL\x10\x02\x12\x19\n\x15\x41PPROVED_WITH_CHANGES\x10\x03\"f\n\x0fMonsterBehavior\x12\x12\n\nmonster_id\x18\x01 \x01(\x05\x12-\n\x06levels\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x10\n\x08\x61pproved\x18\x03 \x01(\x08\"M\n\rLevelBehavior\x12\r\n\x05level\x18\x01 \x01(\x05\x12-\n\x06groups\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.BehaviorGroup\"\xd9\x02\n\rBehaviorGroup\x12;\n\ngroup_type\x18\x01 \x01(\x0e\x32\'.dadguide_proto.BehaviorGroup.GroupType\x12,\n\tcondition\x18\x02 \x01(\x0b\x32\x19.dadguide_proto.Condition\x12.\n\x08\x63hildren\x18\x03 \x03(\x0b\x32\x1c.dadguide_proto.BehaviorItem\"\xac\x01\n\tGroupType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07PASSIVE\x10\x01\x12\x0b\n\x07PREEMPT\x10\x02\x12\x11\n\rDISPEL_PLAYER\x10\x03\x12\x12\n\x0eMONSTER_STATUS\x10\x04\x12\r\n\tREMAINING\x10\x05\x12\x0c\n\x08STANDARD\x10\x06\x12\t\n\x05\x44\x45\x41TH\x10\x07\x12\x0f\n\x0bUNKNOWN_USE\x10\x08\x12\x14\n\x10HIGHEST_PRIORITY\x10\t\"u\n\x0c\x42\x65haviorItem\x12.\n\x05group\x18\x02 \x01(\x0b\x32\x1d.dadguide_proto.BehaviorGroupH\x00\x12,\n\x08\x62\x65havior\x18\x03 \x01(\x0b\x32\x18.dadguide_proto.BehaviorH\x00\x42\x07\n\x05value\"c\n\x08\x42\x65havior\x12,\n\tcondition\x18\x01 \x01(\x0b\x32\x19.dadguide_proto.Condition\x12\x16\n\x0e\x65nemy_skill_id\x18\x02 \x01(\x05\x12\x11\n\tchild_ids\x18\x03 \x03(\x05\"\x80\x04\n\tCondition\x12\x14\n\x0chp_threshold\x18\x01 \x01(\x05\x12\x12\n\nuse_chance\x18\x02 \x01(\x05\x12\x15\n\rrepeats_every\x18\x03 \x01(\x05\x12\x17\n\x0fglobal_one_time\x18\x04 \x01(\x08\x12\x19\n\x11limited_execution\x18\r \x01(\x05\x12!\n\x19trigger_enemies_remaining\x18\x05 \x01(\x05\x12\x13\n\x0bif_defeated\x18\x06 \x01(\x08\x12\x1f\n\x17if_attributes_available\x18\x07 \x01(\x08\x12\x18\n\x10trigger_monsters\x18\x08 \x03(\x05\x12\x16\n\x0etrigger_combos\x18\t \x01(\x05\x12\x1a\n\x12if_nothing_matched\x18\n \x01(\x08\x12\x14\n\x0ctrigger_turn\x18\x0b \x01(\x05\x12\x18\n\x10trigger_turn_end\x18\x0c \x01(\x05\x12\x1c\n\x14\x61lways_trigger_above\x18\x0e \x01(\x05\x12\x14\n\x0c\x61lways_after\x18\x0f \x01(\x05\x12\x11\n\tskill_set\x18\x10 \x01(\x05\x12\x19\n\x11\x65rased_attributes\x18\x11 \x03(\x05\x12\x13\n\x0b\x64\x61mage_done\x18\x12 \x01(\x05\x12\x1b\n\x13\x61ttributes_attacked\x18\x13 \x03(\x05\x12\x13\n\x0bskills_used\x18\x14 \x01(\x05\x62\x06proto3' +) + + + +_MONSTERBEHAVIORWITHOVERRIDES_STATUS = _descriptor.EnumDescriptor( + name='Status', + full_name='dadguide_proto.MonsterBehaviorWithOverrides.Status', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='NOT_APPROVED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='APPROVED_AS_IS', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NEEDS_REAPPROVAL', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='APPROVED_WITH_CHANGES', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=263, + serialized_end=358, +) +_sym_db.RegisterEnumDescriptor(_MONSTERBEHAVIORWITHOVERRIDES_STATUS) + +_BEHAVIORGROUP_GROUPTYPE = _descriptor.EnumDescriptor( + name='GroupType', + full_name='dadguide_proto.BehaviorGroup.GroupType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PASSIVE', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PREEMPT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DISPEL_PLAYER', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='MONSTER_STATUS', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='REMAINING', index=5, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='STANDARD', index=6, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DEATH', index=7, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='UNKNOWN_USE', index=8, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='HIGHEST_PRIORITY', index=9, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=717, + serialized_end=889, +) +_sym_db.RegisterEnumDescriptor(_BEHAVIORGROUP_GROUPTYPE) + + +_MONSTERBEHAVIORWITHOVERRIDES = _descriptor.Descriptor( + name='MonsterBehaviorWithOverrides', + full_name='dadguide_proto.MonsterBehaviorWithOverrides', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='monster_id', full_name='dadguide_proto.MonsterBehaviorWithOverrides.monster_id', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='levels', full_name='dadguide_proto.MonsterBehaviorWithOverrides.levels', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='level_overrides', full_name='dadguide_proto.MonsterBehaviorWithOverrides.level_overrides', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='status', full_name='dadguide_proto.MonsterBehaviorWithOverrides.status', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _MONSTERBEHAVIORWITHOVERRIDES_STATUS, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=39, + serialized_end=358, +) + + +_MONSTERBEHAVIOR = _descriptor.Descriptor( + name='MonsterBehavior', + full_name='dadguide_proto.MonsterBehavior', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='monster_id', full_name='dadguide_proto.MonsterBehavior.monster_id', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='levels', full_name='dadguide_proto.MonsterBehavior.levels', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='approved', full_name='dadguide_proto.MonsterBehavior.approved', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=360, + serialized_end=462, +) + + +_LEVELBEHAVIOR = _descriptor.Descriptor( + name='LevelBehavior', + full_name='dadguide_proto.LevelBehavior', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='level', full_name='dadguide_proto.LevelBehavior.level', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='groups', full_name='dadguide_proto.LevelBehavior.groups', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=464, + serialized_end=541, +) + + +_BEHAVIORGROUP = _descriptor.Descriptor( + name='BehaviorGroup', + full_name='dadguide_proto.BehaviorGroup', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='group_type', full_name='dadguide_proto.BehaviorGroup.group_type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='condition', full_name='dadguide_proto.BehaviorGroup.condition', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='children', full_name='dadguide_proto.BehaviorGroup.children', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _BEHAVIORGROUP_GROUPTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=544, + serialized_end=889, +) + + +_BEHAVIORITEM = _descriptor.Descriptor( + name='BehaviorItem', + full_name='dadguide_proto.BehaviorItem', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='group', full_name='dadguide_proto.BehaviorItem.group', index=0, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='behavior', full_name='dadguide_proto.BehaviorItem.behavior', index=1, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='value', full_name='dadguide_proto.BehaviorItem.value', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=891, + serialized_end=1008, +) + + +_BEHAVIOR = _descriptor.Descriptor( + name='Behavior', + full_name='dadguide_proto.Behavior', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='condition', full_name='dadguide_proto.Behavior.condition', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enemy_skill_id', full_name='dadguide_proto.Behavior.enemy_skill_id', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='child_ids', full_name='dadguide_proto.Behavior.child_ids', index=2, + number=3, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1010, + serialized_end=1109, +) + + +_CONDITION = _descriptor.Descriptor( + name='Condition', + full_name='dadguide_proto.Condition', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='hp_threshold', full_name='dadguide_proto.Condition.hp_threshold', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='use_chance', full_name='dadguide_proto.Condition.use_chance', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='repeats_every', full_name='dadguide_proto.Condition.repeats_every', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='global_one_time', full_name='dadguide_proto.Condition.global_one_time', index=3, + number=4, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='limited_execution', full_name='dadguide_proto.Condition.limited_execution', index=4, + number=13, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_enemies_remaining', full_name='dadguide_proto.Condition.trigger_enemies_remaining', index=5, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='if_defeated', full_name='dadguide_proto.Condition.if_defeated', index=6, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='if_attributes_available', full_name='dadguide_proto.Condition.if_attributes_available', index=7, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_monsters', full_name='dadguide_proto.Condition.trigger_monsters', index=8, + number=8, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_combos', full_name='dadguide_proto.Condition.trigger_combos', index=9, + number=9, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='if_nothing_matched', full_name='dadguide_proto.Condition.if_nothing_matched', index=10, + number=10, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_turn', full_name='dadguide_proto.Condition.trigger_turn', index=11, + number=11, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_turn_end', full_name='dadguide_proto.Condition.trigger_turn_end', index=12, + number=12, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='always_trigger_above', full_name='dadguide_proto.Condition.always_trigger_above', index=13, + number=14, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='always_after', full_name='dadguide_proto.Condition.always_after', index=14, + number=15, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='skill_set', full_name='dadguide_proto.Condition.skill_set', index=15, + number=16, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='erased_attributes', full_name='dadguide_proto.Condition.erased_attributes', index=16, + number=17, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='damage_done', full_name='dadguide_proto.Condition.damage_done', index=17, + number=18, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='attributes_attacked', full_name='dadguide_proto.Condition.attributes_attacked', index=18, + number=19, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='skills_used', full_name='dadguide_proto.Condition.skills_used', index=19, + number=20, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1112, + serialized_end=1624, +) + +_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['levels'].message_type = _LEVELBEHAVIOR +_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['level_overrides'].message_type = _LEVELBEHAVIOR +_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['status'].enum_type = _MONSTERBEHAVIORWITHOVERRIDES_STATUS +_MONSTERBEHAVIORWITHOVERRIDES_STATUS.containing_type = _MONSTERBEHAVIORWITHOVERRIDES +_MONSTERBEHAVIOR.fields_by_name['levels'].message_type = _LEVELBEHAVIOR +_LEVELBEHAVIOR.fields_by_name['groups'].message_type = _BEHAVIORGROUP +_BEHAVIORGROUP.fields_by_name['group_type'].enum_type = _BEHAVIORGROUP_GROUPTYPE +_BEHAVIORGROUP.fields_by_name['condition'].message_type = _CONDITION +_BEHAVIORGROUP.fields_by_name['children'].message_type = _BEHAVIORITEM +_BEHAVIORGROUP_GROUPTYPE.containing_type = _BEHAVIORGROUP +_BEHAVIORITEM.fields_by_name['group'].message_type = _BEHAVIORGROUP +_BEHAVIORITEM.fields_by_name['behavior'].message_type = _BEHAVIOR +_BEHAVIORITEM.oneofs_by_name['value'].fields.append( + _BEHAVIORITEM.fields_by_name['group']) +_BEHAVIORITEM.fields_by_name['group'].containing_oneof = _BEHAVIORITEM.oneofs_by_name['value'] +_BEHAVIORITEM.oneofs_by_name['value'].fields.append( + _BEHAVIORITEM.fields_by_name['behavior']) +_BEHAVIORITEM.fields_by_name['behavior'].containing_oneof = _BEHAVIORITEM.oneofs_by_name['value'] +_BEHAVIOR.fields_by_name['condition'].message_type = _CONDITION +DESCRIPTOR.message_types_by_name['MonsterBehaviorWithOverrides'] = _MONSTERBEHAVIORWITHOVERRIDES +DESCRIPTOR.message_types_by_name['MonsterBehavior'] = _MONSTERBEHAVIOR +DESCRIPTOR.message_types_by_name['LevelBehavior'] = _LEVELBEHAVIOR +DESCRIPTOR.message_types_by_name['BehaviorGroup'] = _BEHAVIORGROUP +DESCRIPTOR.message_types_by_name['BehaviorItem'] = _BEHAVIORITEM +DESCRIPTOR.message_types_by_name['Behavior'] = _BEHAVIOR +DESCRIPTOR.message_types_by_name['Condition'] = _CONDITION +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +MonsterBehaviorWithOverrides = _reflection.GeneratedProtocolMessageType('MonsterBehaviorWithOverrides', (_message.Message,), { + 'DESCRIPTOR' : _MONSTERBEHAVIORWITHOVERRIDES, + '__module__' : 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.MonsterBehaviorWithOverrides) + }) +_sym_db.RegisterMessage(MonsterBehaviorWithOverrides) + +MonsterBehavior = _reflection.GeneratedProtocolMessageType('MonsterBehavior', (_message.Message,), { + 'DESCRIPTOR' : _MONSTERBEHAVIOR, + '__module__' : 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.MonsterBehavior) + }) +_sym_db.RegisterMessage(MonsterBehavior) + +LevelBehavior = _reflection.GeneratedProtocolMessageType('LevelBehavior', (_message.Message,), { + 'DESCRIPTOR' : _LEVELBEHAVIOR, + '__module__' : 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.LevelBehavior) + }) +_sym_db.RegisterMessage(LevelBehavior) + +BehaviorGroup = _reflection.GeneratedProtocolMessageType('BehaviorGroup', (_message.Message,), { + 'DESCRIPTOR' : _BEHAVIORGROUP, + '__module__' : 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.BehaviorGroup) + }) +_sym_db.RegisterMessage(BehaviorGroup) + +BehaviorItem = _reflection.GeneratedProtocolMessageType('BehaviorItem', (_message.Message,), { + 'DESCRIPTOR' : _BEHAVIORITEM, + '__module__' : 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.BehaviorItem) + }) +_sym_db.RegisterMessage(BehaviorItem) + +Behavior = _reflection.GeneratedProtocolMessageType('Behavior', (_message.Message,), { + 'DESCRIPTOR' : _BEHAVIOR, + '__module__' : 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.Behavior) + }) +_sym_db.RegisterMessage(Behavior) + +Condition = _reflection.GeneratedProtocolMessageType('Condition', (_message.Message,), { + 'DESCRIPTOR' : _CONDITION, + '__module__' : 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.Condition) + }) +_sym_db.RegisterMessage(Condition) + + +# @@protoc_insertion_point(module_scope) diff --git a/dungeon/grouped_skillls.py b/dungeon/grouped_skillls.py new file mode 100644 index 00000000..216a1ab6 --- /dev/null +++ b/dungeon/grouped_skillls.py @@ -0,0 +1,41 @@ +class GroupedSkills(object): + """This is a container class that holds the top level condition, and its processed children""" + + def __init__(self, condition: str, type, parent=None): + self.nested_groups = [] + self.skills = [] + self.condition = condition + self.type = type + self.parent = parent + + def add_group(self, group): + self.nested_groups.append(group) + + def add_skill(self, skill): + self.skills.append(skill) + + def give_string2(self, lines, level=0, verbose: bool = False): + """ + When called it stores the necessary text of it's child skills in lines. + @param lines: a list to store lines of text in the following format [number of indents needed, actual line of text] + @param level: how many indents do we need (essentially is this a nested group or not) + @param verbose: display skill effect text or not + """ + condition = self.condition + if condition is not None: + lines.append([level, ["**Condition: {}:**".format(condition)]]) + level += 1 + for g in self.nested_groups: + g.give_string2(lines, level, verbose) + for s in self.skills: + skill_string_components = s.give_string(verbose=verbose) + skill_string_components[0] = "".join(skill_string_components[0]) + skill_string_components[0] = "**{}**".format(skill_string_components[0]) + skill_string_components = [x for x in skill_string_components if x is not None] + lines.append([level, skill_string_components]) + + async def collect_skills(self): + skill_copy = self.skills.copy() + for g in self.nested_groups: + skill_copy.extend(await g.collect_skills()) + return skill_copy diff --git a/dungeon/menu/__init__.py b/dungeon/menu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dungeon/menu/dungeon.py b/dungeon/menu/dungeon.py new file mode 100644 index 00000000..56390506 --- /dev/null +++ b/dungeon/menu/dungeon.py @@ -0,0 +1,126 @@ +from typing import Optional +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from tsutils.menu.panes import MenuPanes, emoji_buttons + +from dungeon.view.dungeon import DungeonViewState, DungeonView + + +class DungeonNames: + home = 'home' + + +class DungeonMenu: + MENU_TYPE = 'DungeonMenu' + message = None + + @staticmethod + def menu(): + embed = EmbedMenu(DungeonMenuPanes.transitions(), DungeonMenu.message_control, + delete_func=DungeonMenu.respond_with_delete) + return embed + + @staticmethod + async def respond_with_message(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + view_state = await DungeonViewState.deserialize(dgcog, color, ims) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_verbose(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + view_state = await DungeonViewState.deserialize(dgcog, color, ims, verbose_toggle=True) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_delete(message: Optional[Message], ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_previous_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, -1) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_next_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, 1) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_previous_floor(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, -1, 0, reset_spawn=True) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_next_floor(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, 1, 0, reset_spawn=True) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_next_page(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + page = ims.get('page') + if page == 0: + page = 1 + else: + page = 0 + view_state = await DungeonViewState.deserialize(dgcog, color, ims, page=page) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + def message_control(state: DungeonViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [DungeonView.embed(state)], + reaction_list + ) + + + + + +class DungeonEmoji: + home = emoji_buttons['home'] + previous_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + next_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + next_floor = '\N{UPWARDS BLACK ARROW}' + previous_floor = '\N{DOWNWARDS BLACK ARROW}' + next_page = '📖' + verbose = '📜' + + +class DungeonMenuPanes(MenuPanes): + INITIAL_EMOJI = DungeonEmoji.home + DATA = { + DungeonEmoji.home: (DungeonMenu.respond_with_message, DungeonView.VIEW_TYPE), + DungeonEmoji.verbose: (DungeonMenu.respond_with_verbose, DungeonView.VIEW_TYPE), + DungeonEmoji.previous_monster_emoji: (DungeonMenu.respond_with_previous_monster, DungeonView.VIEW_TYPE), + DungeonEmoji.next_monster_emoji: (DungeonMenu.respond_with_next_monster, DungeonView.VIEW_TYPE), + DungeonEmoji.previous_floor: (DungeonMenu.respond_with_previous_floor, DungeonView.VIEW_TYPE), + DungeonEmoji.next_floor: (DungeonMenu.respond_with_next_floor, DungeonView.VIEW_TYPE), + DungeonEmoji.next_page: (DungeonMenu.respond_with_next_page, DungeonView.VIEW_TYPE) + } + HIDDEN_EMOJIS = [ + DungeonEmoji.home + ] diff --git a/dungeon/menu/menu_map.py b/dungeon/menu/menu_map.py new file mode 100644 index 00000000..1aac6176 --- /dev/null +++ b/dungeon/menu/menu_map.py @@ -0,0 +1,7 @@ +from dungeon.menu.dungeon import DungeonMenu, DungeonMenuPanes +from dungeon.menu.simple import SimpleMenu, SimpleMenuPanes + +dungeon_menu_map = { + SimpleMenu.MENU_TYPE: (SimpleMenu, SimpleMenuPanes), + DungeonMenu.MENU_TYPE: (DungeonMenu, DungeonMenuPanes) +} diff --git a/dungeon/menu/simple.py b/dungeon/menu/simple.py new file mode 100644 index 00000000..a4bf7d56 --- /dev/null +++ b/dungeon/menu/simple.py @@ -0,0 +1,71 @@ +from typing import Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from tsutils.menu.panes import MenuPanes, emoji_buttons + +from dungeon.view.simple import SimpleViewState, SimpleView + + +class SimpleNames: + home = 'home' + + +class SimpleMenu: + MENU_TYPE = 'SimpleMenu' + message = None + + @staticmethod + def menu(): + embed = EmbedMenu(SimpleMenuPanes.transitions(), SimpleMenu.message_control, + delete_func=SimpleMenu.respond_with_delete) + return embed + + @staticmethod + async def respond_with_message(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + view_state = await SimpleViewState.deserialize(dgcog, color, ims) + control = SimpleMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_delete(message: Optional[Message], ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_right(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + print("test") + view_state = await SimpleViewState.deserialize(dgcog, color, ims, 1) + control = SimpleMenu.message_control(view_state) + return control + + @staticmethod + def message_control(state: SimpleViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [SimpleView.embed(state)], + reaction_list + ) + + +class SimpleEmoji: + home = emoji_buttons['home'] + right = '\N{BLACK RIGHT-POINTING TRIANGLE}' + + +class SimpleMenuPanes(MenuPanes): + INITIAL_EMOJI = SimpleEmoji.home + DATA = { + SimpleEmoji.home: (SimpleMenu.respond_with_message, SimpleNames.home), + SimpleEmoji.right: (SimpleMenu.respond_with_right, SimpleView.VIEW_TYPE) + + } + HIDDEN_EMOJIS = [ + SimpleNames.home, + ] diff --git a/dungeon/models/EnemySkill.py b/dungeon/models/EnemySkill.py new file mode 100644 index 00000000..47048dfd --- /dev/null +++ b/dungeon/models/EnemySkill.py @@ -0,0 +1,26 @@ +class EnemySkill(object): + def __init__(self, raw: "List[str]"): + self.enemy_skill_id = int(raw[0]) + self.name = raw[1].replace('\n', ' ') + self.type = int(raw[2]) + self.flags = int(raw[3], 16) # 16bitmap for params + self.params = [None] * 16 + offset = 0 + p_idx = 4 + while offset < self.flags.bit_length(): + if (self.flags >> offset) & 1 != 0: + p_value = raw[p_idx] + self.params[offset] = int(p_value) if p_value.lstrip('-').isdigit() else p_value + p_idx += 1 + offset += 1 + + def process(self): + return "Unknown: [{},{},{},{}]".format(self.id, self.name, self.type, self.params) + +class ESNone(EnemySkill): + def __init__(self, id = None): + self.enemy_skill_id = id + self.name = None + self.type = None + self.flags = None + self.params = None \ No newline at end of file diff --git a/dungeon/models/__init__.py b/dungeon/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dungeon/processors.py b/dungeon/processors.py new file mode 100644 index 00000000..76cad34d --- /dev/null +++ b/dungeon/processors.py @@ -0,0 +1,271 @@ +from discordmenu.emoji.emoji_cache import emoji_cache + +from dadguide.database_context import DbContext +from dadguide.models.encounter_model import EncounterModel +from dadguide.models.enemy_skill_model import EnemySkillModel +from dungeon.safe_dict import SafeDict +from dungeon.dungeon_monster import DungeonMonster +from dungeon.enemy_skill import ProcessedSkill +from dungeon.enemy_skills_pb2 import Condition, MonsterBehavior, Behavior, BehaviorGroup +from dungeon.grouped_skillls import GroupedSkills + +GroupType = { + 0: "Unspecified", + 1: "Passive", + 2: "Preemptive", + 3: "Dispel Player", + 4: "Monster Status", + 5: "Remaining", + 6: "Standard", + 7: "Death", + 8: "Unknown", + 9: "Highest Priority" +} + + +def format_condition(cond: Condition): + parts = [] + if cond.skill_set: + parts.append('SkillSet {}'.format(cond.skill_set)) + if cond.use_chance not in [0, 100]: + parts.append('{}% chance'.format(cond.use_chance)) + if cond.global_one_time: + parts.append('one time only') + if cond.limited_execution: + parts.append('at most {} times'.format(cond.limited_execution)) + if cond.trigger_enemies_remaining: + parts.append('when {} enemies remain'.format(cond.trigger_enemies_remaining)) + if cond.if_defeated: + parts.append('when defeated') + if cond.if_attributes_available: + parts.append('when required attributes on board') + if cond.trigger_monsters: + parts.append('when {} on team'.format(', '.join(map(str, cond.trigger_monsters)))) + if cond.trigger_combos: + parts.append('when {} combos last turn'.format(cond.trigger_combos)) + if cond.if_nothing_matched: + parts.append('if no other skills matched') + if cond.repeats_every: + if cond.trigger_turn: + if cond.trigger_turn_end: + parts.append('execute repeatedly, turn {}-{} of {}'.format(cond.trigger_turn, + cond.trigger_turn_end, + cond.repeats_every)) + else: + parts.append('execute repeatedly, turn {} of {}'.format(cond.trigger_turn, cond.repeats_every)) + else: + parts.append('repeats every {} turns'.format(cond.repeats_every)) + elif cond.trigger_turn_end: + turn_text = 'turns {}-{}'.format(cond.trigger_turn, cond.trigger_turn_end) + parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) + elif cond.trigger_turn: + turn_text = 'turn {}'.format(cond.trigger_turn) + parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) + + if not parts and cond.hp_threshold in [100, 0]: + return None + + if cond.hp_threshold == 101: + parts.append('when hp is full') + elif cond.hp_threshold: + parts.append('hp <= {}'.format(cond.hp_threshold)) + + return ', '.join(parts) + + +# From pad-data-pipeline +def _cond_hp_timed_text(always_trigger_above: int, turn_text: str) -> str: + text = turn_text + if always_trigger_above == 1: + text = 'always {}'.format(turn_text) + elif always_trigger_above: + text = '{} while HP > {}'.format(turn_text, always_trigger_above) + return text + + +def formatOverview(query): + output = "Dungeon Name: {}".format(query[0]["dungeon_name_en"]) + for q in query: + output += "\nEncounter ID: {} Stage: {} Monster Name: {} Level: {}".format(q["encounter_id"], q["stage"], + q["name_en"], q["level"]) + + return output + + +def behaviorForLevel(mb: MonsterBehavior, num: int): + lev = None + for level in mb.levels: + if level.level == num: lev = level + if lev is None: + return mb.levels[len(mb.levels) - 1] + else: + return lev + + +def process_enemy_skill2(encounter: EncounterModel, skill: EnemySkillModel, emoji_map): + effect = skill.desc_en_emoji + split_effects = effect.split("), ") + non_attack_effects = [] + for e in split_effects: + # print(encounter.monster_id) + e += ")" + if "Attack:" not in e: + non_attack_effects.append(e.format_map(emoji_map)) + + # Damage + atk = encounter.atk + if skill.min_hits != 0: + emoji = emoji_map['attack'] + if skill.min_hits > 1: + emoji = emoji_map['multi_attack'] + damage_per_hit = (int)(atk * (skill.atk_mult / 100.0)) + min_damage = skill.min_hits * damage_per_hit + max_damage = skill.max_hits * damage_per_hit + if min_damage != max_damage: + non_attack_effects.append("({}:{}~{})".format(emoji, f'{min_damage:,}', f'{max_damage:,}')) + else: + non_attack_effects.append("({}:{})".format(emoji, f'{min_damage:,}')) + + return non_attack_effects + + +""" +Process_[behavior, behavior_group, monster]: These functions take the behavior data and convert it to a easier to work +(for me) objects +""" + + +def process_behavior(behavior: Behavior, database, q: EncounterModel, emoji_map, parent: GroupedSkills = None): + skill = database.dungeon.get_enemy_skill(behavior.enemy_skill_id) + if skill is None: + return "Unknown" + skill_name = skill.name_en + skill_effect = skill.desc_en + # skill_processed_text = process_enemy_skill(skill_effect, q, skill) + skill_processed_texts = process_enemy_skill2(q, skill, emoji_map) + + condition = format_condition(behavior.condition) + processed_skill: ProcessedSkill = ProcessedSkill(skill_name, skill_effect, skill_processed_texts, condition, + parent) + # embed.add_field(name="{}Skill Name: {}{}".format(group_type, skill_name, process_enemy_skill(skill_effect, q, skill)), value="{}Effect: {}\n{}".format(indent, skill_effect, condition), inline=False) + return processed_skill + + +def process_behavior_group(group: BehaviorGroup, database, q: EncounterModel, emoji_map, + parent: GroupedSkills = None): + condition = format_condition(group.condition) + processed_group: GroupedSkills = GroupedSkills(condition, GroupType[group.group_type], parent) + for child in group.children: + if child.HasField('group'): + processed_group.add_group(process_behavior_group(child.group, database, q, emoji_map, processed_group)) + elif child.HasField('behavior'): + processed_group.add_skill(process_behavior(child.behavior, database, q, emoji_map, processed_group)) + return processed_group + + +# return output + +# Format: +# Skill Name Type:Preemptive/Passive/Etc +# Skill Effect +# Condition +def process_monster(mb: MonsterBehavior, q: EncounterModel, database: DbContext): + # output = "Behavior for: {} at Level: {}".format(q["name_en"], q["level"]) # TODO: Delete later after testing + """embed = discord.Embed(title="Behavior for: {} at Level: {}".format(q["name_en"], q["level"]), + description="HP:{} ATK:{} DEF:{} TURN:{}".format(q["hp"], q["atk"], q["defence"], q['turns']))""" + emoji_map = SafeDict(resolve_status=emoji_cache.get_by_name('resolve'), + fire_orb=emoji_cache.get_by_name('orb_fire'), + recover_health=emoji_cache.get_by_name('misc_autoheal'), + roulette=emoji_cache.get_by_name('spinner'), + unknown_type='Unknown', + defense_status=emoji_cache.get_by_name('defense'), + combo_absorb_status=emoji_cache.get_by_name('combo'), + absorb_status=emoji_cache.get_by_name('attribute_absorb'), + damage_absorb_status=emoji_cache.get_by_name('damage_absorb'), + damage_void_status=emoji_cache.get_by_name('vdp'), + status_shield_status=emoji_cache.get_by_name('status'), + movetime_buff_status=emoji_cache.get_by_name('time_buff'), + movetime_debuff_status=emoji_cache.get_by_name('time_debuff'), + dispel_status='Dispel', + leader_swap_status=emoji_cache.get_by_name('swap'), + skill_delay=emoji_cache.get_by_name('misc_sb'), + lock_orbs=emoji_cache.get_by_name('lock'), + tape_status=emoji_cache.get_by_name('res_seal'), + fixed_start=emoji_cache.get_by_name('orb_start'), + cloud_status=emoji_cache.get_by_name('res_cloud'), + gravity=emoji_cache.get_by_name('gravity'), + invincible_status=emoji_cache.get_by_name('invincible'), + invincible_off_status=emoji_cache.get_by_name('invincible'), # TODO + force_target_status=emoji_cache.get_by_name('force_target'), + leader_alter_status=emoji_cache.get_by_name('transform'), + board_size_status=emoji_cache.get_by_name('board_size'), + super_resolve_status=emoji_cache.get_by_name('super_resolve'), + turn_change=emoji_cache.get_by_name('turn_change'), + enrage_status=emoji_cache.get_by_name('enrage'), + active_skill_bind=emoji_cache.get_by_name('skill_bound'), + do_nothing="💤", + awoken_skill_bind=emoji_cache.get_by_name('awoken_skill_bind'), + no_skyfall_status=emoji_cache.get_by_name('no_skyfall'), + bind=emoji_cache.get_by_name('res_bind'), + skyfall_status='🌧', + blind='⚫', + super_blind=emoji_cache.get_by_name('blind_orb'), + to='➡️', + attack=emoji_cache.get_by_name('single_hit'), + multi_attack=emoji_cache.get_by_name('multi_hit'), + target_self='Self', + health=emoji_cache.get_by_name('health'), + combo_orb=emoji_cache.get_by_name('orb_combo'), + locked_bomb_orb=emoji_cache.get_by_name('locked_bomb_orb'), + random_attribute='[Random Att]', + water_orb=emoji_cache.get_by_name('orb_water'), + wood_orb=emoji_cache.get_by_name('orb_wood'), + light_orb=emoji_cache.get_by_name('orb_light'), + dark_orb=emoji_cache.get_by_name('orb_dark'), + healer_orb=emoji_cache.get_by_name('heal_orb'), + jammer_orb=emoji_cache.get_by_name('jammer_orb'), + poison_orb=emoji_cache.get_by_name('poison_orb'), + bomb_orb=emoji_cache.get_by_name('bomb_orb'), + mortal_poison_orb=emoji_cache.get_by_name('mortal_poison_orb'), + evo_material_type=emoji_cache.get_by_name('killer_evomat'), + balanced_type=emoji_cache.get_by_name('killer_balance'), + physical_type=emoji_cache.get_by_name('killer_physical'), + healer_type=emoji_cache.get_by_name('killer_healer'), + dragon_type=emoji_cache.get_by_name('killer_dragon'), + god_type=emoji_cache.get_by_name('killer_god'), + attacker_type=emoji_cache.get_by_name('killer_attacker'), + devil_type=emoji_cache.get_by_name('killer_devil'), + machine_type=emoji_cache.get_by_name('killer_machine'), + awakening_material_type=emoji_cache.get_by_name('killer_awoken'), + enhance_material_type=emoji_cache.get_by_name('killer_enhancemat'), + redeemable_material_type='🪙', + no_match_fire=emoji_cache.get_by_name('no_match_fire'), + no_match_wood=emoji_cache.get_by_name('no_match_wood'), + no_match_water=emoji_cache.get_by_name('no_match_water'), + no_match_dark=emoji_cache.get_by_name('no_match_dark'), + no_match_light=emoji_cache.get_by_name('no_match_light'), + no_match_heart=emoji_cache.get_by_name('no_match_heart'), + no_match_jammer=emoji_cache.get_by_name('no_match_jammer'), + no_match_poison=emoji_cache.get_by_name('no_match_poison'), + no_match_heal=emoji_cache.get_by_name('no_match_heal'), + recover_debuff_status=emoji_cache.get_by_name('recover_debuff_status'), + attack_debuff_status='⚔️''⬇️' + ) + monster_model = database.graph.get_monster(q.monster_id) + monster: DungeonMonster = DungeonMonster( + name=monster_model.name_en, + hp=q.hp, + atk=q.atk, + defense=q.defence, + turns=q.turns, + level=q.level + ) + if mb is None: + return monster + levelBehavior = behaviorForLevel(mb, q.level) + if levelBehavior is None: + return monster + for group in levelBehavior.groups: + monster.add_group(process_behavior_group(group, database, q, emoji_map)) + + return monster diff --git a/dungeon/safe_dict.py b/dungeon/safe_dict.py new file mode 100644 index 00000000..4f1007dc --- /dev/null +++ b/dungeon/safe_dict.py @@ -0,0 +1,3 @@ +class SafeDict(dict): + def __missing__(self, key): + return '{' + key + '}' diff --git a/dungeon/view/__init__.py b/dungeon/view/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dungeon/view/dungeon.py b/dungeon/view/dungeon.py new file mode 100644 index 00000000..b389d936 --- /dev/null +++ b/dungeon/view/dungeon.py @@ -0,0 +1,288 @@ +import random +from typing import List + +import discord +from discord import Embed +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state + +from dungeon.enemy_skills_pb2 import MonsterBehavior +from dungeon.processors import process_monster +from padinfo.view.components.view_state_base import ViewStateBase + + +class DungeonViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, color, pm, + sub_dungeon_id, num_floors, floor, num_spawns, floor_index, technical, database, page=0, verbose=False, + reaction_list: List[str] = None): + super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) + self.pm = pm + self.sub_dungeon_id = sub_dungeon_id + self.num_floors = num_floors + self.floor = floor + self.floor_index = floor_index + self.technical = technical + self.color = color + self.database = database + self.num_spawns = num_spawns + self.page = page + self.verbose = verbose + + def serialize(self): + ret = super().serialize() + ret.update({ + 'sub_dungeon_id': self.sub_dungeon_id, + 'num_floors': self.num_floors, + 'floor': self.floor, + 'floor_index': self.floor_index, + 'technical': self.technical, + 'pane_type': DungeonView.VIEW_TYPE, + 'verbose': self.verbose, + 'page': self.page + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, + verbose_toggle: bool = False, page: int = 0, reset_spawn: bool = False): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + raw_query = ims.get('raw_query') + sub_dungeon_id = ims.get('sub_dungeon_id') + num_floors = ims.get('num_floors') + floor = ims.get('floor') + inc_floor + floor_index = ims.get('floor_index') + inc_index + technical = ims.get('technical') + verbose = ims.get('verbose') + # check if we are on final floor/final monster of the floor + if floor > num_floors: + floor = 1 + + if floor < 1: + floor = num_floors + + # toggle verbose + if verbose_toggle: + verbose = not verbose + + # get encounter models for the floor + floor_models = dgcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) + + # check if we are on final monster of the floor + if floor_index >= len(floor_models): + floor_index = 0 + + if floor_index < 0: + floor_index = len(floor_models) + floor_index + + # check if we reset the floor_index + if reset_spawn: + floor_index = 0 + + encounter_model = floor_models[floor_index] + + return cls(original_author_id, menu_type, raw_query, color, encounter_model, sub_dungeon_id, + num_floors, floor, len(floor_models), floor_index, + technical, dgcog.database, verbose=verbose, + reaction_list=ims.get('reaction_list'), page=page) + + +class DungeonView: + VIEW_TYPE = 'DungeonText' + + egg_text = [ + "But nobody came...", + "Come again later", + "Tsubaki loves you", + "Remember 100 turn Hera stalling?", + "Odin x Mermaid are the OG heroes", + "Never forget 10 minute stamina", + "Another 3*...", + "Nice!", + "Maybe one day DBZ will come", + "Special thanks to the PADX team and tactical_retreat!" + ] + + @staticmethod + def indent(level): + """ + Helper function that indents text for the embed + """ + ret = "" + for l in range(level): + if l == 0: + ret += "> " + else: + ret += "\u200b \u200b \u200b \u200b \u200b " + return ret + + @staticmethod + def embed_helper(level, names, values, line): + """ + Adds a line in such a way to maximize discord embed limits. + + We first try to add it to the most recent name part of the name/value pair. + If that fails we try to add it to the corresponding value. If that fails, + we create another name/value pair. + @param level: how many indents the line needs + @param names: an existing list (a list of strings that will be placed in "name fields" + @param values: an existing list (a list of strings that will be placed in the value part of the name/value field + @param line: the line of text we want to add + """ + + current_index = len(names) - 1 + indents = DungeonView.indent(level) + if len(names[current_index]) + len(line) + len(indents) <= 255 and len(names[current_index]) == 0: + names[current_index] += "\n{}{}".format(indents, line) + elif len(values[current_index]) + len(line) + len(indents) <= 1023: + values[current_index] += "\n{}{}".format(indents, line) + else: + names.append("") + values.append("") + DungeonView.embed_helper(level, names, values, line) + + @staticmethod + def embed(state: DungeonViewState): + fields = [] + mb = MonsterBehavior() + encounter_model = state.pm + if (encounter_model.enemy_data is not None) and (encounter_model.enemy_data.behavior is not None): + mb.ParseFromString(encounter_model.enemy_data.behavior) + else: + mb = None + monster = process_monster(mb, encounter_model, state.database) + monster_embed: Embed = \ + DungeonView.make_embed(monster, verbose=state.verbose, spawn=[state.floor_index + 1, state.num_spawns], + floor=[state.floor, state.num_floors], technical=state.technical)[state.page] + hp = f'{monster.hp:,}' + atk = f'{monster.atk:,}' + defense = f'{monster.defense:,}' + turns = f'{monster.turns:,}' + + title = monster_embed.title + desc = monster_embed.description + me_fields = monster_embed.fields + for f in me_fields: + fields.append( + EmbedField(f.name, Box(*[f.value])) + ) + return EmbedView( + EmbedMain( + title=title, + description=desc, + ), + embed_fields=fields, + embed_footer=embed_footer_with_state(state) + ) + + @staticmethod + def make_embed(dungeon_monster, verbose: bool = False, spawn: "list[int]" = None, floor: "list[int]" = None, + technical: int = None): + """ + When called this generates an embed that displays the encounter (what is seen in dungeon_info). + @param dungeon_monster: the processed monster to make an embed for + @param verbose: whether or not to display effect text + @param spawn: used to show what spawn this is on a floor [spawn, max] -> "spawn/max" + @param floor: used to show what floor this is [current floor, number of floors] + @param technical: if the dungeon is a technical dungeon we don't display skills + """ + + embeds = [] + + desc = "" + + # We create two pages as monsters at max will only ever require two pages of embeds + if spawn is not None: + embed = discord.Embed( + title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{} Page:".format(dungeon_monster.name, dungeon_monster.level, spawn[0], + spawn[1], + floor[0], floor[1]), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc) + ) + else: + embed = discord.Embed( + title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc) + ) + + embeds.append(embed) + embeds.append(embed.copy()) + + if technical == 0: + embeds[0].title += " 1/1" + embeds[1].title += " 2/1" + return embeds + embeds[0].title += " 1/" + embeds[1].title += " 2/" + # We collect text from the skills and groups of skills + lines = [] + for group in dungeon_monster.groups: + group.give_string2(lines, 0, verbose) + + # We add the lines using embed_helper + names = [""] + values = [""] + fields = 0 + current_embed = 0 + length = len(embed.title) + len(embed.description) + 1 + first = None + for l in lines: + for comp in l[1]: + DungeonView.embed_helper(l[0], names, values, comp) + + # We add the name/value pairs to the actual embed. If needed we go to the second page + if len(values[len(values) - 1]) == 0: + values[len(values) - 1] = '\u200b' + for index in range(len(names)): + name = names[index] + value = values[index] + if len(name) > 0: + temp_length = length + len(name) + len(value) + if temp_length > 6000: + current_embed += 1 + length = len(embed.title) + len(embed.description) + embeds[current_embed].add_field(name=name, value=value, inline=False) + length += len(name) + len(value) + fields += 1 + # embed.add_field(name=k, value=content, inline=False) + + if current_embed == 0: + embeds[0].title += '1' + embeds[1].title += '1' + + # for fun + random_index = random.randint(0, len(DungeonView.egg_text) - 1) + embeds[1].add_field(name=DungeonView.egg_text[random_index], value="Come back when you see 1/2 for page!") + else: + embeds[0].title += '2' + embeds[1].title += '2' + return embeds + + @staticmethod + async def make_preempt_embed(dungeon_monster, spawn: "list[int]" = None, floor: "list[int]" = None, technical: int = None): + """ + Currently unused: when called it creates an embed that only contains embed information. + """ + skills = await dungeon_monster.collect_skills() + desc = "" + for s in skills: + if "Passive" in s.type or "Preemptive" in s.type: + desc += "\n{}".format(s.give_string(verbose=True)) + if technical == 0: + desc = "" + if spawn is not None: + embed = discord.Embed( + title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{}".format(dungeon_monster.name, dungeon_monster.level, spawn[0], spawn[1], + floor[0], floor[1]), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc)) + else: + embed = discord.Embed( + title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc)) + return [embed, discord.Embed(title="test", desc="test")] diff --git a/dungeon/view/simple.py b/dungeon/view/simple.py new file mode 100644 index 00000000..d6b52d58 --- /dev/null +++ b/dungeon/view/simple.py @@ -0,0 +1,53 @@ +from typing import List + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state + +from padinfo.common.config import UserConfig +from padinfo.view.components.view_state_base import ViewStateBase + +class SimpleViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, + color, message_list, index, + reaction_list: List[str] = None): + super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) + self.message_list = message_list + self.index = index + self.color = color + + def serialize(self): + ret = super().serialize() + ret.update({ + 'message_list': self.message_list, + 'index': self.index + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, color, ims: dict, inc): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + raw_query = ims.get('raw_query') + print("hi") + return cls(original_author_id, menu_type, raw_query, color, ims.get('message_list'), ims.get('index') + inc, + reaction_list=ims.get('reaction_list')) + + +class SimpleView: + VIEW_TYPE = 'SimpleText' + + @staticmethod + def embed(state: SimpleViewState): + fields=[ + EmbedField("Title", Box(*["1", '2'])) + ] + return EmbedView( + EmbedMain( + color=state.color, + description=state.message_list[state.index] + ), + embed_fields=fields, + embed_footer=embed_footer_with_state(state), + ) diff --git a/dungeon/view/test_view.py b/dungeon/view/test_view.py new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt index edd52c3b..55da826e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ setuptools tabulate tsutils wheel +protobuf \ No newline at end of file diff --git a/tests/dadguide/database_context_test.py b/tests/dadguide/database_context_test.py index 923a6219..9bdbd349 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dadguide/database_context_test.py @@ -2,12 +2,15 @@ from dadguide.database_manager import DadguideDatabase from dadguide.database_context import DbContext +from dadguide.dungeon_context import DungeonContext from dadguide.monster_graph import MonsterGraph from dadguide.models.enum_types import InternalEvoType database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) -db_context = DbContext(database, graph) +dungeon = DungeonContext(database) +dungeon = DungeonContext(database) +db_context = DbContext(database, graph, dungeon) get_monster = lambda mid, server=Server.COMBINED: graph.get_monster(mid, server=server) assert get_monster(4).is_farmable From 92a15bed796ce63ed2f4f896bf5aef1f4ac0567b Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 18 Jun 2021 02:27:17 -0700 Subject: [PATCH 0867/1220] DungeonCog updates (#1245) Co-authored-by: loopdeer --- dadguide/dungeon_context.py | 249 ++++++------ dungeoncog/__init__.py | 10 + dungeoncog/dungeon_monster.py | 31 ++ dungeoncog/dungeoncog.py | 173 +++++++++ dungeoncog/enemy_skill.py | 53 +++ dungeoncog/enemy_skills_pb2.py | 635 +++++++++++++++++++++++++++++++ dungeoncog/grouped_skillls.py | 41 ++ dungeoncog/info.json | 31 ++ dungeoncog/menu/__init__.py | 0 dungeoncog/menu/dungeon.py | 123 ++++++ dungeoncog/menu/menu_map.py | 7 + dungeoncog/menu/simple.py | 70 ++++ dungeoncog/models/__init__.py | 0 dungeoncog/models/enemy_skill.py | 31 ++ dungeoncog/processors.py | 269 +++++++++++++ dungeoncog/view/__init__.py | 0 dungeoncog/view/dungeon.py | 304 +++++++++++++++ dungeoncog/view/simple.py | 49 +++ padevents/padevents.py | 4 +- 19 files changed, 1961 insertions(+), 119 deletions(-) create mode 100644 dungeoncog/__init__.py create mode 100644 dungeoncog/dungeon_monster.py create mode 100644 dungeoncog/dungeoncog.py create mode 100644 dungeoncog/enemy_skill.py create mode 100644 dungeoncog/enemy_skills_pb2.py create mode 100644 dungeoncog/grouped_skillls.py create mode 100644 dungeoncog/info.json create mode 100644 dungeoncog/menu/__init__.py create mode 100644 dungeoncog/menu/dungeon.py create mode 100644 dungeoncog/menu/menu_map.py create mode 100644 dungeoncog/menu/simple.py create mode 100644 dungeoncog/models/__init__.py create mode 100644 dungeoncog/models/enemy_skill.py create mode 100644 dungeoncog/processors.py create mode 100644 dungeoncog/view/__init__.py create mode 100644 dungeoncog/view/dungeon.py create mode 100644 dungeoncog/view/simple.py diff --git a/dadguide/dungeon_context.py b/dadguide/dungeon_context.py index c6d782ca..94fe4c7e 100644 --- a/dadguide/dungeon_context.py +++ b/dadguide/dungeon_context.py @@ -1,3 +1,5 @@ +from typing import Optional, List + from dadguide.database_manager import DadguideDatabase from dadguide.models.dungeon_model import DungeonModel from dadguide.models.encounter_model import EncounterModel @@ -5,146 +7,157 @@ from dadguide.models.enemy_skill_model import EnemySkillModel from dadguide.models.sub_dungeon_model import SubDungeonModel -nickname_query = ''' +NICKNAME_QUERY = ''' SELECT -enemy_data.behavior, -encounters.encounter_id, -encounters.dungeon_id, -encounters.sub_dungeon_id, -encounters.enemy_id, -encounters.monster_id, -encounters.stage, -encounters.amount, -encounters.turns, -encounters.level, -encounters.hp, -encounters.atk, -encounters.defence, -sub_dungeons.name_ja AS sub_name_ja, -sub_dungeons.name_en AS sub_name_en, -sub_dungeons.name_ko AS sub_name_ko, -sub_dungeons.technical, -dungeons.name_ja, -dungeons.name_en, -dungeons.name_ko, -dungeons.dungeon_type + enemy_data.behavior, + encounters.encounter_id, + encounters.dungeon_id, + encounters.sub_dungeon_id, + encounters.enemy_id, + encounters.monster_id, + encounters.stage, + encounters.amount, + encounters.turns, + encounters.level, + encounters.hp, + encounters.atk, + encounters.defence, + sub_dungeons.name_ja AS sub_name_ja, + sub_dungeons.name_en AS sub_name_en, + sub_dungeons.name_ko AS sub_name_ko, + sub_dungeons.technical, + dungeons.name_ja, + dungeons.name_en, + dungeons.name_ko, + dungeons.dungeon_type FROM -encounters -LEFT OUTER JOIN dungeons on encounters.dungeon_id = dungeons.dungeon_id -LEFT OUTER JOIN enemy_data on encounters.enemy_id = enemy_data.enemy_id -LEFT OUTER JOIN monsters on encounters.monster_id = monsters.monster_id -LEFT OUTER JOIN sub_dungeons on sub_dungeons.sub_dungeon_id = encounters.sub_dungeon_id + encounters + LEFT OUTER JOIN dungeons on encounters.dungeon_id = dungeons.dungeon_id + LEFT OUTER JOIN enemy_data on encounters.enemy_id = enemy_data.enemy_id + LEFT OUTER JOIN monsters on encounters.monster_id = monsters.monster_id + LEFT OUTER JOIN sub_dungeons on sub_dungeons.sub_dungeon_id = encounters.sub_dungeon_id WHERE -encounters.sub_dungeon_id = {} + encounters.sub_dungeon_id = ? ORDER BY -encounters.sub_dungeon_id, -encounters.stage + encounters.sub_dungeon_id, + encounters.stage ''' -sub_dungeon_query = ''' +SUB_DUNGEON_QUERY = ''' SELECT -sub_dungeons.sub_dungeon_id, -sub_dungeons.dungeon_id, -sub_dungeons.name_ja, -sub_dungeons.name_en, -sub_dungeons.name_ko, -sub_dungeons.technical + sub_dungeons.sub_dungeon_id, + sub_dungeons.dungeon_id, + sub_dungeons.name_ja, + sub_dungeons.name_en, + sub_dungeons.name_ko, + sub_dungeons.technical FROM -sub_dungeons + sub_dungeons WHERE -sub_dungeons.dungeon_id = {} + sub_dungeons.dungeon_id = ? ''' -dungeon_query = ''' +DUNGEON_QUERY = ''' SELECT -dungeons.dungeon_id, -dungeons.name_ja, -dungeons.name_en, -dungeons.name_ko, -dungeons.dungeon_type + dungeons.dungeon_id, + dungeons.name_ja, + dungeons.name_en, + dungeons.name_ko, + dungeons.dungeon_type FROM -dungeons + dungeons WHERE -dungeons.name_en LIKE "{}%" + dungeons.name_en LIKE ? ''' -skill_query = ''' +ES_QUERY = ''' SELECT -enemy_skills.enemy_skill_id, -enemy_skills.name_en, -enemy_skills.desc_en, -enemy_skills.desc_en_emoji, -enemy_skills.min_hits, -enemy_skills.max_hits, -enemy_skills.atk_mult + enemy_skills.enemy_skill_id, + enemy_skills.name_en, + enemy_skills.desc_en, + enemy_skills.desc_en_emoji, + enemy_skills.min_hits, + enemy_skills.max_hits, + enemy_skills.atk_mult FROM -enemy_skills + enemy_skills WHERE -enemy_skill_id = {} + enemy_skill_id = ? ''' -sub_dungeons_query = ''' +SUB_DUNGEONS_QUERY_BY_NAME = ''' SELECT -sub_dungeons.* + * FROM -sub_dungeons + sub_dungeons WHERE -sub_dungeons.dungeon_id = {} AND -sub_dungeons.name_en LIKE "%{}%" + sub_dungeons.dungeon_id = ? AND + sub_dungeons.name_en LIKE ? +ORDER BY + sub_dungeons.sub_dungeon_id +''' + +SUB_DUNGEON_QUERY_BY_INDEX = ''' +SELECT + * +FROM + sub_dungeons +WHERE + sub_dungeons.sub_dungeon_id = ? ORDER BY -sub_dungeons.sub_dungeon_id + sub_dungeons.sub_dungeon_id ''' -encounter_query = ''' +ENCOUNTER_QUERY = ''' SELECT -encounters.encounter_id, -encounters.sub_dungeon_id, -encounters.enemy_id, -encounters.monster_id, -encounters.stage, -encounters.amount, -encounters.turns, -encounters.level, -encounters.hp, -encounters.atk, -encounters.defence + encounters.encounter_id, + encounters.sub_dungeon_id, + encounters.enemy_id, + encounters.monster_id, + encounters.stage, + encounters.amount, + encounters.turns, + encounters.level, + encounters.hp, + encounters.atk, + encounters.defence FROM -encounters + encounters WHERE -encounters.sub_dungeon_id = {} + encounters.sub_dungeon_id = ? ORDER BY -encounters.stage + encounters.stage ''' -specific_floor_query = ''' +SPECIFIC_FLOOR_QUERY = ''' SELECT -encounters.encounter_id, -encounters.sub_dungeon_id, -encounters.enemy_id, -encounters.monster_id, -encounters.stage, -encounters.amount, -encounters.turns, -encounters.level, -encounters.hp, -encounters.atk, -encounters.defence + encounters.encounter_id, + encounters.sub_dungeon_id, + encounters.enemy_id, + encounters.monster_id, + encounters.stage, + encounters.amount, + encounters.turns, + encounters.level, + encounters.hp, + encounters.atk, + encounters.defence FROM -encounters + encounters WHERE -encounters.sub_dungeon_id = {} + encounters.sub_dungeon_id = ? AND -encounters.stage = {} + encounters.stage = ? ''' -enemy_data_query = ''' +ENEMY_DATA_QUERY = ''' SELECT -enemy_data.enemy_id, -enemy_data.behavior + enemy_data.enemy_id, + enemy_data.behavior FROM -enemy_data + enemy_data WHERE -enemy_data.enemy_id = {} + enemy_data.enemy_id = ? ''' # TODO: Move to gdoc @@ -177,7 +190,7 @@ 'shura2': 4400002, 'shura3': 4400003, 'iwoc': 4400001, - 'alt. iwoc': 4400001, + 'alt. iwoc': 4401001, } @@ -185,19 +198,19 @@ class DungeonContext(object): def __init__(self, database: DadguideDatabase): self.database = database - def get_dungeons_from_name(self, name: str): - dungeons_result = self.database.query_many(dungeon_query.format(name), ()) + def get_dungeons_from_name(self, name: str) -> List[DungeonModel]: + dungeons_result = self.database.query_many(DUNGEON_QUERY, (name+"%",)) dungeons = [] for d in dungeons_result: dungeons.append(DungeonModel([], **d)) for dm in dungeons: - subs = self.database.query_many(sub_dungeon_query.format(dm.dungeon_id), ()) + subs = self.database.query_many(SUB_DUNGEON_QUERY, (dm.dungeon_id,)) for s in subs: - encounters = self.database.query_many(encounter_query.format(s['sub_dungeon_id']), ()) + encounters = self.database.query_many(ENCOUNTER_QUERY, (s['sub_dungeon_id'],)) ems = [] for e in encounters: - data = self.database.query_one(enemy_data_query.format(e["enemy_id"]), ()) + data = self.database.query_one(ENEMY_DATA_QUERY, (e["enemy_id"],)) if data is not None: edm = EnemyDataModel(**data) else: @@ -206,15 +219,14 @@ def get_dungeons_from_name(self, name: str): dm.sub_dungeons.append(SubDungeonModel(ems, **s)) return dungeons - def get_dungeons_from_nickname(self, name: str): + def get_dungeons_from_nickname(self, name: str) -> List[DungeonModel]: if name not in DUNGEON_NICKNAMES: - return None + return [] sub_id = DUNGEON_NICKNAMES.get(name) - mega = self.database.query_many(nickname_query.format(sub_id), ()) - print(mega) + mega = self.database.query_many(NICKNAME_QUERY, (sub_id,)) ems = [] for enc in mega: - data = self.database.query_one(enemy_data_query.format(enc["enemy_id"]), ()) + data = self.database.query_one(ENEMY_DATA_QUERY, (enc["enemy_id"],)) if data is not None: edm = EnemyDataModel(**data) else: @@ -229,13 +241,13 @@ def get_dungeons_from_nickname(self, name: str): technical=mega[0]['technical']) return [DungeonModel([sm], **mega[0])] - def get_floor_from_sub_dungeon(self, sub_id, floor): - floor_query = self.database.query_many(specific_floor_query.format(sub_id, floor), ()) - invade_query = self.database.query_many(specific_floor_query.format(sub_id, -1), ()) + def get_floor_from_sub_dungeon(self, sub_id: int, floor: int) -> List[EncounterModel]: + floor_query = self.database.query_many(SPECIFIC_FLOOR_QUERY, (sub_id, floor)) + invade_query = self.database.query_many(SPECIFIC_FLOOR_QUERY, (sub_id, -1)) encounter_models = [] floor_query.extend(invade_query) for f in floor_query: - data = self.database.query_one(enemy_data_query.format(f['enemy_id']), ()) + data = self.database.query_one(ENEMY_DATA_QUERY, (f['enemy_id'],)) if data is not None: edm = EnemyDataModel(**data) else: @@ -243,12 +255,15 @@ def get_floor_from_sub_dungeon(self, sub_id, floor): encounter_models.append(EncounterModel(edm, **f)) return encounter_models - def get_enemy_skill(self, enemy_skill_id): - enemy_skill_query = self.database.query_one(skill_query.format(enemy_skill_id), ()) + def get_enemy_skill(self, enemy_skill_id: int) -> EnemySkillModel: + enemy_skill_query = self.database.query_one(ES_QUERY, (enemy_skill_id,)) return EnemySkillModel(**enemy_skill_query) - def get_sub_dungeon_id_from_name(self, dungeon_id, sub_name: str): - sub_dungeons = self.database.query_many(sub_dungeons_query.format(dungeon_id, sub_name), ()) + def get_sub_dungeon_id_from_name(self, dungeon_id: int, sub_name: str) -> Optional[int]: + if sub_name.isdigit(): + sub_dungeons = self.database.query_many(SUB_DUNGEON_QUERY_BY_INDEX, (dungeon_id * 1000 + int(sub_name),)) + else: + sub_dungeons = self.database.query_many(SUB_DUNGEONS_QUERY_BY_NAME, (dungeon_id, f"%{sub_name}%")) if len(sub_dungeons) == 0: return None elif len(sub_dungeons) > 1: diff --git a/dungeoncog/__init__.py b/dungeoncog/__init__.py new file mode 100644 index 00000000..e65b9960 --- /dev/null +++ b/dungeoncog/__init__.py @@ -0,0 +1,10 @@ +import asyncio + +from .dungeoncog import DungeonCog + + +def setup(bot): + n = DungeonCog(bot) + bot.add_cog(n) + bot.loop.create_task(n.register_menu()) + asyncio.create_task(n.load_emojis()) diff --git a/dungeoncog/dungeon_monster.py b/dungeoncog/dungeon_monster.py new file mode 100644 index 00000000..77fa180b --- /dev/null +++ b/dungeoncog/dungeon_monster.py @@ -0,0 +1,31 @@ +from typing import List + +from dungeoncog.grouped_skillls import GroupedSkills + + +class DungeonMonster(object): + """ + A class that symbolizes an encounter in a dungeon. Why is it not a model? Good question. + """ + + def __init__(self, name: str, hp, atk, defense, turns, level, error=None): + self.name = name + self.hp = hp + self.atk = atk + self.defense = defense + self.defense = hp + self.turns = turns + self.level = level + self.groups: List[GroupedSkills] = [] + self.am_invade = False + + self.error = error + + def add_group(self, group: GroupedSkills): + self.groups.append(group) + + async def collect_skills(self): + skills = [] + for g in self.groups: + skills.extend(await g.collect_skills()) + return skills diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py new file mode 100644 index 00000000..5dbfa6f1 --- /dev/null +++ b/dungeoncog/dungeoncog.py @@ -0,0 +1,173 @@ +import logging +import os +from typing import TYPE_CHECKING + +from discord import Color +from discordmenu.emoji.emoji_cache import emoji_cache +from redbot.core import commands, data_manager +from redbot.core.utils.chat_formatting import pagify + +from dungeoncog.enemy_skills_pb2 import MonsterBehavior +from dungeoncog.menu.dungeon import DungeonMenu +from dungeoncog.menu.menu_map import dungeon_menu_map +from dungeoncog.view.dungeon import DungeonViewState + +if TYPE_CHECKING: + from dadguide.dungeon_context import DungeonContext + +logger = logging.getLogger('red.padbot-cogs.dungeoncog') +EMBED_NOT_GENERATED = -1 + + +class DungeonCog(commands.Cog): + """ + Contains commands that are display information about dungeons. + Right now only one command, dungeon_info which displays information such as spawns in a dungeon. + """ + menu_map = dungeon_menu_map + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + async def load_emojis(self): + await self.bot.wait_until_ready() + logging.debug('load_emojis, dungeon') + emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) + emoji_cache.refresh_from_discord_bot(self.bot) + + async def register_menu(self): + await self.bot.wait_until_ready() + menulistener = self.bot.get_cog("MenuListener") + if menulistener is None: + logger.warning("MenuListener is not loaded.") + return + await menulistener.register(self) + + async def get_menu_default_data(self, ims): + data = { + 'dgcog': await self.get_dgcog(), + 'color': Color.default() + } + return data + + async def get_dgcog(self): + dgcog = self.bot.get_cog("Dadguide") + if dgcog is None: + raise ValueError("Dadguide cog is not loaded") + await dgcog.wait_until_ready() + return dgcog + + async def find_dungeon_from_name2(self, ctx, name, database: "DungeonContext", difficulty: str = None): + """ + Gets the sub_dungeon model given the name of a dungeon and its difficulty. + """ + dungeons = database.get_dungeons_from_nickname(name.lower()) + if not dungeons: + dungeons = database.get_dungeons_from_name(name) + if len(dungeons) == 0: + await ctx.send("No dungeons found!") + return + if len(dungeons) > 1: + header = "Multiple Dungeons Found, please be more specific:\n" + for page in pagify(header + '\n'.join(d.name_en for d in dungeons)): + await ctx.send(page) + return + dungeon = dungeons.pop() + sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty) + sub_dungeon_model = None + if sub_id is None: + sub_id = 0 + for sd in dungeon.sub_dungeons: + if sd.sub_dungeon_id > sub_id: + sub_id = sd.sub_dungeon_id + sub_dungeon_model = sd + else: + for sd in dungeon.sub_dungeons: + if sd.sub_dungeon_id == sub_id: + sub_dungeon_model = sd + break + dungeon.sub_dungeons = [sub_dungeon_model] + else: + dungeon = dungeons.pop() + return dungeon + + @commands.command(aliases=['dgid']) + async def dungeonid(self, ctx, name, difficulty = None, *bad: str): + """ + Name: Name of Dungeon + Difficulty: Difficulty level/name of floor (eg. for A1, "Bipolar Goddess") + """ + if bad: + await ctx.send("Too many arguments. Make sure to surround all" + " arguments with spaces in quotes.") + return + + # load dadguide cog for database access + dgcog = await self.get_dgcog() + dungeon = await self.find_dungeon_from_name2(ctx, name, dgcog.database.dungeon, difficulty) + + if dungeon is not None: + # await ctx.send(format_overview(test_result)) + current_stage = 0 + pm_dungeon = [] + # check for invades: + invades = [] + + # print(dungeon.sub_dungeons[0].encounter_models) + for enc_model in dungeon.sub_dungeons[0].encounter_models: + behavior_test = MonsterBehavior() + if (enc_model.enemy_data is not None) and (enc_model.enemy_data.behavior is not None): + behavior_test.ParseFromString(enc_model.enemy_data.behavior) + else: + behavior_test = None + + # await ctx.send(format_overview(test_result)) + # pm = process_monster(behavior_test, enc_model, dg_cog.database) + if enc_model.stage < 0: + # pm.am_invade = True + invades.append(enc_model) + elif enc_model.stage > current_stage: + current_stage = enc_model.stage + floor = [enc_model] + pm_dungeon.append(floor) + else: + pm_dungeon[current_stage - 1].append(enc_model) + for f in pm_dungeon: + if pm_dungeon.index(f) != (len(pm_dungeon) - 1): + f.extend(invades) + + menu = DungeonMenu.menu() + original_author_id = ctx.message.author.id + test_list = ['1', '2', '3', '4'] + # print(pm_dungeon[0]) + view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, Color.default(), pm_dungeon[0][0], + dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, + len(pm_dungeon[0]), 0, + int(dungeon.sub_dungeons[0].technical), dgcog.database, verbose=False) + await ctx.send( + "EN: {}({})\nJP: {}({})".format(dungeon.name_en, dungeon.sub_dungeons[0].name_en, dungeon.name_ja, + dungeon.sub_dungeons[0].name_ja)) + message = await menu.create(ctx, view_state) + + +''' @commands.command() + async def spinner_help(self, ctx, spin_time, move_time): + """ + spin_time: The cycle the spinner is set at (typically changes once every second) + move_time: How much time you have to move orbs + """ + embed = discord.Embed(title="Spinner Helper {}s Rotation {}s Move Time".format(spin_time, move_time), + description="Assuming orbs are set and are NOT moved during movement:") + casino = ["🔥", "🌊", "🌿", "💡", "🌙", "🩹"] + cycles = float(move_time) / float(spin_time) + rounded = int(cycles) + casino_dict = OrderedDict() + for c in casino: + casino_dict.update({c: casino[(casino.index(c) + rounded) % 6]}) + casino_dict.update({"Other": casino[(rounded - 1) % 6]}) + output = "" + for k, v in casino_dict.items(): + output += "\n{} {} {}".format(k, "➡", v) + embed.add_field(name="Original -> Final", value=output) + await ctx.send(embed=embed)''' diff --git a/dungeoncog/enemy_skill.py b/dungeoncog/enemy_skill.py new file mode 100644 index 00000000..aaf8cb0d --- /dev/null +++ b/dungeoncog/enemy_skill.py @@ -0,0 +1,53 @@ +from typing import List + +from dungeoncog.grouped_skillls import GroupedSkills + + +class ProcessedSkill(object): + """ + This is a representation of a processed enemy skill. It contains the name, effect, + processed (emoji) version of the effect, condition requriements, parents, type (passive, preempt) + The class also contains a function called give_string. When called it returns the necessary strings needed to + display it in dungeon_info2 + """ + + def __init__(self, name: str, effect: str, processed: List[str], condition: str = None, + parent: GroupedSkills = None): + self.name = name + self.effect = effect + self.processed = processed + self.condition = condition + self.parent = parent + self.type = self.find_type() + self.is_passive_preempt = len(self.process_type()) != 0 + + def find_type(self): + """Helper function to determine whether the skill is a preempt/passive from the parent group""" + parent = self.parent + while parent is not None: + if parent.type is not None: + return parent.type + parent = parent.parent + return None + + def process_type(self): + if "Passive" in self.type or "Preemptive" in self.type: + return "({})".format(self.type) + return "" + + def give_string(self, indent: str = "", verbose: bool = False): + """When called returns the strings of a skill in the following format: [emoji, effect, condition]""" + components = [[], None, None] # skills (condensed), effect, condition + # if no skills/not processed just return (N/A) as the skill + if len(self.processed) == 0: + components[0].append("(N/A)") + return components + # For the emoji text we append the skill type to the beginning eg. (Emoji Effect) -> (Passive) (Emoji Effect) + components[0] = [self.process_type()] + components[0].extend(self.processed) + # If verbose information is requested (ie display Effect text) we provide it + if verbose: + components[1] = "E: {}".format(self.effect) + if self.condition is not None: + components[2] = "Cond: {}".format(self.condition) + return components diff --git a/dungeoncog/enemy_skills_pb2.py b/dungeoncog/enemy_skills_pb2.py new file mode 100644 index 00000000..d9c31005 --- /dev/null +++ b/dungeoncog/enemy_skills_pb2.py @@ -0,0 +1,635 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: enemy_skills.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + +DESCRIPTOR = _descriptor.FileDescriptor( + name='enemy_skills.proto', + package='dadguide_proto', + syntax='proto3', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x12\x65nemy_skills.proto\x12\x0e\x64\x61\x64guide_proto\"\xbf\x02\n\x1cMonsterBehaviorWithOverrides\x12\x12\n\nmonster_id\x18\x01 \x01(\x05\x12-\n\x06levels\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x36\n\x0flevel_overrides\x18\x03 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x43\n\x06status\x18\x04 \x01(\x0e\x32\x33.dadguide_proto.MonsterBehaviorWithOverrides.Status\"_\n\x06Status\x12\x10\n\x0cNOT_APPROVED\x10\x00\x12\x12\n\x0e\x41PPROVED_AS_IS\x10\x01\x12\x14\n\x10NEEDS_REAPPROVAL\x10\x02\x12\x19\n\x15\x41PPROVED_WITH_CHANGES\x10\x03\"f\n\x0fMonsterBehavior\x12\x12\n\nmonster_id\x18\x01 \x01(\x05\x12-\n\x06levels\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x10\n\x08\x61pproved\x18\x03 \x01(\x08\"M\n\rLevelBehavior\x12\r\n\x05level\x18\x01 \x01(\x05\x12-\n\x06groups\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.BehaviorGroup\"\xd9\x02\n\rBehaviorGroup\x12;\n\ngroup_type\x18\x01 \x01(\x0e\x32\'.dadguide_proto.BehaviorGroup.GroupType\x12,\n\tcondition\x18\x02 \x01(\x0b\x32\x19.dadguide_proto.Condition\x12.\n\x08\x63hildren\x18\x03 \x03(\x0b\x32\x1c.dadguide_proto.BehaviorItem\"\xac\x01\n\tGroupType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07PASSIVE\x10\x01\x12\x0b\n\x07PREEMPT\x10\x02\x12\x11\n\rDISPEL_PLAYER\x10\x03\x12\x12\n\x0eMONSTER_STATUS\x10\x04\x12\r\n\tREMAINING\x10\x05\x12\x0c\n\x08STANDARD\x10\x06\x12\t\n\x05\x44\x45\x41TH\x10\x07\x12\x0f\n\x0bUNKNOWN_USE\x10\x08\x12\x14\n\x10HIGHEST_PRIORITY\x10\t\"u\n\x0c\x42\x65haviorItem\x12.\n\x05group\x18\x02 \x01(\x0b\x32\x1d.dadguide_proto.BehaviorGroupH\x00\x12,\n\x08\x62\x65havior\x18\x03 \x01(\x0b\x32\x18.dadguide_proto.BehaviorH\x00\x42\x07\n\x05value\"c\n\x08\x42\x65havior\x12,\n\tcondition\x18\x01 \x01(\x0b\x32\x19.dadguide_proto.Condition\x12\x16\n\x0e\x65nemy_skill_id\x18\x02 \x01(\x05\x12\x11\n\tchild_ids\x18\x03 \x03(\x05\"\x80\x04\n\tCondition\x12\x14\n\x0chp_threshold\x18\x01 \x01(\x05\x12\x12\n\nuse_chance\x18\x02 \x01(\x05\x12\x15\n\rrepeats_every\x18\x03 \x01(\x05\x12\x17\n\x0fglobal_one_time\x18\x04 \x01(\x08\x12\x19\n\x11limited_execution\x18\r \x01(\x05\x12!\n\x19trigger_enemies_remaining\x18\x05 \x01(\x05\x12\x13\n\x0bif_defeated\x18\x06 \x01(\x08\x12\x1f\n\x17if_attributes_available\x18\x07 \x01(\x08\x12\x18\n\x10trigger_monsters\x18\x08 \x03(\x05\x12\x16\n\x0etrigger_combos\x18\t \x01(\x05\x12\x1a\n\x12if_nothing_matched\x18\n \x01(\x08\x12\x14\n\x0ctrigger_turn\x18\x0b \x01(\x05\x12\x18\n\x10trigger_turn_end\x18\x0c \x01(\x05\x12\x1c\n\x14\x61lways_trigger_above\x18\x0e \x01(\x05\x12\x14\n\x0c\x61lways_after\x18\x0f \x01(\x05\x12\x11\n\tskill_set\x18\x10 \x01(\x05\x12\x19\n\x11\x65rased_attributes\x18\x11 \x03(\x05\x12\x13\n\x0b\x64\x61mage_done\x18\x12 \x01(\x05\x12\x1b\n\x13\x61ttributes_attacked\x18\x13 \x03(\x05\x12\x13\n\x0bskills_used\x18\x14 \x01(\x05\x62\x06proto3' +) + +_MONSTERBEHAVIORWITHOVERRIDES_STATUS = _descriptor.EnumDescriptor( + name='Status', + full_name='dadguide_proto.MonsterBehaviorWithOverrides.Status', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='NOT_APPROVED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='APPROVED_AS_IS', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NEEDS_REAPPROVAL', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='APPROVED_WITH_CHANGES', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=263, + serialized_end=358, +) +_sym_db.RegisterEnumDescriptor(_MONSTERBEHAVIORWITHOVERRIDES_STATUS) + +_BEHAVIORGROUP_GROUPTYPE = _descriptor.EnumDescriptor( + name='GroupType', + full_name='dadguide_proto.BehaviorGroup.GroupType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PASSIVE', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PREEMPT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DISPEL_PLAYER', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='MONSTER_STATUS', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='REMAINING', index=5, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='STANDARD', index=6, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DEATH', index=7, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='UNKNOWN_USE', index=8, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='HIGHEST_PRIORITY', index=9, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=717, + serialized_end=889, +) +_sym_db.RegisterEnumDescriptor(_BEHAVIORGROUP_GROUPTYPE) + +_MONSTERBEHAVIORWITHOVERRIDES = _descriptor.Descriptor( + name='MonsterBehaviorWithOverrides', + full_name='dadguide_proto.MonsterBehaviorWithOverrides', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='monster_id', full_name='dadguide_proto.MonsterBehaviorWithOverrides.monster_id', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='levels', full_name='dadguide_proto.MonsterBehaviorWithOverrides.levels', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='level_overrides', full_name='dadguide_proto.MonsterBehaviorWithOverrides.level_overrides', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='status', full_name='dadguide_proto.MonsterBehaviorWithOverrides.status', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _MONSTERBEHAVIORWITHOVERRIDES_STATUS, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=39, + serialized_end=358, +) + +_MONSTERBEHAVIOR = _descriptor.Descriptor( + name='MonsterBehavior', + full_name='dadguide_proto.MonsterBehavior', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='monster_id', full_name='dadguide_proto.MonsterBehavior.monster_id', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='levels', full_name='dadguide_proto.MonsterBehavior.levels', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='approved', full_name='dadguide_proto.MonsterBehavior.approved', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=360, + serialized_end=462, +) + +_LEVELBEHAVIOR = _descriptor.Descriptor( + name='LevelBehavior', + full_name='dadguide_proto.LevelBehavior', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='level', full_name='dadguide_proto.LevelBehavior.level', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='groups', full_name='dadguide_proto.LevelBehavior.groups', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=464, + serialized_end=541, +) + +_BEHAVIORGROUP = _descriptor.Descriptor( + name='BehaviorGroup', + full_name='dadguide_proto.BehaviorGroup', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='group_type', full_name='dadguide_proto.BehaviorGroup.group_type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='condition', full_name='dadguide_proto.BehaviorGroup.condition', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='children', full_name='dadguide_proto.BehaviorGroup.children', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _BEHAVIORGROUP_GROUPTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=544, + serialized_end=889, +) + +_BEHAVIORITEM = _descriptor.Descriptor( + name='BehaviorItem', + full_name='dadguide_proto.BehaviorItem', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='group', full_name='dadguide_proto.BehaviorItem.group', index=0, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='behavior', full_name='dadguide_proto.BehaviorItem.behavior', index=1, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='value', full_name='dadguide_proto.BehaviorItem.value', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=891, + serialized_end=1008, +) + +_BEHAVIOR = _descriptor.Descriptor( + name='Behavior', + full_name='dadguide_proto.Behavior', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='condition', full_name='dadguide_proto.Behavior.condition', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enemy_skill_id', full_name='dadguide_proto.Behavior.enemy_skill_id', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='child_ids', full_name='dadguide_proto.Behavior.child_ids', index=2, + number=3, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1010, + serialized_end=1109, +) + +_CONDITION = _descriptor.Descriptor( + name='Condition', + full_name='dadguide_proto.Condition', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='hp_threshold', full_name='dadguide_proto.Condition.hp_threshold', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='use_chance', full_name='dadguide_proto.Condition.use_chance', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='repeats_every', full_name='dadguide_proto.Condition.repeats_every', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='global_one_time', full_name='dadguide_proto.Condition.global_one_time', index=3, + number=4, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='limited_execution', full_name='dadguide_proto.Condition.limited_execution', index=4, + number=13, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_enemies_remaining', full_name='dadguide_proto.Condition.trigger_enemies_remaining', index=5, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='if_defeated', full_name='dadguide_proto.Condition.if_defeated', index=6, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='if_attributes_available', full_name='dadguide_proto.Condition.if_attributes_available', index=7, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_monsters', full_name='dadguide_proto.Condition.trigger_monsters', index=8, + number=8, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_combos', full_name='dadguide_proto.Condition.trigger_combos', index=9, + number=9, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='if_nothing_matched', full_name='dadguide_proto.Condition.if_nothing_matched', index=10, + number=10, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_turn', full_name='dadguide_proto.Condition.trigger_turn', index=11, + number=11, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trigger_turn_end', full_name='dadguide_proto.Condition.trigger_turn_end', index=12, + number=12, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='always_trigger_above', full_name='dadguide_proto.Condition.always_trigger_above', index=13, + number=14, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='always_after', full_name='dadguide_proto.Condition.always_after', index=14, + number=15, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='skill_set', full_name='dadguide_proto.Condition.skill_set', index=15, + number=16, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='erased_attributes', full_name='dadguide_proto.Condition.erased_attributes', index=16, + number=17, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='damage_done', full_name='dadguide_proto.Condition.damage_done', index=17, + number=18, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='attributes_attacked', full_name='dadguide_proto.Condition.attributes_attacked', index=18, + number=19, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='skills_used', full_name='dadguide_proto.Condition.skills_used', index=19, + number=20, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1112, + serialized_end=1624, +) + +_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['levels'].message_type = _LEVELBEHAVIOR +_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['level_overrides'].message_type = _LEVELBEHAVIOR +_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['status'].enum_type = _MONSTERBEHAVIORWITHOVERRIDES_STATUS +_MONSTERBEHAVIORWITHOVERRIDES_STATUS.containing_type = _MONSTERBEHAVIORWITHOVERRIDES +_MONSTERBEHAVIOR.fields_by_name['levels'].message_type = _LEVELBEHAVIOR +_LEVELBEHAVIOR.fields_by_name['groups'].message_type = _BEHAVIORGROUP +_BEHAVIORGROUP.fields_by_name['group_type'].enum_type = _BEHAVIORGROUP_GROUPTYPE +_BEHAVIORGROUP.fields_by_name['condition'].message_type = _CONDITION +_BEHAVIORGROUP.fields_by_name['children'].message_type = _BEHAVIORITEM +_BEHAVIORGROUP_GROUPTYPE.containing_type = _BEHAVIORGROUP +_BEHAVIORITEM.fields_by_name['group'].message_type = _BEHAVIORGROUP +_BEHAVIORITEM.fields_by_name['behavior'].message_type = _BEHAVIOR +_BEHAVIORITEM.oneofs_by_name['value'].fields.append( + _BEHAVIORITEM.fields_by_name['group']) +_BEHAVIORITEM.fields_by_name['group'].containing_oneof = _BEHAVIORITEM.oneofs_by_name['value'] +_BEHAVIORITEM.oneofs_by_name['value'].fields.append( + _BEHAVIORITEM.fields_by_name['behavior']) +_BEHAVIORITEM.fields_by_name['behavior'].containing_oneof = _BEHAVIORITEM.oneofs_by_name['value'] +_BEHAVIOR.fields_by_name['condition'].message_type = _CONDITION +DESCRIPTOR.message_types_by_name['MonsterBehaviorWithOverrides'] = _MONSTERBEHAVIORWITHOVERRIDES +DESCRIPTOR.message_types_by_name['MonsterBehavior'] = _MONSTERBEHAVIOR +DESCRIPTOR.message_types_by_name['LevelBehavior'] = _LEVELBEHAVIOR +DESCRIPTOR.message_types_by_name['BehaviorGroup'] = _BEHAVIORGROUP +DESCRIPTOR.message_types_by_name['BehaviorItem'] = _BEHAVIORITEM +DESCRIPTOR.message_types_by_name['Behavior'] = _BEHAVIOR +DESCRIPTOR.message_types_by_name['Condition'] = _CONDITION +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +MonsterBehaviorWithOverrides = _reflection.GeneratedProtocolMessageType('MonsterBehaviorWithOverrides', + (_message.Message,), { + 'DESCRIPTOR': _MONSTERBEHAVIORWITHOVERRIDES, + '__module__': 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.MonsterBehaviorWithOverrides) + }) +_sym_db.RegisterMessage(MonsterBehaviorWithOverrides) + +MonsterBehavior = _reflection.GeneratedProtocolMessageType('MonsterBehavior', (_message.Message,), { + 'DESCRIPTOR': _MONSTERBEHAVIOR, + '__module__': 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.MonsterBehavior) +}) +_sym_db.RegisterMessage(MonsterBehavior) + +LevelBehavior = _reflection.GeneratedProtocolMessageType('LevelBehavior', (_message.Message,), { + 'DESCRIPTOR': _LEVELBEHAVIOR, + '__module__': 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.LevelBehavior) +}) +_sym_db.RegisterMessage(LevelBehavior) + +BehaviorGroup = _reflection.GeneratedProtocolMessageType('BehaviorGroup', (_message.Message,), { + 'DESCRIPTOR': _BEHAVIORGROUP, + '__module__': 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.BehaviorGroup) +}) +_sym_db.RegisterMessage(BehaviorGroup) + +BehaviorItem = _reflection.GeneratedProtocolMessageType('BehaviorItem', (_message.Message,), { + 'DESCRIPTOR': _BEHAVIORITEM, + '__module__': 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.BehaviorItem) +}) +_sym_db.RegisterMessage(BehaviorItem) + +Behavior = _reflection.GeneratedProtocolMessageType('Behavior', (_message.Message,), { + 'DESCRIPTOR': _BEHAVIOR, + '__module__': 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.Behavior) +}) +_sym_db.RegisterMessage(Behavior) + +Condition = _reflection.GeneratedProtocolMessageType('Condition', (_message.Message,), { + 'DESCRIPTOR': _CONDITION, + '__module__': 'enemy_skills_pb2' + # @@protoc_insertion_point(class_scope:dadguide_proto.Condition) +}) +_sym_db.RegisterMessage(Condition) + +# @@protoc_insertion_point(module_scope) diff --git a/dungeoncog/grouped_skillls.py b/dungeoncog/grouped_skillls.py new file mode 100644 index 00000000..73fa1ab1 --- /dev/null +++ b/dungeoncog/grouped_skillls.py @@ -0,0 +1,41 @@ +class GroupedSkills(object): + """This is a container class that holds the top level condition and its processed children""" + + def __init__(self, condition: str, type, parent=None): + self.nested_groups = [] + self.skills = [] + self.condition = condition + self.type = type + self.parent = parent + + def add_group(self, group): + self.nested_groups.append(group) + + def add_skill(self, skill): + self.skills.append(skill) + + def give_string2(self, lines, level=0, verbose: bool = False): + """ + When called it stores the necessary text of it's child skills in lines. + @param lines: a list to store lines of text in the following format [number of indents needed, actual line of text] + @param level: how many indents do we need (essentially is this a nested group or not) + @param verbose: display skill effect text or not + """ + condition = self.condition + if condition is not None: + lines.append([level, ["**Condition: {}:**".format(condition)]]) + level += 1 + for g in self.nested_groups: + g.give_string2(lines, level, verbose) + for s in self.skills: + skill_string_components = s.give_string(verbose=verbose) + skill_string_components[0] = "".join(skill_string_components[0]) + skill_string_components[0] = "**{}**".format(skill_string_components[0]) + skill_string_components = [x for x in skill_string_components if x is not None] + lines.append([level, skill_string_components]) + + async def collect_skills(self): + skill_copy = self.skills.copy() + for g in self.nested_groups: + skill_copy.extend(await g.collect_skills()) + return skill_copy diff --git a/dungeoncog/info.json b/dungeoncog/info.json new file mode 100644 index 00000000..27d3bc43 --- /dev/null +++ b/dungeoncog/info.json @@ -0,0 +1,31 @@ +{ + "author": [ + "loopdeer", + "The Tsubaki Bot Team" + ], + "description": "Allows users to get data from dungeons.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "Use [p]dgid to query", + "short": "Serves dungeon data", + "min_bot_version": "3.4.0", + "max_bot_version": "0.0.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "required_cogs": { + "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "menulistener": "https://github.com/TsubakiBotPad/misc-cogs" + }, + "requirements": [ + "tsutils>=3.4.0", + "discord-menu>=0.16.0", + "protobuf" + ], + "tags": [ + "PAD" + ] +} diff --git a/dungeoncog/menu/__init__.py b/dungeoncog/menu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dungeoncog/menu/dungeon.py b/dungeoncog/menu/dungeon.py new file mode 100644 index 00000000..4252500d --- /dev/null +++ b/dungeoncog/menu/dungeon.py @@ -0,0 +1,123 @@ +from typing import Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from tsutils.menu.panes import MenuPanes, emoji_buttons + +from dungeoncog.view.dungeon import DungeonViewState, DungeonView + + +class DungeonNames: + home = 'home' + + +class DungeonMenu: + MENU_TYPE = 'DungeonMenu' + message = None + + @staticmethod + def menu(): + embed = EmbedMenu(DungeonMenuPanes.transitions(), DungeonMenu.message_control, + delete_func=DungeonMenu.respond_with_delete) + return embed + + @staticmethod + async def respond_with_message(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + view_state = await DungeonViewState.deserialize(dgcog, color, ims) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_verbose(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + view_state = await DungeonViewState.deserialize(dgcog, color, ims, verbose_toggle=True) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_delete(message: Optional[Message], ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_previous_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, -1) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_next_monster(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, 1) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_previous_floor(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, -1, 0, reset_spawn=True) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_next_floor(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + view_state = await DungeonViewState.deserialize(dgcog, color, ims, 1, 0, reset_spawn=True) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_next_page(message: Optional[Message], ims, **data): + dgcog = data['dgcog'] + color = data['color'] + page = ims.get('page') + if page == 0: + page = 1 + else: + page = 0 + view_state = await DungeonViewState.deserialize(dgcog, color, ims, page=page) + control = DungeonMenu.message_control(view_state) + return control + + @staticmethod + def message_control(state: DungeonViewState): + if state is None: + return None + return EmbedControl( + [DungeonView.embed(state)], + [*DungeonMenuPanes.DATA.keys()] + ) + + +class DungeonEmoji: + home = emoji_buttons['home'] + previous_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + next_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + next_floor = '\N{UPWARDS BLACK ARROW}' + previous_floor = '\N{DOWNWARDS BLACK ARROW}' + next_page = '\N{OPEN BOOK}' + verbose = '\N{SCROLL}' + + +class DungeonMenuPanes(MenuPanes): + INITIAL_EMOJI = DungeonEmoji.home + DATA = { + DungeonEmoji.home: (DungeonMenu.respond_with_message, DungeonView.VIEW_TYPE), + DungeonEmoji.verbose: (DungeonMenu.respond_with_verbose, DungeonView.VIEW_TYPE), + DungeonEmoji.previous_monster_emoji: (DungeonMenu.respond_with_previous_monster, DungeonView.VIEW_TYPE), + DungeonEmoji.next_monster_emoji: (DungeonMenu.respond_with_next_monster, DungeonView.VIEW_TYPE), + DungeonEmoji.previous_floor: (DungeonMenu.respond_with_previous_floor, DungeonView.VIEW_TYPE), + DungeonEmoji.next_floor: (DungeonMenu.respond_with_next_floor, DungeonView.VIEW_TYPE), + DungeonEmoji.next_page: (DungeonMenu.respond_with_next_page, DungeonView.VIEW_TYPE) + } + HIDDEN_EMOJIS = [ + DungeonEmoji.home + ] diff --git a/dungeoncog/menu/menu_map.py b/dungeoncog/menu/menu_map.py new file mode 100644 index 00000000..7f9ccfc0 --- /dev/null +++ b/dungeoncog/menu/menu_map.py @@ -0,0 +1,7 @@ +from dungeoncog.menu.dungeon import DungeonMenu, DungeonMenuPanes +from dungeoncog.menu.simple import SimpleMenu, SimpleMenuPanes + +dungeon_menu_map = { + SimpleMenu.MENU_TYPE: (SimpleMenu, SimpleMenuPanes), + DungeonMenu.MENU_TYPE: (DungeonMenu, DungeonMenuPanes) +} diff --git a/dungeoncog/menu/simple.py b/dungeoncog/menu/simple.py new file mode 100644 index 00000000..897ecd47 --- /dev/null +++ b/dungeoncog/menu/simple.py @@ -0,0 +1,70 @@ +from typing import Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from tsutils.menu.panes import MenuPanes, emoji_buttons + +from dungeoncog.view.simple import SimpleViewState, SimpleView + + +class SimpleNames: + home = 'home' + + +class SimpleMenu: + MENU_TYPE = 'SimpleMenu' + message = None + + @staticmethod + def menu(): + embed = EmbedMenu(SimpleMenuPanes.transitions(), SimpleMenu.message_control, + delete_func=SimpleMenu.respond_with_delete) + return embed + + @staticmethod + async def respond_with_message(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + view_state = await SimpleViewState.deserialize(dgcog, color, ims, 0) + control = SimpleMenu.message_control(view_state) + return control + + @staticmethod + async def respond_with_delete(message: Optional[Message], ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_right(message: Optional[Message], ims, **data): + dgcog = data.get('dgcog') + color = data.get('color') + view_state = await SimpleViewState.deserialize(dgcog, color, ims, 1) + control = SimpleMenu.message_control(view_state) + return control + + @staticmethod + def message_control(state: SimpleViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [SimpleView.embed(state)], + reaction_list + ) + + +class SimpleEmoji: + home = emoji_buttons['home'] + right = '\N{BLACK RIGHT-POINTING TRIANGLE}' + + +class SimpleMenuPanes(MenuPanes): + INITIAL_EMOJI = SimpleEmoji.home + DATA = { + SimpleEmoji.home: (SimpleMenu.respond_with_message, SimpleNames.home), + SimpleEmoji.right: (SimpleMenu.respond_with_right, SimpleView.VIEW_TYPE) + + } + HIDDEN_EMOJIS = [ + SimpleNames.home, + ] diff --git a/dungeoncog/models/__init__.py b/dungeoncog/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dungeoncog/models/enemy_skill.py b/dungeoncog/models/enemy_skill.py new file mode 100644 index 00000000..48ee1945 --- /dev/null +++ b/dungeoncog/models/enemy_skill.py @@ -0,0 +1,31 @@ +from typing import List + + +class EnemySkill(object): + def __init__(self, raw: List[str]): + self.enemy_skill_id = int(raw[0]) + self.name = raw[1].replace('\n', ' ') + self.type = int(raw[2]) + self.flags = int(raw[3], 16) # 16bitmap for params + self.params: List = [None] * 16 + offset = 0 + p_idx = 4 + while offset < self.flags.bit_length(): + if (self.flags >> offset) & 1 != 0: + p_value = raw[p_idx] + self.params[offset] = int(p_value) if p_value.lstrip('-').isdigit() else p_value + p_idx += 1 + offset += 1 + + def process(self): + return "Unknown: [{},{},{},{}]".format(self.enemy_skill_id, self.name, self.type, self.params) + + +class ESNone(EnemySkill): + def __init__(self, esid=None): + super().__init__([esid, "ESNone", -1, 0]) + self.enemy_skill_id = esid + self.name = None + self.type = None + self.flags = None + self.params = None diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py new file mode 100644 index 00000000..51b560f5 --- /dev/null +++ b/dungeoncog/processors.py @@ -0,0 +1,269 @@ +from discordmenu.emoji.emoji_cache import emoji_cache + +from dadguide.database_context import DbContext +from dadguide.models.encounter_model import EncounterModel +from dadguide.models.enemy_skill_model import EnemySkillModel +from dungeoncog.dungeon_monster import DungeonMonster +from dungeoncog.enemy_skill import ProcessedSkill +from dungeoncog.enemy_skills_pb2 import Condition, MonsterBehavior, Behavior, BehaviorGroup +from dungeoncog.grouped_skillls import GroupedSkills + +GROUP_TYPES = { + 0: "Unspecified", + 1: "Passive", + 2: "Preemptive", + 3: "Dispel Player", + 4: "Monster Status", + 5: "Remaining", + 6: "Standard", + 7: "Death", + 8: "Unknown", + 9: "Highest Priority" +} + + +def format_condition(cond: Condition): + parts = [] + if cond.skill_set: + parts.append('SkillSet {}'.format(cond.skill_set)) + if cond.use_chance not in [0, 100]: + parts.append('{}% chance'.format(cond.use_chance)) + if cond.global_one_time: + parts.append('one time only') + if cond.limited_execution: + parts.append('at most {} times'.format(cond.limited_execution)) + if cond.trigger_enemies_remaining: + parts.append('when {} enemies remain'.format(cond.trigger_enemies_remaining)) + if cond.if_defeated: + parts.append('when defeated') + if cond.if_attributes_available: + parts.append('when required attributes on board') + if cond.trigger_monsters: + parts.append('when {} on team'.format(', '.join(map(str, cond.trigger_monsters)))) + if cond.trigger_combos: + parts.append('when {} combos last turn'.format(cond.trigger_combos)) + if cond.if_nothing_matched: + parts.append('if no other skills matched') + if cond.repeats_every: + if cond.trigger_turn: + if cond.trigger_turn_end: + parts.append('execute repeatedly, turn {}-{} of {}'.format(cond.trigger_turn, + cond.trigger_turn_end, + cond.repeats_every)) + else: + parts.append('execute repeatedly, turn {} of {}'.format(cond.trigger_turn, cond.repeats_every)) + else: + parts.append('repeats every {} turns'.format(cond.repeats_every)) + elif cond.trigger_turn_end: + turn_text = 'turns {}-{}'.format(cond.trigger_turn, cond.trigger_turn_end) + parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) + elif cond.trigger_turn: + turn_text = 'turn {}'.format(cond.trigger_turn) + parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) + + if not parts and cond.hp_threshold in [100, 0]: + return None + + if cond.hp_threshold == 101: + parts.append('when hp is full') + elif cond.hp_threshold: + parts.append('hp <= {}'.format(cond.hp_threshold)) + + return ', '.join(parts) + + +# From pad-data-pipeline +def _cond_hp_timed_text(always_trigger_above: int, turn_text: str) -> str: + text = turn_text + if always_trigger_above == 1: + text = 'always {}'.format(turn_text) + elif always_trigger_above: + text = '{} while HP > {}'.format(turn_text, always_trigger_above) + return text + + +def format_overview(query): + output = "Dungeon Name: {}".format(query[0]["dungeon_name_en"]) + for q in query: + output += "\nEncounter ID: {} Stage: {} Monster Name: {} Level: {}".format(q["encounter_id"], q["stage"], + q["name_en"], q["level"]) + + return output + + +def behavior_for_level(mb: MonsterBehavior, num: int): + lev = None + for level in mb.levels: + if level.level == num: lev = level + if lev is None: + return mb.levels[len(mb.levels) - 1] + else: + return lev + + +def process_enemy_skill2(encounter: EncounterModel, skill: EnemySkillModel, emoji_map): + effect = skill.desc_en_emoji + split_effects = effect.split("), ") + non_attack_effects = [] + for e in split_effects: + # print(encounter.monster_id) + e += ")" + if "Attack:" not in e: + non_attack_effects.append(e.format_map(emoji_map)) + + # Damage + atk = encounter.atk + if skill.min_hits != 0: + emoji = emoji_map['attack'] + if skill.min_hits > 1: + emoji = emoji_map['multi_attack'] + damage_per_hit = (int)(atk * (skill.atk_mult / 100.0)) + min_damage = skill.min_hits * damage_per_hit + max_damage = skill.max_hits * damage_per_hit + if min_damage != max_damage: + non_attack_effects.append("({}:{}~{})".format(emoji, f'{min_damage:,}', f'{max_damage:,}')) + else: + non_attack_effects.append("({}:{})".format(emoji, f'{min_damage:,}')) + + return non_attack_effects + + +def process_behavior(behavior: Behavior, database, q: EncounterModel, emoji_map, parent: GroupedSkills = None): + skill = database.dungeon.get_enemy_skill(behavior.enemy_skill_id) + if skill is None: + return "Unknown" + skill_name = skill.name_en + skill_effect = skill.desc_en + # skill_processed_text = process_enemy_skill(skill_effect, q, skill) + skill_processed_texts = process_enemy_skill2(q, skill, emoji_map) + + condition = format_condition(behavior.condition) + processed_skill: ProcessedSkill = ProcessedSkill(skill_name, skill_effect, skill_processed_texts, condition, + parent) + # embed.add_field(name="{}Skill Name: {}{}".format(group_type, skill_name, process_enemy_skill(skill_effect, q, skill)), value="{}Effect: {}\n{}".format(indent, skill_effect, condition), inline=False) + return processed_skill + + +def process_behavior_group(group: BehaviorGroup, database, q: EncounterModel, emoji_map, + parent: GroupedSkills = None): + condition = format_condition(group.condition) + processed_group: GroupedSkills = GroupedSkills(condition, GROUP_TYPES[group.group_type], parent) + for child in group.children: + if child.HasField('group'): + processed_group.add_group(process_behavior_group(child.group, database, q, emoji_map, processed_group)) + elif child.HasField('behavior'): + processed_group.add_skill(process_behavior(child.behavior, database, q, emoji_map, processed_group)) + return processed_group + + +class SafeDgEmojiDict(dict): + def __missing__(self, key): + return '<' + key + '>' + + +# return output + +# Format: +# Skill Name Type:Preemptive/Passive/Etc +# Skill Effect +# Condition +def process_monster(mb: MonsterBehavior, q: EncounterModel, database: DbContext): + # output = "Behavior for: {} at Level: {}".format(q["name_en"], q["level"]) # TODO: Delete later after testing + """embed = discord.Embed(title="Behavior for: {} at Level: {}".format(q["name_en"], q["level"]), + description="HP:{} ATK:{} DEF:{} TURN:{}".format(q["hp"], q["atk"], q["defence"], q['turns']))""" + emoji_map = SafeDgEmojiDict(resolve_status=emoji_cache.get_by_name('resolve'), + fire_orb=emoji_cache.get_by_name('orb_fire'), + recover_health=emoji_cache.get_by_name('misc_autoheal'), + roulette=emoji_cache.get_by_name('spinner'), + unknown_type='Unknown', + defense_status=emoji_cache.get_by_name('defense'), + combo_absorb_status=emoji_cache.get_by_name('combo'), + absorb_status=emoji_cache.get_by_name('attribute_absorb'), + damage_absorb_status=emoji_cache.get_by_name('damage_absorb'), + damage_void_status=emoji_cache.get_by_name('vdp'), + status_shield_status=emoji_cache.get_by_name('status'), + movetime_buff_status=emoji_cache.get_by_name('time_buff'), + movetime_debuff_status=emoji_cache.get_by_name('time_debuff'), + dispel_status='Dispel', + leader_swap_status=emoji_cache.get_by_name('swap'), + skill_delay=emoji_cache.get_by_name('misc_sb'), + lock_orbs=emoji_cache.get_by_name('lock'), + tape_status=emoji_cache.get_by_name('res_seal'), + fixed_start=emoji_cache.get_by_name('orb_start'), + cloud_status=emoji_cache.get_by_name('res_cloud'), + gravity=emoji_cache.get_by_name('gravity'), + invincible_status=emoji_cache.get_by_name('invincible'), + invincible_off_status=emoji_cache.get_by_name('invincible'), # TODO + force_target_status=emoji_cache.get_by_name('force_target'), + leader_alter_status=emoji_cache.get_by_name('transform'), + board_size_status=emoji_cache.get_by_name('board_size'), + super_resolve_status=emoji_cache.get_by_name('super_resolve'), + turn_change=emoji_cache.get_by_name('turn_change'), + enrage_status=emoji_cache.get_by_name('enrage'), + active_skill_bind=emoji_cache.get_by_name('skill_bound'), + do_nothing='\N{SLEEPING SYMBOL}', + awoken_skill_bind=emoji_cache.get_by_name('awoken_skill_bind'), + no_skyfall_status=emoji_cache.get_by_name('no_skyfall'), + bind=emoji_cache.get_by_name('res_bind'), + skyfall_status='\N{CLOUD WITH RAIN}', + blind='\N{MEDIUM BLACK CIRCLE}', + super_blind=emoji_cache.get_by_name('blind_orb'), + to='\N{BLACK RIGHTWARDS ARROW}️', + attack=emoji_cache.get_by_name('single_hit'), + multi_attack=emoji_cache.get_by_name('multi_hit'), + target_self='Self', + health=emoji_cache.get_by_name('health'), + combo_orb=emoji_cache.get_by_name('orb_combo'), + locked_bomb_orb=emoji_cache.get_by_name('locked_bomb_orb'), + random_attribute='[Random Att]', + water_orb=emoji_cache.get_by_name('orb_water'), + wood_orb=emoji_cache.get_by_name('orb_wood'), + light_orb=emoji_cache.get_by_name('orb_light'), + dark_orb=emoji_cache.get_by_name('orb_dark'), + healer_orb=emoji_cache.get_by_name('heal_orb'), + jammer_orb=emoji_cache.get_by_name('jammer_orb'), + poison_orb=emoji_cache.get_by_name('poison_orb'), + bomb_orb=emoji_cache.get_by_name('bomb_orb'), + mortal_poison_orb=emoji_cache.get_by_name('mortal_poison_orb'), + evo_material_type=emoji_cache.get_by_name('killer_evomat'), + balanced_type=emoji_cache.get_by_name('killer_balance'), + physical_type=emoji_cache.get_by_name('killer_physical'), + healer_type=emoji_cache.get_by_name('killer_healer'), + dragon_type=emoji_cache.get_by_name('killer_dragon'), + god_type=emoji_cache.get_by_name('killer_god'), + attacker_type=emoji_cache.get_by_name('killer_attacker'), + devil_type=emoji_cache.get_by_name('killer_devil'), + machine_type=emoji_cache.get_by_name('killer_machine'), + awakening_material_type=emoji_cache.get_by_name('killer_awoken'), + enhance_material_type=emoji_cache.get_by_name('killer_enhancemat'), + redeemable_material_type='\U0001fa99', # Coin Emoji + no_match_fire=emoji_cache.get_by_name('no_match_fire'), + no_match_wood=emoji_cache.get_by_name('no_match_wood'), + no_match_water=emoji_cache.get_by_name('no_match_water'), + no_match_dark=emoji_cache.get_by_name('no_match_dark'), + no_match_light=emoji_cache.get_by_name('no_match_light'), + no_match_heart=emoji_cache.get_by_name('no_match_heart'), + no_match_jammer=emoji_cache.get_by_name('no_match_jammer'), + no_match_poison=emoji_cache.get_by_name('no_match_poison'), + no_match_heal=emoji_cache.get_by_name('no_match_heal'), + recover_debuff_status=emoji_cache.get_by_name('recover_debuff_status'), + attack_debuff_status='\N{CROSSED SWORDS}\N{DOWNWARDS BLACK ARROW}️' + ) + monster_model = database.graph.get_monster(q.monster_id) + monster: DungeonMonster = DungeonMonster( + name=monster_model.name_en, + hp=q.hp, + atk=q.atk, + defense=q.defence, + turns=q.turns, + level=q.level + ) + if mb is None: + return monster + level_behavior = behavior_for_level(mb, q.level) + if level_behavior is None: + return monster + for group in level_behavior.groups: + monster.add_group(process_behavior_group(group, database, q, emoji_map)) + + return monster diff --git a/dungeoncog/view/__init__.py b/dungeoncog/view/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py new file mode 100644 index 00000000..fb1b6ee6 --- /dev/null +++ b/dungeoncog/view/dungeon.py @@ -0,0 +1,304 @@ +import random +from typing import TYPE_CHECKING + +import discord +from discord import Embed +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView +from discordmenu.embed.view_state import ViewState +from tsutils import embed_footer_with_state + +from dungeoncog.enemy_skills_pb2 import MonsterBehavior +from dungeoncog.processors import process_monster + +if TYPE_CHECKING: + from dadguide.models.encounter_model import EncounterModel + + +class DungeonViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, color, encounter: "EncounterModel", + sub_dungeon_id, num_floors, floor, num_spawns, floor_index, technical, database, + page=0, verbose=False): + super().__init__(original_author_id, menu_type, raw_query) + self.encounter = encounter + self.sub_dungeon_id = sub_dungeon_id + self.num_floors = num_floors + self.floor = floor + self.floor_index = floor_index + self.technical = technical + self.color = color + self.database = database + self.num_spawns = num_spawns + self.page = page + self.verbose = verbose + + def serialize(self): + ret = super().serialize() + ret.update({ + 'sub_dungeon_id': self.sub_dungeon_id, + 'num_floors': self.num_floors, + 'floor': self.floor, + 'floor_index': self.floor_index, + 'technical': self.technical, + 'pane_type': DungeonView.VIEW_TYPE, + 'verbose': self.verbose, + 'page': self.page + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, + verbose_toggle: bool = False, page: int = 0, reset_spawn: bool = False): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + raw_query = ims.get('raw_query') + sub_dungeon_id = ims.get('sub_dungeon_id') + num_floors = ims.get('num_floors') + floor = ims.get('floor') + inc_floor + floor_index = ims.get('floor_index') + inc_index + technical = ims.get('technical') + verbose = ims.get('verbose') + # check if we are on final floor/final monster of the floor + if floor > num_floors: + floor = 1 + + if floor < 1: + floor = num_floors + + # toggle verbose + if verbose_toggle: + verbose = not verbose + + # get encounter models for the floor + floor_models = dgcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) + + # check if we are on final monster of the floor + if floor_index >= len(floor_models): + floor_index = 0 + + if floor_index < 0: + floor_index = len(floor_models) + floor_index + + # check if we reset the floor_index + if reset_spawn: + floor_index = 0 + + encounter_model = floor_models[floor_index] + + return cls(original_author_id, menu_type, raw_query, color, encounter_model, sub_dungeon_id, + num_floors, floor, len(floor_models), floor_index, + technical, dgcog.database, verbose=verbose, page=page) + + +class DungeonView: + VIEW_TYPE = 'DungeonText' + + egg_text = [ + "But nobody came...", + "Come again later", + "Tsubaki loves you", + "Remember 100 turn Hera stalling?", + "Odin x Mermaid are the OG heroes", + "Never forget 10 minute stamina", + "Another 3*...", + "Nice!", + "Maybe one day DBZ will come", + "Special thanks to the PADX team and tactical_retreat!" + ] + + @staticmethod + def indent(level): + """ + Helper function that indents text for the embed + """ + ret = "" + for lv in range(level): + if lv == 0: + ret += "> " + else: + ret += "\u200b \u200b \u200b \u200b \u200b " + return ret + + @staticmethod + def embed_helper(level, names, values, line): + """ + Adds a line in such a way to maximize discord embed limits. + + We first try to add it to the most recent name part of the name/value pair. + If that fails we try to add it to the corresponding value. If that fails, + we create another name/value pair. + @param level: how many indents the line needs + @param names: an existing list (a list of strings that will be placed in "name fields" + @param values: an existing list (a list of strings that will be placed in the value part of the name/value field + @param line: the line of text we want to add + """ + + current_index = len(names) - 1 + indents = DungeonView.indent(level) + if len(names[current_index]) + len(line) + len(indents) <= 255 and len(names[current_index]) == 0: + names[current_index] += "\n{}{}".format(indents, line) + elif len(values[current_index]) + len(line) + len(indents) <= 1023: + values[current_index] += "\n{}{}".format(indents, line) + else: + names.append("") + values.append("") + DungeonView.embed_helper(level, names, values, line) + + @staticmethod + def embed(state: DungeonViewState): + fields = [] + mb = MonsterBehavior() + encounter_model = state.encounter + if encounter_model.enemy_data is not None and encounter_model.enemy_data.behavior is not None: + mb.ParseFromString(encounter_model.enemy_data.behavior) + else: + mb = None + monster = process_monster(mb, encounter_model, state.database) + + monster_embed: Embed = DungeonView.make_embed( + monster, verbose=state.verbose, spawn=[state.floor_index + 1, state.num_spawns], + floor=[state.floor, state.num_floors], technical=state.technical + )[state.page] + + hp = f'{monster.hp:,}' + atk = f'{monster.atk:,}' + defense = f'{monster.defense:,}' + turns = f'{monster.turns:,}' + title = monster_embed.title + desc = monster_embed.description + me_fields = monster_embed.fields + + for f in me_fields: + fields.append( + EmbedField(f.name, Box(*[f.value])) + ) + return EmbedView( + EmbedMain( + title=title, + description=desc, + ), + embed_fields=fields, + embed_footer=embed_footer_with_state(state) + ) + + @staticmethod + def make_embed(dungeon_monster, verbose: bool = False, spawn: "list[int]" = None, floor: "list[int]" = None, + technical: int = None): + """ + When called this generates an embed that displays the encounter (what is seen in dungeon_info). + @param dungeon_monster: the processed monster to make an embed for + @param verbose: whether or not to display effect text + @param spawn: used to show what spawn this is on a floor [spawn, max] -> "spawn/max" + @param floor: used to show what floor this is [current floor, number of floors] + @param technical: if the dungeon is a technical dungeon we don't display skills + """ + + embeds = [] + + desc = "" + + # We create two pages as monsters at max will only ever require two pages of embeds + if spawn is not None: + embed = discord.Embed( + title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{} Page:".format(dungeon_monster.name, + dungeon_monster.level, spawn[0], + spawn[1], + floor[0], floor[1]), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', + f'{dungeon_monster.atk:,}', + f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc) + ) + else: + embed = discord.Embed( + title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', + f'{dungeon_monster.atk:,}', + f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc) + ) + + embeds.append(embed) + embeds.append(embed.copy()) + + if technical == 0: + embeds[0].title += " 1/1" + embeds[1].title += " 2/1" + return embeds + embeds[0].title += " 1/" + embeds[1].title += " 2/" + # We collect text from the skills and groups of skills + lines = [] + for group in dungeon_monster.groups: + group.give_string2(lines, 0, verbose) + + # We add the lines using embed_helper + names = [""] + values = [""] + fields = 0 + current_embed = 0 + length = len(embed.title) + len(embed.description) + 1 + first = None + for line in lines: + for comp in line[1]: + DungeonView.embed_helper(line[0], names, values, comp) + + # We add the name/value pairs to the actual embed. If needed we go to the second page + if len(values[len(values) - 1]) == 0: + values[len(values) - 1] = '\u200b' + for index in range(len(names)): + name = names[index] + value = values[index] + if len(name) > 0: + temp_length = length + len(name) + len(value) + if temp_length > 6000: + current_embed += 1 + length = len(embed.title) + len(embed.description) + embeds[current_embed].add_field(name=name, value=value, inline=False) + length += len(name) + len(value) + fields += 1 + # embed.add_field(name=k, value=content, inline=False) + + if current_embed == 0: + embeds[0].title += '1' + embeds[1].title += '1' + + # for fun + random_index = random.randint(0, len(DungeonView.egg_text) - 1) + embeds[1].add_field(name=DungeonView.egg_text[random_index], value="Come back when you see 1/2 for page!") + else: + embeds[0].title += '2' + embeds[1].title += '2' + return embeds + + @staticmethod + async def make_preempt_embed(dungeon_monster, spawn: "list[int]" = None, floor: "list[int]" = None, + technical: int = None): + """ + Currently unused: when called it creates an embed that only contains embed information. + """ + skills = await dungeon_monster.collect_skills() + desc = "" + for s in skills: + if "Passive" in s.type or "Preemptive" in s.type: + desc += "\n{}".format(s.give_string(verbose=True)) + if technical == 0: + desc = "" + if spawn is not None: + embed = discord.Embed( + title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{}".format(dungeon_monster.name, + dungeon_monster.level, spawn[0], spawn[1], + floor[0], floor[1]), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', + f'{dungeon_monster.atk:,}', + f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc)) + else: + embed = discord.Embed( + title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), + description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', + f'{dungeon_monster.atk:,}', + f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc)) + return [embed, discord.Embed(title="test", desc="test")] diff --git a/dungeoncog/view/simple.py b/dungeoncog/view/simple.py new file mode 100644 index 00000000..ecbe2c8a --- /dev/null +++ b/dungeoncog/view/simple.py @@ -0,0 +1,49 @@ +from typing import List + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.view import EmbedView +from discordmenu.embed.view_state import ViewState +from tsutils import embed_footer_with_state + + +class SimpleViewState(ViewState): + def __init__(self, original_author_id, menu_type, raw_query, + color, message_list, index): + super().__init__(original_author_id, menu_type, raw_query) + self.message_list = message_list + self.index = index + self.color = color + + def serialize(self): + ret = super().serialize() + ret.update({ + 'message_list': self.message_list, + 'index': self.index + }) + return ret + + @classmethod + async def deserialize(cls, dgcog, color, ims: dict, inc): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + raw_query = ims.get('raw_query') + return cls(original_author_id, menu_type, raw_query, color, ims.get('message_list'), ims.get('index') + inc) + + +class SimpleView: + VIEW_TYPE = 'SimpleText' + + @staticmethod + def embed(state: SimpleViewState): + fields = [ + EmbedField("Title", Box(*["1", '2'])) + ] + return EmbedView( + EmbedMain( + color=state.color, + description=state.message_list[state.index] + ), + embed_fields=fields, + embed_footer=embed_footer_with_state(state), + ) diff --git a/padevents/padevents.py b/padevents/padevents.py index 12a609f6..221231a9 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1051,10 +1051,10 @@ def in_type(self, event_types): return self.with_func(lambda e: e.event_type in event_types) def with_dungeon_type(self, dungeon_type, exclude=False): - return self.with_func(lambda e: e.dungeon_type == dungeon_type, exclude) + return self.with_func(lambda e: e.technical == dungeon_type, exclude) def in_dungeon_type(self, dungeon_types, exclude=False): - return self.with_func(lambda e: e.dungeon_type in dungeon_types, exclude) + return self.with_func(lambda e: e.technical in dungeon_types, exclude) def is_grouped(self, exclude=False): return self.with_func(lambda e: e.group is not None, exclude) From 9ce9220e9f0f77f92ed77959e9a3b96fd7ff3ddb Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sat, 19 Jun 2021 17:38:21 -0400 Subject: [PATCH 0868/1220] Make buttoninfo an embed & visual improvements (#1250) --- dadguide/models/monster_stats.py | 2 - padinfo/core/button_info.py | 33 +++++++------ padinfo/menu/closable_embed.py | 2 + padinfo/padinfo.py | 11 +++-- padinfo/view/button_info.py | 85 ++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 padinfo/view/button_info.py diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 6e54e689..bc7d13bd 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -77,12 +77,10 @@ def stat(self, monster_model, key: StatType, lv, *, plus=99, inherit=False, is_p s_val += inherit_bonus.get_awakening_addition(key) if multiplayer: - print(s_val) num_multiboost = monster_model.awakening_count(AwokenSkills.MULTIBOOST.value) num_multiboost += inherited_monster.awakening_count( AwokenSkills.MULTIBOOST.value) if inherited_monster else 0 s_val = round(round(s_val) * (1.5 ** num_multiboost)) - print(s_val) return s_val diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 13cb6cbc..17097b69 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -21,25 +21,27 @@ {} """ -TEAM_BUTTON_FORMAT = "As [{}] {} base ({}x {}): Contributes {}" -CARD_BUTTON_FORMAT = "As [{}] {} base ({}x): Deals {}" +TEAM_BUTTON_FORMAT = "[{}] {} ({}x {}): {}" +CARD_BUTTON_FORMAT = "[{}] {} ({}x): {}" MonsterStat = namedtuple('MonsterStat', 'name id mult att') TEAM_BUTTONS = [ - MonsterStat("Nergi Hunter", 4172, 40, [2, 4]), - MonsterStat("Oversoul", 5273, 50, [0, 1, 2, 3, 4]), - MonsterStat("Ryuno Ume", 5252, 80, [2, 4]) + # account for attributes when spacing + MonsterStat("Nergi Hunter{}", 4172, 40, [2, 4]), + MonsterStat("Oversoul{} ", 5273, 50, [0, 1, 2, 3, 4]), + MonsterStat("Ryuno Ume{} ", 5252, 80, [2, 4]) ] CARD_BUTTONS = [ - MonsterStat("Satan", 4286, 300, []), - MonsterStat("Durandalf Equip", 4723, 300, []), - MonsterStat("Brachydios Equip", 4152, 350, []), - MonsterStat("Rajang", 5527, 550, []), - MonsterStat("Balrog", 5108, 450, []) + MonsterStat("Satan{} ", 4286, 300, []), + MonsterStat("Durandalf Eq{} ", 4723, 300, []), + MonsterStat("Brachydios Eq{}", 4152, 350, []), + MonsterStat("Rajang Eq{} ", 5530, 550, []), + MonsterStat("Balrog{} ", 5108, 450, []) ] COLORS = ["R", "B", "G", "L", "D"] +NIL_ATT = 'Nil' class ButtonInfo: @@ -76,6 +78,8 @@ def get_info(self, dgcog, monster_model): dgcog, monster_model, max_level, max_atk_latents) result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent + result.card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dgcog, monster_model) + result.team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dgcog, monster_model) return result def _calculate_damage(self, dgcog, monster_model, level, num_atkpp_latent=0): @@ -114,12 +118,12 @@ def _get_card_btn_damage(self, card_buttons, dgcog, monster): stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=monster.latent_slots / 2) dmg = int(round(dgcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level))) + oncolor = '*' if monster.attr1.value == inherit_model.attr1.value or monster.attr1.name == NIL_ATT else ' ' lines.append(CARD_BUTTON_FORMAT.format( - card.id, card.name, card.mult, (dmg * card.mult))) + card.id, card.name.format(oncolor), card.mult, round(dmg * card.mult, 2))) return "\n".join(lines) def _get_team_btn_damage(self, team_buttons, dgcog, monster): - # TODO: calculate with oncolor assist damage and ATK+ eq (Oversoul) lines = [] team_buttons.sort(key=lambda x: x.mult) for card in team_buttons: @@ -137,8 +141,9 @@ def _get_team_btn_damage(self, team_buttons, dgcog, monster): colors_str = "" for i in card.att: colors_str += COLORS[i] - lines.append(TEAM_BUTTON_FORMAT.format(card.id, card.name, - card.mult, colors_str, (total_dmg * card.mult))) + oncolor = '*' if monster.attr1.value == inherit_model.attr1.value or monster.attr1.name == NIL_ATT else ' ' + lines.append(TEAM_BUTTON_FORMAT.format(card.id, card.name.format(oncolor), + card.mult, colors_str, round(total_dmg * card.mult, 2))) return "\n".join(lines) diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index 456e6539..4ea9c57e 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -3,11 +3,13 @@ from tsutils.menu.panes import MenuPanes from padinfo.view.awakening_help import AwakeningHelpView +from padinfo.view.button_info import ButtonInfoView from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.id_traceback import IdTracebackView view_types = { AwakeningHelpView.VIEW_TYPE: AwakeningHelpView, + ButtonInfoView.VIEW_TYPE: ButtonInfoView, IdTracebackView.VIEW_TYPE: IdTracebackView, } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 0077fe45..7972f332 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -40,6 +40,7 @@ from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.awakening_help import AwakeningHelpView, AwakeningHelpViewProps from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListSortTypes +from padinfo.view.button_info import ButtonInfoView, ButtonInfoViewProps from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.common import invalid_monster_text from padinfo.view.components.monster.header import MonsterHeader @@ -580,9 +581,13 @@ async def buttoninfo(self, ctx, *, query: str): return info = button_info.get_info(dgcog, monster) - info_str = button_info.to_string(dgcog, monster, info) - for page in pagify(info_str): - await ctx.send(box(page)) + original_author_id = ctx.message.author.id + menu = ClosableEmbedMenu.menu() + color = await self.get_user_embed_color(ctx) + props = ButtonInfoViewProps(monster=monster, info=info) + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, + color, ButtonInfoView.VIEW_TYPE, props) + await menu.create(ctx, state) @commands.command() @checks.bot_has_permissions(embed_links=True) diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py new file mode 100644 index 00000000..ef156cd0 --- /dev/null +++ b/padinfo/view/button_info.py @@ -0,0 +1,85 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedAuthor, EmbedField, EmbedMain +from discordmenu.embed.text import Text, BlockText +from discordmenu.embed.view import EmbedView +from tsutils import embed_footer_with_state + +from padinfo.common.external_links import puzzledragonx +from padinfo.core.button_info import button_info +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dadguide.models.monster_model import MonsterModel + + +class ButtonInfoViewProps: + def __init__(self, monster: "MonsterModel", info): + self.monster = monster + self.info = info + + +def get_stats_without_latents(info): + return BlockText( + Box( + Text('Base: {}'.format(int(round(info.main_damage)))), + Text('Subattr: {}'.format(int(round(info.sub_damage)))), + Text('Total: {}'.format(int(round(info.total_damage)))) + ) + ) + + +def get_stats_with_latents(info): + return BlockText( + Box( + Text('Base: {}'.format(int(round(info.main_damage_with_atk_latent)))), + Text('Subattr: {}'.format(int(round(info.sub_damage_with_atk_latent)))), + Text('Total: {}'.format(int(round(info.total_damage_with_atk_latent)))) + ) + ) + + +class ButtonInfoView: + VIEW_TYPE = 'ButtonInfo' + + @staticmethod + def embed(state, props: ButtonInfoViewProps): + monster = props.monster + info = props.info + + fields = [ + EmbedField('Without Latents', get_stats_without_latents(info)), + EmbedField('With Latents', get_stats_with_latents(info)), + EmbedField( + 'Common Buttons', + Box( + Text('*Inherits are assumed to be the max possible level (up to 110) and +297.*'), + # janky, but python gives DeprecationWarnings when using \* in a regular string + Text(r'*\* = on-color stat bonus applied*'), + Text('Card Button Damage'), + # done this way to not have the whitespace after code block + Box( + BlockText(info.card_btn_str), + Text('Team Button Contribution'), + delimiter='' + ), + BlockText(info.team_btn_str) + ) + ) + ] + + return EmbedView( + EmbedMain( + color=state.color, + description='(Co-op mode)' + ), + embed_author=EmbedAuthor( + MonsterHeader.long_v2(monster).to_markdown(), + puzzledragonx(monster), + MonsterImage.icon(monster) + ), + embed_footer=embed_footer_with_state(state), + embed_fields=fields + ) From 6a5e7a15c1984f0a384c9f9ad666874a042c94c8 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 21 Jun 2021 17:28:15 -0700 Subject: [PATCH 0869/1220] Fix PDE Typo (#1252) --- dungeoncog/dungeoncog.py | 3 +-- padevents/padevents.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 5dbfa6f1..e7eaa81a 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -1,10 +1,9 @@ import logging -import os from typing import TYPE_CHECKING from discord import Color from discordmenu.emoji.emoji_cache import emoji_cache -from redbot.core import commands, data_manager +from redbot.core import commands from redbot.core.utils.chat_formatting import pagify from dungeoncog.enemy_skills_pb2 import MonsterBehavior diff --git a/padevents/padevents.py b/padevents/padevents.py index 221231a9..59868aeb 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -6,7 +6,7 @@ from datetime import timedelta from enum import Enum from io import BytesIO -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List import discord import itertools @@ -1033,7 +1033,7 @@ def to_partial_event(self, pe): class EventList: def __init__(self, event_list): - self.event_list = event_list + self.event_list: List[Event] = event_list def with_func(self, func, exclude=False): if exclude: @@ -1051,10 +1051,10 @@ def in_type(self, event_types): return self.with_func(lambda e: e.event_type in event_types) def with_dungeon_type(self, dungeon_type, exclude=False): - return self.with_func(lambda e: e.technical == dungeon_type, exclude) + return self.with_func(lambda e: e.dungeon_type == dungeon_type, exclude) def in_dungeon_type(self, dungeon_types, exclude=False): - return self.with_func(lambda e: e.technical in dungeon_types, exclude) + return self.with_func(lambda e: e.dungeon_type in dungeon_types, exclude) def is_grouped(self, exclude=False): return self.with_func(lambda e: e.group is not None, exclude) From 08bb4d9e16552eced4eb70ad3ce55c7eacdaa300 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:48:46 -0500 Subject: [PATCH 0870/1220] Rename AEP set offset function (#1259) --- padevents/padevents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 59868aeb..225f17b9 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -523,7 +523,7 @@ async def aep_s_disabled(self, ctx, key, disabled: bool = True): await ctx.tick() @aep_set.command(name="offset") - async def aep_s_disabled(self, ctx, key, offset: int): + async def aep_s_offset(self, ctx, key, offset: int): """Sets how many minutes before event should ping happen""" if offset < 0: await ctx.send("Offset cannot be negative.") From 161fe16fecc42abc1c6cabcd5860665a18a4d9e5 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 23 Jun 2021 15:22:40 -0700 Subject: [PATCH 0871/1220] Merge MWTokens more faithfully (#1261) --- dadguide/find_monster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/find_monster.py b/dadguide/find_monster.py index 8d008759..84ac55eb 100644 --- a/dadguide/find_monster.py +++ b/dadguide/find_monster.py @@ -73,7 +73,7 @@ def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: break else: skip = len(mwt) - 1 - result.append("".join(mwt)) + result.append("".join(tokens[c1:c1+len(mwt)])) break else: result.append(token1) From 07f97b4b125506cf20c1848779f4e9ff25e36280 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 27 Jun 2021 12:44:14 -0400 Subject: [PATCH 0872/1220] Add level 120 stats to buttoninfo (#1262) --- dadguide/models/monster_stats.py | 20 ++++++---- padinfo/core/button_info.py | 38 ++++++++++++++++--- padinfo/view/button_info.py | 63 +++++++++++++++++++++++++++++--- 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index bc7d13bd..374c724c 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -5,14 +5,18 @@ class MonsterStatModifierInput: - def __init__(self, num_hp=0, num_atk=0, num_rcv=0, num_hppp=0, num_atkpp=0, num_rcvpp=0, num_all_stat=0, - num_hp_awakening=0, num_atk_awakening=0, num_rcv_awakening=0, num_voice_awakening=0): + def __init__(self, num_hp=0, num_atk=0, num_rcv=0, num_hpplus=0, num_atkplus=0, num_rcvplus=0, num_hpplus2=0, + num_atkplus2=0, num_rcvplus2=0, num_all_stat=0, num_hp_awakening=0, num_atk_awakening=0, + num_rcv_awakening=0, num_voice_awakening=0): self.num_hp = num_hp self.num_atk = num_atk self.num_rcv = num_rcv - self.num_hppp = num_hppp - self.num_atkpp = num_atkpp - self.num_rcvpp = num_rcvpp + self.num_hpplus = num_hpplus + self.num_atkplus = num_atkplus + self.num_rcvplus = num_rcvplus + self.num_hpplus2 = num_hpplus2 + self.num_atkplus2 = num_atkplus2 + self.num_rcvplus2 = num_rcvplus2 self.num_all_stat = num_all_stat self.num_hp_awakening = num_hp_awakening self.num_atk_awakening = num_atk_awakening @@ -21,11 +25,11 @@ def __init__(self, num_hp=0, num_atk=0, num_rcv=0, num_hppp=0, num_atkpp=0, num_ def get_latent_multiplier(self, key: StatType): if key == 'hp': - return self.num_hp * 0.015 + self.num_hppp * 0.045 + self.num_all_stat * 0.03 + return self.num_hp * 0.015 + self.num_hpplus * 0.045 + self.num_hpplus2 * 0.1 + self.num_all_stat * 0.03 elif key == 'atk': - return self.num_atk * 0.01 + self.num_atkpp * 0.03 + self.num_all_stat * 0.02 + return self.num_atk * 0.01 + self.num_atkplus * 0.03 + self.num_atkplus2 * 0.05 + self.num_all_stat * 0.02 elif key == 'rcv': - return self.num_rcv * 0.1 + self.num_rcvpp * 0.3 + self.num_all_stat * 0.2 + return self.num_rcv * 0.1 + self.num_rcvplus * 0.3 + self.num_rcvplus2 * 0.35 + self.num_all_stat * 0.2 return 0 def get_awakening_addition(self, key: StatType): diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 17097b69..e6fc3d98 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -1,6 +1,7 @@ from collections import namedtuple LIMIT_BREAK_LEVEL = 110 +SUPER_LIMIT_BREAK_LEVEL = 120 INFO_STRING = """[{}] {} (Co-op mode) @@ -65,25 +66,44 @@ def get_info(self, dgcog, monster_model): just subattr: whatever """ max_level = LIMIT_BREAK_LEVEL if monster_model.limit_mult != 0 else monster_model.level + slb_level = SUPER_LIMIT_BREAK_LEVEL if monster_model.limit_mult != 0 else None max_atk_latents = monster_model.latent_slots / 2 sub_attr_multiplier = self._get_sub_attr_multiplier(monster_model) result = ButtonInfoResult() - result.main_damage = self._calculate_damage(dgcog, monster_model, max_level, 0) + result.main_damage = self._calculate_damage(dgcog, monster_model, max_level) result.sub_damage = result.main_damage * sub_attr_multiplier result.total_damage = result.main_damage + result.sub_damage + if slb_level is None: + result.main_slb_damage = None + result.sub_slb_damage = None + result.total_slb_damage = None + else: + result.main_slb_damage = self._calculate_damage(dgcog, monster_model, slb_level) + result.sub_slb_damage = result.main_slb_damage * sub_attr_multiplier + result.total_slb_damage = result.main_slb_damage + result.sub_slb_damage result.main_damage_with_atk_latent = self._calculate_damage( - dgcog, monster_model, max_level, max_atk_latents) + dgcog, monster_model, max_level, num_atkplus_latent=max_atk_latents) result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent + if slb_level is None: + result.main_damage_with_slb_atk_latent = None + result.sub_damage_with_slb_atk_latent = None + result.total_damage_with_slb_atk_latent = None + else: + result.main_damage_with_slb_atk_latent = self._calculate_damage( + dgcog, monster_model, slb_level, num_atkplus2_latent=max_atk_latents) + result.sub_damage_with_slb_atk_latent = result.main_damage_with_slb_atk_latent * sub_attr_multiplier + result.total_damage_with_slb_atk_latent = (result.main_damage_with_slb_atk_latent + + result.sub_damage_with_slb_atk_latent) result.card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dgcog, monster_model) result.team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dgcog, monster_model) return result - def _calculate_damage(self, dgcog, monster_model, level, num_atkpp_latent=0): - stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=num_atkpp_latent) + def _calculate_damage(self, dgcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): + stat_latents = dgcog.MonsterStatModifierInput(num_atkplus=num_atkplus_latent, num_atkplus2=num_atkplus2_latent) stat_latents.num_atk_awakening = len( [x for x in monster_model.awakenings if x.awoken_skill_id == 1]) @@ -115,7 +135,7 @@ def _get_card_btn_damage(self, card_buttons, dgcog, monster): inherit_model = dgcog.get_monster(card.id, server=monster.server_priority) max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level - stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=monster.latent_slots / 2) + stat_latents = dgcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) dmg = int(round(dgcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level))) oncolor = '*' if monster.attr1.value == inherit_model.attr1.value or monster.attr1.name == NIL_ATT else ' ' @@ -131,7 +151,7 @@ def _get_team_btn_damage(self, team_buttons, dgcog, monster): inherit_model = dgcog.get_monster(card.id, server=monster.server_priority) max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level - stat_latents = dgcog.MonsterStatModifierInput(num_atkpp=monster.latent_slots / 2) + stat_latents = dgcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) dmg = dgcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level) if(monster.attr1.value in card.att): @@ -151,9 +171,15 @@ class ButtonInfoResult: main_damage: float total_damage: float sub_damage: float + main_slb_damage: float + total_slb_damage: float + sub_slb_damage: float main_damage_with_atk_latent: float total_damage_with_atk_latent: float sub_damage_with_atk_latent: float + main_damage_with_slb_atk_latent: float + total_damage_with_slb_atk_latent: float + sub_damage_with_slb_atk_latent: float button_info = ButtonInfo() diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index ef156cd0..cab5f778 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -7,7 +7,7 @@ from tsutils import embed_footer_with_state from padinfo.common.external_links import puzzledragonx -from padinfo.core.button_info import button_info +from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage @@ -21,7 +21,12 @@ def __init__(self, monster: "MonsterModel", info): self.info = info -def get_stats_without_latents(info): +def get_max_level(monster): + level_text = str(LIMIT_BREAK_LEVEL) if monster.limit_mult != 0 else 'Max ({})'.format(monster.level) + return 'Lv. {}'.format(level_text) + + +def get_max_stats_without_latents(info): return BlockText( Box( Text('Base: {}'.format(int(round(info.main_damage)))), @@ -31,7 +36,17 @@ def get_stats_without_latents(info): ) -def get_stats_with_latents(info): +def get_120_stats_without_latents(info): + return BlockText( + Box( + Text('Base: {}'.format(int(round(info.main_slb_damage)))), + Text('Subattr: {}'.format(int(round(info.sub_slb_damage)))), + Text('Total: {}'.format(int(round(info.total_slb_damage)))) + ) + ) + + +def get_max_stats_with_latents(info): return BlockText( Box( Text('Base: {}'.format(int(round(info.main_damage_with_atk_latent)))), @@ -41,6 +56,16 @@ def get_stats_with_latents(info): ) +def get_120_stats_with_latents(info): + return BlockText( + Box( + Text('Base: {}'.format(int(round(info.main_damage_with_slb_atk_latent)))), + Text('Subattr: {}'.format(int(round(info.sub_damage_with_slb_atk_latent)))), + Text('Total: {}'.format(int(round(info.total_damage_with_slb_atk_latent)))) + ) + ) + + class ButtonInfoView: VIEW_TYPE = 'ButtonInfo' @@ -50,10 +75,36 @@ def embed(state, props: ButtonInfoViewProps): info = props.info fields = [ - EmbedField('Without Latents', get_stats_without_latents(info)), - EmbedField('With Latents', get_stats_with_latents(info)), EmbedField( - 'Common Buttons', + get_max_level(monster), + Box( + Text('Without Latents'), + # avoid whitespace after code block + Box( + get_max_stats_without_latents(info), + Text('With Latents (Atk+)'), + delimiter='' + ), + get_max_stats_with_latents(info) + ), + inline=True + ), + EmbedField( + 'Lv. 120', + Box( + Text('Without Latents'), + # avoid whitespace after code block + Box( + get_120_stats_without_latents(info), + Text('With Latents (Atk++)'), + delimiter='' + ), + get_120_stats_with_latents(info) + ), + inline=True + ) if monster.limit_mult != 0 else None, + EmbedField( + 'Common Buttons - {}'.format(get_max_level(monster)), Box( Text('*Inherits are assumed to be the max possible level (up to 110) and +297.*'), # janky, but python gives DeprecationWarnings when using \* in a regular string From d7033113f0b6100b2d1d2f8d64c3fcc6d6069d3a Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:07:37 -0400 Subject: [PATCH 0873/1220] Update slb atk latent to 8% (#1264) --- dadguide/models/monster_stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dadguide/models/monster_stats.py b/dadguide/models/monster_stats.py index 374c724c..7cb6b2a7 100644 --- a/dadguide/models/monster_stats.py +++ b/dadguide/models/monster_stats.py @@ -27,7 +27,7 @@ def get_latent_multiplier(self, key: StatType): if key == 'hp': return self.num_hp * 0.015 + self.num_hpplus * 0.045 + self.num_hpplus2 * 0.1 + self.num_all_stat * 0.03 elif key == 'atk': - return self.num_atk * 0.01 + self.num_atkplus * 0.03 + self.num_atkplus2 * 0.05 + self.num_all_stat * 0.02 + return self.num_atk * 0.01 + self.num_atkplus * 0.03 + self.num_atkplus2 * 0.08 + self.num_all_stat * 0.02 elif key == 'rcv': return self.num_rcv * 0.1 + self.num_rcvplus * 0.3 + self.num_rcvplus2 * 0.35 + self.num_all_stat * 0.2 return 0 From d5e99a58ad6a1aede45c2485b98131045e619b24 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Thu, 1 Jul 2021 01:12:00 -0400 Subject: [PATCH 0874/1220] Make enhancements to buttoninfo (#1267) --- padinfo/core/button_info.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index e6fc3d98..9c54bcbb 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -22,8 +22,8 @@ {} """ -TEAM_BUTTON_FORMAT = "[{}] {} ({}x {}): {}" -CARD_BUTTON_FORMAT = "[{}] {} ({}x): {}" +TEAM_BUTTON_FORMAT = "[{}] {} ({} {}): {}" +CARD_BUTTON_FORMAT = "[{}] {} ({}): {}" MonsterStat = namedtuple('MonsterStat', 'name id mult att') @@ -34,6 +34,7 @@ MonsterStat("Ryuno Ume{} ", 5252, 80, [2, 4]) ] CARD_BUTTONS = [ + MonsterStat("Assassin{} ", 5021, 200, []), MonsterStat("Satan{} ", 4286, 300, []), MonsterStat("Durandalf Eq{} ", 4723, 300, []), MonsterStat("Brachydios Eq{}", 4152, 350, []), From 449f2828ea43a34952b9e95f90fd08fe45be375d Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 3 Jul 2021 22:44:32 -0700 Subject: [PATCH 0875/1220] Improve AEP (#1270) Closes #1269 Closes #1268 Closes #1257 Closes #1255 --- padevents/padevents.py | 126 +++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 225f17b9..c86e9c5f 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -6,7 +6,7 @@ from datetime import timedelta from enum import Enum from io import BytesIO -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional import discord import itertools @@ -14,8 +14,8 @@ import pytz from redbot.core import checks from redbot.core import commands, Config -from redbot.core.utils.chat_formatting import box, pagify -from tsutils import CogSettings, confirm_message, normalize_server_name, DummyObject, is_donor +from redbot.core.utils.chat_formatting import box, pagify, humanize_timedelta +from tsutils import CogSettings, confirm_message, normalize_server_name, DummyObject, is_donor, rmdiacritics if TYPE_CHECKING: from dadguide.models.scheduled_event_model import ScheduledEventModel @@ -146,7 +146,11 @@ async def check_started(self): if aep['offset']: offsetstr = " in {} minute(s)".format(aep['offset']) try: - await channel.send("{}{} {}".format(event.name_and_modifier, offsetstr, ment), + timestr = humanize_timedelta(timedelta=event.close_datetime-event.open_datetime) + await channel.send("{} will be active for {}{}. {}".format(event.name_and_modifier, + offsetstr, + timestr, + ment), allowed_mentions=discord.AllowedMentions(roles=True)) except Exception: logger.exception("Failed to send AEP in channel {}".format(channel.id)) @@ -163,11 +167,13 @@ async def check_started(self): continue self.rolepinged_events.add((aed['key'], event.key)) if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): - offsetstr = " starts now!" + offsetstr = "now" + timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) if aed['offset']: - offsetstr = " starts in {} minute(s)!".format(aed['offset']) + offsetstr = "in {} minute(s)".format(aed['offset']) try: - await user.send(event.clean_dungeon_name + offsetstr) + await user.send(f"{event.clean_dungeon_name} starts {offsetstr}!" + f" It will be active for {timestr}.") except Exception: logger.exception("Failed to send AED to user {}".format(user.id)) @@ -324,35 +330,59 @@ async def rmchanneldaily(self, ctx, server): async def autoeventping(self, ctx): """Auto Event Pings""" - @autoeventping.command(name="add") - async def aep_add(self, ctx, key, server=None, searchstr=None, red: discord.Role = None, blue: discord.Role = None, - green: discord.Role = None): - """Add a new autoeventping""" - if green is None and server is not None: - await ctx.send("Multi-word keys must be in quotes.") + @autoeventping.command(name="add", ignore_extra=False) + async def aep_add(self, ctx, key, server=None, searchstr=None, + red_role: Optional[discord.Role] = None, red_channel: Optional[discord.TextChannel] = None, + blue_role: discord.Role = None, blue_channel: Optional[discord.TextChannel] = None, + green_role: discord.Role = None, green_channel: Optional[discord.TextChannel] = None): + """Add a new autoeventping. + + Usage: + `[p]autoeventping add newping NA "Extreme King Metal Dragon" @redrole @bluerole @greenrole` + `[p]autoeventping add onerole JP "monday dungeon" @single_role_to_ping` + `[p]autoeventping add channels KR "tama" @redrole #redchannel @bluerole #bluechannel @greenrole #greenchannel` + `[p]autoeventping add singlechan NA "wallace" @single_role_to_ping #channel_to_ping_in` + `[p]autoeventping add roles NA "infestation" @redrole #channel_to_ping_in @bluerole @greenrole` + `[p]autoeventping add noping NA "infestation" #channel_to_ping_in` + """ + if " " in key: + await ctx.send("Multi-word keys are not allowed.") return + defchan = None if red_channel is None else red_channel.id + default = { 'roles': [None, None, None], - 'channels': [None, None, None], - 'server': 'NA', - 'searchstr': None, + 'channels': [defchan, defchan, defchan], + 'server': server or 'NA', + 'searchstr': searchstr and rmdiacritics(searchstr), 'regex': False, - 'enabled': False, + 'enabled': bool(red_role or red_channel), 'offset': 0, } - if green is not None: + if red_role is not None: + if red_channel is None: + if blue_channel is not None or green_channel is not None: + await ctx.send("You must supply all three channels or just red_channel.") + return + red_channel = blue_channel = green_channel = ctx.channel + elif (blue_channel is None) ^ (green_channel is None): + await ctx.send("You must supply all three channels or just red_channel.") + return + else: + blue_channel = green_channel = red_channel + + if blue_role is None: + blue_role = green_role = red_role + server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") + await ctx.send(f"Unsupported server `{server}`, pick one of NA, KR, JP") return default.update({ - 'roles': [red.id, blue.id, green.id], - 'channels': [ctx.channel.id] * 3, - 'server': server, - 'searchstr': searchstr, - 'enabled': True, + 'roles': [red_role.id, blue_role.id, green_role.id], + 'channels': [red_channel.id, blue_channel.id, green_channel.id], }) async with self.config.guild(ctx.guild).pingroles() as pingroles: @@ -360,7 +390,7 @@ async def aep_add(self, ctx, key, server=None, searchstr=None, red: discord.Role await ctx.tick() @autoeventping.command(name="remove", aliases=['rm', 'delete']) - async def aep_remove(self, ctx, *, key): + async def aep_remove(self, ctx, key): """Remove an autoeventping""" async with self.config.guild(ctx.guild).pingroles() as pingroles: if key not in pingroles: @@ -370,7 +400,7 @@ async def aep_remove(self, ctx, *, key): await ctx.tick() @autoeventping.command(name="show") - async def aep_show(self, ctx, *, key): + async def aep_show(self, ctx, key): """Show specifics of an autoeventping""" pingroles = await self.config.guild(ctx.guild).pingroles() if key not in pingroles: @@ -410,17 +440,17 @@ async def aep_list(self, ctx): async def aep_set(self, ctx): """Sets specific parts of an autoeventping""" - async def aepc(self, ctx, key, k, f): + async def aepchange(self, ctx, key, k, f): async with self.config.guild(ctx.guild).pingroles() as pingroles: if key not in pingroles: await ctx.send("That key does not exist.") return pingroles[key][k] = f(pingroles[key][k]) - async def aeps(self, ctx, key, k, v): - await self.aepc(ctx, key, k, lambda x: v) + async def aepset(self, ctx, key, k, v): + await self.aepchange(ctx, key, k, lambda x: v) - async def aepg(self, ctx, key): + async def aepget(self, ctx, key): pingroles = await self.config.guild(ctx.guild).pingroles() if key not in pingroles: await ctx.send("That key does not exist.") @@ -430,49 +460,49 @@ async def aepg(self, ctx, key): @aep_set.command(name="channel") async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping for all groups""" - await self.aeps(ctx, key, 'channels', [channel.id] * 3) + await self.aepset(ctx, key, 'channels', [channel.id] * 3) await ctx.tick() @aep_set.command(name="redchannel") async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping when event is red""" - await self.aepc(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) + await self.aepchange(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) await ctx.tick() @aep_set.command(name="bluechannel") async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping when event is blue""" - await self.aepc(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) + await self.aepchange(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) await ctx.tick() @aep_set.command(name="greenchannel") async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping when event is green""" - await self.aepc(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) + await self.aepchange(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) await ctx.tick() @aep_set.command(name="roles") async def aep_s_roles(self, ctx, key, red: discord.Role, blue: discord.Role, green: discord.Role): """Sets roles to ping""" - await self.aeps(ctx, key, 'roles', [red.id, blue.id, green.id]) + await self.aepset(ctx, key, 'roles', [red.id, blue.id, green.id]) await ctx.tick() @aep_set.command(name="redrole") async def aep_s_redrole(self, ctx, key, role: discord.Role): """Sets role to ping when event is red""" - await self.aepc(ctx, key, 'roles', lambda x: [role.id, x[1], x[2]]) + await self.aepchange(ctx, key, 'roles', lambda x: [role.id, x[1], x[2]]) await ctx.tick() @aep_set.command(name="bluerole") async def aep_s_bluerole(self, ctx, key, role: discord.Role): """Sets role to ping when event is blue""" - await self.aepc(ctx, key, 'roles', lambda x: [x[0], role.id, x[2]]) + await self.aepchange(ctx, key, 'roles', lambda x: [x[0], role.id, x[2]]) await ctx.tick() @aep_set.command(name="greenrole") async def aep_s_greenrole(self, ctx, key, role: discord.Role): """Sets role to ping when event is green""" - await self.aepc(ctx, key, 'roles', lambda x: [x[0], x[1], role.id]) + await self.aepchange(ctx, key, 'roles', lambda x: [x[0], x[1], role.id]) await ctx.tick() @aep_set.command(name="server") @@ -482,20 +512,20 @@ async def aep_s_server(self, ctx, key, server): if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return - await self.aeps(ctx, key, 'server', server) + await self.aepset(ctx, key, 'server', server) await ctx.tick() @aep_set.command(name="searchstr") async def aep_s_searchstr(self, ctx, key, *, searchstr): """Sets what string is tested against event name""" - searchstr = searchstr.strip('"') - if (await self.aepg(ctx, key))['regex']: + searchstr = rmdiacritics(searchstr).strip('"') + if (await self.aepget(ctx, key))['regex']: try: re.compile(searchstr) except re.error: await ctx.send("Invalid regex searchstr. (`{}`)".format(searchstr)) return - await self.aeps(ctx, key, 'searchstr', searchstr) + await self.aepset(ctx, key, 'searchstr', searchstr) await ctx.tick() @aep_set.command(name="regex") @@ -503,23 +533,23 @@ async def aep_s_regex(self, ctx, key, regex: bool): """Sets whether searchstr is calculated via regex""" if regex: try: - re.compile((await self.aepg(ctx, key))['searchstr']) + re.compile((await self.aepget(ctx, key))['searchstr']) except re.error: - await ctx.send("Invalid regex searchstr. (`{}`)".format((await self.aepg(ctx, key))['searchstr'])) + await ctx.send("Invalid regex searchstr. (`{}`)".format((await self.aepget(ctx, key))['searchstr'])) return - await self.aeps(ctx, key, 'regex', regex) + await self.aepset(ctx, key, 'regex', regex) await ctx.tick() @aep_set.command(name="enabled", aliases=['enable']) async def aep_s_enabled(self, ctx, key, enabled: bool = True): """Sets whether or not ping is enabled""" - await self.aeps(ctx, key, 'enabled', enabled) + await self.aepset(ctx, key, 'enabled', enabled) await ctx.tick() @aep_set.command(name="disabled", aliases=['disable']) async def aep_s_disabled(self, ctx, key, disabled: bool = True): """Sets whether or not ping is disabled""" - await self.aeps(ctx, key, 'enabled', not disabled) + await self.aepset(ctx, key, 'enabled', not disabled) await ctx.tick() @aep_set.command(name="offset") @@ -528,7 +558,7 @@ async def aep_s_offset(self, ctx, key, offset: int): if offset < 0: await ctx.send("Offset cannot be negative.") return - await self.aeps(ctx, key, 'offset', offset) + await self.aepset(ctx, key, 'offset', offset) await ctx.tick() @commands.group(aliases=['aed']) From 114269093515836db2e0852a8dc20075aaa2aee2 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 3 Jul 2021 23:33:32 -0700 Subject: [PATCH 0876/1220] Change signature and docstring (#1273) --- padevents/padevents.py | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index c86e9c5f..e115278e 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -332,46 +332,38 @@ async def autoeventping(self, ctx): @autoeventping.command(name="add", ignore_extra=False) async def aep_add(self, ctx, key, server=None, searchstr=None, - red_role: Optional[discord.Role] = None, red_channel: Optional[discord.TextChannel] = None, - blue_role: discord.Role = None, blue_channel: Optional[discord.TextChannel] = None, - green_role: discord.Role = None, green_channel: Optional[discord.TextChannel] = None): - """Add a new autoeventping. + red_role: Optional[discord.Role] = None, + blue_role: Optional[discord.Role] = None, + green_role: Optional[discord.Role] = None, + channel: discord.TextChannel = None): + """Add a new autoeventping. Use `[p]aep set` with your chosen key to finish or update config. Usage: - `[p]autoeventping add newping NA "Extreme King Metal Dragon" @redrole @bluerole @greenrole` - `[p]autoeventping add onerole JP "monday dungeon" @single_role_to_ping` - `[p]autoeventping add channels KR "tama" @redrole #redchannel @bluerole #bluechannel @greenrole #greenchannel` - `[p]autoeventping add singlechan NA "wallace" @single_role_to_ping #channel_to_ping_in` - `[p]autoeventping add roles NA "infestation" @redrole #channel_to_ping_in @bluerole @greenrole` - `[p]autoeventping add noping NA "infestation" #channel_to_ping_in` + `[p]aep add diamondra` + `[p]aep add rubydra NA "ruby dragon"` + `[p]aep add ekmd NA "Extreme King Metal Dragon" @red @blue @green` + `[p]aep add monday JP "monday dungeon" @eventping` + `[p]aep add pluspoints NA "star treasure" @red @blue @green #channel` + `[p]aep add wallace KR "wallace" @eventping #channel` + `[p]aep add tama NA "tama" #channel` """ if " " in key: await ctx.send("Multi-word keys are not allowed.") return - defchan = None if red_channel is None else red_channel.id - default = { 'roles': [None, None, None], - 'channels': [defchan, defchan, defchan], + 'channels': [channel and channel.id] * 3, 'server': server or 'NA', 'searchstr': searchstr and rmdiacritics(searchstr), 'regex': False, - 'enabled': bool(red_role or red_channel), + 'enabled': bool(red_role or channel), 'offset': 0, } if red_role is not None: - if red_channel is None: - if blue_channel is not None or green_channel is not None: - await ctx.send("You must supply all three channels or just red_channel.") - return - red_channel = blue_channel = green_channel = ctx.channel - elif (blue_channel is None) ^ (green_channel is None): - await ctx.send("You must supply all three channels or just red_channel.") - return - else: - blue_channel = green_channel = red_channel + if channel is None: + channel = ctx.channel if blue_role is None: blue_role = green_role = red_role @@ -382,7 +374,7 @@ async def aep_add(self, ctx, key, server=None, searchstr=None, return default.update({ 'roles': [red_role.id, blue_role.id, green_role.id], - 'channels': [red_channel.id, blue_channel.id, green_channel.id], + 'channels': [channel.id] * 3, }) async with self.config.guild(ctx.guild).pingroles() as pingroles: From efa45e22bb5803f5f42d0b57996e522ab17d530e Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 4 Jul 2021 12:23:20 -0700 Subject: [PATCH 0877/1220] More aep add UX improvements (#1278) Closes #1274 Closes #1276 Closes #1277 --- padevents/padevents.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index e115278e..80d912cd 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -336,24 +336,32 @@ async def aep_add(self, ctx, key, server=None, searchstr=None, blue_role: Optional[discord.Role] = None, green_role: Optional[discord.Role] = None, channel: discord.TextChannel = None): - """Add a new autoeventping. Use `[p]aep set` with your chosen key to finish or update config. + """Add a new autoeventping. + Use `[p]aep set` with your chosen key to finish or update config. + `channel` defaults to the current channel. Usage: `[p]aep add diamondra` - `[p]aep add rubydra NA "ruby dragon"` - `[p]aep add ekmd NA "Extreme King Metal Dragon" @red @blue @green` - `[p]aep add monday JP "monday dungeon" @eventping` `[p]aep add pluspoints NA "star treasure" @red @blue @green #channel` `[p]aep add wallace KR "wallace" @eventping #channel` - `[p]aep add tama NA "tama" #channel` """ + if " " in key: await ctx.send("Multi-word keys are not allowed.") return + if server: + server = normalize_server_name(server) + if server not in SUPPORTED_SERVERS: + await ctx.send(f"Unsupported server `{server}`, pick one of NA, KR, JP") + return + + if blue_role is None: + blue_role = green_role = red_role + default = { 'roles': [None, None, None], - 'channels': [channel and channel.id] * 3, + 'channels': [(channel or ctx.channel).id] * 3, 'server': server or 'NA', 'searchstr': searchstr and rmdiacritics(searchstr), 'regex': False, @@ -362,24 +370,13 @@ async def aep_add(self, ctx, key, server=None, searchstr=None, } if red_role is not None: - if channel is None: - channel = ctx.channel - - if blue_role is None: - blue_role = green_role = red_role - - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send(f"Unsupported server `{server}`, pick one of NA, KR, JP") - return - default.update({ - 'roles': [red_role.id, blue_role.id, green_role.id], - 'channels': [channel.id] * 3, - }) + default['roles'] = [red_role.id, blue_role.id, green_role and green_role.id] async with self.config.guild(ctx.guild).pingroles() as pingroles: pingroles[key] = default - await ctx.tick() + + await ctx.send("New AEP created:") + await self.aep_show(ctx, key) @autoeventping.command(name="remove", aliases=['rm', 'delete']) async def aep_remove(self, ctx, key): From 8e1128367549b7ec9a7f5116ac8f281b15749f1d Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Mon, 5 Jul 2021 19:44:41 -0500 Subject: [PATCH 0878/1220] Add optional group parameter to ^events (#1280) --- padevents/padevents.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 80d912cd..94cc773e 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -852,26 +852,30 @@ def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=Fa return header + tbl.get_string() + "\n" @commands.command(aliases=['events']) - async def eventsna(self, ctx): + async def eventsna(self, ctx, group=None): """Display upcoming daily events for NA.""" - await self.do_partial(ctx, 'NA') + await self.do_partial(ctx, 'NA', group) @commands.command() - async def eventsjp(self, ctx): + async def eventsjp(self, ctx, group=None): """Display upcoming daily events for JP.""" - await self.do_partial(ctx, 'JP') + await self.do_partial(ctx, 'JP', group) @commands.command() - async def eventskr(self, ctx): + async def eventskr(self, ctx, group=None): """Display upcoming daily events for KR.""" - await self.do_partial(ctx, 'KR') + await self.do_partial(ctx, 'KR', group) - async def do_partial(self, ctx, server): + async def do_partial(self, ctx, server, group=None): server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return + if group is not None and group.lower() not in GROUPS: + await ctx.send("Unsupported group, pick one of red, blue, green") + return + events = EventList(self.events) events = events.with_server(server) events = events.in_dungeon_type([DungeonType.Etc, DungeonType.CoinDailyOther]) @@ -880,6 +884,10 @@ async def do_partial(self, ctx, server): active_events = events.active_only().items_by_open_time(reverse=True) pending_events = events.pending_only().items_by_open_time(reverse=True) + if group is not None: + active_events = [e for e in active_events if e.group == group.lower()] + pending_events = [e for e in pending_events if e.group == group.lower()] + group_to_active_event = {e.group: e for e in active_events} group_to_pending_event = {e.group: e for e in pending_events} From 0b90d2cd403f5b231331c069c177976a58c92321 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Wed, 7 Jul 2021 22:14:15 -0500 Subject: [PATCH 0879/1220] Add show subcommand to aed, other aep/aed improvements (#1290) --- padevents/padevents.py | 56 ++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 94cc773e..37719450 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -148,9 +148,9 @@ async def check_started(self): try: timestr = humanize_timedelta(timedelta=event.close_datetime-event.open_datetime) await channel.send("{} will be active for {}{}. {}".format(event.name_and_modifier, - offsetstr, - timestr, - ment), + timestr, + offsetstr, + ment), allowed_mentions=discord.AllowedMentions(roles=True)) except Exception: logger.exception("Failed to send AEP in channel {}".format(channel.id)) @@ -408,12 +408,12 @@ async def aep_show(self, ctx, key): in pingroles[key]['channels'] ] pr = (f"{key} ({'enabled' if pingroles[key]['enabled'] else 'disabled'})\n" - f"\tSearch String: '{pingroles[key]['searchstr']}' {'(regex search)' * pingroles[key]['regex']}\n" + f"\tSearch string: `{pingroles[key]['searchstr']}` {'(regex search)' * pingroles[key]['regex']}\n" f"\tServer: {pingroles[key]['server']}\n" f"\tRed: {roles[0]} (In {chans[0]})\n" f"\tBlue: {roles[1]} (In {chans[1]})\n" f"\tGreen: {roles[2]} (In {chans[2]})\n" - f"\tOffset: {pingroles[key]['offset']} minutes") + f"\tOffset: `{pingroles[key]['offset']} minutes`") await ctx.send(pr) @autoeventping.command(name="list") @@ -555,16 +555,26 @@ async def autoeventdm(self, ctx): """Auto Event DMs""" @autoeventdm.command(name="add") - async def aed_add(self, ctx, server, searchstr, group, time_offset: int = 0): + async def aed_add(self, ctx, server, searchstr, group=None, time_offset: int = 0): """Add a new autoeventdm""" server = normalize_server_name(server) if server not in SUPPORTED_SERVERS: await ctx.send("Unsupported server, pick one of NA, KR, JP") return - group = group.lower() - if group not in GROUPS: - await ctx.send("Unsupported group, pick one of red, blue, green") - return + + #work around for 2 optional parameters + if group is not None and group.isdigit(): + time_offset = int(group) + group = None + + if group is None: + group = "red" + else: + group = group.lower() + if group not in GROUPS: + await ctx.send("Unsupported group, pick one of red, blue, green") + return + if time_offset and not user_is_donor(ctx): await ctx.send("You must be a donor to set a time offset!") return @@ -580,9 +590,9 @@ async def aed_add(self, ctx, server, searchstr, group, time_offset: int = 0): 'offset': time_offset, } - async with self.config.user(ctx.author).dmevents() as dmevents: - dmevents.append(default) - await ctx.tick() + index = await self._do_aed_add(ctx, default) + await ctx.send("New AED created:") + await self.aed_show(ctx, index) @autoeventdm.command(name="remove", aliases=['rm', 'delete']) async def aed_remove(self, ctx, index: int): @@ -597,6 +607,20 @@ async def aed_remove(self, ctx, index: int): dmevents.pop(index - 1) await ctx.tick() + @autoeventdm.command(name="show") + async def aed_show(self, ctx, index: int): + """Show specifics of an autoeventdm""" + dmevents = await self.config.user(ctx.author).dmevents() + if not 0 < index <= len(dmevents): + await ctx.send("That isn't a valid index.") + return + ret = (f"Lookup number: `{index}`\n" + f"\tSearch string: `{dmevents[index - 1]['searchstr']}`\n" + f"\tServer: {dmevents[index - 1]['server']}\n" + f"\tGroup: {dmevents[index - 1]['group'].title()} \n" + f"\tOffset (Donor Only): `{dmevents[index - 1]['offset']} minutes`") + await ctx.send(ret) + @autoeventdm.command(name="list") async def aed_list(self, ctx): """List current autoeventdms""" @@ -621,6 +645,12 @@ async def aed_purge(self, ctx): @autoeventdm.group(name="edit") async def aed_e(self, ctx): """Edit a property of the autoeventdm""" + + async def _do_aed_add(self, ctx, item): + """Add autoeventdm and return its index""" + async with self.config.user(ctx.author).dmevents() as dmevents: + dmevents.append(item) + return len(dmevents) @is_donor() @aed_e.command(name="offset") From dde18127e34e4eb1ba46b3484a8e98850f81021c Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Jul 2021 02:57:32 -0700 Subject: [PATCH 0880/1220] Update for Tsutils 4.0 breaking changes (#1294) --- crud/crud.py | 6 +++--- dadguide/idtest_mixin.py | 22 ++++++++++----------- dadguide/info.json | 2 +- padevents/info.json | 2 +- padevents/padevents.py | 6 +++--- padglobal/info.json | 2 +- padglobal/padglobal.py | 36 +++++++++++++++++----------------- padinfo/info.json | 2 +- padinfo/menu/awakening_list.py | 4 ++-- padinfo/menu/transforminfo.py | 2 +- padinfo/padinfo.py | 6 +++--- 11 files changed, 45 insertions(+), 45 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index a9744ae5..e4a661ab 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -9,7 +9,7 @@ import pymysql from redbot.core import checks, commands, Config, errors from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import auth_check, confirm_message +from tsutils import auth_check, get_user_confirmation logger = logging.getLogger('red.padbot-cogs.crud') @@ -126,7 +126,7 @@ async def editmonsname(self, ctx, monster_id: int, *, name): await ctx.send("There is no monster with id: {}".format(monster_id)) return old_val = old_val[0]['name_en_override'] - if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + if not await get_user_confirmation(ctx, ("Are you sure you want to change monster #{}'s" " english override from `{}` to `{}`?" "").format(monster_id, old_val, name)): return @@ -147,7 +147,7 @@ async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): await ctx.send("There is no monster with id: {}".format(monster_id)) return old_val = old_val[0]['series_id'] - if not await confirm_message(ctx, ("Are you sure you want to change monster #{}'s" + if not await get_user_confirmation(ctx, ("Are you sure you want to change monster #{}'s" " series id from `{}` to `{}`?" "").format(monster_id, old_val, series_id)): return diff --git a/dadguide/idtest_mixin.py b/dadguide/idtest_mixin.py index fdd24544..c1a2027d 100644 --- a/dadguide/idtest_mixin.py +++ b/dadguide/idtest_mixin.py @@ -30,7 +30,7 @@ async def idt_add(self, ctx, mid: int, *, query): query, *reason = query.split("|") query = query.strip() if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to add to the id3 test suite?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to add to the id3 test suite?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -45,7 +45,7 @@ async def idt_add(self, ctx, mid: int, *, query): 'reason': reason[0].strip() if reason else '' } - if await tsutils.get_reaction(ctx, f"Added test case `{mid}: {query}`" + if await tsutils.get_user_reaction(ctx, f"Added test case `{mid}: {query}`" f" with ref `{sorted(suite).index(query)}`", "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): if oldd: @@ -81,7 +81,7 @@ async def idtf_add(self, ctx, mid: int, token, server: Optional[Server] = DEFAUL async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to add to the fluff/name test suite?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -94,7 +94,7 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): old = None if any(t['id'] == mid and t['token'] == token for t in suite): old = [t for t in suite if t['id'] == mid and t['token'] == token][0] - if not await tsutils.confirm_message(ctx, f"Are you sure you want to change" + if not await tsutils.get_user_confirmation(ctx, f"Are you sure you want to change" f" the type of test case #{suite.index(old)}" f" `{mid}: {token}` from " f" **{'fluff' if fluffy else 'name'}** to" @@ -115,7 +115,7 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): suite.append(case) suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) - if await tsutils.get_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " + if await tsutils.get_user_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " f"case `{mid}: {token}` with ref `{suite.index(case)}`", "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): suite.pop() @@ -132,7 +132,7 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): async def idt_import(self, ctx, *, queries): """Import id3 tests""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -156,7 +156,7 @@ async def idtf_import(self, ctx, *, queries): async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -178,7 +178,7 @@ async def norf_import(self, ctx, queries): async def idt_remove(self, ctx, *, item): """Remove an id3 test""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -209,7 +209,7 @@ async def idtf_remove(self, ctx, *, item: int): async def norf_remove(self, ctx, item): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -230,7 +230,7 @@ async def idt_setreason(self, ctx, number: int, *, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **query**?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -257,7 +257,7 @@ async def norf_setreason(self, ctx, number, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.confirm_message(ctx, "Are you sure you want to edit **name/fluff**?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') diff --git a/dadguide/info.json b/dadguide/info.json index 37a3e8e6..3cae4915 100644 --- a/dadguide/info.json +++ b/dadguide/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils>=3.3.3", + "tsutils>=4.0.0", "pytz", "romkan", "networkx" diff --git a/padevents/info.json b/padevents/info.json index 07057df8..b514c892 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -19,7 +19,7 @@ "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.0.0", + "tsutils>=4.0.0", "pytz", "prettytable" ], diff --git a/padevents/padevents.py b/padevents/padevents.py index 37719450..e8a8f7bf 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -15,7 +15,7 @@ from redbot.core import checks from redbot.core import commands, Config from redbot.core.utils.chat_formatting import box, pagify, humanize_timedelta -from tsutils import CogSettings, confirm_message, normalize_server_name, DummyObject, is_donor, rmdiacritics +from tsutils import CogSettings, get_user_confirmation, normalize_server_name, DummyObject, is_donor, rmdiacritics if TYPE_CHECKING: from dadguide.models.scheduled_event_model import ScheduledEventModel @@ -601,7 +601,7 @@ async def aed_remove(self, ctx, index: int): if not 0 < index <= len(dmevents): await ctx.send("That isn't a valid index.") return - if not await confirm_message(ctx, ("Are you sure you want to delete autoeventdm with searchstring '{}'" + if not await get_user_confirmation(ctx, ("Are you sure you want to delete autoeventdm with searchstring '{}'" "").format(dmevents[index - 1]['searchstr'])): return dmevents.pop(index - 1) @@ -637,7 +637,7 @@ async def aed_purge(self, ctx): if not await self.config.user(ctx.author).dmevents(): await ctx.send("You don't have any autoeventdms.") return - if not await confirm_message(ctx, "Are you sure you want to purge your autoeventdms?"): + if not await get_user_confirmation(ctx, "Are you sure you want to purge your autoeventdms?"): return await self.config.user(ctx.author).dmevents.set([]) await ctx.tick() diff --git a/padglobal/info.json b/padglobal/info.json index 5cd0a349..1814d4df 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -20,7 +20,7 @@ "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.0.2", + "tsutils>=4.0.0", "aiohttp", "prettytable", "pytz" diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index c6fba7a8..418315e5 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -15,8 +15,8 @@ from discord import Color from redbot.core import checks, data_manager, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta, bold -from tsutils import CogSettings, clean_global_mentions, confirm_message, replace_emoji_names_with_code, safe_read_json, \ - auth_check, get_reaction +from tsutils import CogSettings, clean_global_mentions, get_user_confirmation, replace_emoji_names_with_code, safe_read_json, \ + auth_check, get_user_reaction from padglobal.menu.closable_embed import ClosableEmbedMenu from padglobal.menu.menu_map import padglobal_menu_map @@ -237,7 +237,7 @@ async def _add(self, ctx, command, text, confirm=True): return if self.c_commands[text] in self.c_commands: source = self.c_commands[text] - if await confirm_message(ctx, '{} is already an alias for {}, and you can\'t alias to an alias.' + if await get_user_confirmation(ctx, '{} is already an alias for {}, and you can\'t alias to an alias.' .format(inline(text), inline(source)) + ' Would you like to alias to {} instead?'.format(inline(source))): # change target @@ -252,7 +252,7 @@ async def _add(self, ctx, command, text, confirm=True): ted = self.c_commands[ted] alias = True if confirm: - conf = await confirm_message(ctx, "Are you sure you want to edit the {}command {}?" + conf = await get_user_confirmation(ctx, "Are you sure you want to edit the {}command {}?" .format("alias to " if alias else "", ted)) if not conf: return @@ -273,7 +273,7 @@ async def delete(self, ctx, command: str): aliases = await self._find_aliases(command) if aliases: - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, 'Are you sure? `{}` has {} alias(es): `{}` which will also be deleted.' .format(command, bold(str(len(aliases))), '`, `'.join(aliases))): await ctx.send('Cancelling delete of `{}`.'.format(command)) @@ -553,7 +553,7 @@ async def _pglossary_add(self, ctx, term, definition, need_confirm=True): op = 'edited' if term in self.settings.glossary() else 'added' if op == 'edited' and need_confirm: - if not await confirm_message(ctx, "Are you sure you want to edit the glossary info for {}?".format(term)): + if not await get_user_confirmation(ctx, "Are you sure you want to edit the glossary info for {}?".format(term)): return self.settings.addGlossary(term, definition) await ctx.send("PAD glossary term successfully {}.".format(bold(op))) @@ -565,7 +565,7 @@ async def pglossary_remove(self, ctx, *, term): if term not in self.settings.glossary(): await ctx.send("Glossary item doesn't exist.") return - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, "Are you sure you want to globally remove the glossary data for {}?".format(term)): return self.settings.rmGlossary(term) @@ -661,7 +661,7 @@ async def _pboss_add(self, ctx, term, definition, need_confirm=True): op = 'edited' if base.monster_id in self.settings.boss() else 'added' if op == 'edited' and need_confirm: - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, "Are you sure you want to edit the boss info for {}?".format(base.name_en)): return definition = clean_global_mentions(definition) @@ -687,7 +687,7 @@ async def pboss_remove(self, ctx, *, term): if base.monster_id not in self.settings.boss(): await ctx.send("Boss mechanics item doesn't exist.") return - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, "Are you sure you want to globally remove the boss data for {}?".format( base.name_en)): return @@ -843,7 +843,7 @@ async def _pwhich_add(self, ctx, term, definition, need_confirm=True): op = 'edited' if name in self.settings.which() else 'added' if not is_int or op == 'edited' and need_confirm: - if not await confirm_message(ctx, "Are you sure you want to {} which info for {} [{}] {}?".format( + if not await get_user_confirmation(ctx, "Are you sure you want to {} which info for {} [{}] {}?".format( 'edit the' if op == 'edited' else 'add new', pdicog.get_attribute_emoji_by_monster(m), m.monster_no_na, @@ -865,7 +865,7 @@ async def pwhich_remove(self, ctx, *, monster_id: int): if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) - if not await confirm_message(ctx, "Are you sure you want to globally remove the which data for {}?".format( + if not await get_user_confirmation(ctx, "Are you sure you want to globally remove the which data for {}?".format( m.name_en)): return name = m.monster_id @@ -905,7 +905,7 @@ async def _concatenate_which(self, ctx, term: str, operation: str, addition): # ask for extra confirmation if the term was not an id if not re.fullmatch(r'\d+', term): - if not await confirm_message(ctx, 'Are you sure you want to {} to the which info for {} [{}] {}?'.format( + if not await get_user_confirmation(ctx, 'Are you sure you want to {} to the which info for {} [{}] {}?'.format( operation, pdicog.get_attribute_emoji_by_monster(m), m.monster_no_na, @@ -913,7 +913,7 @@ async def _concatenate_which(self, ctx, term: str, operation: str, addition): return if mon_id not in self.settings.which(): - if await confirm_message(ctx, "No which info exists for {}. Would you like to add a new entry?".format( + if await get_user_confirmation(ctx, "No which info exists for {}. Would you like to add a new entry?".format( m.name_en)): self.settings.addWhich(mon_id, addition) await ctx.send("PAD which info successfully {}.".format(bold('added'))) @@ -956,7 +956,7 @@ async def pwhich_list(self, ctx): cancel = '\N{CROSS MARK}' destination = channel if len(self.settings.which()) > MAX_WHICH_LIST_BEFORE_DM_PROMPT: - destination = await get_reaction(ctx, + destination = await get_user_reaction(ctx, 'This will send a lot of messages. Are you sure? ' + '(Yes / DM me instead / Cancel)', channel, @@ -1291,7 +1291,7 @@ async def _dungeon_add(self, ctx, term: str, definition: str, need_confirm=True) term = term.lower() op = 'edited' if term in self.settings.dungeonGuide() else 'added' if op == 'edited' and need_confirm: - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, "Are you sure you want to edit the dungeon guide info for {}?".format(term)): return self.settings.addDungeonGuide(term, definition) @@ -1304,7 +1304,7 @@ async def dungeon_remove(self, ctx, term: str): if term not in self.settings.dungeonGuide(): await ctx.send("DungeonGuide doesn't exist.") return - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, "Are you sure you want to globally remove the dungeonguide data for {}?".format( term)): return @@ -1335,7 +1335,7 @@ async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm= op = 'edited' if name in self.settings.leaderGuide() else 'added' if op == 'edited' and need_confirm: - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, "Are you sure you want to edit the leader guide for {}?".format(m.name_en)): return self.settings.addLeaderGuide(name, definition) @@ -1350,7 +1350,7 @@ async def leader_remove(self, ctx, monster_id: int): if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) - if not await confirm_message(ctx, + if not await get_user_confirmation(ctx, "Are you sure you want to globally remove the leaderguide data for {}?".format( m.name_en)): return diff --git a/padinfo/info.json b/padinfo/info.json index e75a0096..0e399b4b 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -20,7 +20,7 @@ "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=3.4.0", + "tsutils>=4.0.0", "prettytable", "python-levenshtein", "discord-menu>=0.16.0", diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index 95aeae2d..11ee1f13 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -73,7 +73,7 @@ async def respond_with_awakening_list(message: Optional[Message], ims, **data): def awakening_list_control(state: AwakeningListViewState): if state is None: return None - reaction_list = AwakeningListMenuPanes.get_reaction_list(state.sort_type) + reaction_list = AwakeningListMenuPanes.get_user_reaction_list(state.sort_type) return EmbedControl( [AwakeningListView.embed(state)], reaction_list @@ -103,7 +103,7 @@ class AwakeningListMenuPanes(MenuPanes): ] @classmethod - def get_reaction_list(cls, cur_sort_type): + def get_user_reaction_list(cls, cur_sort_type): other_toggle_reaction = None if cur_sort_type == AwakeningListSortTypes.alphabetical: other_toggle_reaction = AwakeningListEmoji.numerical diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 920cf1d7..ebffa6c2 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -114,7 +114,7 @@ class TransformInfoMenuPanes(MenuPanes): } @classmethod - def get_reaction_list(cls, number_of_monsters: int): + def get_user_reaction_list(cls, number_of_monsters: int): if number_of_monsters > 2: cls.HIDDEN_EMOJIS = TransformInfoEmoji.up else: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7972f332..be9f8e03 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -290,7 +290,7 @@ async def send_survey_after(self, ctx, query, result_monster): 'entry.1787446565': str(ctx.author)}) url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) - userres = await tsutils.confirm_message(ctx, "Was this the monster you were looking for?", + userres = await tsutils.get_user_confirmation(ctx, "Was this the monster you were looking for?", yemoji=char_to_emoji('y'), nemoji=char_to_emoji('n')) if userres is True: await self.config.good.set(await self.config.good() + 1) @@ -818,7 +818,7 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id acquire_raw = await TransformInfoViewState.do_query(dgcog, base_mon, transformed_mon) - reaction_list = TransformInfoMenuPanes.get_reaction_list(len(monster_ids)) + reaction_list = TransformInfoMenuPanes.get_user_reaction_list(len(monster_ids)) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) @@ -844,7 +844,7 @@ async def awakenings(self, ctx, *, query=None): menu = AwakeningListMenu.menu() state = AwakeningListViewState(ctx.message.author.id, AwakeningListMenu.MENU_TYPE, color, sort_type, paginated_skills, 0, - reaction_list=AwakeningListMenuPanes.get_reaction_list(sort_type)) + reaction_list=AwakeningListMenuPanes.get_user_reaction_list(sort_type)) await menu.create(ctx, state) return From 4b162286fd82e54c20ab1dec998a700573b7eaa0 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Jul 2021 03:07:55 -0700 Subject: [PATCH 0881/1220] Fix Cross-Cog imports in DungeonCog (#1295) --- dungeoncog/processors.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index 51b560f5..57858d35 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -1,13 +1,17 @@ +from typing import TYPE_CHECKING + from discordmenu.emoji.emoji_cache import emoji_cache -from dadguide.database_context import DbContext -from dadguide.models.encounter_model import EncounterModel from dadguide.models.enemy_skill_model import EnemySkillModel from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skill import ProcessedSkill from dungeoncog.enemy_skills_pb2 import Condition, MonsterBehavior, Behavior, BehaviorGroup from dungeoncog.grouped_skillls import GroupedSkills +if TYPE_CHECKING: + from dadguide.database_context import DbContext + from dadguide.models.encounter_model import EncounterModel + GROUP_TYPES = { 0: "Unspecified", 1: "Passive", @@ -94,14 +98,15 @@ def format_overview(query): def behavior_for_level(mb: MonsterBehavior, num: int): lev = None for level in mb.levels: - if level.level == num: lev = level + if level.level == num: + lev = level if lev is None: return mb.levels[len(mb.levels) - 1] else: return lev -def process_enemy_skill2(encounter: EncounterModel, skill: EnemySkillModel, emoji_map): +def process_enemy_skill2(encounter: "EncounterModel", skill: EnemySkillModel, emoji_map): effect = skill.desc_en_emoji split_effects = effect.split("), ") non_attack_effects = [] @@ -128,7 +133,7 @@ def process_enemy_skill2(encounter: EncounterModel, skill: EnemySkillModel, emoj return non_attack_effects -def process_behavior(behavior: Behavior, database, q: EncounterModel, emoji_map, parent: GroupedSkills = None): +def process_behavior(behavior: Behavior, database, q: "EncounterModel", emoji_map, parent: GroupedSkills = None): skill = database.dungeon.get_enemy_skill(behavior.enemy_skill_id) if skill is None: return "Unknown" @@ -144,7 +149,7 @@ def process_behavior(behavior: Behavior, database, q: EncounterModel, emoji_map, return processed_skill -def process_behavior_group(group: BehaviorGroup, database, q: EncounterModel, emoji_map, +def process_behavior_group(group: BehaviorGroup, database, q: "EncounterModel", emoji_map, parent: GroupedSkills = None): condition = format_condition(group.condition) processed_group: GroupedSkills = GroupedSkills(condition, GROUP_TYPES[group.group_type], parent) @@ -167,7 +172,7 @@ def __missing__(self, key): # Skill Name Type:Preemptive/Passive/Etc # Skill Effect # Condition -def process_monster(mb: MonsterBehavior, q: EncounterModel, database: DbContext): +def process_monster(mb: MonsterBehavior, q: "EncounterModel", database: "DbContext"): # output = "Behavior for: {} at Level: {}".format(q["name_en"], q["level"]) # TODO: Delete later after testing """embed = discord.Embed(title="Behavior for: {} at Level: {}".format(q["name_en"], q["level"]), description="HP:{} ATK:{} DEF:{} TURN:{}".format(q["hp"], q["atk"], q["defence"], q['turns']))""" From 414f7f8eee56c48ce355287207f6b7c413995138 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Sun, 11 Jul 2021 14:18:14 -0500 Subject: [PATCH 0882/1220] Improve add UX of channelmirror (#1286) --- channelmirror/channelmirror.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index fc856033..daaec9e4 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -9,7 +9,8 @@ import tsutils from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline, pagify, box -from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server +from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server, \ + get_user_confirmation, send_confirmation_message, send_cancellation_message logger = logging.getLogger('red.misc-cogs.channelmirror') @@ -60,8 +61,14 @@ async def add(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) return + conf = await get_user_confirmation(ctx, "Set up mirror of <#{}> to mirror to <#{}>?" + .format(source_channel_id, dest_channel_id)) + if not conf: + await send_cancellation_message(ctx, "Action cancelled. No action was taken.") + return self.settings.add_mirrored_channel(source_channel_id, dest_channel_id) - await ctx.tick() + await send_confirmation_message(ctx, "Okay, I set up a mirror of <#{}> to <#{}>" + .format(source_channel_id, dest_channel_id)) @channelmirror.command(aliases=['rmmirror', 'rm', 'delete']) @checks.is_owner() From f3139228c8fc2611e281fa1d85cecbaa126e5449 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 11 Jul 2021 16:41:33 -0700 Subject: [PATCH 0883/1220] Support fluff nickname tokens in gdoc (#1296) --- dadguide/monster_index.py | 122 +++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 56 deletions(-) diff --git a/dadguide/monster_index.py b/dadguide/monster_index.py index 890bafd7..d0d11e77 100644 --- a/dadguide/monster_index.py +++ b/dadguide/monster_index.py @@ -3,6 +3,7 @@ import io import logging import re +from typing import Dict, List import aiohttp import tsutils @@ -37,6 +38,7 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.monster_id_to_nickname = defaultdict(set) self.monster_id_to_nametokens = defaultdict(set) + self.monster_id_to_forcedfluff = defaultdict(set) self.monster_id_to_treename = defaultdict(set) self.series_id_to_pantheon_nickname = \ defaultdict(set, {m.series_id: {m.series.name_en.lower().replace(" ", "")} @@ -54,99 +56,110 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.remove_mods = defaultdict(set) self.treename_overrides = set() - nickname_data, treenames_data, pantheon_data, nt_alias_data, mod_data, treemod_data = await asyncio.gather( - sheet_to_reader(NICKNAME_OVERRIDES_SHEET, 5), - sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, 5), - sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, 2), - sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, 2), - sheet_to_reader(MODIFIER_OVERRIDE_SHEET, 3), - sheet_to_reader(TREE_MODIFIER_OVERRIDE_SHEET, 2), + treenames_data, nickname_data, pantheon_data, nt_alias_data, treemod_data, mod_data = await asyncio.gather( + sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, + ('base_id', 'new_treename', 'normal_prio', 'overrides')), + sheet_to_reader(NICKNAME_OVERRIDES_SHEET, + ('monster_id', 'name_en', 'normal_prio', 'overrides', 'fluff')), + sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, + ('series_id', 'alias')), + sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, + ('tokens', 'alias')), + sheet_to_reader(TREE_MODIFIER_OVERRIDE_SHEET, + ('base_id', 'modifiers')), + sheet_to_reader(MODIFIER_OVERRIDE_SHEET, + ('monster_id', 'modifiers', 'remove')), ) - for m_id, name, lp, ov, i in nickname_data: - if m_id.isdigit() and not i: + for data in treenames_data: + if data['base_id'].isdigit(): + mid = int(data['base_id']) + name = data['new_treename'].strip().lower() try: - monster = graph.get_monster(int(m_id), server=server) + monster = graph.get_monster(mid, server=server) except InvalidGraphState: continue if not monster: continue - name = name.strip().lower() - mid = int(m_id) - if lp: - self.monster_id_to_nametokens[mid].update(self._name_to_tokens(name)) - if ov: - self.treename_overrides.add(mid) + if data['overrides']: + for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): + self.treename_overrides.add(evomid) + if data['normal_prio']: + for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): + self.monster_id_to_nametokens[evomid].update(self._name_to_tokens(name)) else: if " " in name: self.mwtoken_creators[name.lower().replace(" ", "")].add(graph.get_monster(mid, server=server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.monster_id_to_nickname[mid].add(name.lower().replace(" ", "")) + self.monster_id_to_treename[mid].add(name.lower().replace(" ", "")) - for m_id, name, mp, ov, i in treenames_data: - if m_id.isdigit() and not i: + for data in nickname_data: + if data['monster_id'].isdigit(): + mid = int(data['monster_id']) + name = data['name_en'].strip().lower() try: - monster = graph.get_monster(int(m_id), server=server) + monster = graph.get_monster(mid, server=server) except InvalidGraphState: continue if not monster: continue - name = name.strip().lower() - mid = int(m_id) - if ov: - for emid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): - self.treename_overrides.add(emid) - if mp: - for emid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): - self.monster_id_to_nametokens[emid].update(self._name_to_tokens(name)) + if data['fluff']: + self.monster_id_to_forcedfluff[mid].update(self._name_to_tokens(name)) + if data['normal_prio'] or data['overrides']: + self.issues.append(f"Invalid sheet settings for nickname `{name}`.") + continue + if data['normal_prio']: + self.monster_id_to_nametokens[mid].update(self._name_to_tokens(name)) + if data['overrides']: + self.treename_overrides.add(mid) else: if " " in name: self.mwtoken_creators[name.lower().replace(" ", "")].add(graph.get_monster(mid, server=server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.monster_id_to_treename[mid].add(name.lower().replace(" ", "")) + self.monster_id_to_nickname[mid].add(name.lower().replace(" ", "")) - for sid, name in pantheon_data: - if sid.isdigit(): - name = name.strip().lower() + for data in pantheon_data: + if data['series_id'].isdigit(): + sid = int(data['series_id']) + name = data['alias'].strip().lower() if " " in name: self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.series_id_to_pantheon_nickname[int(sid)].add(name.lower().replace(" ", "")) + self.series_id_to_pantheon_nickname[sid].add(name.lower().replace(" ", "")) - next(nt_alias_data) # Skip over heading - for tokens, alias in nt_alias_data: - self.replacement_tokens[frozenset(re.split(r'[,\s]+', tokens))].add(alias) + for data in nt_alias_data: + self.replacement_tokens[frozenset(re.split(r'[,\s]+', data['tokens']))].add(data['alias']) self.manual_prefixes = defaultdict(set) - for mid, mods, rmv in mod_data: - if mid.isdigit(): + for data in mod_data: + if data['monster_id'].isdigit(): + mid = int(data['monster_id']) try: - monster = graph.get_monster(int(mid), server=server) + monster = graph.get_monster(mid, server=server) except InvalidGraphState: continue if not monster: continue - mid = int(mid) - for mod in mods.split(","): + for mod in data['modifiers'].split(","): mod = mod.strip().lower() if " " in mod: self.multi_word_tokens.add(tuple(mod.lower().split(" "))) mod = mod.lower().replace(" ", "") - if rmv: + if data['remove']: self.remove_mods[mid].update(mod) else: self.manual_prefixes[mid].update(get_modifier_aliases(mod)) - for mid, mods in treemod_data: - if mid.isdigit() and graph.get_monster(int(mid), server=server): - mid = int(mid) - for mod in mods.split(","): + for data in treemod_data: + if data['base_id'].isdigit() and graph.get_monster(int(data['base_id']), server=server): + mid = int(data['base_id']) + for mod in data['modifiers'].split(","): mod = mod.strip().lower() if " " in mod: self.multi_word_tokens.add(tuple(mod.split(" "))) mod = mod.replace(" ", "") aliases = get_modifier_aliases(mod) - for emid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): - self.manual_prefixes[emid].update(aliases) + for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): + self.manual_prefixes[evomid].update(aliases) self._known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() for x in xs}.union(KNOWN_MODIFIERS) @@ -248,7 +261,7 @@ async def _build_monster_index(self, monsters): self.add_name_token(self.name_tokens, token2, m) # Fluff tokens - for token in nametokens: + for token in nametokens + list(self.monster_id_to_forcedfluff[m.monster_id]): if m in self.name_tokens[token.lower()]: continue self.add_name_token(self.fluff_tokens, token, m) @@ -486,15 +499,12 @@ def add_numbered_modifier(self, monster, curr_mods, added_mods, condition, *, el curr_mods.update(else_mods) -# TODO: Move this to TSUtils -async def sheet_to_reader(url, length=None): +async def sheet_to_reader(url, headers) -> List[Dict[str, str]]: async with aiohttp.ClientSession() as session: async with session.get(url) as response: - file = io.StringIO(await response.text()) - if length is None: - return csv.reader(file, delimiter=',') - else: - return ((line + [None] * length)[:length] for line in csv.reader(file, delimiter=',')) + reader = csv.reader(io.StringIO(await response.text()), delimiter=',') + next(reader) + return [dict(zip(headers, line[:len(headers)])) for line in reader] def copydict(token_dict): From 40bd3b07d112ec73fe018d21dc5c769f70c46abb Mon Sep 17 00:00:00 2001 From: Hans Xu Date: Mon, 12 Jul 2021 01:04:27 -0500 Subject: [PATCH 0884/1220] dungeonid fixes - arena names & search (#1297) --- dadguide/dungeon_context.py | 8 +++++--- dungeon/dungeoncog.py | 2 +- dungeoncog/dungeoncog.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dadguide/dungeon_context.py b/dadguide/dungeon_context.py index 94fe4c7e..57268de7 100644 --- a/dadguide/dungeon_context.py +++ b/dadguide/dungeon_context.py @@ -163,15 +163,15 @@ # TODO: Move to gdoc DUNGEON_NICKNAMES = { 'a1': 1022001, - 'arena1': 102201, + 'arena1': 1022001, 'bipolar goddess 1': 1022001, 'bp1': 1022001, 'a2': 1022002, - 'arena2': 102202, + 'arena2': 1022002, 'bipolar goddess 2': 1022002, 'bp2': 1022002, 'a3': 1022003, - 'arena3': 102203, + 'arena3': 1022003, 'bipolar goddess 3': 1022003, 'bp3': 1022003, 'a4': 1022004, @@ -260,6 +260,8 @@ def get_enemy_skill(self, enemy_skill_id: int) -> EnemySkillModel: return EnemySkillModel(**enemy_skill_query) def get_sub_dungeon_id_from_name(self, dungeon_id: int, sub_name: str) -> Optional[int]: + if sub_name is None: + sub_name = "" if sub_name.isdigit(): sub_dungeons = self.database.query_many(SUB_DUNGEON_QUERY_BY_INDEX, (dungeon_id * 1000 + int(sub_name),)) else: diff --git a/dungeon/dungeoncog.py b/dungeon/dungeoncog.py index 3f4a0cd8..8818688f 100644 --- a/dungeon/dungeoncog.py +++ b/dungeon/dungeoncog.py @@ -67,7 +67,7 @@ async def find_dungeon_from_name2(self, ctx, name: str, database: "DungeonContex Gets the sub_dungeon model given the name of a dungeon and its difficulty. """ dungeon = database.get_dungeons_from_nickname(name.lower()) - if dungeon is None: + if len(dungeon) == 0: dungeons = database.get_dungeons_from_name(name) if len(dungeons) == 0: await ctx.send("No dungeons found!") diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index e7eaa81a..6c3a53d3 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -92,7 +92,7 @@ async def find_dungeon_from_name2(self, ctx, name, database: "DungeonContext", d return dungeon @commands.command(aliases=['dgid']) - async def dungeonid(self, ctx, name, difficulty = None, *bad: str): + async def dungeonid(self, ctx, name, difficulty=None, *bad: str): """ Name: Name of Dungeon Difficulty: Difficulty level/name of floor (eg. for A1, "Bipolar Goddess") From e164032d2db399f1d4848675835c9086f7832449 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 12 Jul 2021 00:05:25 -0700 Subject: [PATCH 0885/1220] Remove cross-cog import from dungeoncog (#1299) --- dungeoncog/processors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index 57858d35..03ac4a36 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -2,13 +2,13 @@ from discordmenu.emoji.emoji_cache import emoji_cache -from dadguide.models.enemy_skill_model import EnemySkillModel from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skill import ProcessedSkill from dungeoncog.enemy_skills_pb2 import Condition, MonsterBehavior, Behavior, BehaviorGroup from dungeoncog.grouped_skillls import GroupedSkills if TYPE_CHECKING: + from dadguide.models.enemy_skill_model import EnemySkillModel from dadguide.database_context import DbContext from dadguide.models.encounter_model import EncounterModel @@ -106,7 +106,7 @@ def behavior_for_level(mb: MonsterBehavior, num: int): return lev -def process_enemy_skill2(encounter: "EncounterModel", skill: EnemySkillModel, emoji_map): +def process_enemy_skill2(encounter: "EncounterModel", skill: "EnemySkillModel", emoji_map): effect = skill.desc_en_emoji split_effects = effect.split("), ") non_attack_effects = [] From 9bc31e75c3626157d2b9d61fd12edc99898bfe02 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 12 Jul 2021 02:55:26 -0700 Subject: [PATCH 0886/1220] Allow channelmirror to go up to 4k characters (#1300) --- channelmirror/channelmirror.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index daaec9e4..4447cfe8 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -116,7 +116,7 @@ async def config(self, ctx, server_id: int = None): continue channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - multiedit = await self.config.channel(discord.Object(id=mc_id)).multiedit() + multiedit = await self.config.channel_from_id(mc_id).multiedit() msg += '\n\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) for channel_id in config['channels']: if server_id is not None and mc_id not in gchs and not channel_id not in gchs: @@ -146,7 +146,7 @@ async def guildconfig(self, ctx, server_id: int): continue channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - multiedit = await self.config.channel(discord.Object(id=mc_id)).multiedit() + multiedit = await self.config.channel_from_id(mc_id).multiedit() msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) for channel_id in config['channels']: channel = self.bot.get_channel(channel_id) @@ -161,7 +161,7 @@ async def guildconfig(self, ctx, server_id: int): continue channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - multiedit = await self.config.channel(discord.Object(id=mc_id)).multiedit() + multiedit = await self.config.channel_from_id(mc_id).multiedit() msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) for channel_id in config['channels']: if channel_id not in gchs: @@ -302,7 +302,7 @@ async def mirror_msg(self, message): small = False if attribution_required: msg = self.makeheader(message, author) - if len(fmessage) > 500: + if len(fmessage) > 1000: await dest_channel.send(msg) else: fmessage = msg + '\n' + fmessage @@ -416,8 +416,8 @@ async def mirror_msg_mod(self, message, fcontent = await self.mformat(new_message_content, channel, dest_message.channel) if await self.config.custom("dest_message", dest_message.id).small(): fcontent = self.makeheader(message) + '\n' + fcontent - if len(fcontent) > 2000: - fcontent = fcontent[:1971] + "... *(Continued in original)*" + if len(fcontent) > 4000: + fcontent = fcontent[:3971] + "... *(Continued in original)*" await dest_message.edit(content=fcontent) elif new_message_reaction: try: From 2c0c7bed79d1216ebda64ba194b0d33df57429bd Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Mon, 12 Jul 2021 08:17:15 -0500 Subject: [PATCH 0887/1220] Improve aep show display with status emoji (#1301) --- padevents/padevents.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index e8a8f7bf..d60324df 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -15,7 +15,8 @@ from redbot.core import checks from redbot.core import commands, Config from redbot.core.utils.chat_formatting import box, pagify, humanize_timedelta -from tsutils import CogSettings, get_user_confirmation, normalize_server_name, DummyObject, is_donor, rmdiacritics +from tsutils import CogSettings, get_user_confirmation, normalize_server_name, DummyObject, is_donor, rmdiacritics, \ + YES_EMOJI, NO_EMOJI if TYPE_CHECKING: from dadguide.models.scheduled_event_model import ScheduledEventModel @@ -407,7 +408,8 @@ async def aep_show(self, ctx, key): for c in pingroles[key]['channels'] ] - pr = (f"{key} ({'enabled' if pingroles[key]['enabled'] else 'disabled'})\n" + pr = (f"Key: `{key}`\n" + f"\tStatus: {YES_EMOJI + ' enabled' if pingroles[key]['enabled'] else NO_EMOJI + ' disabled'}\n" f"\tSearch string: `{pingroles[key]['searchstr']}` {'(regex search)' * pingroles[key]['regex']}\n" f"\tServer: {pingroles[key]['server']}\n" f"\tRed: {roles[0]} (In {chans[0]})\n" From 1108e14b0882bd19919659271eaf6b36233abfb6 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Tue, 13 Jul 2021 08:52:25 -0500 Subject: [PATCH 0888/1220] Add toggle3p option to AED (#1316) --- padevents/padevents.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index d60324df..39909c42 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -16,7 +16,7 @@ from redbot.core import commands, Config from redbot.core.utils.chat_formatting import box, pagify, humanize_timedelta from tsutils import CogSettings, get_user_confirmation, normalize_server_name, DummyObject, is_donor, rmdiacritics, \ - YES_EMOJI, NO_EMOJI + YES_EMOJI, NO_EMOJI, send_confirmation_message, send_cancellation_message if TYPE_CHECKING: from dadguide.models.scheduled_event_model import ScheduledEventModel @@ -166,6 +166,8 @@ async def check_started(self): or event.server != aed['server'] \ or (aed['key'], event.key) in self.rolepinged_events: continue + if not aed['include3p'] and event.clean_dungeon_name.startswith("Multiplayer"): + continue self.rolepinged_events.add((aed['key'], event.key)) if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): offsetstr = "now" @@ -589,6 +591,7 @@ async def aed_add(self, ctx, server, searchstr, group=None, time_offset: int = 0 'server': server, 'group': group, 'searchstr': searchstr, + 'include3p': True, 'offset': time_offset, } @@ -601,7 +604,7 @@ async def aed_remove(self, ctx, index: int): """Remove an autoeventdm""" async with self.config.user(ctx.author).dmevents() as dmevents: if not 0 < index <= len(dmevents): - await ctx.send("That isn't a valid index.") + await send_cancellation_message(ctx, "That isn't a valid index.") return if not await get_user_confirmation(ctx, ("Are you sure you want to delete autoeventdm with searchstring '{}'" "").format(dmevents[index - 1]['searchstr'])): @@ -614,10 +617,11 @@ async def aed_show(self, ctx, index: int): """Show specifics of an autoeventdm""" dmevents = await self.config.user(ctx.author).dmevents() if not 0 < index <= len(dmevents): - await ctx.send("That isn't a valid index.") + await send_cancellation_message(ctx, "That isn't a valid index.") return ret = (f"Lookup number: `{index}`\n" f"\tSearch string: `{dmevents[index - 1]['searchstr']}`\n" + f"\t3P: {'included' if dmevents[index - 1]['include3p'] else 'excluded'}\n" f"\tServer: {dmevents[index - 1]['server']}\n" f"\tGroup: {dmevents[index - 1]['group'].title()} \n" f"\tOffset (Donor Only): `{dmevents[index - 1]['offset']} minutes`") @@ -644,7 +648,7 @@ async def aed_purge(self, ctx): await self.config.user(ctx.author).dmevents.set([]) await ctx.tick() - @autoeventdm.group(name="edit") + @autoeventdm.group(name="edit", aliases=["set"]) async def aed_e(self, ctx): """Edit a property of the autoeventdm""" @@ -656,29 +660,47 @@ async def _do_aed_add(self, ctx, item): @is_donor() @aed_e.command(name="offset") - async def aed_e_offset(self, ctx, index, offset): + async def aed_e_offset(self, ctx, index: int, offset: int): """(DONOR ONLY) Set time offset to an AED to allow you to prepare for a dungeon""" if offset < 0: - await ctx.send("Offset cannot be negative") + await send_cancellation_message(ctx, "Offset cannot be negative") return async with self.config.user(ctx.author).dmevents() as dmevents: if not 0 < index <= len(dmevents): - await ctx.send("That isn't a valid index.") + await send_cancellation_message(ctx, "That isn't a valid index.") return dmevents[index - 1]['offset'] = offset await ctx.tick() @aed_e.command(name="searchstr") - async def aed_e_searchstr(self, ctx, index, *, searchstr): + async def aed_e_searchstr(self, ctx, index: int, *, searchstr): """Set search string of an autoeventdm""" searchstr = searchstr.strip('"') async with self.config.user(ctx.author).dmevents() as dmevents: if not 0 < index <= len(dmevents): - await ctx.send("That isn't a valid index.") + await send_cancellation_message(ctx, "That isn't a valid index.") return dmevents[index - 1]['searchstr'] = searchstr await ctx.tick() + @aed_e.command(name="toggle3p") + async def aed_e_toggle3p(self, ctx, index: int): + """Include/exclude 3-player dungeons in an autoeventdm""" + async with self.config.user(ctx.author).dmevents() as dmevents: + if not 0 < index <= len(dmevents): + await send_cancellation_message(ctx, "That isn't a valid index.") + return + event = dmevents[index - 1]['searchstr'] + if dmevents[index - 1]['include3p']: + dmevents[index - 1]['include3p'] = False + await send_confirmation_message(ctx, "I will **exclude** 3P dungeons for {}. `{}`" + .format(index, event)) + return + dmevents[index - 1]['include3p'] = True + await send_confirmation_message(ctx, "I will **include** 3P dungeons for {}. `{}`" + .format(index, event)) + return + @padevents.command() @checks.is_owner() async def listallchannels(self, ctx): From b72a29e3622214efd7c9bf95e45f41f35df12ff8 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Tue, 13 Jul 2021 09:44:56 -0500 Subject: [PATCH 0889/1220] Support legacy AEDs for include3p (#1318) --- padevents/padevents.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/padevents/padevents.py b/padevents/padevents.py index 39909c42..3832c195 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -166,6 +166,9 @@ async def check_started(self): or event.server != aed['server'] \ or (aed['key'], event.key) in self.rolepinged_events: continue + if aed.get('include3p') is None: + # case of legacy configs + aed['include3p'] = True if not aed['include3p'] and event.clean_dungeon_name.startswith("Multiplayer"): continue self.rolepinged_events.add((aed['key'], event.key)) @@ -619,6 +622,9 @@ async def aed_show(self, ctx, index: int): if not 0 < index <= len(dmevents): await send_cancellation_message(ctx, "That isn't a valid index.") return + if dmevents.get('include3p') is None: + # case of legacy configs + dmevents[index - 1]['include3p'] = True ret = (f"Lookup number: `{index}`\n" f"\tSearch string: `{dmevents[index - 1]['searchstr']}`\n" f"\t3P: {'included' if dmevents[index - 1]['include3p'] else 'excluded'}\n" @@ -691,6 +697,9 @@ async def aed_e_toggle3p(self, ctx, index: int): await send_cancellation_message(ctx, "That isn't a valid index.") return event = dmevents[index - 1]['searchstr'] + if dmevents.get('include3p') is None: + # case of legacy configs + dmevents[index - 1]['include3p'] = True if dmevents[index - 1]['include3p']: dmevents[index - 1]['include3p'] = False await send_confirmation_message(ctx, "I will **exclude** 3P dungeons for {}. `{}`" From bd203c73821244b7f7f43c1a4956fa00b3db59fc Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:03:30 -0500 Subject: [PATCH 0890/1220] Actually support legacy AEDs for include 3p (#1319) --- padevents/padevents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 3832c195..4930b9bf 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -622,7 +622,7 @@ async def aed_show(self, ctx, index: int): if not 0 < index <= len(dmevents): await send_cancellation_message(ctx, "That isn't a valid index.") return - if dmevents.get('include3p') is None: + if dmevents[index - 1].get('include3p') is None: # case of legacy configs dmevents[index - 1]['include3p'] = True ret = (f"Lookup number: `{index}`\n" @@ -697,7 +697,7 @@ async def aed_e_toggle3p(self, ctx, index: int): await send_cancellation_message(ctx, "That isn't a valid index.") return event = dmevents[index - 1]['searchstr'] - if dmevents.get('include3p') is None: + if dmevents[index - 1].get('include3p') is None: # case of legacy configs dmevents[index - 1]['include3p'] = True if dmevents[index - 1]['include3p']: From 6222d61b90f4336a4ef5c5a56d3d8911c2e51126 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Fri, 16 Jul 2021 22:33:57 -0500 Subject: [PATCH 0891/1220] Support string inputs for AED patterns (#1322) --- padevents/padevents.py | 104 ++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 4930b9bf..65490108 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -603,34 +603,34 @@ async def aed_add(self, ctx, server, searchstr, group=None, time_offset: int = 0 await self.aed_show(ctx, index) @autoeventdm.command(name="remove", aliases=['rm', 'delete']) - async def aed_remove(self, ctx, index: int): + async def aed_remove(self, ctx, index): """Remove an autoeventdm""" + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return async with self.config.user(ctx.author).dmevents() as dmevents: - if not 0 < index <= len(dmevents): - await send_cancellation_message(ctx, "That isn't a valid index.") - return - if not await get_user_confirmation(ctx, ("Are you sure you want to delete autoeventdm with searchstring '{}'" - "").format(dmevents[index - 1]['searchstr'])): + if not await get_user_confirmation(ctx, "Are you sure you want to delete autoeventdm {}. `{}`?" + .format(corrected_index + 1, dmevents[corrected_index]['searchstr'])): return - dmevents.pop(index - 1) + dmevents.pop(corrected_index) await ctx.tick() @autoeventdm.command(name="show") - async def aed_show(self, ctx, index: int): + async def aed_show(self, ctx, index): """Show specifics of an autoeventdm""" dmevents = await self.config.user(ctx.author).dmevents() - if not 0 < index <= len(dmevents): - await send_cancellation_message(ctx, "That isn't a valid index.") + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: return - if dmevents[index - 1].get('include3p') is None: + if dmevents[corrected_index].get('include3p') is None: # case of legacy configs - dmevents[index - 1]['include3p'] = True - ret = (f"Lookup number: `{index}`\n" - f"\tSearch string: `{dmevents[index - 1]['searchstr']}`\n" - f"\t3P: {'included' if dmevents[index - 1]['include3p'] else 'excluded'}\n" - f"\tServer: {dmevents[index - 1]['server']}\n" - f"\tGroup: {dmevents[index - 1]['group'].title()} \n" - f"\tOffset (Donor Only): `{dmevents[index - 1]['offset']} minutes`") + dmevents[corrected_index]['include3p'] = True + ret = (f"Lookup number: `{corrected_index + 1}`\n" + f"\tSearch string: `{dmevents[corrected_index]['searchstr']}`\n" + f"\t3P: {'included' if dmevents[corrected_index]['include3p'] else 'excluded'}\n" + f"\tServer: {dmevents[corrected_index]['server']}\n" + f"\tGroup: {dmevents[corrected_index]['group'].title()} \n" + f"\tOffset (Donor Only): `{dmevents[corrected_index]['offset']} minutes`") await ctx.send(ret) @autoeventdm.command(name="list") @@ -666,50 +666,78 @@ async def _do_aed_add(self, ctx, item): @is_donor() @aed_e.command(name="offset") - async def aed_e_offset(self, ctx, index: int, offset: int): + async def aed_e_offset(self, ctx, index, offset: int): """(DONOR ONLY) Set time offset to an AED to allow you to prepare for a dungeon""" if offset < 0: await send_cancellation_message(ctx, "Offset cannot be negative") return + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return async with self.config.user(ctx.author).dmevents() as dmevents: - if not 0 < index <= len(dmevents): - await send_cancellation_message(ctx, "That isn't a valid index.") + if not await get_user_confirmation(ctx, "Modify the offset for {}. `{}` to {} minutes?" + .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], offset)): return - dmevents[index - 1]['offset'] = offset + dmevents[corrected_index]['offset'] = offset await ctx.tick() @aed_e.command(name="searchstr") - async def aed_e_searchstr(self, ctx, index: int, *, searchstr): + async def aed_e_searchstr(self, ctx, index, *, searchstr): """Set search string of an autoeventdm""" searchstr = searchstr.strip('"') + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return async with self.config.user(ctx.author).dmevents() as dmevents: - if not 0 < index <= len(dmevents): - await send_cancellation_message(ctx, "That isn't a valid index.") + if not await get_user_confirmation(ctx, "Modify the search string for {}. `{}` to `{}`?" + .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], searchstr)): return - dmevents[index - 1]['searchstr'] = searchstr + dmevents[corrected_index]['searchstr'] = searchstr await ctx.tick() @aed_e.command(name="toggle3p") - async def aed_e_toggle3p(self, ctx, index: int): + async def aed_e_toggle3p(self, ctx, index): """Include/exclude 3-player dungeons in an autoeventdm""" + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return async with self.config.user(ctx.author).dmevents() as dmevents: - if not 0 < index <= len(dmevents): - await send_cancellation_message(ctx, "That isn't a valid index.") - return - event = dmevents[index - 1]['searchstr'] - if dmevents[index - 1].get('include3p') is None: + event = dmevents[corrected_index]['searchstr'] + if dmevents[corrected_index].get('include3p') is None: # case of legacy configs - dmevents[index - 1]['include3p'] = True - if dmevents[index - 1]['include3p']: - dmevents[index - 1]['include3p'] = False + dmevents[corrected_index]['include3p'] = True + if dmevents[corrected_index]['include3p']: + dmevents[corrected_index]['include3p'] = False await send_confirmation_message(ctx, "I will **exclude** 3P dungeons for {}. `{}`" - .format(index, event)) + .format(corrected_index + 1, event)) return - dmevents[index - 1]['include3p'] = True + dmevents[corrected_index]['include3p'] = True await send_confirmation_message(ctx, "I will **include** 3P dungeons for {}. `{}`" - .format(index, event)) + .format(corrected_index + 1, event)) return + async def _get_and_validate_aed_index(self, ctx, user_index): + """Returns the corrected index for the autoeventdm of interest, based on Python list indexing""" + async with self.config.user(ctx.author).dmevents() as dmevents: + if isinstance(user_index, int) or user_index.isdigit(): + user_index = int(user_index) + if not 0 < user_index <= len(dmevents): + await send_cancellation_message(ctx, "That isn't a valid numerical index.") + return None + else: + str_index = None + for i in range(1, len(dmevents) + 1, 1): + if dmevents[i - 1]['searchstr'] == user_index: + str_index = i + break + user_index = str_index + if user_index is None: + await send_cancellation_message(ctx, "That string did not match any existing patterns. " + "Printing all of your Auto Event DMs (you can also see this with `^aed list`):") + await self.aed_list(ctx) + return None + return user_index - 1 + @padevents.command() @checks.is_owner() async def listallchannels(self, ctx): From 19f0dd8d15b252b192d88e9153a90d89c371782c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 16 Jul 2021 23:53:18 -0500 Subject: [PATCH 0892/1220] Use custom emojis for +0 & 1x ls (#1323) --- padinfo/view/common.py | 2 +- padinfo/view/id.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/view/common.py b/padinfo/view/common.py index faad331e..34a51944 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -47,7 +47,7 @@ def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform return Box( BoldText('Leader Skill'), BoldText(ls_multiplier_text(m.leader_skill) if lsmultiplier == LsMultiplier.lsdouble - else char_to_emoji(1) + ' ' + ls_single_multiplier_text(m.leader_skill)), + else get_emoji('1x') + ' ' + ls_single_multiplier_text(m.leader_skill)), BoldText('(' + get_emoji( '\N{DOWN-POINTING RED TRIANGLE}') + '7x6)') if m != transform_base and transform_base.leader_skill.is_7x6 else None, delimiter=' ' diff --git a/padinfo/view/id.py b/padinfo/view/id.py index c9d11518..c268c07b 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -269,7 +269,7 @@ def stats(m: "MonsterModel", cardplus: CardPlusModifier): def stats_header(m: "MonsterModel", cardplus: CardPlusModifier): voice_emoji = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' # TODO: Get a +0 emoji for the +0 setting - plus_297_emoji = get_emoji('plus_297') if cardplus == CardPlusModifier.plus297 else '' + plus_297_emoji = get_emoji('plus_297') if cardplus == CardPlusModifier.plus297 else get_emoji('plus_0') header = Box( Text(voice_emoji), Text(plus_297_emoji), From 02dd1180afbec5a069aff7b65655bd1767870cfe Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Sun, 18 Jul 2021 22:08:40 -0500 Subject: [PATCH 0893/1220] Support removal of channels for AEP (#1324) --- padevents/padevents.py | 46 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 65490108..1349eeee 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -453,6 +453,22 @@ async def aepget(self, ctx, key): return return pingroles[key] + async def aep_remove_channel(self, ctx, key, group, f): + if key not in await self.config.guild(ctx.guild).pingroles(): + await ctx.send("That key does not exist.") + return + + if not await get_user_confirmation(ctx, "Are you sure you want to remove the {} channel for AEP `{}`?" + .format(group, key)): + await send_cancellation_message(ctx, "No action was taken. You can set a channel with `{}aep set {}channel {} #channel_name`" + .format(ctx.prefix, group, key)) + return + else: + await self.aepchange(ctx, key, 'channels', f) + await send_confirmation_message(ctx, "Okay, I removed the {} channel for the AEP `{}`" + .format(group, key)) + return + @aep_set.command(name="channel") async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): """Sets channel to ping for all groups""" @@ -460,22 +476,31 @@ async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): await ctx.tick() @aep_set.command(name="redchannel") - async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel): + async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel=None): """Sets channel to ping when event is red""" - await self.aepchange(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) - await ctx.tick() + if channel is None: + await self.aep_remove_channel(ctx, key, "red", lambda x: [None, x[1], x[2]]) + else: + await self.aepchange(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) + await ctx.tick() @aep_set.command(name="bluechannel") - async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel): + async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel=None): """Sets channel to ping when event is blue""" - await self.aepchange(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) - await ctx.tick() + if channel is None: + await self.aep_remove_channel(ctx, key, "blue", lambda x: [x[0], None, x[2]]) + else: + await self.aepchange(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) + await ctx.tick() @aep_set.command(name="greenchannel") - async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel): + async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel=None): """Sets channel to ping when event is green""" - await self.aepchange(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) - await ctx.tick() + if channel is None: + await self.aep_remove_channel(ctx, key, "green", lambda x: [x[0], x[1], None]) + else: + await self.aepchange(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) + await ctx.tick() @aep_set.command(name="roles") async def aep_s_roles(self, ctx, key, red: discord.Role, blue: discord.Role, green: discord.Role): @@ -733,7 +758,8 @@ async def _get_and_validate_aed_index(self, ctx, user_index): user_index = str_index if user_index is None: await send_cancellation_message(ctx, "That string did not match any existing patterns. " - "Printing all of your Auto Event DMs (you can also see this with `^aed list`):") + "Printing all of your Auto Event DMs (you can also see this with `{}aed list`):" + .format(ctx.prefix)) await self.aed_list(ctx) return None return user_index - 1 From 2f6fb5466f5ebc19c1e988c27519b206c54c5f0f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Jul 2021 11:33:26 -0700 Subject: [PATCH 0894/1220] Properly escape emoji in channelmirror (#1330) Closes #1325 --- channelmirror/channelmirror.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index 4447cfe8..d93a4d05 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -487,7 +487,7 @@ async def mformat(self, text, from_channel, dest_channel): text = re.sub(r"@everyone\b", "@\u200beveryone", text) text = re.sub(r"@here\b", "@\u200bhere", text) # EMOJI - # text = self.emojify(text) + text = self.emojify(text) return text def emojify(self, message): From 2c888b896f366e3d40645919c30ef5fc67a79e82 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Jul 2021 11:33:57 -0700 Subject: [PATCH 0895/1220] Bring padinfo to tsutils 4.0 (#1329) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index be9f8e03..ed408d60 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -291,7 +291,7 @@ async def send_survey_after(self, ctx, query, result_monster): url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) userres = await tsutils.get_user_confirmation(ctx, "Was this the monster you were looking for?", - yemoji=char_to_emoji('y'), nemoji=char_to_emoji('n')) + yes_emoji=char_to_emoji('y'), no_emoji=char_to_emoji('n')) if userres is True: await self.config.good.set(await self.config.good() + 1) elif userres is False: From 4d9d05b1108d1941f445644f294e662d8a50c592 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Jul 2021 11:34:11 -0700 Subject: [PATCH 0896/1220] Fix emoji in padglobal commands (#1328) --- padglobal/padglobal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 418315e5..b17193b6 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -1171,7 +1171,7 @@ def format_cc(self, command, message): for result in results: param = self.transform_parameter(result, message) command = command.replace("{" + result + "}", param) - return command + return self.emojify(command) def transform_parameter(self, result, message): """ From 6f6cfb7549ffe0f3d03175fc09cf5fd514859bb4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 22 Jul 2021 13:35:50 -0500 Subject: [PATCH 0897/1220] Stop using emoji cache early for menu reactions (#1327) --- padinfo/menu/id.py | 19 ++++++------------- padinfo/menu/leader_skill.py | 7 +++---- padinfo/menu/leader_skill_single.py | 7 +++---- padinfo/menu/na_diff.py | 7 ++----- padinfo/padinfo.py | 12 ++++++------ padinfo/reaction_list.py | 4 +--- 6 files changed, 21 insertions(+), 35 deletions(-) diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 5f1a56d1..d126f12b 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -2,7 +2,6 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji from tsutils.enums import ChildMenuType from tsutils.menu.panes import MenuPanes @@ -132,60 +131,54 @@ async def respond_with_otherinfo(message: Optional[Message], ims, **data): def id_control(state: IdViewState): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [IdView.embed(state)], - reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + state.reaction_list or IdMenuPanes.emoji_names() ) @staticmethod def evos_control(state: Optional[EvosViewState]): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [EvosView.embed(state)], - reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + state.reaction_list or IdMenuPanes.emoji_names() ) @staticmethod def mats_control(state: Optional[MaterialsViewState]): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [MaterialsView.embed(state)], - reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + state.reaction_list or IdMenuPanes.emoji_names() ) @staticmethod def pic_control(state: PicViewState): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [PicView.embed(state)], - reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + state.reaction_list or IdMenuPanes.emoji_names() ) @staticmethod def pantheon_control(state: Optional[PantheonViewState]): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [PantheonView.embed(state)], - reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + state.reaction_list or IdMenuPanes.emoji_names() ) @staticmethod def otherinfo_control(state: OtherInfoViewState): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [OtherInfoView.embed(state)], - reaction_list or [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + state.reaction_list or IdMenuPanes.emoji_names() ) diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 5bf5c1d5..7e9863ed 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -2,10 +2,9 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from discordmenu.emoji.emoji_cache import emoji_cache from tsutils import char_to_emoji - from tsutils.menu.panes import MenuPanes, emoji_buttons + from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillViewState @@ -55,14 +54,14 @@ async def respond_with_house(message: Optional[Message], ims, **data): def ls_control(state: LeaderSkillViewState): return EmbedControl( [LeaderSkillView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillMenuPanes.emoji_names()] + LeaderSkillMenuPanes.emoji_names() ) @staticmethod def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillMenuPanes.emoji_names()] + LeaderSkillMenuPanes.emoji_names() ) diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index 39477fdb..f9f2feaf 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -2,9 +2,8 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from discordmenu.emoji.emoji_cache import emoji_cache - from tsutils.menu.panes import MenuPanes, emoji_buttons + from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill_single import LeaderSkillSingleView, LeaderSkillSingleViewState @@ -33,14 +32,14 @@ async def respond_with_id(message: Optional[Message], ims, *, dgcog, user_config def ls_control(state: LeaderSkillSingleViewState): return EmbedControl( [LeaderSkillSingleView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenuPanes.emoji_names()] + LeaderSkillSingleMenuPanes.emoji_names() ) @staticmethod def id_control(state: IdViewState): return EmbedControl( [IdView.embed(state)], - [emoji_cache.get_by_name(e) for e in LeaderSkillSingleMenuPanes.emoji_names()] + LeaderSkillSingleMenuPanes.emoji_names() ) diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index 65f3479f..d67cc507 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -3,7 +3,6 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from discordmenu.emoji.emoji_cache import emoji_cache from tsutils.enums import Server, ChildMenuType from tsutils.menu.panes import MenuPanes @@ -71,20 +70,18 @@ async def respond_with_delete(message: Optional[Message], ims, **data): def id_control(state: IdViewState): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [IdView.embed(state)], - reaction_list or [emoji_cache.get_by_name(e) for e in NaDiffMenuPanes.emoji_names()] + state.reaction_list or NaDiffMenuPanes.emoji_names() ) @staticmethod def message_control(state: SimpleTextViewState): if state is None: return None - reaction_list = state.reaction_list return EmbedControl( [SimpleTextView.embed(state)], - reaction_list + state.reaction_list ) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ed408d60..3c6eb918 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -261,7 +261,7 @@ async def send_message(): alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ await IdViewState.do_query(dgcog, monster) - full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) @@ -389,7 +389,7 @@ async def evos(self, ctx, *, query: str): await self.send_invalid_monster_message(ctx, query, monster, ', which has no alt evos or gems') return - full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, @@ -426,7 +426,7 @@ async def evomats(self, ctx, *, query: str): alt_monsters = MaterialsViewState.get_alt_monsters_and_evos(dgcog, monster) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, @@ -462,7 +462,7 @@ async def pantheon(self, ctx, *, query: str): alt_monsters = PantheonViewState.get_alt_monsters_and_evos(dgcog, monster) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, @@ -493,7 +493,7 @@ async def pic(self, ctx, *, query: str): alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, @@ -523,7 +523,7 @@ async def otherinfo(self, ctx, *, query: str): alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index 4f764323..021e4d45 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -1,7 +1,5 @@ from typing import List, Optional, TYPE_CHECKING -from discordmenu.emoji.emoji_cache import emoji_cache - from padinfo.core.padinfo_settings import settings from padinfo.menu.id import IdMenuPanes, IdMenuEmoji from padinfo.view.evos import EvosViewState @@ -16,7 +14,7 @@ async def get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", full_reaction_list: List[Optional[str]] = None, force_evoscroll=False): # hide some panes if we're in evo scroll mode if not full_reaction_list: - full_reaction_list = [emoji_cache.get_by_name(e) for e in IdMenuPanes.emoji_names()] + full_reaction_list = IdMenuPanes.emoji_names() if not force_evoscroll and not settings.checkEvoID(ctx.author.id): return full_reaction_list alt_versions, gem_versions = await EvosViewState.do_query(dgcog, monster) From 08928e792ce5d4d59f999d47a1b8f910435e92b2 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 22 Jul 2021 14:41:39 -0700 Subject: [PATCH 0898/1220] Use new manual monster tables in CRUD (#1332) --- crud/crud.py | 72 ++++++++++++++++++++++++++++++++------------------ crud/info.json | 3 ++- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index e4a661ab..7ccef643 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -118,43 +118,65 @@ async def editmonsname(self, ctx, monster_id: int, *, name): if name.lower() in ["none", "null", ""]: name = None + with await self.get_cursor() as cursor: - cursor.execute("SELECT name_en_override FROM monsters WHERE monster_id = %s", (monster_id,)) + # Get the most recent english translation override + cursor.execute("""SELECT + monsters.name_en AS monster_name, + monster_name_overrides.name_en AS old_override + FROM monster_name_overrides + RIGHT JOIN monsters + ON monsters.monster_id = monster_name_overrides.monster_id + WHERE monsters.monster_id = %s AND (is_translation = 1 OR is_translation IS NULL) + ORDER BY monster_name_overrides.tstamp DESC LIMIT 1""", (monster_id,)) old_val = cursor.fetchall() if not old_val: - await ctx.send("There is no monster with id: {}".format(monster_id)) + await ctx.send(f"There is no monster with id {monster_id}") return - old_val = old_val[0]['name_en_override'] - if not await get_user_confirmation(ctx, ("Are you sure you want to change monster #{}'s" - " english override from `{}` to `{}`?" - "").format(monster_id, old_val, name)): + monster_name = old_val[0]['monster_name'] + old_name = old_val[0]['old_override'] + if not await get_user_confirmation(ctx, (f"Are you sure you want to change `{monster_name} ({monster_id})`'s" + f" english override from `{old_name}` to `{name}`?")): return - sql = ('UPDATE monsters' - ' SET name_en_override = %s' - ' WHERE monster_id = %s') - await self.execute_write(ctx, sql, (name, monster_id)) + sql = (f'INSERT INTO monster_name_overrides (monster_id, name_en, is_translation, tstamp)' + f' VALUES (%s, %s, 1, UNIX_TIMESTAMP())') + await self.execute_write(ctx, sql, (monster_id, name)) @crud.command() - async def editmonsseries(self, ctx, monster_id: int, *, series_id: int): - """Change a monster's series_id""" + async def editmonsseries(self, ctx, monster_id: int, series_id: int): + """Change a monster's main series_id""" with await self.get_cursor() as cursor: - cursor.execute("SELECT series_id FROM monsters WHERE monster_id = %s", (monster_id,)) - old_val = cursor.fetchall() - - if not old_val: - await ctx.send("There is no monster with id: {}".format(monster_id)) - return - old_val = old_val[0]['series_id'] - if not await get_user_confirmation(ctx, ("Are you sure you want to change monster #{}'s" - " series id from `{}` to `{}`?" - "").format(monster_id, old_val, series_id)): + cursor.execute("""SELECT + COALESCE(mno.name_en, monsters.name_en) AS monster_name, + series.name_en AS series_name + FROM monster_series + JOIN monsters ON monsters.monster_id = monster_series.monster_id + JOIN series ON series.series_id = monster_series.series_id + LEFT JOIN monster_name_overrides mno + ON mno.monster_id = monsters.monster_id AND is_translation = 1 + WHERE monsters.monster_id = %s AND priority = 1""", (monster_id,)) + rows = cursor.fetchall() + if not rows: + await ctx.send("There is no monster with id: {}".format(monster_id)) + return + monster_name = rows[0]['monster_name'] + old_series = rows[0]['series_name'] + + cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) + rows = cursor.fetchall() + if not rows: + await ctx.send(f"There is no series with id {series_id}") + return + new_series = rows[0]['name_en'] + + if not await get_user_confirmation(ctx, (f"Are you sure you want to change `{monster_name} ({monster_id})`'s" + f" series id from `{old_series}` to `{new_series}`?")): return - sql = ('UPDATE monsters' - ' SET series_id = %s' - ' WHERE monster_id = %s') + sql = (f'UPDATE monster_series SET series_id = %s, tstamp = UNIX_TIMESTAMP()' + f' WHERE monster_id = %s AND priority = 1') await self.execute_write(ctx, sql, (series_id, monster_id)) @crud.command() diff --git a/crud/info.json b/crud/info.json index 76399d25..82a5c7b9 100644 --- a/crud/info.json +++ b/crud/info.json @@ -19,7 +19,8 @@ "requirements": [ "tsutils", "pymysql", - "aiofiles" + "aiofiles", + "cryptography" ], "tags": [ "PAD" From a2f90b0276b53143808e1ad4d887b6aa89bf8dbe Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 24 Jul 2021 02:40:40 -0700 Subject: [PATCH 0899/1220] [CRUD] Add auto-pushing to GitHub (#1333) * Verify branch is on master when editing JSONs via CRUD * Add to info.json * Add auto-pushing to github * Fix diff check * Password -> token --- crud/crud.py | 44 +++++++++++++++++++++++++++++++++++++++++++- crud/info.json | 3 ++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 7ccef643..8cad8f3f 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -6,10 +6,11 @@ import aiofiles import discord +import pygit2 import pymysql from redbot.core import checks, commands, Config, errors from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import auth_check, get_user_confirmation +from tsutils import auth_check, get_user_confirmation, send_cancellation_message logger = logging.getLogger('red.padbot-cogs.crud') @@ -101,6 +102,41 @@ async def execute_read(self, ctx, sql, replacements): for page in pagify(msg): await ctx.send(box(page)) + async def git_verify(self, ctx, filepath): + try: + repo = pygit2.Repository(await self.config.pipeline_base()) + except pygit2.GitError: + return + if not repo.diff(): + return + if not repo.lookup_branch("master").is_checked_out(): + await send_cancellation_message(ctx, f"Hey {ctx.author.mention}, the pipeline branch is currently **not**" + f" set to master! Please inform a sysadmin that this crud change" + f" was only **temporarily** made!") + return + if any(diff for diff in repo.diff().deltas if diff.old_file.path != filepath): + await send_cancellation_message(ctx, f"Hey {ctx.author.mention}, there are currently staged changes." + f" Please inform a sysadmin so that your changes can be" + f" manually committed.") + + keys = await self.bot.get_shared_api_tokens("github") + if "username" not in keys or "token" not in keys: + await send_cancellation_message(ctx, f"Github credentials unset. Add via `{ctx.prefix}set api" + f" github username token `") + + index = repo.index + index.add(filepath) + index.write() + tree = index.write_tree() + author = pygit2.Signature(str(ctx.author), "famiel@tsubakibot.com") + commiter = pygit2.Signature("Famiel", "famiel@tsubakibot.com") + parent, ref = repo.resolve_refish(refish=repo.head.name) + repo.create_commit(ref.name, author, commiter, "Updating JSON", tree, [parent.oid]) + upcred = pygit2.UserPass(keys['username'], keys['token']) + remote = discord.utils.get(repo.remotes, name="origin") + remote.push(['refs/heads/master'], callbacks=pygit2.RemoteCallbacks(credentials=upcred)) + + @commands.group() @auth_check('crud') @commands.check(check_crud_channel) @@ -250,6 +286,7 @@ async def series_add(self, ctx, *elements): }) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + await self.git_verify(ctx, 'etl/pad/storage_processor/series.json') @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *elements): @@ -288,6 +325,7 @@ async def series_edit(self, ctx, series_id: int, *elements): e.update(elements) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + await self.git_verify(ctx, 'etl/pad/storage_processor/series.json') @series.command(name="delete") @checks.is_owner() @@ -306,6 +344,7 @@ async def series_delete(self, ctx, series_id: int): j.remove(e) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + await self.git_verify(ctx, 'etl/pad/storage_processor/series.json') @crud.group(aliases=["awos"]) async def awokenskill(self, ctx): @@ -383,6 +422,7 @@ async def awokenskill_add(self, ctx, *elements): }) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + await self.git_verify(ctx, 'etl/pad/storage_processor/awoken_skill.json') @awokenskill.command(name="edit") async def awokenskill_edit(self, ctx, awoken_skill, *elements): @@ -436,6 +476,7 @@ async def awokenskill_edit(self, ctx, awoken_skill, *elements): e.update(elements) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + await self.git_verify(ctx, 'etl/pad/storage_processor/awoken_skill.json') @awokenskill.command(name="delete") @checks.is_owner() @@ -454,6 +495,7 @@ async def awokenskill_delete(self, ctx, awoken_skill_id: int): j.remove(e) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) + await self.git_verify(ctx, 'etl/pad/storage_processor/awoken_skill.json') @crud.command() @checks.is_owner() diff --git a/crud/info.json b/crud/info.json index 82a5c7b9..73e5ac58 100644 --- a/crud/info.json +++ b/crud/info.json @@ -20,7 +20,8 @@ "tsutils", "pymysql", "aiofiles", - "cryptography" + "cryptography", + "pygit2" ], "tags": [ "PAD" From ef8a282777415a0ce0af547fed2743000ffa544c Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 24 Jul 2021 02:57:35 -0700 Subject: [PATCH 0900/1220] Add new awakenings (#1334) --- dadguide/models/enum_types.py | 3 +++ dadguide/token_mappings.py | 3 +++ padinfo/common/emoji_map.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/dadguide/models/enum_types.py b/dadguide/models/enum_types.py index 7f739ace..3bd46a2b 100644 --- a/dadguide/models/enum_types.py +++ b/dadguide/models/enum_types.py @@ -137,3 +137,6 @@ class AwokenSkills(Enum): LIGHTCOMBOCOUNT = 76 DARKCOMBOCOUNT = 77 CROSSATTACK = 78 + ATTR3BOOST = 79 + ATTR4BOOST = 80 + ATTR5BOOST = 81 diff --git a/dadguide/token_mappings.py b/dadguide/token_mappings.py index 9917fe22..ab3097c8 100644 --- a/dadguide/token_mappings.py +++ b/dadguide/token_mappings.py @@ -166,6 +166,9 @@ class EvoTypes(Enum): AwokenSkills.LIGHTCOMBOCOUNT: ('ccl', 'cc'), AwokenSkills.DARKCOMBOCOUNT: ('ccd', 'cc'), AwokenSkills.CROSSATTACK: ('crossattack', 'crossblind'), + AwokenSkills.ATTR3BOOST: ('attr3', '3attr'), + AwokenSkills.ATTR4BOOST: ('attr4', '4attr'), + AwokenSkills.ATTR5BOOST: ('attr5', '5attr'), } diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index c6c2cd77..f2395165 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -86,6 +86,9 @@ 76: 'cc_light', 77: 'cc_dark', 78: 'cross_attack', + 79: 'boost_attr3', + 80: 'boost_attr4', + 81: 'boost_attr5', } AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { From 7c080a50c989ea107b6ed100f0faebe928c9c859 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 26 Jul 2021 01:21:47 -0500 Subject: [PATCH 0901/1220] Remove now-unnecessary delete from awakening_list (#1337) --- padinfo/menu/awakening_list.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index 11ee1f13..f2d03837 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -10,7 +10,6 @@ class AwakeningListEmoji: - delete = '\N{CROSS MARK}' refresh = '\N{ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS}' home = emoji_buttons['home'] prev_page = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' @@ -29,10 +28,6 @@ def menu(initial_control=None): embed = EmbedMenu(AwakeningListMenuPanes.transitions(), initial_control) return embed - @staticmethod - async def respond_with_delete(message: Message, ims, **data): - return await message.delete() - @staticmethod async def respond_with_left(message: Optional[Message], ims, **data): cur_page = ims['current_page'] @@ -84,7 +79,6 @@ class AwakeningListMenuPanes(MenuPanes): INITIAL_EMOJI = AwakeningListEmoji.home DATA = { - AwakeningListEmoji.delete: (AwakeningListMenu.respond_with_delete, None), AwakeningListEmoji.home: (AwakeningListMenu.respond_with_awakening_list, AwakeningListView.VIEW_TYPE), AwakeningListEmoji.prev_page: (AwakeningListMenu.respond_with_left, AwakeningListView.VIEW_TYPE), AwakeningListEmoji.next_page: (AwakeningListMenu.respond_with_right, AwakeningListView.VIEW_TYPE), From c5fe0fee9498684d236bb30f756074479c4e0c9a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 26 Jul 2021 01:40:10 -0700 Subject: [PATCH 0902/1220] Order alt versions consistently (#1338) Closes #1137 --- dadguide/monster_graph.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 8f5dcc2b..98c59cbd 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -2,7 +2,7 @@ import logging import re from collections import defaultdict -from typing import Optional, List, Set, Dict +from typing import Optional, List, Set, Dict, Tuple, Any from networkx import MultiDiGraph from tsutils.enums import Server @@ -365,14 +365,25 @@ def process_alt_monsters_from_base(self, base_monster: MonsterModel) -> List[Mon if transform and (transform.monster_id > base_monster.monster_id or transform.monster_id == 5802): # I hate DMG very much ids += self.process_alt_monsters_from_base(transform) - for evo in sorted(self.get_next_evolutions(base_monster), key=lambda m: m.monster_id): + for evo in sorted(self.get_next_evolutions(base_monster), key=self.get_id_order_key): ids += self.process_alt_monsters_from_base(evo) return ids - def get_alt_ids_fast_by_id_and_server(self, monster_id, server: Server) -> List[int]: - # This is for the index. I'm too lazy to remove this, and it might have impacts on speed. - return self.graph_dict[server].nodes[monster_id]['alt_versions'] - + def get_id_order_key(self, monster: MonsterModel) -> Tuple[Any, ...]: + evo_ordering = { + InternalEvoType.Reincarnated: -1, + InternalEvoType.SuperReincarnated: -1, + InternalEvoType.Base: 0, + InternalEvoType.Normal: 0, + InternalEvoType.Ultimate: 1, + InternalEvoType.Pixel: 2, + InternalEvoType.Assist: 3, + } + return ( + evo_ordering[self.true_evo_type(monster)], + '覚醒' in monster.name_ja or 'awoken' in monster.name_en.lower(), + ) + def get_alt_ids(self, monster: MonsterModel) -> List[int]: return self.graph_dict[monster.server_priority].nodes[monster.monster_id]['alt_versions'] From 32faefe4d6df76c0676cc8a411e58d52bf6509fa Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 26 Jul 2021 03:45:10 -0500 Subject: [PATCH 0903/1220] Revert "Order alt versions consistently" (#1339) This reverts commit c5fe0fee9498684d236bb30f756074479c4e0c9a. --- dadguide/monster_graph.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 98c59cbd..8f5dcc2b 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -2,7 +2,7 @@ import logging import re from collections import defaultdict -from typing import Optional, List, Set, Dict, Tuple, Any +from typing import Optional, List, Set, Dict from networkx import MultiDiGraph from tsutils.enums import Server @@ -365,25 +365,14 @@ def process_alt_monsters_from_base(self, base_monster: MonsterModel) -> List[Mon if transform and (transform.monster_id > base_monster.monster_id or transform.monster_id == 5802): # I hate DMG very much ids += self.process_alt_monsters_from_base(transform) - for evo in sorted(self.get_next_evolutions(base_monster), key=self.get_id_order_key): + for evo in sorted(self.get_next_evolutions(base_monster), key=lambda m: m.monster_id): ids += self.process_alt_monsters_from_base(evo) return ids - def get_id_order_key(self, monster: MonsterModel) -> Tuple[Any, ...]: - evo_ordering = { - InternalEvoType.Reincarnated: -1, - InternalEvoType.SuperReincarnated: -1, - InternalEvoType.Base: 0, - InternalEvoType.Normal: 0, - InternalEvoType.Ultimate: 1, - InternalEvoType.Pixel: 2, - InternalEvoType.Assist: 3, - } - return ( - evo_ordering[self.true_evo_type(monster)], - '覚醒' in monster.name_ja or 'awoken' in monster.name_en.lower(), - ) - + def get_alt_ids_fast_by_id_and_server(self, monster_id, server: Server) -> List[int]: + # This is for the index. I'm too lazy to remove this, and it might have impacts on speed. + return self.graph_dict[server].nodes[monster_id]['alt_versions'] + def get_alt_ids(self, monster: MonsterModel) -> List[int]: return self.graph_dict[monster.server_priority].nodes[monster.monster_id]['alt_versions'] From 0ffba2486d1798d55555fc01ebfe38781c14b801 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 26 Jul 2021 01:54:26 -0700 Subject: [PATCH 0904/1220] Order alt versions consistently (try 2) (#1340) Before, some tests were failing with split evos --- dadguide/monster_graph.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 8f5dcc2b..12d6e03d 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -2,7 +2,7 @@ import logging import re from collections import defaultdict -from typing import Optional, List, Set, Dict +from typing import Optional, List, Set, Dict, Tuple, Any from networkx import MultiDiGraph from tsutils.enums import Server @@ -365,14 +365,26 @@ def process_alt_monsters_from_base(self, base_monster: MonsterModel) -> List[Mon if transform and (transform.monster_id > base_monster.monster_id or transform.monster_id == 5802): # I hate DMG very much ids += self.process_alt_monsters_from_base(transform) - for evo in sorted(self.get_next_evolutions(base_monster), key=lambda m: m.monster_id): + for evo in sorted(self.get_next_evolutions(base_monster), key=self.get_id_order_key): ids += self.process_alt_monsters_from_base(evo) return ids - def get_alt_ids_fast_by_id_and_server(self, monster_id, server: Server) -> List[int]: - # This is for the index. I'm too lazy to remove this, and it might have impacts on speed. - return self.graph_dict[server].nodes[monster_id]['alt_versions'] - + def get_id_order_key(self, monster: MonsterModel) -> Tuple[Any, ...]: + evo_ordering = { + InternalEvoType.Reincarnated: -1, + InternalEvoType.SuperReincarnated: -1, + InternalEvoType.Base: 0, + InternalEvoType.Normal: 0, + InternalEvoType.Ultimate: 1, + InternalEvoType.Pixel: 2, + InternalEvoType.Assist: 3, + } + return ( + evo_ordering[self.true_evo_type(monster)], + '覚醒' in monster.name_ja or 'awoken' in monster.name_en.lower(), + monster.monster_id, + ) + def get_alt_ids(self, monster: MonsterModel) -> List[int]: return self.graph_dict[monster.server_priority].nodes[monster.monster_id]['alt_versions'] From 7cf13d615128b2382dfc0bde0ee761b3b15e5282 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 26 Jul 2021 05:12:05 -0700 Subject: [PATCH 0905/1220] Implement Tsubaki-only (debug) mode (#1331) --- dadguide/dadguide.py | 67 +++++++++++++++++++++++++++++++++--- dadguide/database_context.py | 11 ++++-- dadguide/database_loader.py | 6 ++-- dadguide/monster_graph.py | 59 ++++++++++++++++++++++++------- 4 files changed, 120 insertions(+), 23 deletions(-) diff --git a/dadguide/dadguide.py b/dadguide/dadguide.py index 58d06dc0..d7293916 100644 --- a/dadguide/dadguide.py +++ b/dadguide/dadguide.py @@ -76,7 +76,8 @@ def __init__(self, bot, *args, **kwargs): self.fm_flags_default = {'na_prio': True, 'server': DEFAULT_SERVER} self.config = Config.get_conf(self, identifier=64667103) - self.config.register_global(datafile='', indexlog=0, test_suite={}, fluff_suite=[], typo_mods=[]) + self.config.register_global(datafile='', indexlog=0, test_suite={}, fluff_suite=[], typo_mods=[], + debug_mode=False, debug_mode_monsters = [3260]) self.config.register_user(lastaction=None, fm_flags={}) self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') @@ -171,7 +172,7 @@ async def check_index(self): issues.extend(index.issues) issues.extend(await self.run_tests()) - if issues: + if issues and self.database.graph.debug_monster_ids is None: channels = [self.bot.get_channel(await self.config.indexlog())] if not any(channels): channels = [owner for oid in self.bot.owner_ids if (owner := self.bot.get_user(oid))] @@ -229,7 +230,7 @@ async def download_and_refresh_nicknames(self): await self._download_files() logger.info('Loading dg database') - self.database = load_database(self.database) + self.database = load_database(self.database, await self.get_debug_monsters()) logger.info('Building dg monster index') await self.create_index() @@ -257,9 +258,65 @@ async def setfailurechannel(self, ctx, channel: discord.TextChannel): await self.config.indexlog.set(channel.id) await ctx.tick() + @dadguide.group(aliases=["debug"]) + @checks.is_owner() + async def debugmode(self, ctx): + """Tsubaki-Only Mode settings""" + + @debugmode.command(name="enable", aliases=["enabled"]) + async def debug_enable(self, ctx, enabled: bool): + """Sets tsubaki-only mode and reloads the index and graph""" + await self.config.debug_mode.set(enabled) + if enabled: + await tsutils.send_confirmation_message(ctx, f"You have set your bot to **Tsubaki mode** (aka debug mode)." + f" Only **Tsubaki** will be available in the graph and id" + f" queries, to allow for a faster restart time. To turn" + f" off **Tsubaki mode** run" + f" `{ctx.prefix}dadguide debugmode false`.") + else: + await tsutils.send_confirmation_message(ctx, f"You have turned off **Tsubaki mode**. Your bot will behave" + f" normally. To return to **Tsubaki mode**, run" + f" `{ctx.prefix}dadguide debugmode true.`") + await self.forceindexreload(ctx) + + @debugmode.group(name="monsters") + async def debug_monsters(self, ctx): + """Monsters allowed in Tsubaki-Only mode""" + + @debug_monsters.command(name="add") + async def debug_monsters_add(self, ctx, *monsters: int): + async with self.config.debug_mode_monsters() as ts_monsters: + for monster in monsters: + if monster not in ts_monsters: + ts_monsters.append(monster) + if await self.config.debug_mode(): + await self.forceindexreload(ctx) + await ctx.tick() + + @debug_monsters.command(name="remove", aliases=["rm", "del", "delete"]) + async def debug_monsters_rm(self, ctx, *monsters: int): + async with self.config.debug_mode_monsters() as ts_monsters: + for monster in monsters: + if monster in ts_monsters: + ts_monsters.remove(monster) + if await self.config.debug_mode(): + await self.forceindexreload(ctx) + await ctx.tick() + + @debug_monsters.command(name="list") + async def debug_monsters_list(self, ctx): + text = "\n".join(f'{m} {mon.name_en}' if (mon := self.get_monster(m)) else f"Invalid monster {m}" + for m in await self.config.debug_mode_monsters()) + for page in pagify(text): + await ctx.send(box(page)) + + async def get_debug_monsters(self) -> Optional[List[int]]: + if await self.config.debug_mode(): + return await self.config.debug_mode_monsters() + return None + async def get_fm_flags(self, author_id): - # noinspection PyTypeChecker - return {**self.fm_flags_default, **(await self.config.user(discord.Object(author_id)).fm_flags())} + return {**self.fm_flags_default, **(await self.config.user_from_id(author_id).fm_flags())} async def find_monster(self, query: str, author_id: int = 0) -> MonsterModel: return await FindMonster(self, await self.get_fm_flags(author_id)).find_monster(query) diff --git a/dadguide/database_context.py b/dadguide/database_context.py index cbb0b275..6bf0f4b8 100644 --- a/dadguide/database_context.py +++ b/dadguide/database_context.py @@ -1,4 +1,4 @@ -from typing import Generator, List +from typing import Generator, List, Optional from tsutils.enums import Server @@ -22,11 +22,14 @@ class DbContext(object): - def __init__(self, database: DadguideDatabase, graph: MonsterGraph, dungeon: DungeonContext): + def __init__(self, database: DadguideDatabase, graph: MonsterGraph, dungeon: DungeonContext, + debug_monster_ids: Optional[List[int]] = None): self.database = database self.graph = graph self.dungeon = dungeon + self.debug_monster_ids = debug_monster_ids + self.awoken_skill_map = {awsk.awoken_skill_id: awsk for awsk in self.get_all_awoken_skills()} def get_awoken_skill_ids(self): @@ -45,6 +48,10 @@ def get_monsters_by_active(self, active_skill_id: int, *, server: Server): return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id, server=server) def get_all_monster_ids_query(self, server: Server): + # We don't need to query if we're in debug mode. We already know exactly which monsters we're working with + if self.debug_monster_ids is not None: + return self.debug_monster_ids + table = 'monsters_na' if server == Server.NA else 'monsters' query = self.database.query_many( self.database.select_builder(tables={table: ('monster_id',)}), (), diff --git a/dadguide/database_loader.py b/dadguide/database_loader.py index 32aac1d9..ba6ff567 100644 --- a/dadguide/database_loader.py +++ b/dadguide/database_loader.py @@ -13,7 +13,7 @@ def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) -def load_database(existing_db): +def load_database(existing_db, debug_monster_ids): DB_DUMP_FILE = _data_file('dadguide.sqlite') DB_DUMP_WORKING_FILE = _data_file('dadguide_working.sqlite') # Release the handle to the database file if it has one @@ -24,7 +24,7 @@ def load_database(existing_db): shutil.copy2(DB_DUMP_FILE, DB_DUMP_WORKING_FILE) # Open the new working copy. database = DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) - graph = MonsterGraph(database) + graph = MonsterGraph(database, debug_monster_ids) dungeon = DungeonContext(database) - db_context = DbContext(database, graph, dungeon) + db_context = DbContext(database, graph, dungeon, debug_monster_ids) return db_context diff --git a/dadguide/monster_graph.py b/dadguide/monster_graph.py index 12d6e03d..3f892337 100644 --- a/dadguide/monster_graph.py +++ b/dadguide/monster_graph.py @@ -8,7 +8,6 @@ from tsutils.enums import Server from .database_manager import DadguideDatabase -from .errors import InvalidGraphState from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel @@ -118,12 +117,13 @@ class MonsterGraph(object): - def __init__(self, database: DadguideDatabase): + def __init__(self, database: DadguideDatabase, debug_monster_ids: Optional[List[int]] = None): self.issues = [] + self.debug_monster_ids = debug_monster_ids self.database = database self.max_monster_id = -1 - self.graph_dict: Dict[Server, MultiDiGraph] = { # noqa + self.graph_dict: Dict[Server, MultiDiGraph] = { Server.COMBINED: self.build_graph(Server.COMBINED), Server.NA: self.build_graph(Server.NA), } @@ -160,6 +160,8 @@ def build_graph(self, server: Server) -> MultiDiGraph: mtoegg[idx][e_type] = True for m in ms: + if self.debug_monster_ids is not None and m.monster_id not in self.debug_monster_ids: + continue ls_model = LeaderSkillModel(leader_skill_id=m.leader_skill_id, name_ja=m.ls_name_ja, name_en=m.ls_name_en, @@ -254,19 +256,36 @@ def build_graph(self, server: Server) -> MultiDiGraph: ) if not m_model: continue + graph.add_node(m.monster_id, model=m_model) + self.max_monster_id = max(self.max_monster_id, m.monster_id) + if m.linked_monster_id: - graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') - graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') + if self.debug_monster_ids is None or m.linked_monster_id in self.debug_monster_ids: + graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') + graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') if m.evo_gem_id: - graph.add_edge(m.monster_id, m.evo_gem_id, type='evo_gem_from') - graph.add_edge(m.evo_gem_id, m.monster_id, type='evo_gem_of') - - self.max_monster_id = max(self.max_monster_id, m.monster_id) + if self.debug_monster_ids is None or m.evo_gem_id in self.debug_monster_ids: + graph.add_edge(m.monster_id, m.evo_gem_id, type='evo_gem_from') + graph.add_edge(m.evo_gem_id, m.monster_id, type='evo_gem_of') for e in es: - evo_model = EvolutionModel(**e) + evo_model = EvolutionModel( + evolution_type=e.evolution_type, + reversible=e.reversible, + from_id=e.from_id, + to_id=e.to_id, + mat_1_id=self.debug_validate_id(e.mat_1_id), + mat_2_id=self.debug_validate_id(e.mat_2_id), + mat_3_id=self.debug_validate_id(e.mat_3_id), + mat_4_id=self.debug_validate_id(e.mat_4_id), + mat_5_id=self.debug_validate_id(e.mat_5_id), + tstamp=e.tstamp, + ) + if self.debug_monster_ids is not None: + if evo_model.from_id not in self.debug_monster_ids or evo_model.to_id not in self.debug_monster_ids: + continue graph.add_edge( evo_model.from_id, evo_model.to_id, type='evolution', model=evo_model) @@ -284,6 +303,9 @@ def build_graph(self, server: Server) -> MultiDiGraph: for ex in exs: for vendor_id in re.findall(r'\d+', ex.required_monster_ids): + if self.debug_monster_ids is not None: + if ex.target_monster_id not in self.debug_monster_ids or int(vendor_id) not in self.debug_monster_ids: + continue graph.add_edge( ex.target_monster_id, int(vendor_id), type='exchange_from') graph.add_edge( @@ -295,8 +317,10 @@ def _cache_graphs(self) -> None: for server in self.graph_dict: for mid in self.graph_dict[server].nodes: if 'model' in self.graph_dict[server].nodes[mid]: - self.graph_dict[server].nodes[mid]['alt_versions'] = self.process_alt_ids( - self.get_monster(mid, server=server)) + alt_ids = self.process_alt_ids(self.get_monster(mid, server=server)) + self.graph_dict[server].nodes[mid]['alt_versions'] = alt_ids + if self.debug_monster_ids is not None: + self.graph_dict[server].nodes[mid]['model'].base_evo_id = alt_ids[0] else: self.issues.append(f"{mid} has no model in the {server.name} graph.") @@ -400,7 +424,12 @@ def get_base_id(self, monster) -> int: and list(prevs)[0].monster_id < monster.monster_id: monster = prevs.pop() - return monster.base_evo_id + if self.debug_monster_ids is not None: + while (prev := self.get_prev_evolution(monster)): + monster = prev + return monster.monster_id + else: + return monster.base_evo_id def get_base_monster(self, monster: MonsterModel) -> MonsterModel: return self.get_monster(self.get_base_id(monster), server=monster.server_priority) @@ -609,3 +638,7 @@ def monster_difference(self, monster: MonsterModel, server) -> MonsterDifference def monster_is_discrepant(self, monster: MonsterModel) -> bool: return any((md := self.monster_difference(monster, server)) and not md.existance for server in SERVERS) + + def debug_validate_id(self, monster_id: int) -> Optional[int]: + if self.debug_monster_ids is None or monster_id in self.debug_monster_ids: + return monster_id From 4281a8b4f9a181536daad06e179749fef3049d6a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 26 Jul 2021 05:59:28 -0700 Subject: [PATCH 0906/1220] Rename Dadguide -> DBCog (#1342) --- README.md | 4 +- {dadguide => dbcog}/__init__.py | 4 +- {dadguide => dbcog}/database_context.py | 4 +- {dadguide => dbcog}/database_loader.py | 6 +- {dadguide => dbcog}/database_manager.py | 8 +- dadguide/dadguide.py => dbcog/dbcog.py | 46 +-- {dadguide => dbcog}/dungeon_context.py | 14 +- {dadguide => dbcog}/errors.py | 0 {dadguide => dbcog}/find_monster.py | 34 +-- {dadguide => dbcog}/find_monster_tokens.py | 6 +- {dadguide => dbcog}/idtest_mixin.py | 4 +- {dadguide => dbcog}/info.json | 2 +- {dadguide => dbcog}/models/__init__.py | 0 .../models/active_skill_model.py | 0 {dadguide => dbcog}/models/awakening_model.py | 0 .../models/awoken_skill_model.py | 0 {dadguide => dbcog}/models/base_model.py | 0 {dadguide => dbcog}/models/dungeon_model.py | 0 {dadguide => dbcog}/models/encounter_model.py | 0 .../models/enemy_data_model.py | 0 .../models/enemy_skill_model.py | 0 {dadguide => dbcog}/models/enum_types.py | 0 {dadguide => dbcog}/models/evolution_model.py | 0 .../models/leader_skill_model.py | 0 .../models/monster/__init__.py | 0 .../models/monster/monster_difference.py | 2 +- {dadguide => dbcog}/models/monster_model.py | 0 {dadguide => dbcog}/models/monster_stats.py | 2 +- .../models/scheduled_event_model.py | 0 {dadguide => dbcog}/models/series_model.py | 0 .../models/sub_dungeon_model.py | 0 {dadguide => dbcog}/monster_graph.py | 6 +- {dadguide => dbcog}/monster_index.py | 2 +- {dadguide => dbcog}/token_mappings.py | 0 dungeon/dungeoncog.py | 30 +- dungeon/menu/dungeon.py | 28 +- dungeon/menu/simple.py | 8 +- dungeon/processors.py | 6 +- dungeon/view/dungeon.py | 6 +- dungeon/view/simple.py | 2 +- dungeoncog/dungeoncog.py | 26 +- dungeoncog/info.json | 2 +- dungeoncog/menu/dungeon.py | 28 +- dungeoncog/menu/simple.py | 8 +- dungeoncog/processors.py | 6 +- dungeoncog/view/dungeon.py | 8 +- dungeoncog/view/simple.py | 2 +- monidlistener/monidlistener.py | 8 +- padbuildimg/info.json | 2 +- padbuildimg/padbuildimg.py | 8 +- padevents/info.json | 2 +- padevents/padevents.py | 18 +- padglobal/info.json | 2 +- padglobal/padglobal.py | 78 ++--- padinfo/common/emoji_map.py | 4 +- padinfo/common/external_links.py | 2 +- padinfo/core/button_info.py | 42 +-- padinfo/core/leader_skills.py | 10 +- padinfo/core/transforminfo.py | 8 +- padinfo/info.json | 2 +- padinfo/menu/awakening_list.py | 4 +- padinfo/menu/id.py | 36 +-- padinfo/menu/leader_skill.py | 12 +- padinfo/menu/leader_skill_single.py | 8 +- padinfo/menu/monster_list.py | 4 +- padinfo/menu/na_diff.py | 12 +- padinfo/menu/series_scroll.py | 34 +-- padinfo/menu/simple_text.py | 4 +- padinfo/menu/transforminfo.py | 16 +- padinfo/padinfo.py | 284 +++++++++--------- padinfo/reaction_list.py | 10 +- padinfo/view/awakening_help.py | 4 +- padinfo/view/awakening_list.py | 8 +- padinfo/view/button_info.py | 2 +- padinfo/view/common.py | 10 +- padinfo/view/components/monster/header.py | 2 +- padinfo/view/components/monster/image.py | 2 +- padinfo/view/components/view_state_base_id.py | 26 +- padinfo/view/evos.py | 16 +- padinfo/view/id.py | 26 +- padinfo/view/id_traceback.py | 2 +- padinfo/view/leader_skill.py | 4 +- padinfo/view/leader_skill_single.py | 4 +- padinfo/view/links.py | 2 +- padinfo/view/lookup.py | 2 +- padinfo/view/materials.py | 18 +- padinfo/view/monster_list/all_mats.py | 12 +- padinfo/view/monster_list/id_search.py | 12 +- padinfo/view/monster_list/monster_list.py | 12 +- .../view/monster_list/static_monster_list.py | 12 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 16 +- padinfo/view/series_scroll.py | 36 +-- padinfo/view/simple_text.py | 2 +- padinfo/view/transforminfo.py | 18 +- padmonitor/info.json | 2 +- padmonitor/padmonitor.py | 4 +- padsearch/info.json | 2 +- padsearch/padsearch.py | 10 +- pricecheck/info.json | 2 +- pricecheck/pricecheck.py | 34 +-- .../database_context_test.py | 12 +- 102 files changed, 615 insertions(+), 615 deletions(-) rename {dadguide => dbcog}/__init__.py (74%) rename {dadguide => dbcog}/database_context.py (96%) rename {dadguide => dbcog}/database_loader.py (87%) rename {dadguide => dbcog}/database_manager.py (94%) rename dadguide/dadguide.py => dbcog/dbcog.py (91%) rename {dadguide => dbcog}/dungeon_context.py (94%) rename {dadguide => dbcog}/errors.py (100%) rename {dadguide => dbcog}/find_monster.py (93%) rename {dadguide => dbcog}/find_monster_tokens.py (93%) rename {dadguide => dbcog}/idtest_mixin.py (99%) rename {dadguide => dbcog}/info.json (87%) rename {dadguide => dbcog}/models/__init__.py (100%) rename {dadguide => dbcog}/models/active_skill_model.py (100%) rename {dadguide => dbcog}/models/awakening_model.py (100%) rename {dadguide => dbcog}/models/awoken_skill_model.py (100%) rename {dadguide => dbcog}/models/base_model.py (100%) rename {dadguide => dbcog}/models/dungeon_model.py (100%) rename {dadguide => dbcog}/models/encounter_model.py (100%) rename {dadguide => dbcog}/models/enemy_data_model.py (100%) rename {dadguide => dbcog}/models/enemy_skill_model.py (100%) rename {dadguide => dbcog}/models/enum_types.py (100%) rename {dadguide => dbcog}/models/evolution_model.py (100%) rename {dadguide => dbcog}/models/leader_skill_model.py (100%) rename {dadguide => dbcog}/models/monster/__init__.py (100%) rename {dadguide => dbcog}/models/monster/monster_difference.py (98%) rename {dadguide => dbcog}/models/monster_model.py (100%) rename {dadguide => dbcog}/models/monster_stats.py (99%) rename {dadguide => dbcog}/models/scheduled_event_model.py (100%) rename {dadguide => dbcog}/models/series_model.py (100%) rename {dadguide => dbcog}/models/sub_dungeon_model.py (100%) rename {dadguide => dbcog}/monster_graph.py (99%) rename {dadguide => dbcog}/monster_index.py (99%) rename {dadguide => dbcog}/token_mappings.py (100%) rename tests/{dadguide => dbcog}/database_context_test.py (77%) diff --git a/README.md b/README.md index a7f6f140..00b94161 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type * Some cogs will have cross dependencies on each other. Use `^traceback 1` if you encounter any errors to see your traceback. * Common dependencies include: - * dadguide + * dbcog * padinfo ### Emoji @@ -85,7 +85,7 @@ Most cogs here relate to the mobile game 'Puzzle & Dragons'. | azurelane | **DISCONTINUED** Azur Lane card lookup | | channelmirror | A better version of the built-in discord announcement channel | | crud | Database editor. Not useful for bots other than prod Tsubaki | -| dadguide | Central cog to host all PAD data | +| dbcog | Central cog to host all PAD data | | damagecalc | Simple attack damage calculator | | feedback | A specialized feedback cog for Tsubaki specifically | | padboard | Converts board images to dawnglare board/solved board links | diff --git a/dadguide/__init__.py b/dbcog/__init__.py similarity index 74% rename from dadguide/__init__.py rename to dbcog/__init__.py index dbe92ee4..6206b544 100644 --- a/dadguide/__init__.py +++ b/dbcog/__init__.py @@ -1,9 +1,9 @@ -from .dadguide import Dadguide +from .dbcog import DBCog __red_end_user_data_statement__ = "No personal data is stored." def setup(bot): - n = Dadguide(bot) + n = DBCog(bot) bot.add_cog(n) n.bot.loop.create_task(n.reload_data_task()) diff --git a/dadguide/database_context.py b/dbcog/database_context.py similarity index 96% rename from dadguide/database_context.py rename to dbcog/database_context.py index 6bf0f4b8..39a5e963 100644 --- a/dadguide/database_context.py +++ b/dbcog/database_context.py @@ -2,7 +2,7 @@ from tsutils.enums import Server -from .database_manager import DadguideDatabase +from .database_manager import DBCogDatabase from .dungeon_context import DungeonContext from .models.awoken_skill_model import AwokenSkillModel from .models.dungeon_model import DungeonModel @@ -22,7 +22,7 @@ class DbContext(object): - def __init__(self, database: DadguideDatabase, graph: MonsterGraph, dungeon: DungeonContext, + def __init__(self, database: DBCogDatabase, graph: MonsterGraph, dungeon: DungeonContext, debug_monster_ids: Optional[List[int]] = None): self.database = database self.graph = graph diff --git a/dadguide/database_loader.py b/dbcog/database_loader.py similarity index 87% rename from dadguide/database_loader.py rename to dbcog/database_loader.py index ba6ff567..cc57cd28 100644 --- a/dadguide/database_loader.py +++ b/dbcog/database_loader.py @@ -4,13 +4,13 @@ from redbot.core import data_manager from .database_context import DbContext -from .database_manager import DadguideDatabase +from .database_manager import DBCogDatabase from .dungeon_context import DungeonContext from .monster_graph import MonsterGraph def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='dbcog')), file_name) def load_database(existing_db, debug_monster_ids): @@ -23,7 +23,7 @@ def load_database(existing_db, debug_monster_ids): if os.path.exists(DB_DUMP_FILE): shutil.copy2(DB_DUMP_FILE, DB_DUMP_WORKING_FILE) # Open the new working copy. - database = DadguideDatabase(data_file=DB_DUMP_WORKING_FILE) + database = DBCogDatabase(data_file=DB_DUMP_WORKING_FILE) graph = MonsterGraph(database, debug_monster_ids) dungeon = DungeonContext(database) db_context = DbContext(database, graph, dungeon, debug_monster_ids) diff --git a/dadguide/database_manager.py b/dbcog/database_manager.py similarity index 94% rename from dadguide/database_manager.py rename to dbcog/database_manager.py index bb773986..4d521463 100644 --- a/dadguide/database_manager.py +++ b/dbcog/database_manager.py @@ -2,15 +2,15 @@ import logging -logger = logging.getLogger('red.padbot-cogs.dadguide.database_manager') +logger = logging.getLogger('red.padbot-cogs.dbcog.database_manager') -class DadguideTableNotFound(Exception): +class DBCogTableNotFound(Exception): def __init__(self, table_name): self.message = '{} not found'.format(table_name) -class DadguideDatabase(object): +class DBCogDatabase(object): def __init__(self, data_file): self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) self._con.row_factory = lite.Row @@ -99,7 +99,7 @@ def get_table_fields(self, table_name: str): pk = c['name'] fields.append(c['name']) if len(fields) == 0: - raise DadguideTableNotFound(table_name) + raise DBCogTableNotFound(table_name) return fields, pk diff --git a/dadguide/dadguide.py b/dbcog/dbcog.py similarity index 91% rename from dadguide/dadguide.py rename to dbcog/dbcog.py index d7293916..b2f2c745 100644 --- a/dadguide/dadguide.py +++ b/dbcog/dbcog.py @@ -30,11 +30,11 @@ from .models.enum_types import DEFAULT_SERVER, SERVERS from .monster_index import MonsterIndex -logger = logging.getLogger('red.padbot-cogs.dadguide') +logger = logging.getLogger('red.padbot-cogs.dbcog') def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='dadguide')), file_name) + return os.path.join(str(data_manager.cog_data_path(raw_name='dbcog')), file_name) try: @@ -60,8 +60,8 @@ def _data_file(file_name: str) -> str: pass -class Dadguide(commands.Cog, IdTest): - """Dadguide database manager""" +class DBCog(commands.Cog, IdTest): + """Database manager""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -77,7 +77,7 @@ def __init__(self, bot, *args, **kwargs): self.fm_flags_default = {'na_prio': True, 'server': DEFAULT_SERVER} self.config = Config.get_conf(self, identifier=64667103) self.config.register_global(datafile='', indexlog=0, test_suite={}, fluff_suite=[], typo_mods=[], - debug_mode=False, debug_mode_monsters = [3260]) + debug_mode=False, debug_mode_monsters=[3260]) self.config.register_user(lastaction=None, fm_flags={}) self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') @@ -95,17 +95,17 @@ def __init__(self, bot, *args, **kwargs): GADMIN_COG.register_perm("contentadmin") async def wait_until_ready(self) -> None: - """Wait until the Dadguide cog is ready. + """Wait until the DBCog cog is ready. - Call this from other cogs to wait until Dadguide finishes refreshing its database + Call this from other cogs to wait until DBCog finishes refreshing its database for the first time. """ await self._is_ready.wait() async def wait_until_ready_verbose(self, ctx) -> None: - """Wait until the Dadguide cog is ready but loudly. + """Wait until the DBCog cog is ready but loudly. - Call this from other cogs to wait until Dadguide finishes refreshing its database + Call this from other cogs to wait until DBCog finishes refreshing its database for the first time. """ if self._is_ready.is_set(): @@ -177,7 +177,7 @@ async def check_index(self): if not any(channels): channels = [owner for oid in self.bot.owner_ids if (owner := self.bot.get_user(oid))] for channel in channels: - await channel.send("Use `{}dadguide setfailurechannel `" + await channel.send("Use `{}dbcog setfailurechannel `" " to move these out of your DMs!" "".format((await self.bot.get_valid_prefixes())[0])) for page in pagify(f"Load Warnings:\n" + "\n".join(issues[:100])): @@ -190,7 +190,7 @@ def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> Mo def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty - logger.info('Unloading Dadguide') + logger.info('Unloading DBCog') if self.database: self.database.close() self.database = None @@ -203,22 +203,22 @@ async def reload_data_task(self): if self.database and self.database.has_database(): logger.info('Using stored database at load') - while self == self.bot.get_cog('Dadguide'): + while self == self.bot.get_cog('DBCog'): short_wait = False try: async with tsutils.StatusManager(self.bot): await self.download_and_refresh_nicknames() - logger.info('Done refreshing Dadguide, triggering ready') + logger.info('Done refreshing DBCog, triggering ready') self._is_ready.set() except Exception as ex: short_wait = True - logger.exception("dadguide data download/refresh failed: %s", ex) + logger.exception("dbcog data download/refresh failed: %s", ex) try: wait_time = 60 if short_wait else 60 * 60 * 4 await asyncio.sleep(wait_time) except Exception as ex: - logger.exception("dadguide data wait loop failed: %s", ex) + logger.exception("dbcog data wait loop failed: %s", ex) raise async def download_and_refresh_nicknames(self): @@ -242,23 +242,23 @@ async def _download_files(self): @commands.group() @checks.is_owner() - async def dadguide(self, ctx): - """Dadguide database settings""" + async def dbcog(self, ctx): + """DBCog database settings""" - @dadguide.command() + @dbcog.command() @checks.is_owner() async def setdatafile(self, ctx, data_file): - """Set a local path to dadguide data instead of downloading it.""" + """Set a local path to dbcog data instead of downloading it.""" await self.config.datafile.set(data_file) await ctx.tick() - @dadguide.command() + @dbcog.command() @checks.is_owner() async def setfailurechannel(self, ctx, channel: discord.TextChannel): await self.config.indexlog.set(channel.id) await ctx.tick() - @dadguide.group(aliases=["debug"]) + @dbcog.group(aliases=["debug"]) @checks.is_owner() async def debugmode(self, ctx): """Tsubaki-Only Mode settings""" @@ -272,11 +272,11 @@ async def debug_enable(self, ctx, enabled: bool): f" Only **Tsubaki** will be available in the graph and id" f" queries, to allow for a faster restart time. To turn" f" off **Tsubaki mode** run" - f" `{ctx.prefix}dadguide debugmode false`.") + f" `{ctx.prefix}dbcog debugmode false`.") else: await tsutils.send_confirmation_message(ctx, f"You have turned off **Tsubaki mode**. Your bot will behave" f" normally. To return to **Tsubaki mode**, run" - f" `{ctx.prefix}dadguide debugmode true.`") + f" `{ctx.prefix}dbcog debugmode true.`") await self.forceindexreload(ctx) @debugmode.group(name="monsters") diff --git a/dadguide/dungeon_context.py b/dbcog/dungeon_context.py similarity index 94% rename from dadguide/dungeon_context.py rename to dbcog/dungeon_context.py index 57268de7..f831fb47 100644 --- a/dadguide/dungeon_context.py +++ b/dbcog/dungeon_context.py @@ -1,11 +1,11 @@ from typing import Optional, List -from dadguide.database_manager import DadguideDatabase -from dadguide.models.dungeon_model import DungeonModel -from dadguide.models.encounter_model import EncounterModel -from dadguide.models.enemy_data_model import EnemyDataModel -from dadguide.models.enemy_skill_model import EnemySkillModel -from dadguide.models.sub_dungeon_model import SubDungeonModel +from dbcog.database_manager import DBCogDatabase +from dbcog.models.dungeon_model import DungeonModel +from dbcog.models.encounter_model import EncounterModel +from dbcog.models.enemy_data_model import EnemyDataModel +from dbcog.models.enemy_skill_model import EnemySkillModel +from dbcog.models.sub_dungeon_model import SubDungeonModel NICKNAME_QUERY = ''' SELECT @@ -195,7 +195,7 @@ class DungeonContext(object): - def __init__(self, database: DadguideDatabase): + def __init__(self, database: DBCogDatabase): self.database = database def get_dungeons_from_name(self, name: str) -> List[DungeonModel]: diff --git a/dadguide/errors.py b/dbcog/errors.py similarity index 100% rename from dadguide/errors.py rename to dbcog/errors.py diff --git a/dadguide/find_monster.py b/dbcog/find_monster.py similarity index 93% rename from dadguide/find_monster.py rename to dbcog/find_monster.py index 84ac55eb..11fe65a7 100644 --- a/dadguide/find_monster.py +++ b/dbcog/find_monster.py @@ -8,8 +8,8 @@ from tsutils.query_settings import QuerySettings from tsutils.enums import Server -from dadguide.find_monster_tokens import Token, SPECIAL_TOKEN_TYPES -from dadguide.models.monster_model import MonsterModel +from dbcog.find_monster_tokens import Token, SPECIAL_TOKEN_TYPES +from dbcog.models.monster_model import MonsterModel SERIES_TYPE_PRIORITY = { "regular": 4, @@ -41,15 +41,15 @@ class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 - def __init__(self, dgcog, flags: Dict[str, Any]): - self.dgcog = dgcog + def __init__(self, dbcog, flags: Dict[str, Any]): + self.dbcog = dbcog self.flags = flags - self.index = self.dgcog.indexes[Server(flags['server'])] + self.index = self.dbcog.indexes[Server(flags['server'])] def _process_settings(self, original_query: str) -> str: query_settings = QuerySettings.extract(self.flags, original_query) - self.index = self.dgcog.indexes[query_settings.server] + self.index = self.dbcog.indexes[query_settings.server] return re.sub(r'\s*(--|—)\w+(:{.+?})?\s*', ' ', original_query) @@ -105,7 +105,7 @@ def _string_to_token(self, value: str) -> Token: if re.fullmatch(r'.+-.+', value): for special in SPECIAL_TOKEN_TYPES: if re.fullmatch(special.RE_MATCH, value): - return special(value, negated=negated, exact=exact, database=self.dgcog.database) + return special(value, negated=negated, exact=exact, database=self.dbcog.database) return Token(value, negated=negated, exact=exact) def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: @@ -164,7 +164,7 @@ def _process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) if matched_mons is not None: matched_mons.difference_update(invalid) else: - matched_mons = set(self.dgcog.database.get_all_monsters(self.index.server)).difference(invalid) + matched_mons = set(self.dbcog.database.get_all_monsters(self.index.server)).difference(invalid) else: valid = self._get_valid_monsters_from_name_token(name_token, matches) if matched_mons is not None: @@ -223,7 +223,7 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = tokenized_query = [] return (matches[monster].score, - not self.dgcog.database.graph.monster_is_evo_gem(monster), + not self.dbcog.database.graph.monster_is_evo_gem(monster), # Don't deprio evos with new modifier not monster.is_equip if not {m[0] for m in matches[monster].mod}.intersection( {'new', 'base'}) else True, @@ -231,10 +231,10 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = bool(monster.monster_id > 10000 and re.search(r"\d{4}", " ".join(tokenized_query))), SERIES_TYPE_PRIORITY.get(monster.series.series_type), monster.on_na if monster.series.series_type == "collab" else True, - self.dgcog.database.graph.monster_is_rem_evo(monster), + self.dbcog.database.graph.monster_is_rem_evo(monster), not all(t.value in [0, 12, 14, 15] for t in monster.types), not any(t.value in [0, 12, 14, 15] for t in monster.types), - -self.dgcog.database.graph.get_base_id(monster), + -self.dbcog.database.graph.get_base_id(monster), monster.on_na if self.flags['na_prio'] else True, not monster.is_equip, monster.rarity, @@ -243,7 +243,7 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: monster_evos = set() for monster in sorted(matched_mons, key=lambda m: matches[m].score, reverse=True): - for evo in self.dgcog.database.graph.get_alt_monsters(monster): + for evo in self.dbcog.database.graph.get_alt_monsters(monster): monster_evos.add(evo) if matches[evo].score < matches[monster].score: matches[evo].name = {(t[0], t[1], @@ -270,7 +270,7 @@ async def _find_monster_search(self, tokenized_query: List[str]) -> \ for mod_token in mod_tokens: if mod_token.value not in self.index.all_modifiers: - async with self.dgcog.config.typo_mods() as typo_mods: + async with self.dbcog.config.typo_mods() as typo_mods: typo_mods.append(mod_token.value) matches = defaultdict(MonsterMatch) @@ -282,7 +282,7 @@ async def _find_monster_search(self, tokenized_query: List[str]) -> \ matched_mons = self._get_monster_evos(matched_mons, matches) else: # There are no name tokens in the query - matched_mons = {*self.dgcog.database.get_all_monsters(self.index.server)} + matched_mons = {*self.dbcog.database.get_all_monsters(self.index.server)} monster_score = defaultdict(int) # Expand search to the evo tree @@ -302,7 +302,7 @@ async def find_monster_debug(self, query: str) -> \ This gives info that isn't necessary for non-debug functions. Consider using findmonster or findmonsters instead.""" - await self.dgcog.wait_until_ready() + await self.dbcog.wait_until_ready() query = rmdiacritics(query).lower().replace(",", "") query = re.sub(r'(\s|^)\'(\S+)\'(\s|$)', r'\1"\2"\3', query) # Replace ' with " around tokens @@ -317,8 +317,8 @@ async def find_monster_debug(self, query: str) -> \ key=lambda t: t[1].get(t[0], MonsterMatch()).score ) - self.dgcog.historic_lookups[query] = best_monster.monster_id if best_monster else -1 - json.dump(self.dgcog.historic_lookups, open(self.dgcog.historic_lookups_file_path, "w+")) + self.dbcog.historic_lookups[query] = best_monster.monster_id if best_monster else -1 + json.dump(self.dbcog.historic_lookups, open(self.dbcog.historic_lookups_file_path, "w+")) return best_monster, matches_dict, valid_monsters, 0 diff --git a/dadguide/find_monster_tokens.py b/dbcog/find_monster_tokens.py similarity index 93% rename from dadguide/find_monster_tokens.py rename to dbcog/find_monster_tokens.py index b7869f44..d2beb6ea 100644 --- a/dadguide/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -1,8 +1,8 @@ import re -from dadguide.models.monster_model import MonsterModel -from dadguide.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP -from dadguide.models.enum_types import AwokenSkills +from dbcog.models.monster_model import MonsterModel +from dbcog.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP +from dbcog.models.enum_types import AwokenSkills def regexlist(tokens): diff --git a/dadguide/idtest_mixin.py b/dbcog/idtest_mixin.py similarity index 99% rename from dadguide/idtest_mixin.py rename to dbcog/idtest_mixin.py index c1a2027d..695e011a 100644 --- a/dadguide/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -7,8 +7,8 @@ from redbot.core import commands, Config, checks from redbot.core.utils.chat_formatting import box, pagify -from dadguide.models.enum_types import DEFAULT_SERVER -from dadguide.monster_index import MonsterIndex +from dbcog.models.enum_types import DEFAULT_SERVER +from dbcog.monster_index import MonsterIndex class IdTest: diff --git a/dadguide/info.json b/dbcog/info.json similarity index 87% rename from dadguide/info.json rename to dbcog/info.json index 3cae4915..e4666637 100644 --- a/dadguide/info.json +++ b/dbcog/info.json @@ -4,7 +4,7 @@ ], "description": "No commands, but loads all the monster data stored in Tsubaki.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]dadguide setdatafile if it's applicable to you.", + "install_msg": "Use [p]dbcog setdatafile if it's applicable to you.", "short": "Loads monster data (LOAD THIS FIRST)", "min_bot_version": "3.4.0", "max_bot_version": "0.0.0", diff --git a/dadguide/models/__init__.py b/dbcog/models/__init__.py similarity index 100% rename from dadguide/models/__init__.py rename to dbcog/models/__init__.py diff --git a/dadguide/models/active_skill_model.py b/dbcog/models/active_skill_model.py similarity index 100% rename from dadguide/models/active_skill_model.py rename to dbcog/models/active_skill_model.py diff --git a/dadguide/models/awakening_model.py b/dbcog/models/awakening_model.py similarity index 100% rename from dadguide/models/awakening_model.py rename to dbcog/models/awakening_model.py diff --git a/dadguide/models/awoken_skill_model.py b/dbcog/models/awoken_skill_model.py similarity index 100% rename from dadguide/models/awoken_skill_model.py rename to dbcog/models/awoken_skill_model.py diff --git a/dadguide/models/base_model.py b/dbcog/models/base_model.py similarity index 100% rename from dadguide/models/base_model.py rename to dbcog/models/base_model.py diff --git a/dadguide/models/dungeon_model.py b/dbcog/models/dungeon_model.py similarity index 100% rename from dadguide/models/dungeon_model.py rename to dbcog/models/dungeon_model.py diff --git a/dadguide/models/encounter_model.py b/dbcog/models/encounter_model.py similarity index 100% rename from dadguide/models/encounter_model.py rename to dbcog/models/encounter_model.py diff --git a/dadguide/models/enemy_data_model.py b/dbcog/models/enemy_data_model.py similarity index 100% rename from dadguide/models/enemy_data_model.py rename to dbcog/models/enemy_data_model.py diff --git a/dadguide/models/enemy_skill_model.py b/dbcog/models/enemy_skill_model.py similarity index 100% rename from dadguide/models/enemy_skill_model.py rename to dbcog/models/enemy_skill_model.py diff --git a/dadguide/models/enum_types.py b/dbcog/models/enum_types.py similarity index 100% rename from dadguide/models/enum_types.py rename to dbcog/models/enum_types.py diff --git a/dadguide/models/evolution_model.py b/dbcog/models/evolution_model.py similarity index 100% rename from dadguide/models/evolution_model.py rename to dbcog/models/evolution_model.py diff --git a/dadguide/models/leader_skill_model.py b/dbcog/models/leader_skill_model.py similarity index 100% rename from dadguide/models/leader_skill_model.py rename to dbcog/models/leader_skill_model.py diff --git a/dadguide/models/monster/__init__.py b/dbcog/models/monster/__init__.py similarity index 100% rename from dadguide/models/monster/__init__.py rename to dbcog/models/monster/__init__.py diff --git a/dadguide/models/monster/monster_difference.py b/dbcog/models/monster/monster_difference.py similarity index 98% rename from dadguide/models/monster/monster_difference.py rename to dbcog/models/monster/monster_difference.py index d88f7c36..140b6435 100644 --- a/dadguide/models/monster/monster_difference.py +++ b/dbcog/models/monster/monster_difference.py @@ -1,7 +1,7 @@ from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class MonsterDifference: diff --git a/dadguide/models/monster_model.py b/dbcog/models/monster_model.py similarity index 100% rename from dadguide/models/monster_model.py rename to dbcog/models/monster_model.py diff --git a/dadguide/models/monster_stats.py b/dbcog/models/monster_stats.py similarity index 99% rename from dadguide/models/monster_stats.py rename to dbcog/models/monster_stats.py index 7cb6b2a7..63f7ce41 100644 --- a/dadguide/models/monster_stats.py +++ b/dbcog/models/monster_stats.py @@ -1,5 +1,5 @@ from typing import Literal -from dadguide.models.enum_types import AwokenSkills +from dbcog.models.enum_types import AwokenSkills StatType = Literal['hp', 'atk', 'rcv'] diff --git a/dadguide/models/scheduled_event_model.py b/dbcog/models/scheduled_event_model.py similarity index 100% rename from dadguide/models/scheduled_event_model.py rename to dbcog/models/scheduled_event_model.py diff --git a/dadguide/models/series_model.py b/dbcog/models/series_model.py similarity index 100% rename from dadguide/models/series_model.py rename to dbcog/models/series_model.py diff --git a/dadguide/models/sub_dungeon_model.py b/dbcog/models/sub_dungeon_model.py similarity index 100% rename from dadguide/models/sub_dungeon_model.py rename to dbcog/models/sub_dungeon_model.py diff --git a/dadguide/monster_graph.py b/dbcog/monster_graph.py similarity index 99% rename from dadguide/monster_graph.py rename to dbcog/monster_graph.py index 3f892337..2d87818e 100644 --- a/dadguide/monster_graph.py +++ b/dbcog/monster_graph.py @@ -7,7 +7,7 @@ from networkx import MultiDiGraph from tsutils.enums import Server -from .database_manager import DadguideDatabase +from .database_manager import DBCogDatabase from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel @@ -19,7 +19,7 @@ from .models.monster.monster_difference import MonsterDifference from .models.series_model import SeriesModel -logger = logging.getLogger('red.padbot-cogs.dadguide') +logger = logging.getLogger('red.padbot-cogs.dbcog') MONSTER_QUERY = """SELECT monsters{0}.*, @@ -117,7 +117,7 @@ class MonsterGraph(object): - def __init__(self, database: DadguideDatabase, debug_monster_ids: Optional[List[int]] = None): + def __init__(self, database: DBCogDatabase, debug_monster_ids: Optional[List[int]] = None): self.issues = [] self.debug_monster_ids = debug_monster_ids diff --git a/dadguide/monster_index.py b/dbcog/monster_index.py similarity index 99% rename from dadguide/monster_index.py rename to dbcog/monster_index.py index d0d11e77..ce723ada 100644 --- a/dadguide/monster_index.py +++ b/dbcog/monster_index.py @@ -25,7 +25,7 @@ MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('2089525837') TREE_MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('1372419168') -logger = logging.getLogger('red.pad-cogs.dadguide.monster_index') +logger = logging.getLogger('red.pad-cogs.dbcog.monster_index') class MonsterIndex(tsutils.aobject): diff --git a/dadguide/token_mappings.py b/dbcog/token_mappings.py similarity index 100% rename from dadguide/token_mappings.py rename to dbcog/token_mappings.py diff --git a/dungeon/dungeoncog.py b/dungeon/dungeoncog.py index 8818688f..e2e336f0 100644 --- a/dungeon/dungeoncog.py +++ b/dungeon/dungeoncog.py @@ -50,17 +50,17 @@ async def register_menu(self): async def get_menu_default_data(self, ims): data = { - 'dgcog': await self.get_dgcog(), + 'dbcog': await self.get_dbcog(), 'color': Color.default() } return data - async def get_dgcog(self): - dgcog = self.bot.get_cog("Dadguide") - if dgcog is None: - raise ValueError("Dadguide cog is not loaded") - await dgcog.wait_until_ready() - return dgcog + async def get_dbcog(self): + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: + raise ValueError("DBCog cog is not loaded") + await dbcog.wait_until_ready() + return dbcog async def find_dungeon_from_name2(self, ctx, name: str, database: "DungeonContext", difficulty: str = None): """ @@ -121,14 +121,14 @@ async def dgid(self, ctx, name: str, difficulty: str = None): Name: Name of Dungeon Difficulty: Difficulty level/name of floor (eg. for A1, "Bipolar Goddess") ''' - # load dadguide cog for database access - dgcog = self.bot.get_cog('Dadguide') - if not dgcog: - logger.warning("Cog 'Dadguide' not loaded") + # load dbcog cog for database access + dbcog = self.bot.get_cog('DBCog') + if not dbcog: + logger.warning("Cog 'DBCog' not loaded") return logger.info('Waiting until DG is ready') - await dgcog.wait_until_ready() - dungeon = await self.find_dungeon_from_name2(ctx=ctx, name=name, database=dgcog.database.dungeon, + await dbcog.wait_until_ready() + dungeon = await self.find_dungeon_from_name2(ctx=ctx, name=name, database=dbcog.database.dungeon, difficulty=difficulty) if dungeon is not None: @@ -147,7 +147,7 @@ async def dgid(self, ctx, name: str, difficulty: str = None): behavior_test = None # await ctx.send(formatOverview(test_result)) - # pm = process_monster(behavior_test, enc_model, dg_cog.database) + # pm = process_monster(behavior_test, enc_model, db_cog.database) if enc_model.stage < 0: # pm.am_invade = True invades.append(enc_model) @@ -169,7 +169,7 @@ async def dgid(self, ctx, name: str, difficulty: str = None): view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, Color.default(), pm_dungeon[0][0], dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, len(pm_dungeon[0]), 0, - int(dungeon.sub_dungeons[0].technical), dgcog.database, verbose=False, + int(dungeon.sub_dungeons[0].technical), dbcog.database, verbose=False, reaction_list=full_reaction_list) await ctx.send( "EN: {}({})\nJP: {}({})".format(dungeon.name_en, dungeon.sub_dungeons[0].name_en, dungeon.name_ja, diff --git a/dungeon/menu/dungeon.py b/dungeon/menu/dungeon.py index 56390506..c4d866d5 100644 --- a/dungeon/menu/dungeon.py +++ b/dungeon/menu/dungeon.py @@ -23,17 +23,17 @@ def menu(): @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') - view_state = await DungeonViewState.deserialize(dgcog, color, ims) + view_state = await DungeonViewState.deserialize(dbcog, color, ims) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_verbose(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') - view_state = await DungeonViewState.deserialize(dgcog, color, ims, verbose_toggle=True) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, verbose_toggle=True) control = DungeonMenu.message_control(view_state) return control @@ -43,46 +43,46 @@ async def respond_with_delete(message: Optional[Message], ims, **data): @staticmethod async def respond_with_previous_monster(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, -1) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, -1) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_monster(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, 1) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, 1) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_previous_floor(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, -1, 0, reset_spawn=True) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, -1, 0, reset_spawn=True) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_floor(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, 1, 0, reset_spawn=True) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, 1, 0, reset_spawn=True) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_page(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] page = ims.get('page') if page == 0: page = 1 else: page = 0 - view_state = await DungeonViewState.deserialize(dgcog, color, ims, page=page) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, page=page) control = DungeonMenu.message_control(view_state) return control diff --git a/dungeon/menu/simple.py b/dungeon/menu/simple.py index a4bf7d56..2a877136 100644 --- a/dungeon/menu/simple.py +++ b/dungeon/menu/simple.py @@ -24,9 +24,9 @@ def menu(): @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') - view_state = await SimpleViewState.deserialize(dgcog, color, ims) + view_state = await SimpleViewState.deserialize(dbcog, color, ims) control = SimpleMenu.message_control(view_state) return control @@ -36,10 +36,10 @@ async def respond_with_delete(message: Optional[Message], ims, **data): @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') print("test") - view_state = await SimpleViewState.deserialize(dgcog, color, ims, 1) + view_state = await SimpleViewState.deserialize(dbcog, color, ims, 1) control = SimpleMenu.message_control(view_state) return control diff --git a/dungeon/processors.py b/dungeon/processors.py index 76cad34d..6ecb0094 100644 --- a/dungeon/processors.py +++ b/dungeon/processors.py @@ -1,8 +1,8 @@ from discordmenu.emoji.emoji_cache import emoji_cache -from dadguide.database_context import DbContext -from dadguide.models.encounter_model import EncounterModel -from dadguide.models.enemy_skill_model import EnemySkillModel +from dbcog.database_context import DbContext +from dbcog.models.encounter_model import EncounterModel +from dbcog.models.enemy_skill_model import EnemySkillModel from dungeon.safe_dict import SafeDict from dungeon.dungeon_monster import DungeonMonster from dungeon.enemy_skill import ProcessedSkill diff --git a/dungeon/view/dungeon.py b/dungeon/view/dungeon.py index b389d936..44756a67 100644 --- a/dungeon/view/dungeon.py +++ b/dungeon/view/dungeon.py @@ -45,7 +45,7 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, + async def deserialize(cls, dbcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, verbose_toggle: bool = False, page: int = 0, reset_spawn: bool = False): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] @@ -68,7 +68,7 @@ async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_inde verbose = not verbose # get encounter models for the floor - floor_models = dgcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) + floor_models = dbcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) # check if we are on final monster of the floor if floor_index >= len(floor_models): @@ -85,7 +85,7 @@ async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_inde return cls(original_author_id, menu_type, raw_query, color, encounter_model, sub_dungeon_id, num_floors, floor, len(floor_models), floor_index, - technical, dgcog.database, verbose=verbose, + technical, dbcog.database, verbose=verbose, reaction_list=ims.get('reaction_list'), page=page) diff --git a/dungeon/view/simple.py b/dungeon/view/simple.py index d6b52d58..d028277c 100644 --- a/dungeon/view/simple.py +++ b/dungeon/view/simple.py @@ -26,7 +26,7 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, color, ims: dict, inc): + async def deserialize(cls, dbcog, color, ims: dict, inc): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] raw_query = ims.get('raw_query') diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 6c3a53d3..f7db0b45 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -12,7 +12,7 @@ from dungeoncog.view.dungeon import DungeonViewState if TYPE_CHECKING: - from dadguide.dungeon_context import DungeonContext + from dbcog.dungeon_context import DungeonContext logger = logging.getLogger('red.padbot-cogs.dungeoncog') EMBED_NOT_GENERATED = -1 @@ -45,17 +45,17 @@ async def register_menu(self): async def get_menu_default_data(self, ims): data = { - 'dgcog': await self.get_dgcog(), + 'dbcog': await self.get_dbcog(), 'color': Color.default() } return data - async def get_dgcog(self): - dgcog = self.bot.get_cog("Dadguide") - if dgcog is None: - raise ValueError("Dadguide cog is not loaded") - await dgcog.wait_until_ready() - return dgcog + async def get_dbcog(self): + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: + raise ValueError("DBCog cog is not loaded") + await dbcog.wait_until_ready() + return dbcog async def find_dungeon_from_name2(self, ctx, name, database: "DungeonContext", difficulty: str = None): """ @@ -102,9 +102,9 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): " arguments with spaces in quotes.") return - # load dadguide cog for database access - dgcog = await self.get_dgcog() - dungeon = await self.find_dungeon_from_name2(ctx, name, dgcog.database.dungeon, difficulty) + # load dbcog cog for database access + dbcog = await self.get_dbcog() + dungeon = await self.find_dungeon_from_name2(ctx, name, dbcog.database.dungeon, difficulty) if dungeon is not None: # await ctx.send(format_overview(test_result)) @@ -122,7 +122,7 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): behavior_test = None # await ctx.send(format_overview(test_result)) - # pm = process_monster(behavior_test, enc_model, dg_cog.database) + # pm = process_monster(behavior_test, enc_model, db_cog.database) if enc_model.stage < 0: # pm.am_invade = True invades.append(enc_model) @@ -143,7 +143,7 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, Color.default(), pm_dungeon[0][0], dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, len(pm_dungeon[0]), 0, - int(dungeon.sub_dungeons[0].technical), dgcog.database, verbose=False) + int(dungeon.sub_dungeons[0].technical), dbcog.database, verbose=False) await ctx.send( "EN: {}({})\nJP: {}({})".format(dungeon.name_en, dungeon.sub_dungeons[0].name_en, dungeon.name_ja, dungeon.sub_dungeons[0].name_ja)) diff --git a/dungeoncog/info.json b/dungeoncog/info.json index 27d3bc43..46883343 100644 --- a/dungeoncog/info.json +++ b/dungeoncog/info.json @@ -17,7 +17,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", "menulistener": "https://github.com/TsubakiBotPad/misc-cogs" }, "requirements": [ diff --git a/dungeoncog/menu/dungeon.py b/dungeoncog/menu/dungeon.py index 4252500d..0e65db2e 100644 --- a/dungeoncog/menu/dungeon.py +++ b/dungeoncog/menu/dungeon.py @@ -24,17 +24,17 @@ def menu(): @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') - view_state = await DungeonViewState.deserialize(dgcog, color, ims) + view_state = await DungeonViewState.deserialize(dbcog, color, ims) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_verbose(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') - view_state = await DungeonViewState.deserialize(dgcog, color, ims, verbose_toggle=True) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, verbose_toggle=True) control = DungeonMenu.message_control(view_state) return control @@ -44,46 +44,46 @@ async def respond_with_delete(message: Optional[Message], ims, **data): @staticmethod async def respond_with_previous_monster(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, -1) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, -1) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_monster(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, 0, 1) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, 1) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_previous_floor(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, -1, 0, reset_spawn=True) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, -1, 0, reset_spawn=True) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_floor(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] - view_state = await DungeonViewState.deserialize(dgcog, color, ims, 1, 0, reset_spawn=True) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, 1, 0, reset_spawn=True) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_page(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] color = data['color'] page = ims.get('page') if page == 0: page = 1 else: page = 0 - view_state = await DungeonViewState.deserialize(dgcog, color, ims, page=page) + view_state = await DungeonViewState.deserialize(dbcog, color, ims, page=page) control = DungeonMenu.message_control(view_state) return control diff --git a/dungeoncog/menu/simple.py b/dungeoncog/menu/simple.py index 897ecd47..ba714224 100644 --- a/dungeoncog/menu/simple.py +++ b/dungeoncog/menu/simple.py @@ -24,9 +24,9 @@ def menu(): @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') - view_state = await SimpleViewState.deserialize(dgcog, color, ims, 0) + view_state = await SimpleViewState.deserialize(dbcog, color, ims, 0) control = SimpleMenu.message_control(view_state) return control @@ -36,9 +36,9 @@ async def respond_with_delete(message: Optional[Message], ims, **data): @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') color = data.get('color') - view_state = await SimpleViewState.deserialize(dgcog, color, ims, 1) + view_state = await SimpleViewState.deserialize(dbcog, color, ims, 1) control = SimpleMenu.message_control(view_state) return control diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index 03ac4a36..7aa13e89 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -8,9 +8,9 @@ from dungeoncog.grouped_skillls import GroupedSkills if TYPE_CHECKING: - from dadguide.models.enemy_skill_model import EnemySkillModel - from dadguide.database_context import DbContext - from dadguide.models.encounter_model import EncounterModel + from dbcog.models.enemy_skill_model import EnemySkillModel + from dbcog.database_context import DbContext + from dbcog.models.encounter_model import EncounterModel GROUP_TYPES = { 0: "Unspecified", diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index fb1b6ee6..839c75d0 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -13,7 +13,7 @@ from dungeoncog.processors import process_monster if TYPE_CHECKING: - from dadguide.models.encounter_model import EncounterModel + from dbcog.models.encounter_model import EncounterModel class DungeonViewState(ViewState): @@ -48,7 +48,7 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, + async def deserialize(cls, dbcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, verbose_toggle: bool = False, page: int = 0, reset_spawn: bool = False): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] @@ -71,7 +71,7 @@ async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_inde verbose = not verbose # get encounter models for the floor - floor_models = dgcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) + floor_models = dbcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) # check if we are on final monster of the floor if floor_index >= len(floor_models): @@ -88,7 +88,7 @@ async def deserialize(cls, dgcog, color, ims: dict, inc_floor: int = 0, inc_inde return cls(original_author_id, menu_type, raw_query, color, encounter_model, sub_dungeon_id, num_floors, floor, len(floor_models), floor_index, - technical, dgcog.database, verbose=verbose, page=page) + technical, dbcog.database, verbose=verbose, page=page) class DungeonView: diff --git a/dungeoncog/view/simple.py b/dungeoncog/view/simple.py index ecbe2c8a..5da677cd 100644 --- a/dungeoncog/view/simple.py +++ b/dungeoncog/view/simple.py @@ -24,7 +24,7 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, color, ims: dict, inc): + async def deserialize(cls, dbcog, color, ims: dict, inc): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] raw_query = ims.get('raw_query') diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 513c5465..740d91e3 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -42,9 +42,9 @@ async def on_message(self, message): return if await self.is_command(message): # skip commands return - dgcog = self.bot.get_cog("Dadguide") - if dgcog is None: - await channel.send("Error: Dadguide Cog not loaded. Please alert a bot owner.") + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: + await channel.send("Error: DBCog Cog not loaded. Please alert a bot owner.") return if re.search(r'\b\d{3}[ -,]{0,2}\d{3}[ -,]{0,2}\d{3}\b', content): # friend code return @@ -56,7 +56,7 @@ async def on_message(self, message): for i in matches: if(i=="100"): # skip when people say "is 100 mp or over" ~~and ryan's posts~~ continue - m = await dgcog.find_monster(i, message.author.id) + m = await dbcog.find_monster(i, message.author.id) if not m: # monster not found continue ret += "[{}] {}\n".format(i, m.name_en) diff --git a/padbuildimg/info.json b/padbuildimg/info.json index 91176506..86ece959 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -17,7 +17,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index db75b966..64b28f3a 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -441,7 +441,7 @@ class PadBuildImageGenerator(object): def __init__(self, params, ctx, build_name='pad_build'): self.params = params self.ctx = ctx - self.dgcog = ctx.bot.get_cog("Dadguide") + self.dbcog = ctx.bot.get_cog("DBCog") self.lexer = PaDTeamLexer().build() self.build = { 'NAME': build_name, @@ -512,7 +512,7 @@ async def process_card(self, card_str, is_assist=False): for tok in iter(self.lexer.token, None): if tok.type == 'ASSIST': assist_str = tok.value - ass_card = await self.dgcog.find_monster(tok.value, self.ctx.author.id) + ass_card = await self.dbcog.find_monster(tok.value, self.ctx.author.id) if ass_card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') elif tok.type == 'REPEAT': @@ -522,7 +522,7 @@ async def process_card(self, card_str, is_assist=False): result_card['ID'] = DELAY_BUFFER card = DELAY_BUFFER else: - card = await self.dgcog.find_monster(tok.value, self.ctx.author.id) + card = await self.dbcog.find_monster(tok.value, self.ctx.author.id) if card is None: raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') if not card.is_inheritable: @@ -853,7 +853,7 @@ async def refreshassets(self, ctx): Refresh assets folder """ await ctx.send('Downloading assets to {}'.format(self.settings.buildImgParams().ASSETS_DIR)) - awk_ids = self.bot.get_cog('Dadguide').database.get_awoken_skill_ids() + awk_ids = self.bot.get_cog('DBCog').database.get_awoken_skill_ids() await self.settings.downloadAllAssets(awk_ids) await ctx.tick() diff --git a/padevents/info.json b/padevents/info.json index b514c892..c119b5dd 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ "tsutils>=4.0.0", diff --git a/padevents/padevents.py b/padevents/padevents.py index 1349eeee..49542fd2 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -19,7 +19,7 @@ YES_EMOJI, NO_EMOJI, send_confirmation_message, send_cancellation_message if TYPE_CHECKING: - from dadguide.models.scheduled_event_model import ScheduledEventModel + from dbcog.models.scheduled_event_model import ScheduledEventModel def user_is_donor(ctx, only_patron=False): @@ -90,14 +90,14 @@ async def reload_padevents(self): await asyncio.sleep(60 * 60 * 1) async def refresh_data(self): - dgcog = self.bot.get_cog('Dadguide') - await dgcog.wait_until_ready() - scheduled_events = dgcog.database.get_all_events() + dbcog = self.bot.get_cog('DBCog') + await dbcog.wait_until_ready() + scheduled_events = dbcog.database.get_all_events() new_events = [] for se in scheduled_events: try: - db_context = self.bot.get_cog("Dadguide").database + db_context = self.bot.get_cog("DBCog").database new_events.append(Event(se, db_context)) except Exception as ex: logger.exception("Refresh error:") @@ -238,10 +238,10 @@ async def testevent(self, ctx, server, seconds: int = 0): await ctx.send("Unsupported server, pick one of NA, KR, JP") return - dgcog = self.bot.get_cog('Dadguide') - await dgcog.wait_until_ready() + dbcog = self.bot.get_cog('DBCog') + await dbcog.wait_until_ready() # TODO: Don't use this awful importing hack - dg_module = __import__('.'.join(dgcog.__module__.split('.')[:-1]) + ".models.scheduled_event_model") + dg_module = __import__('.'.join(dbcog.__module__.split('.')[:-1]) + ".models.scheduled_event_model") timestamp = int((datetime.datetime.now(pytz.utc) + timedelta(seconds=seconds)).timestamp()) self.fake_uid -= 1 @@ -259,7 +259,7 @@ async def testevent(self, ctx, server, seconds: int = 0): dungeon_id=1, ) ) - self.events.append(Event(te, self.bot.get_cog('Dadguide').database)) + self.events.append(Event(te, self.bot.get_cog('DBCog').database)) await ctx.tick() @padevents.command() diff --git a/padglobal/info.json b/padglobal/info.json index 1814d4df..070d75e9 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index b17193b6..01ca2d67 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -595,15 +595,15 @@ async def bosslist(self, ctx): await ctx.author.send(page) async def lookup_boss(self, term, ctx): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') - m = await dgcog.find_monster(term, ctx.author.id) + m = await dbcog.find_monster(term, ctx.author.id) if m is None: return None, None - m = dgcog.database.graph.get_base_monster(m) + m = dbcog.database.graph.get_base_monster(m) name = pdicog.get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() monster_id = m.monster_id definition = self.settings.boss().get(monster_id, None) @@ -612,11 +612,11 @@ async def lookup_boss(self, term, ctx): async def boss_to_text(self, ctx): bosses = self.settings.boss() - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() msg = '__**Available PAD Boss Mechanics (also check out {0}pad / {0}padfaq / {0}boards / {0}which / {0}glossary)**__'.format( ctx.prefix) for term in sorted(bosses.keys()): - m = await dgcog.find_monster(str(term), ctx.author.id) + m = await dbcog.find_monster(str(term), ctx.author.id) if not m: # monster not found continue msg += '\n[{}] {}'.format(term, m.name_en) @@ -648,16 +648,16 @@ async def pboss_edit(self, ctx, term, *, definition): await self._pboss_add(ctx, term, definition, False) async def _pboss_add(self, ctx, term, definition, need_confirm=True): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() - m = await dgcog.find_monster(term, ctx.author.id) + m = await dbcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(f"No monster found for `{term}`") return - base = dgcog.database.graph.get_base_monster(m) + base = dbcog.database.graph.get_base_monster(m) op = 'edited' if base.monster_id in self.settings.boss() else 'added' if op == 'edited' and need_confirm: @@ -673,16 +673,16 @@ async def _pboss_add(self, ctx, term, definition, need_confirm=True): @pboss.command(name='remove', aliases=['rm', 'delete', 'del']) async def pboss_remove(self, ctx, *, term): """Removes a set of boss mechanics.""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() - m = await dgcog.find_monster(term, ctx.author.id) + m = await dbcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(f"No monster found for `{term}`. Make sure you didn't use quotes.") return - base = dgcog.database.graph.get_base_monster(m) + base = dbcog.database.graph.get_base_monster(m) if base.monster_id not in self.settings.boss(): await ctx.send("Boss mechanics item doesn't exist.") @@ -722,12 +722,12 @@ async def which(self, ctx, *, term: str = None): await menu.create(ctx, state) async def _resolve_which(self, ctx, term): - dgcog = await self.get_dgcog() - db_context = dgcog.database + dbcog = await self.get_dbcog() + db_context = dbcog.database padinfo = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') - m = await dgcog.find_monster(term, ctx.author.id) + m = await dbcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(inline('No monster matched that query')) return None, None, None, None @@ -745,7 +745,7 @@ async def _resolve_which(self, ctx, term): if definition is not None: return name, definition, timestamp, True - monster = dgcog.get_monster(monster_id) + monster = dbcog.get_monster(monster_id) if db_context.graph.monster_is_mp_evo(monster) and not monster.in_rem: return name, MP_BUY_MSG.format(ctx.prefix), None, False @@ -777,7 +777,7 @@ async def whichto(self, ctx, to_user: discord.Member, *, term: str): async def which_to_text(self): monsters = defaultdict(list) for monster_id in self.settings.which(): - m = (await self.get_dgcog()).get_monster(monster_id) + m = (await self.get_dbcog()).get_monster(monster_id) if m is None: continue name = m.name_en.split(", ")[-1] @@ -824,16 +824,16 @@ async def pwhich_edit(self, ctx, term, *, definition): await self._pwhich_add(ctx, term, definition, False) async def _pwhich_add(self, ctx, term, definition, need_confirm=True): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() - m = await dgcog.find_monster(term, ctx.author.id) + m = await dbcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(f"No monster found for `{term}`") return - base_monster = dgcog.database.graph.get_base_monster(m) + base_monster = dbcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -859,9 +859,9 @@ async def _pwhich_add(self, ctx, term, definition, need_confirm=True): @pwhich.command(name='remove', aliases=['rm', 'delete', 'del']) async def pwhich_remove(self, ctx, *, monster_id: int): """Removes an entry from the which monster evo list.""" - dgcog = await self.get_dgcog() - m = dgcog.get_monster(monster_id) - base_monster = dgcog.database.graph.get_base_monster(m) + dbcog = await self.get_dbcog() + m = dbcog.get_monster(monster_id) + base_monster = dbcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -888,16 +888,16 @@ async def pwhich_append(self, ctx, term: str, *, addition): await self._concatenate_which(ctx, term, 'append', addition) async def _concatenate_which(self, ctx, term: str, operation: str, addition): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() pdicog = self.bot.get_cog("PadInfo") term = term.lower() - m = await dgcog.find_monster(term, ctx.author.id) + m = await dbcog.find_monster(term, ctx.author.id) if m is None: await ctx.send(f"No monster found for `{term}`") return - base_monster = dgcog.database.graph.get_base_monster(m) + base_monster = dbcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -970,7 +970,7 @@ async def pwhich_list(self, ctx): for w in self.settings.which(): w %= 10000 - m = (await self.get_dgcog()).get_monster(w) + m = (await self.get_dbcog()).get_monster(w) name = m.name_en.split(', ')[-1] result = self.settings.which()[w] @@ -1210,16 +1210,16 @@ async def guide(self, ctx, *, term: str = None): await ctx.send(self.emojify(text)) async def get_guide_text(self, term: str, ctx): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() term = term.lower() if term in self.settings.dungeonGuide(): return term, self.settings.dungeonGuide()[term], None - m = await dgcog.find_monster(term, ctx.author.id) + m = await dbcog.find_monster(term, ctx.author.id) if m is None: return None, None, 'No dungeon or monster matched that query' - m = dgcog.database.graph.get_base_monster(m) + m = dbcog.database.graph.get_base_monster(m) name = m.name_en definition = self.settings.leaderGuide().get(m.monster_id, None) @@ -1241,7 +1241,7 @@ async def guide_to_text(self): msg += '\n\n__**Leader Guides**__' for monster_id, definition in self.settings.leaderGuide().items(): - m = (await self.get_dgcog()).get_monster(monster_id) + m = (await self.get_dbcog()).get_monster(monster_id) if m is None: continue name = m.name_en.split(', ')[-1].title() @@ -1326,8 +1326,8 @@ async def leader_edit(self, ctx, monster_id: int, *, definition: str): await self._leader_add(ctx, monster_id, definition, False) async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm=True): - m = (await self.get_dgcog()).get_monster(monster_id) - base_monster = (await self.get_dgcog()).database.graph.get_base_monster(m) + m = (await self.get_dbcog()).get_monster(monster_id) + base_monster = (await self.get_dbcog()).database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1344,9 +1344,9 @@ async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm= @leader.command(name='remove', aliases=['rm', 'delete', 'del']) async def leader_remove(self, ctx, monster_id: int): """Removes a leader guide from the [p]guide command""" - dgcog = await self.get_dgcog() - m = dgcog.get_monster(monster_id) - base_monster = dgcog.database.graph.get_base_monster(m) + dbcog = await self.get_dbcog() + m = dbcog.get_monster(monster_id) + base_monster = dbcog.database.graph.get_base_monster(m) if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) @@ -1419,10 +1419,10 @@ async def daychange(self, ctx): await ctx.send(msg) - async def get_dgcog(self): - dgcog = self.bot.get_cog("Dadguide") - await dgcog.wait_until_ready() - return dgcog + async def get_dbcog(self): + dbcog = self.bot.get_cog("DBCog") + await dbcog.wait_until_ready() + return dbcog def emojify(self, message): emojis = list() diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index f2395165..99a24317 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -2,8 +2,8 @@ from typing import Union, Optional, Literal, TYPE_CHECKING if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.models.enum_types import AwakeningRestrictedLatent, MonsterType + from dbcog.models.monster_model import MonsterModel + from dbcog.models.enum_types import AwakeningRestrictedLatent, MonsterType from discordmenu.emoji.emoji_cache import emoji_cache diff --git a/padinfo/common/external_links.py b/padinfo/common/external_links.py index 79f2fc98..18ca7db7 100644 --- a/padinfo/common/external_links.py +++ b/padinfo/common/external_links.py @@ -4,7 +4,7 @@ import tsutils if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 9c54bcbb..0e2e4402 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -47,7 +47,7 @@ class ButtonInfo: - def get_info(self, dgcog, monster_model): + def get_info(self, dbcog, monster_model): """ Usage: ^buttoninfo Vajrayasaka @@ -73,7 +73,7 @@ def get_info(self, dgcog, monster_model): sub_attr_multiplier = self._get_sub_attr_multiplier(monster_model) result = ButtonInfoResult() - result.main_damage = self._calculate_damage(dgcog, monster_model, max_level) + result.main_damage = self._calculate_damage(dbcog, monster_model, max_level) result.sub_damage = result.main_damage * sub_attr_multiplier result.total_damage = result.main_damage + result.sub_damage if slb_level is None: @@ -81,12 +81,12 @@ def get_info(self, dgcog, monster_model): result.sub_slb_damage = None result.total_slb_damage = None else: - result.main_slb_damage = self._calculate_damage(dgcog, monster_model, slb_level) + result.main_slb_damage = self._calculate_damage(dbcog, monster_model, slb_level) result.sub_slb_damage = result.main_slb_damage * sub_attr_multiplier result.total_slb_damage = result.main_slb_damage + result.sub_slb_damage result.main_damage_with_atk_latent = self._calculate_damage( - dgcog, monster_model, max_level, num_atkplus_latent=max_atk_latents) + dbcog, monster_model, max_level, num_atkplus_latent=max_atk_latents) result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent if slb_level is None: @@ -95,20 +95,20 @@ def get_info(self, dgcog, monster_model): result.total_damage_with_slb_atk_latent = None else: result.main_damage_with_slb_atk_latent = self._calculate_damage( - dgcog, monster_model, slb_level, num_atkplus2_latent=max_atk_latents) + dbcog, monster_model, slb_level, num_atkplus2_latent=max_atk_latents) result.sub_damage_with_slb_atk_latent = result.main_damage_with_slb_atk_latent * sub_attr_multiplier result.total_damage_with_slb_atk_latent = (result.main_damage_with_slb_atk_latent + result.sub_damage_with_slb_atk_latent) - result.card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dgcog, monster_model) - result.team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dgcog, monster_model) + result.card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dbcog, monster_model) + result.team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dbcog, monster_model) return result - def _calculate_damage(self, dgcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): - stat_latents = dgcog.MonsterStatModifierInput(num_atkplus=num_atkplus_latent, num_atkplus2=num_atkplus2_latent) + def _calculate_damage(self, dbcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): + stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=num_atkplus_latent, num_atkplus2=num_atkplus2_latent) stat_latents.num_atk_awakening = len( [x for x in monster_model.awakenings if x.awoken_skill_id == 1]) - dmg = dgcog.monster_stats.stat(monster_model, 'atk', level, + dmg = dbcog.monster_stats.stat(monster_model, 'atk', level, stat_latents=stat_latents, multiplayer=True) return int(round(dmg)) @@ -120,40 +120,40 @@ def _get_sub_attr_multiplier(self, monster_model): return 1 / 10 return 1 / 3 - def to_string(self, dgcog, monster, info): - card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dgcog, monster) - team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dgcog, monster) + def to_string(self, dbcog, monster, info): + card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dbcog, monster) + team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dbcog, monster) return INFO_STRING.format(monster.monster_id, monster.name_en, int(round(info.main_damage)), int(round(info.sub_damage)), int(round(info.total_damage)), int(round(info.main_damage_with_atk_latent)), int( round(info.sub_damage_with_atk_latent)), int(round(info.total_damage_with_atk_latent)), card_btn_str, team_btn_str) - def _get_card_btn_damage(self, card_buttons, dgcog, monster): + def _get_card_btn_damage(self, card_buttons, dbcog, monster): lines = [] card_buttons.sort(key=lambda x: x.mult) for card in card_buttons: - inherit_model = dgcog.get_monster(card.id, server=monster.server_priority) + inherit_model = dbcog.get_monster(card.id, server=monster.server_priority) max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level - stat_latents = dgcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) - dmg = int(round(dgcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, + stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) + dmg = int(round(dbcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level))) oncolor = '*' if monster.attr1.value == inherit_model.attr1.value or monster.attr1.name == NIL_ATT else ' ' lines.append(CARD_BUTTON_FORMAT.format( card.id, card.name.format(oncolor), card.mult, round(dmg * card.mult, 2))) return "\n".join(lines) - def _get_team_btn_damage(self, team_buttons, dgcog, monster): + def _get_team_btn_damage(self, team_buttons, dbcog, monster): lines = [] team_buttons.sort(key=lambda x: x.mult) for card in team_buttons: total_dmg = 0 - inherit_model = dgcog.get_monster(card.id, server=monster.server_priority) + inherit_model = dbcog.get_monster(card.id, server=monster.server_priority) max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level - stat_latents = dgcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) - dmg = dgcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, + stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) + dmg = dbcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level) if(monster.attr1.value in card.att): total_dmg += dmg diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index 0043bcd1..ede2155a 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -69,7 +69,7 @@ def _format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): return f"[{_format_number(hp)}/{_format_number(atk)}/{_format_number(rcv)}{resist}] {extras}" -async def leaderskill_query(dgcog, raw_query, author_id): +async def leaderskill_query(dbcog, raw_query, author_id): # Remove unicode quotation marks query = re.sub("[\u201c\u201d]", '"', raw_query) @@ -84,17 +84,17 @@ async def leaderskill_query(dgcog, raw_query, author_id): right_query = ' '.join(q for q in right_query) if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" - m = await dgcog.find_monster(query, author_id) - if m and left_query in dgcog.indexes[m.server_priority].modifiers[m]: + m = await dbcog.find_monster(query, author_id) + if m and left_query in dbcog.indexes[m.server_priority].modifiers[m]: left_query = query right_query = None break else: # no separators left_query, right_query = query, None - left_m = await dgcog.find_monster(left_query, author_id) + left_m = await dbcog.find_monster(left_query, author_id) if right_query: - right_m = await dgcog.find_monster(right_query, author_id) + right_m = await dbcog.find_monster(right_query, author_id) else: right_m = left_m return left_m, left_query, right_m, right_query diff --git a/padinfo/core/transforminfo.py b/padinfo/core/transforminfo.py index 4c87808c..7db8e334 100644 --- a/padinfo/core/transforminfo.py +++ b/padinfo/core/transforminfo.py @@ -1,7 +1,7 @@ -async def perform_transforminfo_query(dgcog, raw_query, author_id): - db_context = dgcog.database - mgraph = dgcog.database.graph - found_monster = await dgcog.find_monster(raw_query, author_id) +async def perform_transforminfo_query(dbcog, raw_query, author_id): + db_context = dbcog.database + mgraph = dbcog.database.graph + found_monster = await dbcog.find_monster(raw_query, author_id) if not found_monster: return None, None, None diff --git a/padinfo/info.json b/padinfo/info.json index 0e399b4b..47ecf85c 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", "speech": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index f2d03837..ffa6663c 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -58,9 +58,9 @@ async def respond_with_numerical(message: Optional[Message], ims, **data): @staticmethod async def respond_with_awakening_list(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await AwakeningListViewState.deserialize(dgcog, user_config, ims) + view_state = await AwakeningListViewState.deserialize(dbcog, user_config, ims) control = AwakeningListMenu.awakening_list_control(view_state) return control diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index d126f12b..4f62e8a0 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -30,12 +30,12 @@ def menu(initial_control=None): @staticmethod async def respond_with_left(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] # Figure out the new monster before doing all of the queries necessary for # The specific pane type. For now just deserialize as the base Id ViewState. - view_state = await ViewStateBaseId.deserialize(dgcog, user_config, ims) - view_state.decrement_monster(dgcog, ims) + view_state = await ViewStateBaseId.deserialize(dbcog, user_config, ims) + view_state.decrement_monster(dbcog, ims) pane_type = ims['pane_type'] pane_type_to_func_map = IdMenuPanes.pane_types() @@ -44,10 +44,10 @@ async def respond_with_left(message: Optional[Message], ims, **data): @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await ViewStateBaseId.deserialize(dgcog, user_config, ims) - view_state.increment_monster(dgcog, ims) + view_state = await ViewStateBaseId.deserialize(dbcog, user_config, ims) + view_state.increment_monster(dbcog, ims) pane_type = ims.get('pane_type') pane_type_to_func_map = IdMenuPanes.pane_types() @@ -75,55 +75,55 @@ async def respond_with_delete(message: Optional[Message], ims, **data): @staticmethod async def respond_with_current_id(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await IdViewState.deserialize(dgcog, user_config, ims) + view_state = await IdViewState.deserialize(dbcog, user_config, ims) control = IdMenu.id_control(view_state) return control @staticmethod async def respond_with_evos(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await EvosViewState.deserialize(dgcog, user_config, ims) + view_state = await EvosViewState.deserialize(dbcog, user_config, ims) control = IdMenu.evos_control(view_state) return control @staticmethod async def respond_with_mats(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await MaterialsViewState.deserialize(dgcog, user_config, ims) + view_state = await MaterialsViewState.deserialize(dbcog, user_config, ims) control = IdMenu.mats_control(view_state) return control @staticmethod async def respond_with_picture(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await PicViewState.deserialize(dgcog, user_config, ims) + view_state = await PicViewState.deserialize(dbcog, user_config, ims) control = IdMenu.pic_control(view_state) return control @staticmethod async def respond_with_pantheon(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await PantheonViewState.deserialize(dgcog, user_config, ims) + view_state = await PantheonViewState.deserialize(dbcog, user_config, ims) control = IdMenu.pantheon_control(view_state) return control @staticmethod async def respond_with_otherinfo(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await OtherInfoViewState.deserialize(dgcog, user_config, ims) + view_state = await OtherInfoViewState.deserialize(dbcog, user_config, ims) control = IdMenu.otherinfo_control(view_state) return control diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 7e9863ed..37f55b0c 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -18,35 +18,35 @@ def menu(): @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] # Extract the query from the ls state ims['query'] = ims['r_query'] ims['query_settings'] = ims['r_query_settings'] ims['resolved_monster_id'] = None - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_view_state = await IdViewState.deserialize(dbcog, user_config, ims) id_control = LeaderSkillMenu.id_control(id_view_state) return id_control @staticmethod async def respond_with_left(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] # Extract the query from the ls state ims['query'] = ims['l_query'] ims['query_settings'] = ims['l_query_settings'] ims['resolved_monster_id'] = None - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_view_state = await IdViewState.deserialize(dbcog, user_config, ims) id_control = LeaderSkillMenu.id_control(id_view_state) return id_control @staticmethod async def respond_with_house(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - ls_view_state = await LeaderSkillViewState.deserialize(dgcog, user_config, ims) + ls_view_state = await LeaderSkillViewState.deserialize(dbcog, user_config, ims) ls_control = LeaderSkillMenu.ls_control(ls_view_state) return ls_control diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index f9f2feaf..09defeb8 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -17,14 +17,14 @@ def menu(): LeaderSkillSingleMenu.ls_control) @staticmethod - async def respond_with_ls(message: Optional[Message], ims, *, dgcog, user_config, **data): - ls_view_state = await LeaderSkillSingleViewState.deserialize(dgcog, user_config, ims) + async def respond_with_ls(message: Optional[Message], ims, *, dbcog, user_config, **data): + ls_view_state = await LeaderSkillSingleViewState.deserialize(dbcog, user_config, ims) ls_control = LeaderSkillSingleMenu.ls_control(ls_view_state) return ls_control @staticmethod - async def respond_with_id(message: Optional[Message], ims, *, dgcog, user_config, **data): - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + async def respond_with_id(message: Optional[Message], ims, *, dbcog, user_config, **data): + id_view_state = await IdViewState.deserialize(dbcog, user_config, ims) id_control = LeaderSkillSingleMenu.id_control(id_view_state) return id_control diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 0ab7fd72..189debfa 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -59,10 +59,10 @@ def menu(initial_control=None): @classmethod async def _get_view_state(cls, ims: dict, **data) -> MonsterListViewState: - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] view_state_class = cls.view_state_types.get(ims['menu_type']) or MonsterListMenu.MENU_TYPE - return await view_state_class.deserialize(dgcog, user_config, ims) + return await view_state_class.deserialize(dbcog, user_config, ims) @staticmethod async def respond_with_refresh(message: Optional[Message], ims, **data): diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index d67cc507..b7321a25 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -30,11 +30,11 @@ def menu(initial_control=None): @staticmethod async def respond_with_home(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await IdViewState.deserialize(dgcog, user_config, ims) + view_state = await IdViewState.deserialize(dbcog, user_config, ims) if view_state.set_na_diff_invalid_message(ims): - new_view_state = await SimpleTextViewState.deserialize(dgcog, user_config, ims) + new_view_state = await SimpleTextViewState.deserialize(dbcog, user_config, ims) return NaDiffMenu.message_control(new_view_state) return await NaDiffMenu.respond_with_na(message, ims, **data) @@ -48,11 +48,11 @@ async def respond_with_jp(message: Optional[Message], ims, **data): @staticmethod async def respond_with_id(message: Optional[Message], ims, server: Server, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - view_state = await IdViewState.deserialize(dgcog, user_config, ims) - await view_state.set_server(dgcog, server) + view_state = await IdViewState.deserialize(dbcog, user_config, ims) + await view_state.set_server(dbcog, server) control = NaDiffMenu.id_control(view_state) return control diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index b9d1a944..98024634 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -48,9 +48,9 @@ def menu(initial_control=None): @classmethod async def _get_view_state(cls, ims: dict, **data) -> SeriesScrollViewState: - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - return await SeriesScrollViewState.deserialize(dgcog, user_config, ims) + return await SeriesScrollViewState.deserialize(dbcog, user_config, ims) @staticmethod async def respond_with_refresh(message: Optional[Message], ims, **data): @@ -66,16 +66,16 @@ async def respond_with_reset(message: Optional[Message], ims, **data): @classmethod async def respond_with_left(cls, message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] view_state = await cls._get_view_state(ims, **data) - await view_state.decrement_page(dgcog) + await view_state.decrement_page(dbcog) return SeriesScrollMenu.monster_list_control(view_state) @classmethod async def respond_with_right(cls, message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] view_state = await cls._get_view_state(ims, **data) - await view_state.increment_page(dgcog) + await view_state.increment_page(dbcog) return SeriesScrollMenu.monster_list_control(view_state) @classmethod @@ -85,16 +85,16 @@ async def respond_with_monster_list(cls, message: Optional[Message], ims, **data @classmethod async def respond_with_previous_monster(cls, message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] view_state = await cls._get_view_state(ims, **data) - await view_state.decrement_index(dgcog) + await view_state.decrement_index(dbcog) return SeriesScrollMenu.monster_list_control(view_state) @classmethod async def respond_with_next_monster(cls, message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] view_state = await cls._get_view_state(ims, **data) - await view_state.increment_index(dgcog) + await view_state.increment_index(dbcog) return SeriesScrollMenu.monster_list_control(view_state) @staticmethod @@ -163,7 +163,7 @@ def monster_list_control(state: SeriesScrollViewState): @classmethod async def click_child_number(cls, ims, emoji_clicked, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] emoji_response = IdMenuEmoji.refresh \ if SeriesScrollMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: @@ -176,19 +176,19 @@ async def click_child_number(cls, ims, emoji_clicked, **data): @classmethod async def auto_scroll_child_left(cls, ims, _emoji_clicked, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] view_state = await cls._get_view_state(ims, **data) - return await SeriesScrollMenu._scroll_child(view_state, view_state.decrement_index, dgcog) + return await SeriesScrollMenu._scroll_child(view_state, view_state.decrement_index, dbcog) @classmethod async def auto_scroll_child_right(cls, ims, _emoji_clicked, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] view_state = await cls._get_view_state(ims, **data) - return await SeriesScrollMenu._scroll_child(view_state, view_state.increment_index, dgcog) + return await SeriesScrollMenu._scroll_child(view_state, view_state.increment_index, dbcog) @staticmethod - async def _scroll_child(view_state: SeriesScrollViewState, update_fn, dgcog): - await update_fn(dgcog) + async def _scroll_child(view_state: SeriesScrollViewState, update_fn, dbcog): + await update_fn(dbcog) extra_ims = view_state.get_serialized_child_extra_ims(IdMenuPanes.emoji_names(), IdMenu.MENU_TYPE) return IdMenuEmoji.refresh, extra_ims diff --git a/padinfo/menu/simple_text.py b/padinfo/menu/simple_text.py index 53c6fdbb..6da5d41d 100644 --- a/padinfo/menu/simple_text.py +++ b/padinfo/menu/simple_text.py @@ -24,9 +24,9 @@ def menu(): @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): - dgcog = data.get('dgcog') + dbcog = data.get('dbcog') user_config = data.get('user_config') - view_state = await SimpleTextViewState.deserialize(dgcog, user_config, ims) + view_state = await SimpleTextViewState.deserialize(dbcog, user_config, ims) control = SimpleTextMenu.message_control(view_state) return control diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index ebffa6c2..14875f73 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -18,31 +18,31 @@ def menu(): @staticmethod async def respond_with_base(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] # base is always first ims['resolved_monster_id'] = ims['resolved_monster_ids'][0] - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_view_state = await IdViewState.deserialize(dbcog, user_config, ims) reaction_list = ims['reaction_list'] id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) return id_control @staticmethod async def respond_with_transform(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] # transform is always second ims['resolved_monster_id'] = ims['resolved_monster_ids'][1] - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_view_state = await IdViewState.deserialize(dbcog, user_config, ims) reaction_list = ims['reaction_list'] id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) return id_control @staticmethod async def respond_with_n(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] n = TransformInfoMenuPanes.get_n_from_reaction(data['reaction']) # does this work? is this check necessary? is reaction guaranteed to resolve? @@ -50,16 +50,16 @@ async def respond_with_n(message: Optional[Message], ims, **data): return None ims['resolved_monster_id'] = ims['resolved_monster_ids'][n] - id_view_state = await IdViewState.deserialize(dgcog, user_config, ims) + id_view_state = await IdViewState.deserialize(dbcog, user_config, ims) reaction_list = ims['reaction_list'] id_control = TransformInfoMenu.id_control(id_view_state, reaction_list) return id_control @staticmethod async def respond_with_overview(message: Optional[Message], ims, **data): - dgcog = data['dgcog'] + dbcog = data['dbcog'] user_config = data['user_config'] - tf_view_state = await TransformInfoViewState.deserialize(dgcog, user_config, ims) + tf_view_state = await TransformInfoViewState.deserialize(dbcog, user_config, ims) tf_control = TransformInfoMenu.tf_control(tf_view_state) return tf_control diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3c6eb918..3c7f8852 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -64,9 +64,9 @@ from padinfo.view.transforminfo import TransformInfoViewState if TYPE_CHECKING: - from dadguide.dadguide import Dadguide - from dadguide.models.monster_model import MonsterModel - from dadguide.models.series_model import SeriesModel + from dbcog.dbcog import DBCog + from dbcog.models.monster_model import MonsterModel + from dbcog.models.series_model import SeriesModel logger = logging.getLogger('red.padbot-cogs.padinfo') @@ -150,23 +150,23 @@ async def reload_nicknames(self): try: emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) await asyncio.sleep(wait_time) - async def get_dgcog(self) -> "Dadguide": - dgcog = self.bot.get_cog("Dadguide") - if dgcog is None: - raise ValueError("Dadguide cog is not loaded") - await dgcog.wait_until_ready() - return dgcog + async def get_dbcog(self) -> "DBCog": + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: + raise ValueError("DBCog cog is not loaded") + await dbcog.wait_until_ready() + return dbcog async def get_menu_default_data(self, ims): data = { - 'dgcog': await self.get_dgcog(), + 'dbcog': await self.get_dbcog(), 'user_config': await BotConfig.get_user(self.config, ims['original_author_id']) } return data @@ -174,8 +174,8 @@ async def get_menu_default_data(self, ims): @commands.command() async def jpname(self, ctx, *, query: str): """Show the Japanese name of a monster""" - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if monster is not None: await ctx.send(MonsterHeader.short(monster)) await ctx.send(box(monster.name_ja)) @@ -207,28 +207,28 @@ async def id3(self, ctx, *, query: str): await self._do_id(ctx, query) async def _do_id(self, ctx, query: str): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id goodquery = None - if query[0] in dgcog.token_maps.ID1_SUPPORTED \ - and query[1:] in dgcog.indexes[Server.COMBINED].all_name_tokens: + if query[0] in dbcog.token_maps.ID1_SUPPORTED \ + and query[1:] in dbcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dgcog.token_maps.ID1_SUPPORTED \ - and query[2:] in dgcog.indexes[Server.COMBINED].all_name_tokens: + elif query[:2] in dbcog.token_maps.ID1_SUPPORTED \ + and query[2:] in dbcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: bad = False - for monster in dgcog.indexes[Server.COMBINED].all_name_tokens[goodquery[1]]: - for modifier in dgcog.indexes[Server.COMBINED].modifiers[monster]: + for monster in dbcog.indexes[Server.COMBINED].all_name_tokens[goodquery[1]]: + for modifier in dbcog.indexes[Server.COMBINED].modifiers[monster]: if modifier == 'xm' and goodquery[0] == 'x': goodquery[0] = 'xm' if modifier == goodquery[0]: bad = True - if bad and query not in dgcog.indexes[Server.COMBINED].all_name_tokens: + if bad and query not in dbcog.indexes[Server.COMBINED].all_name_tokens: async def send_message(): await asyncio.sleep(1) await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" @@ -240,7 +240,7 @@ async def send_message(): async with self.config.bad_queries() as bad_queries: bad_queries.append((raw_query, ctx.author.id)) - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(raw_query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) @@ -258,12 +258,12 @@ async def send_message(): if await self.config.do_survey(): asyncio.create_task(self.send_survey_after(ctx, query, monster)) - alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) + alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ - await IdViewState.do_query(dgcog, monster) + await IdViewState.do_query(dbcog, monster) full_reaction_list = IdMenuPanes.emoji_names() - initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, @@ -274,12 +274,12 @@ async def send_message(): await menu.create(ctx, state) async def send_survey_after(self, ctx, query, result_monster): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() survey_mode = await self.config.user(ctx.author).survey_mode() survey_chance = (1, await self.config.sometimes_perc() / 100, 0)[survey_mode] if random.random() < survey_chance: mid1 = self.historic_lookups.get(query) - m1 = mid1 and dgcog.get_monster(mid1) + m1 = mid1 and dbcog.get_monster(mid1) id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" params = urllib.parse.urlencode( @@ -333,22 +333,22 @@ async def send_invalid_monster_message(ctx, query: str, monster: "MonsterModel", @checks.bot_has_permissions(embed_links=True) async def nadiff(self, ctx, *, query: str): """Show differences between NA & JP versions of a card""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(raw_query, ctx.author.id) await self.log_id_result(ctx, monster.monster_id) if monster is None: await self.send_id_failure_message(ctx, query) return - alt_monsters = IdViewState.get_alt_monsters_and_evos(dgcog, monster) + alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ - await IdViewState.do_query(dgcog, monster) + await IdViewState.do_query(dbcog, monster) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = IdViewState(original_author_id, NaDiffMenu.MENU_TYPE, raw_query, query, color, monster, @@ -367,12 +367,12 @@ async def nadiff(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evos(self, ctx, *, query: str): """Monster info (evolutions tab)""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -380,9 +380,9 @@ async def evos(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) - alt_monsters = EvosViewState.get_alt_monsters_and_evos(dgcog, monster) - alt_versions, gem_versions = await EvosViewState.do_query(dgcog, monster) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + alt_monsters = EvosViewState.get_alt_monsters_and_evos(dbcog, monster) + alt_versions, gem_versions = await EvosViewState.do_query(dbcog, monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) if alt_versions is None: @@ -390,7 +390,7 @@ async def evos(self, ctx, *, query: str): return full_reaction_list = IdMenuPanes.emoji_names() - initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -404,11 +404,11 @@ async def evos(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(raw_query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) @@ -417,17 +417,17 @@ async def evomats(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ - await MaterialsViewState.do_query(dgcog, monster) + await MaterialsViewState.do_query(dbcog, monster) if mats is None: await ctx.send(inline("This monster has no mats or skillups and isn't used in any evolutions")) return - alt_monsters = MaterialsViewState.get_alt_monsters_and_evos(dgcog, monster) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + alt_monsters = MaterialsViewState.get_alt_monsters_and_evos(dbcog, monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = IdMenuPanes.emoji_names() - initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -441,12 +441,12 @@ async def evomats(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -454,16 +454,16 @@ async def pantheon(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) - pantheon_list, series_name, base_monster = await PantheonViewState.do_query(dgcog, monster) + pantheon_list, series_name, base_monster = await PantheonViewState.do_query(dbcog, monster) if pantheon_list is None: await ctx.send('Unable to find a pantheon for the result of your query,' + ' [{}] {}.'.format(monster.monster_id, monster.name_en)) return - alt_monsters = PantheonViewState.get_alt_monsters_and_evos(dgcog, monster) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + alt_monsters = PantheonViewState.get_alt_monsters_and_evos(dbcog, monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = IdMenuPanes.emoji_names() - initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -477,12 +477,12 @@ async def pantheon(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def pic(self, ctx, *, query: str): """Monster info (full image tab)""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -490,11 +490,11 @@ async def pic(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) - alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + alt_monsters = PicViewState.get_alt_monsters_and_evos(dbcog, monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = IdMenuPanes.emoji_names() - initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -507,12 +507,12 @@ async def pic(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() raw_query = query color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - monster = await dgcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(raw_query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -520,11 +520,11 @@ async def otherinfo(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) - alt_monsters = PicViewState.get_alt_monsters_and_evos(dgcog, monster) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + alt_monsters = PicViewState.get_alt_monsters_and_evos(dbcog, monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) full_reaction_list = IdMenuPanes.emoji_names() - initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dgcog, monster, full_reaction_list) + initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -537,8 +537,8 @@ async def otherinfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def links(self, ctx, *, query: str): """Monster links""" - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -551,8 +551,8 @@ async def links(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) return @@ -574,13 +574,13 @@ async def log_id_result(self, ctx, monster_id: int): @checks.bot_has_permissions(embed_links=True) async def buttoninfo(self, ctx, *, query: str): """Button farming theorycrafting info""" - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) return - info = button_info.get_info(dgcog, monster) + info = button_info.get_info(dbcog, monster) original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() color = await self.get_user_embed_color(ctx) @@ -593,39 +593,39 @@ async def buttoninfo(self, ctx, *, query: str): @checks.bot_has_permissions(embed_links=True) async def allmats(self, ctx, *, query: str): """Monster info (evo materials tab)""" - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) return - monster_list = await AllMatsViewState.do_query(dgcog, monster) + monster_list = await AllMatsViewState.do_query(dbcog, monster) if monster_list is None: await ctx.send(inline("This monster is not a mat for anything nor does it have a gem")) return - _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.do_query(dgcog, monster) + _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.do_query(dbcog, monster) title = 'Material For' if usedin else 'Gem is Material For' - await self._do_monster_list(ctx, dgcog, query, monster_list, title, AllMatsViewState) + await self._do_monster_list(ctx, dbcog, query, monster_list, title, AllMatsViewState) @commands.command() @checks.bot_has_permissions(embed_links=True) async def evolist(self, ctx, *, query): - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) return - monster_list, _ = await EvosViewState.do_query(dgcog, monster) + monster_list, _ = await EvosViewState.do_query(dbcog, monster) if monster_list is None: await self.send_invalid_monster_message(ctx, query, monster, ', which has no alt evos') return - await self._do_monster_list(ctx, dgcog, query, monster_list, 'Evolution List', StaticMonsterListViewState) + await self._do_monster_list(ctx, dbcog, query, monster_list, 'Evolution List', StaticMonsterListViewState) - async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterModel"], + async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterModel"], title, view_state_type: Type[MonsterListViewState], child_menu_type: Optional[str] = None, child_reaction_list: Optional[List] = None @@ -655,7 +655,7 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM ims = state.serialize() user_config = await BotConfig.get_user(self.config, ctx.author.id) data = { - 'dgcog': dgcog, + 'dbcog': dbcog, 'user_config': user_config, } child_state = SimpleTextViewState(original_author_id, view_state_type.VIEW_STATE_TYPE, raw_query, color, @@ -676,8 +676,8 @@ async def _do_monster_list(self, ctx, dgcog, query, monster_list: List["MonsterM @commands.command(aliases=['collabscroll', 'ss']) @checks.bot_has_permissions(embed_links=True) async def seriesscroll(self, ctx, *, query): - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -689,11 +689,11 @@ async def seriesscroll(self, ctx, *, query): query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) rarity = None for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: - paginated_monsters = await SeriesScrollViewState.do_query(dgcog, monster.series_id, + paginated_monsters = await SeriesScrollViewState.do_query(dbcog, monster.series_id, rarity, monster.server_priority) if paginated_monsters: break - all_rarities = SeriesScrollViewState.query_all_rarities(dgcog, series_id, monster.server_priority) + all_rarities = SeriesScrollViewState.query_all_rarities(dbcog, series_id, monster.server_priority) raw_query = query original_author_id = ctx.message.author.id @@ -713,7 +713,7 @@ async def seriesscroll(self, ctx, *, query): ims = state.serialize() user_config = await BotConfig.get_user(self.config, ctx.author.id) data = { - 'dgcog': dgcog, + 'dbcog': dbcog, 'user_config': user_config, } child_state = SimpleTextViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, color, @@ -742,8 +742,8 @@ async def leaderskill(self, ctx, *, raw_query): [p]ls r sonia "revo lu bu" [p]ls sonia lubu """ - dgcog = await self.get_dgcog() - l_mon, l_query, r_mon, r_query = await leaderskill_query(dgcog, raw_query, ctx.author.id) + dbcog = await self.get_dbcog() + l_mon, l_query, r_mon, r_query = await leaderskill_query(dbcog, raw_query, ctx.author.id) err_msg = ('{} query failed to match a monster: [ {} ]. If your query is multiple words,' ' try separating the queries with / or wrap with quotes.') @@ -785,8 +785,8 @@ async def get_user_friends(self, original_author_id): @commands.command(aliases=['lssingle']) @checks.bot_has_permissions(embed_links=True) async def leaderskillsingle(self, ctx, *, query): - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) return @@ -804,8 +804,8 @@ async def leaderskillsingle(self, ctx, *, query): @checks.bot_has_permissions(embed_links=True) async def transforminfo(self, ctx, *, query): """Show info about a transform card, including some helpful details about the base card.""" - dgcog = await self.get_dgcog() - base_mon, transformed_mon, monster_ids = await perform_transforminfo_query(dgcog, query, ctx.author.id) + dbcog = await self.get_dbcog() + base_mon, transformed_mon, monster_ids = await perform_transforminfo_query(dbcog, query, ctx.author.id) if not base_mon: await self.send_id_failure_message(ctx, query) @@ -817,9 +817,9 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - acquire_raw = await TransformInfoViewState.do_query(dgcog, base_mon, transformed_mon) + acquire_raw = await TransformInfoViewState.do_query(dbcog, base_mon, transformed_mon) reaction_list = TransformInfoMenuPanes.get_user_reaction_list(len(monster_ids)) - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(base_mon) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, @@ -835,12 +835,12 @@ async def awakenings(self, ctx, *, query=None): """Describe a monster's regular and super awakenings in detail. Leave blank to see a list of every awakening in the game.""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() color = await self.get_user_embed_color(ctx) if not query: sort_type = AwakeningListSortTypes.numerical - paginated_skills = await AwakeningListViewState.do_query(dgcog, sort_type) + paginated_skills = await AwakeningListViewState.do_query(dbcog, sort_type) menu = AwakeningListMenu.menu() state = AwakeningListViewState(ctx.message.author.id, AwakeningListMenu.MENU_TYPE, color, sort_type, paginated_skills, 0, @@ -848,7 +848,7 @@ async def awakenings(self, ctx, *, query=None): await menu.create(ctx, state) return - monster = await dgcog.find_monster(query, ctx.author.id) + monster = await dbcog.find_monster(query, ctx.author.id) if not monster: await self.send_id_failure_message(ctx, query) @@ -865,15 +865,15 @@ async def awakenings(self, ctx, *, query=None): @checks.bot_has_permissions(embed_links=True) async def idhistory(self, ctx): """Show a list of the 11 most recent monsters that the user looked up.""" - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() history = await self.config.user(ctx.author).id_history() - monster_list = [dgcog.get_monster(m) for m in history] + monster_list = [dbcog.get_monster(m) for m in history] if not monster_list: await ctx.send('Did not find any recent queries in history.') return - await self._do_monster_list(ctx, dgcog, '', monster_list, 'Result History', StaticMonsterListViewState) + await self._do_monster_list(ctx, dbcog, '', monster_list, 'Result History', StaticMonsterListViewState) @commands.command() async def padsay(self, ctx, server, *, query: str = None): @@ -894,8 +894,8 @@ async def padsay(self, ctx, server, *, query: str = None): server = 'na' query = query.strip().lower() - dgcog = await self.get_dgcog() - monster = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) if monster is not None: voice_id = monster.voice_id_jp if server == 'jp' else monster.voice_id_na if voice_id is None: @@ -973,7 +973,7 @@ async def embedcolor(self, ctx, *, color): @idset.command(usage="") async def naprio(self, ctx, value: bool): """Change whether [p]id will default away from new evos of monsters that aren't in NA yet""" - async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: # The current na_prio enum has 0 and 1 instead of False and True as its values fm_flags['na_prio'] = int(value) await ctx.send(f"NA monster prioritization has been **{'en' if value else 'dis'}abled**.") @@ -985,14 +985,14 @@ async def server(self, ctx, server: str): `[p]idset server default`: Include both NA & JP cards. You can still use `--na` as needed. `[p]idset server na`: Include only NA cards. You can still use `--allservers` as needed. """ - dgcog = await self.get_dgcog() - async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + dbcog = await self.get_dbcog() + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: if server.upper() == "DEFAULT": - fm_flags['server'] = dgcog.DEFAULT_SERVER.value - elif server.upper() in [s.value for s in dgcog.SERVERS]: + fm_flags['server'] = dbcog.DEFAULT_SERVER.value + elif server.upper() in [s.value for s in dbcog.SERVERS]: fm_flags['server'] = server.upper() else: - if dgcog.DEFAULT_SERVER == Server.COMBINED: + if dbcog.DEFAULT_SERVER == Server.COMBINED: await ctx.send("Server must be `default` or `na`") else: await ctx.send("Server must be `na` or `combined`") @@ -1006,8 +1006,8 @@ async def evosort(self, ctx, value: str): `[p]idset evosort numerical`: Show alt evos sorted by card ID number. `[p]idset evosort dfs`: Show alt evos in a depth-first-sort order, starting with the base of the tree. """ - dgcog = await self.get_dgcog() - async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + dbcog = await self.get_dbcog() + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: value = value.lower() if value == "dfs": fm_flags['evosort'] = AltEvoSort.dfs.value @@ -1026,8 +1026,8 @@ async def lsmultiplier(self, ctx, value: str): `[p]idset lsmultiplier double`: [Default] Show leader skill multipliers assuming the card is paired with itself. `[p]idset lsmultiplier single`: Show leader skill multipliers of just the single card. """ - dgcog = await self.get_dgcog() - async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + dbcog = await self.get_dbcog() + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: value = value.lower() if value == "double": fm_flags['lsmultiplier'] = LsMultiplier.lsdouble.value @@ -1051,8 +1051,8 @@ async def cardplus(self, ctx, value: str): `[p]idset cardplus 297`: [Default] Show cards with +297 stats. `[p]idset cardplus 0`: Show cards with +0 stats. """ - dgcog = await self.get_dgcog() - async with self.bot.get_cog("Dadguide").config.user(ctx.author).fm_flags() as fm_flags: + dbcog = await self.get_dbcog() + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: value = value.lower() if value == "297": fm_flags['cardplus'] = CardPlusModifier.plus297.value @@ -1129,28 +1129,28 @@ async def send_id_failure_message(ctx, query: str): @commands.command(aliases=["iddebug", "dbid", "iddb"]) async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, query): """Get helpful id information about a monster""" - dgcog = await self.get_dgcog() - mon = await dgcog.find_monster(query, ctx.author.id) + dbcog = await self.get_dbcog() + mon = await dbcog.find_monster(query, ctx.author.id) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) return - base_monster = dgcog.database.graph.get_base_monster(mon) - mods = dgcog.indexes[server].modifiers[mon] - manual_modifiers = dgcog.indexes[server].manual_prefixes[mon.monster_id] - EVOANDTYPE = dgcog.token_maps.EVO_TOKENS.union(dgcog.token_maps.TYPE_TOKENS) + base_monster = dbcog.database.graph.get_base_monster(mon) + mods = dbcog.indexes[server].modifiers[mon] + manual_modifiers = dbcog.indexes[server].manual_prefixes[mon.monster_id] + EVOANDTYPE = dbcog.token_maps.EVO_TOKENS.union(dbcog.token_maps.TYPE_TOKENS) ret = (f"[{mon.monster_id}] {mon.name_en}\n" f"Base: [{base_monster.monster_id}] {base_monster.name_en}\n" f"Series: {mon.series.name_en} ({mon.series_id}, {mon.series.series_type})\n\n" - f"[Name Tokens] {' '.join(sorted(t for t, ms in dgcog.indexes[server].name_tokens.items() if mon in ms))}\n" - f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dgcog.indexes[server].fluff_tokens.items() if mon in ms))}\n\n" + f"[Name Tokens] {' '.join(sorted(t for t, ms in dbcog.indexes[server].name_tokens.items() if mon in ms))}\n" + f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dbcog.indexes[server].fluff_tokens.items() if mon in ms))}\n\n" f"[Manual Tokens]\n" - f" Treenames: {' '.join(sorted(t for t, ms in dgcog.indexes[server].manual_tree.items() if mon in ms))}\n" - f" Nicknames: {' '.join(sorted(t for t, ms in dgcog.indexes[server].manual_nick.items() if mon in ms))}\n\n" + f" Treenames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_tree.items() if mon in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_nick.items() if mon in ms))}\n\n" f"[Modifier Tokens]\n" - f" Attribute: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.COLOR_TOKENS))}\n" - f" Awakening: {' '.join(sorted(t for t in mods if t in dgcog.token_maps.AWAKENING_TOKENS))}\n" + f" Attribute: {' '.join(sorted(t for t in mods if t in dbcog.token_maps.COLOR_TOKENS))}\n" + f" Awakening: {' '.join(sorted(t for t in mods if t in dbcog.token_maps.AWAKENING_TOKENS))}\n" f" Evo & Type: {' '.join(sorted(t for t in mods if t in EVOANDTYPE))}\n" - f" Other: {' '.join(sorted(t for t in mods if t not in dgcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" + f" Other: {' '.join(sorted(t for t in mods if t not in dbcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" f"Manually Added: {' '.join(sorted(manual_modifiers))}\n") for page in pagify(ret): await ctx.send(box(page)) @@ -1162,19 +1162,19 @@ async def debugiddist(self, ctx, s1, s2): For name tokens, the full word goes second as name token matching is not commutitive """ - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() - dist = dgcog.mon_finder.calc_ratio_modifier(s1, s2) - dist2 = dgcog.mon_finder.calc_ratio_name(s1, s2) + dist = dbcog.mon_finder.calc_ratio_modifier(s1, s2) + dist2 = dbcog.mon_finder.calc_ratio_name(s1, s2) yes = '\N{WHITE HEAVY CHECK MARK}' no = '\N{CROSS MARK}' await ctx.send(f"Printing info for {inline(s1)}, {inline(s2)}\n" + box(f"Jaro-Winkler Distance: {round(dist, 4)}\n" f"Name Matching Distance: {round(dist2, 4)}\n" - f"Modifier token threshold: {dgcog.mon_finder.MODIFIER_JW_DISTANCE} " - f"{yes if dist >= dgcog.mon_finder.MODIFIER_JW_DISTANCE else no}\n" - f"Name token threshold: {dgcog.mon_finder.TOKEN_JW_DISTANCE} " - f"{yes if dist2 >= dgcog.mon_finder.TOKEN_JW_DISTANCE else no}")) + f"Modifier token threshold: {dbcog.mon_finder.MODIFIER_JW_DISTANCE} " + f"{yes if dist >= dbcog.mon_finder.MODIFIER_JW_DISTANCE else no}\n" + f"Name token threshold: {dbcog.mon_finder.TOKEN_JW_DISTANCE} " + f"{yes if dist2 >= dbcog.mon_finder.TOKEN_JW_DISTANCE else no}")) @commands.command(aliases=['helpid']) async def idhelp(self, ctx, *, query=""): @@ -1190,7 +1190,7 @@ async def idhelp(self, ctx, *, query=""): @commands.command() async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] = "COMBINED"): server = Server(server) - DGCOG = await self.get_dgcog() + DGCOG = await self.get_dbcog() maps = DGCOG.token_maps awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} series = {s.series_id: s for s in DGCOG.database.get_all_series()} @@ -1229,7 +1229,7 @@ async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] async def idmeaning(self, ctx, token, server: Optional[Server] = Server.COMBINED): """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") - DGCOG = await self.get_dgcog() + DGCOG = await self.get_dbcog() tms = DGCOG.token_maps awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_SKILL_MAP.values() for aw in aws) + ")" @@ -1325,9 +1325,9 @@ async def idtraceback(self, ctx, *, query): return selected_monster_id = int(selected_monster_id.strip()) - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() - bestmatch, matches, _, _ = await dgcog.mon_finder.find_monster_debug(query) + bestmatch, matches, _, _ = await dbcog.mon_finder.find_monster_debug(query) if bestmatch is None: await ctx.send("No monster matched.") @@ -1352,10 +1352,10 @@ async def idtraceback(self, ctx, *, query): lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) mtokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" - f" ({round(dgcog.mon_finder.calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" + f" ({round(dbcog.mon_finder.calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" for t in sorted(mtokens)) ntokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" - f" ({round(dgcog.mon_finder.calc_ratio_name(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" + f" ({round(dbcog.mon_finder.calc_ratio_name(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" f" {t[2]}" for t in sorted(ntokens)) @@ -1381,18 +1381,18 @@ async def nadiffs(self, ctx, *, query): async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): - dgcog = await self.get_dgcog() + dbcog = await self.get_dbcog() - monster_list = await IdSearchViewState.do_query(dgcog, query, ctx.author.id) + monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id) if monster_list is None: await ctx.send("No monster matched.") return - await self._do_monster_list(ctx, dgcog, query, monster_list, 'ID Search Results', + await self._do_monster_list(ctx, dbcog, query, monster_list, 'ID Search Results', IdSearchViewState, child_menu_type=child_menu_type, child_reaction_list=child_reaction_list) async def get_fm_flags(self, user: discord.User): - return await self.bot.get_cog("Dadguide").config.user(user).fm_flags() + return await self.bot.get_cog("DBCog").config.user(user).fm_flags() diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index 021e4d45..ec528ad6 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -7,25 +7,25 @@ from padinfo.view.pantheon import PantheonViewState if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel -async def get_id_menu_initial_reaction_list(ctx, dgcog, monster: "MonsterModel", +async def get_id_menu_initial_reaction_list(ctx, dbcog, monster: "MonsterModel", full_reaction_list: List[Optional[str]] = None, force_evoscroll=False): # hide some panes if we're in evo scroll mode if not full_reaction_list: full_reaction_list = IdMenuPanes.emoji_names() if not force_evoscroll and not settings.checkEvoID(ctx.author.id): return full_reaction_list - alt_versions, gem_versions = await EvosViewState.do_query(dgcog, monster) + alt_versions, gem_versions = await EvosViewState.do_query(dbcog, monster) if alt_versions is None and gem_versions is None: full_reaction_list.remove(IdMenuEmoji.left) full_reaction_list.remove(IdMenuEmoji.right) full_reaction_list.remove(IdMenuEmoji.evos) - pantheon_list, _, _ = await PantheonViewState.do_query(dgcog, monster) + pantheon_list, _, _ = await PantheonViewState.do_query(dbcog, monster) if pantheon_list is None: full_reaction_list.remove(IdMenuEmoji.pantheon) - mats, usedin, gemid, _, skillups, _, _, _ = await MaterialsViewState.do_query(dgcog, monster) + mats, usedin, gemid, _, skillups, _, _, _ = await MaterialsViewState.do_query(dbcog, monster) if mats is None and usedin is None and gemid is None and skillups is None: full_reaction_list.remove(IdMenuEmoji.mats) return full_reaction_list diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 5afd9ed1..d454503c 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -13,8 +13,8 @@ from padinfo.view.components.monster.image import MonsterImage if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.models.awakening_model import AwakeningModel + from dbcog.models.monster_model import MonsterModel + from dbcog.models.awakening_model import AwakeningModel ORDINAL_WORDS = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth'] diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index 04258867..fd4482d1 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -38,9 +38,9 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): sort_type = ims['sort_type'] - paginated_skills = await cls.do_query(dgcog, sort_type) + paginated_skills = await cls.do_query(dbcog, sort_type) original_author_id = ims['original_author_id'] current_page = ims['current_page'] menu_type = ims['menu_type'] @@ -49,8 +49,8 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): current_page, reaction_list=reaction_list) @classmethod - async def do_query(cls, dgcog, sort_type): - awoken_skills = dgcog.database.get_all_awoken_skills() + async def do_query(cls, dbcog, sort_type): + awoken_skills = dbcog.database.get_all_awoken_skills() if sort_type == AwakeningListSortTypes.alphabetical: awoken_skills = sorted(awoken_skills, key=lambda awo: awo.name_en) elif sort_type == AwakeningListSortTypes.numerical: diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index cab5f778..290d2bbc 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -12,7 +12,7 @@ from padinfo.view.components.monster.image import MonsterImage if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class ButtonInfoViewProps: diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 34a51944..80ed4dba 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -11,19 +11,19 @@ from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.models.awakening_model import AwokenSkillModel + from dbcog.models.monster_model import MonsterModel + from dbcog.models.awakening_model import AwokenSkillModel -async def get_monster_from_ims(dgcog, ims: dict): +async def get_monster_from_ims(dbcog, ims: dict): query = ims.get('query') or ims['raw_query'] query_settings = QuerySettings.deserialize(ims.get('query_settings')) resolved_monster_id_str = ims.get('resolved_monster_id') resolved_monster_id = int(resolved_monster_id_str or 0) if resolved_monster_id: - return dgcog.database.graph.get_monster(resolved_monster_id, server=query_settings.server) - monster = await dgcog.find_monster(query, ims['original_author_id']) + return dbcog.database.graph.get_monster(resolved_monster_id, server=query_settings.server) + monster = await dbcog.find_monster(query, ims['original_author_id']) return monster diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 3af9b59d..44746bb1 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -8,7 +8,7 @@ from padinfo.common.external_links import puzzledragonx if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class MonsterHeader: diff --git a/padinfo/view/components/monster/image.py b/padinfo/view/components/monster/image.py index 03a548cf..301944d6 100644 --- a/padinfo/view/components/monster/image.py +++ b/padinfo/view/components/monster/image.py @@ -3,7 +3,7 @@ from discordmenu.embed.components import EmbedThumbnail if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' ICON_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 7ab96df3..8667f3f4 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -8,9 +8,9 @@ from padinfo.view.common import get_monster_from_ims if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.models.evolution_model import EvolutionModel - from dadguide.database_context import DbContext + from dbcog.models.monster_model import MonsterModel + from dbcog.models.evolution_model import EvolutionModel + from dbcog.database_context import DbContext class MonsterEvolution(NamedTuple): @@ -54,12 +54,12 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None - monster = await get_monster_from_ims(dgcog, ims) + monster = await get_monster_from_ims(dbcog, ims) - alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query @@ -68,7 +68,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -77,13 +77,13 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): extra_state=ims) @classmethod - def get_alt_monsters_and_evos(cls, dgcog, monster) -> List[MonsterEvolution]: - graph = dgcog.database.graph + def get_alt_monsters_and_evos(cls, dbcog, monster) -> List[MonsterEvolution]: + graph = dbcog.database.graph alt_monsters = graph.get_alt_monsters(monster) return [MonsterEvolution(m, graph.get_evolution(m)) for m in alt_monsters] - def decrement_monster(self, dgcog, ims: dict): - db_context: "DbContext" = dgcog.database + def decrement_monster(self, dbcog, ims: dict): + db_context: "DbContext" = dbcog.database if self.use_evo_scroll: index = self.alt_monster_ids.index(self.monster.monster_id) prev_monster_id = self.alt_monster_ids[index - 1] @@ -94,8 +94,8 @@ def decrement_monster(self, dgcog, ims: dict): ims['unsupported_transition'] = True ims['resolved_monster_id'] = str(prev_monster_id) - def increment_monster(self, dgcog, ims: dict): - db_context: "DbContext" = dgcog.database + def increment_monster(self, dbcog, ims: dict): + db_context: "DbContext" = dbcog.database if self.use_evo_scroll: index = self.alt_monster_ids.index(self.monster.monster_id) if index == len(self.alt_monster_ids) - 1: diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 3974c21b..9c340320 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -15,7 +15,7 @@ from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class EvosViewState(ViewStateBaseId): @@ -41,15 +41,15 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None - monster = await get_monster_from_ims(dgcog, ims) - alt_versions, gem_versions = await EvosViewState.do_query(dgcog, monster) + monster = await get_monster_from_ims(dbcog, ims) + alt_versions, gem_versions = await EvosViewState.do_query(dbcog, monster) if alt_versions is None: return None - alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query query_settings = QuerySettings.deserialize(ims.get('query_settings')) @@ -57,7 +57,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -67,8 +67,8 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): extra_state=ims) @staticmethod - async def do_query(dgcog, monster): - db_context = dgcog.database + async def do_query(dbcog, monster): + db_context = dbcog.database alt_versions = sorted(db_context.graph.get_alt_monsters(monster), key=lambda x: x.monster_id) gem_versions = list(filter(None, map(db_context.graph.evo_gem_monster, alt_versions))) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index c268c07b..98eef5ca 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -18,8 +18,8 @@ from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.models.awakening_model import AwakeningModel + from dbcog.models.monster_model import MonsterModel + from dbcog.models.awakening_model import AwakeningModel def alt_fmt(monsterevo, state): @@ -64,14 +64,14 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): # for numberscroll getting to a gap in monster book, or 1, or last monster if ims.get('unsupported_transition'): return None - monster = await get_monster_from_ims(dgcog, ims) - alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) + monster = await get_monster_from_ims(dbcog, ims) + alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ - await IdViewState.do_query(dgcog, monster) + await IdViewState.do_query(dbcog, monster) raw_query = ims['raw_query'] # This is to support the 2 vs 1 monster query difference between ^ls and ^id @@ -83,7 +83,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') fallback_message = ims.get('message') is_child = ims.get('is_child') - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -94,19 +94,19 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): is_child=is_child, extra_state=ims) - async def set_server(self, dgcog, server: Server): + async def set_server(self, dbcog, server: Server): self.query_settings.server = server - self.monster = dgcog.database.graph.get_monster(self.monster.monster_id, server=server) - self.alt_monsters = self.get_alt_monsters_and_evos(dgcog, self.monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity = await self.do_query(dgcog, self.monster) + self.monster = dbcog.database.graph.get_monster(self.monster.monster_id, server=server) + self.alt_monsters = self.get_alt_monsters_and_evos(dbcog, self.monster) + transform_base, true_evo_type_raw, acquire_raw, base_rarity = await self.do_query(dbcog, self.monster) self.transform_base = transform_base self.true_evo_type_raw = true_evo_type_raw self.acquire_raw = acquire_raw self.base_rarity = base_rarity @classmethod - async def do_query(cls, dgcog, monster): - db_context = dgcog.database + async def do_query(cls, dbcog, monster): + db_context = dbcog.database acquire_raw, base_rarity, transform_base, true_evo_type_raw = \ await IdViewState._get_monster_misc_info(db_context, monster) diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index 8b692de0..1d95634b 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -9,7 +9,7 @@ from padinfo.common.emoji_map import get_attribute_emoji_by_monster if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class IdTracebackViewProps: diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 0c043ba5..3c34c150 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -40,14 +40,14 @@ def serialize(self): return ret @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): + async def deserialize(dbcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] l_query_settings = QuerySettings.deserialize(ims['l_query_settings']) r_query_settings = QuerySettings.deserialize(ims['r_query_settings']) - l_mon, l_query, r_mon, r_query = await leaderskill_query(dgcog, raw_query, ims['original_author_id']) + l_mon, l_query, r_mon, r_query = await leaderskill_query(dbcog, raw_query, ims['original_author_id']) return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, r_query, l_query_settings, r_query_settings) diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index 777e80ec..8cf25203 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -32,13 +32,13 @@ def serialize(self): return ret @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): + async def deserialize(dbcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] query_settings = QuerySettings.deserialize(ims['query_settings']) - mon = await get_monster_from_ims(dgcog, ims) + mon = await get_monster_from_ims(dbcog, ims) return LeaderSkillSingleViewState(original_author_id, menu_type, raw_query, query_settings, user_config.color, mon) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 977601a9..254f25a2 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -10,7 +10,7 @@ from padinfo.view.components.monster.image import MonsterImage if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class LinksView: diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index 3d4f44d2..f5db5128 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -8,7 +8,7 @@ from padinfo.view.components.monster.image import MonsterImage if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class LookupView: diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 2d2547e7..5f7dfb55 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -18,7 +18,7 @@ from padinfo.view.id import evos_embed_field if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel MAX_MONS_TO_SHOW = 5 @@ -55,17 +55,17 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None - monster = await get_monster_from_ims(dgcog, ims) + monster = await get_monster_from_ims(dbcog, ims) mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable = \ - await MaterialsViewState.do_query(dgcog, monster) + await MaterialsViewState.do_query(dbcog, monster) if mats is None: return None - alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query query_settings = QuerySettings.deserialize(ims.get('query_settings')) @@ -73,7 +73,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = ims.get('reaction_list') - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -83,8 +83,8 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): extra_state=ims) @staticmethod - async def do_query(dgcog, monster): - db_context = dgcog.database + async def do_query(dbcog, monster): + db_context = dbcog.database mats = db_context.graph.evo_mats(monster) usedin = db_context.graph.material_of_monsters(monster) evo_gem = db_context.graph.evo_gem_monster(monster) @@ -114,7 +114,7 @@ async def do_query(dgcog, monster): return None, None, None, None, None, None, None, None if not any([mats, usedin, skillups and not monster.is_stackable]): mats, gemusedin, _, usedin, skillups, skillup_evo_count, link, _ \ - = await MaterialsViewState.do_query(dgcog, evo_gem) + = await MaterialsViewState.do_query(dbcog, evo_gem) gem_override = True return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override diff --git a/padinfo/view/monster_list/all_mats.py b/padinfo/view/monster_list/all_mats.py index a380a0bc..2f44fe3c 100644 --- a/padinfo/view/monster_list/all_mats.py +++ b/padinfo/view/monster_list/all_mats.py @@ -4,22 +4,22 @@ from padinfo.view.monster_list.monster_list import MonsterListViewState if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class AllMatsViewState(MonsterListViewState): VIEW_STATE_TYPE = "AllMats" @classmethod - async def do_query(cls, dgcog, monster: "MonsterModel") -> Optional[List["MonsterModel"]]: - _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.do_query(dgcog, monster) + async def do_query(cls, dbcog, monster: "MonsterModel") -> Optional[List["MonsterModel"]]: + _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.do_query(dbcog, monster) if usedin is None and gemusedin is None: return None monster_list = usedin or gemusedin return monster_list @classmethod - async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: - monster = await dgcog.find_monster(ims['raw_query'], ims['original_author_id']) - monster_list = await cls.do_query(dgcog, monster) + async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: + monster = await dbcog.find_monster(ims['raw_query'], ims['original_author_id']) + monster_list = await cls.do_query(dbcog, monster) return monster_list diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py index 3e3db6e8..1ae4911c 100644 --- a/padinfo/view/monster_list/id_search.py +++ b/padinfo/view/monster_list/id_search.py @@ -3,15 +3,15 @@ from padinfo.view.monster_list.monster_list import MonsterListViewState if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class IdSearchViewState(MonsterListViewState): VIEW_STATE_TYPE = "IdSearch" @classmethod - async def do_query(cls, dgcog, query, original_author_id) -> Optional[List["MonsterModel"]]: - found_monsters = await dgcog.find_monsters(query, original_author_id) + async def do_query(cls, dbcog, query, original_author_id) -> Optional[List["MonsterModel"]]: + found_monsters = await dbcog.find_monsters(query, original_author_id) if not found_monsters: return None @@ -19,7 +19,7 @@ async def do_query(cls, dgcog, query, original_author_id) -> Optional[List["Mons used = set() monster_list = [] for mon in found_monsters: - base_id = dgcog.database.graph.get_base_id(mon) + base_id = dbcog.database.graph.get_base_id(mon) if base_id not in used: used.add(base_id) monster_list.append(mon) @@ -27,6 +27,6 @@ async def do_query(cls, dgcog, query, original_author_id) -> Optional[List["Mons return monster_list @classmethod - async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: - monster_list = await cls.do_query(dgcog, ims['raw_query'], ims['original_author_id']) + async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: + monster_list = await cls.do_query(dbcog, ims['raw_query'], ims['original_author_id']) return monster_list diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 7a68d878..aef36184 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -12,7 +12,7 @@ from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class MonsterListViewState(ViewStateBase): @@ -85,12 +85,12 @@ def get_serialized_child_extra_ims(self): return extra_ims @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None title = ims['title'] - monster_list = await cls.query_from_ims(dgcog, ims) + monster_list = await cls.query_from_ims(dbcog, ims) current_page = ims['current_page'] current_index = ims.get('current_index') @@ -117,7 +117,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): ) @classmethod - async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: + async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: ... @staticmethod @@ -127,8 +127,8 @@ def paginate(monster_list: List["MonsterModel"]) -> List[List["MonsterModel"]]: return paginated_monsters @classmethod - async def query_paginated_from_ims(cls, dgcog, ims) -> List[List["MonsterModel"]]: - return cls.paginate(await cls.query_from_ims(dgcog, ims)) + async def query_paginated_from_ims(cls, dbcog, ims) -> List[List["MonsterModel"]]: + return cls.paginate(await cls.query_from_ims(dbcog, ims)) def increment_page(self): if self.current_page < len(self.paginated_monsters) - 1: diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index 57d761e2..5fe2ee33 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -6,8 +6,8 @@ from padinfo.view.monster_list.monster_list import MonsterListViewState if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.database_context import DbContext + from dbcog.models.monster_model import MonsterModel + from dbcog.database_context import DbContext class StaticMonsterListViewState(MonsterListViewState): @@ -21,14 +21,14 @@ def serialize(self): return ret @classmethod - async def do_query(cls, dgcog, monster_list, server) -> List["MonsterModel"]: - db_context: "DbContext" = dgcog.database + async def do_query(cls, dbcog, monster_list, server) -> List["MonsterModel"]: + db_context: "DbContext" = dbcog.database monster_list = [db_context.graph.get_monster(int(m), server=server) for m in monster_list] return monster_list @classmethod - async def query_from_ims(cls, dgcog, ims) -> List["MonsterModel"]: + async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: monster_ids = ims['full_monster_list'] query_settings = QuerySettings.deserialize(ims['query_settings']) - monster_list = await cls.do_query(dgcog, monster_ids, query_settings.server) + monster_list = await cls.do_query(dbcog, monster_ids, query_settings.server) return monster_list diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 6acfb62b..70770855 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -16,7 +16,7 @@ from padinfo.view.links import LinksView if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel class OtherInfoViewState(ViewStateBaseId): diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 8c9e673c..1dd4f80d 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -18,7 +18,7 @@ from padinfo.view.id import evos_embed_field if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel MAX_MONS_TO_SHOW = 20 MAT_TYPES = ['Evolve', 'Awoken', 'Enhance', 'Vendor'] @@ -49,16 +49,16 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None - monster = await get_monster_from_ims(dgcog, ims) - pantheon_list, series_name, base_monster = await PantheonViewState.do_query(dgcog, monster) + monster = await get_monster_from_ims(dbcog, ims) + pantheon_list, series_name, base_monster = await PantheonViewState.do_query(dbcog, monster) if pantheon_list is None: return None - alt_monsters = cls.get_alt_monsters_and_evos(dgcog, monster) + alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) raw_query = ims['raw_query'] query = ims.get('query') or raw_query query_settings = QuerySettings.deserialize(ims.get('query_settings')) @@ -66,7 +66,7 @@ async def deserialize(cls, dgcog, user_config: UserConfig, ims: dict): use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(monster) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, @@ -80,7 +80,7 @@ def make_series_name(cls, series_name, filter_strings): return '{} [Filters: {}]'.format(series_name, ' '.join(filter_strings)) @classmethod - async def do_query(cls, dgcog, monster): + async def do_query(cls, dbcog, monster): # filtering rules: # 1. don't show monsters that only have mat types (or show only mats if monster is a mat) # 2. if still too many, show only monsters with the same base rarity @@ -89,7 +89,7 @@ async def do_query(cls, dgcog, monster): # 4. if still too many, show only monsters that match both base attributes # 5. if still too many, truncate (and probably redefine pantheon) - db_context = dgcog.database + db_context = dbcog.database series_name = monster.series.name_en full_pantheon = db_context.get_monsters_by_series(monster.series_id, server=monster.server_priority) if not full_pantheon: diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 6c84094c..85cde690 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -12,8 +12,8 @@ from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel - from dadguide.database_context import DbContext + from dbcog.models.monster_model import MonsterModel + from dbcog.database_context import DbContext class SeriesScrollViewState(ViewStateBase): @@ -93,14 +93,14 @@ def get_serialized_child_extra_ims(self, emoji_names, menu_type): return extra_ims @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): + async def deserialize(dbcog, user_config: UserConfig, ims: dict): if ims.get('unsupported_transition'): return None series_id = ims['series_id'] rarity = ims['rarity'] all_rarities = ims['all_rarities'] query_settings = QuerySettings.deserialize(ims.get('query_settings')) - paginated_monsters = await SeriesScrollViewState.do_query(dgcog, series_id, rarity, query_settings.server) + paginated_monsters = await SeriesScrollViewState.do_query(dbcog, series_id, rarity, query_settings.server) current_page = ims['current_page'] title = ims['title'] @@ -126,8 +126,8 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): child_message_id=child_message_id) @staticmethod - async def do_query(dgcog, series_id, rarity, server): - db_context: "DbContext" = dgcog.database + async def do_query(dbcog, series_id, rarity, server): + db_context: "DbContext" = dbcog.database all_series_monsters = db_context.get_monsters_by_series(series_id, server=server) base_monsters_of_rarity = list(filter( lambda m: db_context.graph.monster_is_base(m) and m.rarity == rarity, all_series_monsters)) @@ -137,20 +137,20 @@ async def do_query(dgcog, series_id, rarity, server): return paginated_monsters @staticmethod - def query_all_rarities(dgcog, series_id, server): - db_context: "DbContext" = dgcog.database + def query_all_rarities(dbcog, series_id, server): + db_context: "DbContext" = dbcog.database return sorted({m.rarity for m in db_context.get_all_monsters(server) if m.series_id == series_id and db_context.graph.monster_is_base(m)}) @staticmethod - async def query_from_ims(dgcog, ims) -> List[List["MonsterModel"]]: + async def query_from_ims(dbcog, ims) -> List[List["MonsterModel"]]: series_id = ims['series_id'] rarity = ims['rarity'] query_settings = QuerySettings.deserialize(ims['query_settings']) - paginated_monsters = await SeriesScrollViewState.do_query(dgcog, series_id, rarity, query_settings.server) + paginated_monsters = await SeriesScrollViewState.do_query(dbcog, series_id, rarity, query_settings.server) return paginated_monsters - async def decrement_page(self, dgcog): + async def decrement_page(self, dbcog): if self.current_page > 0: self.current_page = self.current_page - 1 self.current_index = None @@ -159,7 +159,7 @@ async def decrement_page(self, dgcog): if len(self.all_rarities) > 1: rarity_index = self.all_rarities.index(self.rarity) self.rarity = self.all_rarities[rarity_index - 1] - self.paginated_monsters = await SeriesScrollViewState.do_query(dgcog, self.series_id, self.rarity, + self.paginated_monsters = await SeriesScrollViewState.do_query(dbcog, self.series_id, self.rarity, self.query_settings.server) self.current_index = None self.current_page = len(self.paginated_monsters) - 1 @@ -167,7 +167,7 @@ async def decrement_page(self, dgcog): if len(self.paginated_monsters) > 1: self.current_index = None - async def increment_page(self, dgcog): + async def increment_page(self, dbcog): if self.current_page < len(self.paginated_monsters) - 1: self.current_page = self.current_page + 1 self.current_index = None @@ -176,7 +176,7 @@ async def increment_page(self, dgcog): if len(self.all_rarities) > 1: rarity_index = self.all_rarities.index(self.rarity) self.rarity = self.all_rarities[(rarity_index + 1) % len(self.all_rarities)] - self.paginated_monsters = await SeriesScrollViewState.do_query(dgcog, self.series_id, self.rarity, + self.paginated_monsters = await SeriesScrollViewState.do_query(dbcog, self.series_id, self.rarity, self.query_settings.server) self.current_index = None self.current_page = 0 @@ -184,24 +184,24 @@ async def increment_page(self, dgcog): if len(self.paginated_monsters) > 1: self.current_index = None - async def decrement_index(self, dgcog): + async def decrement_index(self, dbcog): if self.current_index is None: self.current_index = len(self.monster_list) - 1 return if self.current_index > 0: self.current_index = self.current_index - 1 return - await self.decrement_page(dgcog) + await self.decrement_page(dbcog) self.current_index = len(self.monster_list) - 1 - async def increment_index(self, dgcog): + async def increment_index(self, dbcog): if self.current_index is None: self.current_index = 0 return if self.current_index < len(self.monster_list) - 1: self.current_index = self.current_index + 1 return - await self.increment_page(dgcog) + await self.increment_page(dbcog) self.current_index = 0 def set_index(self, new_index: int): diff --git a/padinfo/view/simple_text.py b/padinfo/view/simple_text.py index 4b5d00ec..6b516ea3 100644 --- a/padinfo/view/simple_text.py +++ b/padinfo/view/simple_text.py @@ -26,7 +26,7 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, _dgcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, _dbcog, user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] raw_query = ims.get('raw_query') diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 125b918c..aed8798c 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -17,7 +17,7 @@ from padinfo.view.id import IdView if TYPE_CHECKING: - from dadguide.models.monster_model import MonsterModel + from dbcog.models.monster_model import MonsterModel BASE_EMOJI = '\N{DOWN-POINTING RED TRIANGLE}' TRANSFORM_EMOJI = '\N{UP-POINTING RED TRIANGLE}' @@ -47,7 +47,7 @@ def serialize(self): return ret @staticmethod - async def deserialize(dgcog, user_config: UserConfig, ims: dict): + async def deserialize(dbcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] @@ -56,13 +56,13 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): transformed_mon_id = monster_ids[1] query_settings = QuerySettings.deserialize(ims.get('query_settings')) - base_mon = dgcog.get_monster(base_mon_id, server=query_settings.server) - transformed_mon = dgcog.get_monster(transformed_mon_id, server=query_settings.server) + base_mon = dbcog.get_monster(base_mon_id, server=query_settings.server) + transformed_mon = dbcog.get_monster(transformed_mon_id, server=query_settings.server) - acquire_raw = await TransformInfoViewState.do_query(dgcog, base_mon, transformed_mon) + acquire_raw = await TransformInfoViewState.do_query(dbcog, base_mon, transformed_mon) reaction_list = ims['reaction_list'] - is_jp_buffed = dgcog.database.graph.monster_is_discrepant(base_mon) \ - or dgcog.database.graph.monster_is_discrepant(transformed_mon) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(base_mon) \ + or dbcog.database.graph.monster_is_discrepant(transformed_mon) return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, base_mon, transformed_mon, acquire_raw, monster_ids, is_jp_buffed, @@ -70,8 +70,8 @@ async def deserialize(dgcog, user_config: UserConfig, ims: dict): reaction_list=reaction_list) @staticmethod - async def do_query(dgcog, base_mon, transformed_mon): - db_context = dgcog.database + async def do_query(dbcog, base_mon, transformed_mon): + db_context = dbcog.database acquire_raw = db_context.graph.monster_acquisition(transformed_mon) return acquire_raw diff --git a/padmonitor/info.json b/padmonitor/info.json index 4b8336d2..02065ead 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ "tsutils>=3.0.0" diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 31875086..f3e5db0a 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -10,7 +10,7 @@ logger = logging.getLogger('red.padbot-cogs.padmonitor') if TYPE_CHECKING: - from dadguide.database_context import DbContext + from dbcog.database_context import DbContext class PadMonitor(commands.Cog): @@ -44,7 +44,7 @@ async def check_seen_loop(self): async def check_seen(self): """Refresh the monster indexes.""" - DGCOG = self.bot.get_cog('Dadguide') + DGCOG = self.bot.get_cog('DBCog') await DGCOG.wait_until_ready() db_context: "DbContext" = DGCOG.database all_monsters = [*db_context.get_all_monsters()] diff --git a/padsearch/info.json b/padsearch/info.json index 1a11f28b..32984638 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ "tsutils", diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index d806b9de..b515811f 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -737,11 +737,11 @@ async def search(self, ctx, *, filter_spec: str): except: # If it still failed, raise the original exception raise ex - dgcog = self.bot.get_cog('Dadguide') - if dgcog is None: - await ctx.send("Dadguide cog not loaded.") + dbcog = self.bot.get_cog('DBCog') + if dbcog is None: + await ctx.send("DBCog cog not loaded.") return - monsters = dgcog.database.get_all_monsters() + monsters = dbcog.database.get_all_monsters() rmvGemFilter = self._make_search_config('remove( gem)') monsters = list(filter(rmvGemFilter.check_filters, monsters)) @@ -786,5 +786,5 @@ async def search(self, ctx, *, filter_spec: str): def _make_search_config(self, input): lexer = PadSearchLexer().build() lexer.input(input) - db_context = self.bot.get_cog("Dadguide").database + db_context = self.bot.get_cog("DBCog").database return SearchConfig(lexer, db_context) diff --git a/pricecheck/info.json b/pricecheck/info.json index faa006b2..de6531e5 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": { "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", - "dadguide": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index d88cda08..8d0cfd57 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -46,17 +46,17 @@ async def red_delete_data_for_user(self, *, requester, user_id): @commands.group(invoke_without_command=True) async def pricecheck(self, ctx, *, query): """Displays pricing data for a tradable non-collab gem.""" - dgcog = self.bot.get_cog("Dadguide") - if dgcog is None: + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query, ctx.author.id) + m = await dbcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return - base_id = str(dgcog.database.graph.get_base_id(m)) + base_id = str(dbcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: if base_id not in pcs: if m.sell_mp < 100: @@ -87,17 +87,17 @@ async def pcadmin(self, ctx): @pcadmin.command(aliases=['add']) async def set(self, ctx, stam_cost: float, *, query): """Adds stamina cost data to a card.""" - dgcog = self.bot.get_cog("Dadguide") - if dgcog is None: + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query, ctx.author.id) + m = await dbcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return - base_id = str(dgcog.database.graph.get_base_id(m)) + base_id = str(dbcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: foot = "" if base_id in pcs: @@ -108,17 +108,17 @@ async def set(self, ctx, stam_cost: float, *, query): @pcadmin.command(aliases=['addfooter', 'addfoot', 'setfoot']) async def setfooter(self, ctx, query, *, footer=""): """Adds notes regarding the stamina cost of a card.""" - dgcog = self.bot.get_cog('Dadguide') - if dgcog is None: + dbcog = self.bot.get_cog('DBCog') + if dbcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query, ctx.author.id) + m = await dbcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return - base_id = str(dgcog.database.graph.get_base_id(m)) + base_id = str(dbcog.database.graph.get_base_id(m)) async with self.config.pcs() as pcs: sc = -1 if base_id in pcs: @@ -129,21 +129,21 @@ async def setfooter(self, ctx, query, *, footer=""): @pcadmin.command(aliases=['delete', 'del', 'rm']) async def remove(self, ctx, *, query): """Removes stamina cost data from a card.""" - dgcog = self.bot.get_cog('Dadguide') - if dgcog is None: + dbcog = self.bot.get_cog('DBCog') + if dbcog is None: await ctx.send(inline("Error: Cog not loaded. Please alert a bot owner.")) return if "gem" not in query.lower(): query += " gem" - m = await dgcog.find_monster(query, ctx.author.id) + m = await dbcog.find_monster(query, ctx.author.id) if not m: await ctx.send("Monster not found.") return async with self.config.pcs() as pcs: - if str(dgcog.database.graph.get_base_id(m)) not in pcs: + if str(dbcog.database.graph.get_base_id(m)) not in pcs: await ctx.send("{} does not have PC data.".format(m.name_en)) return - del pcs[str(dgcog.database.graph.get_base_id(m))] + del pcs[str(dbcog.database.graph.get_base_id(m))] await ctx.send(inline("Removed PC data from {}.".format(m.name_en))) @pcadmin.command() diff --git a/tests/dadguide/database_context_test.py b/tests/dbcog/database_context_test.py similarity index 77% rename from tests/dadguide/database_context_test.py rename to tests/dbcog/database_context_test.py index 9bdbd349..38c910c6 100644 --- a/tests/dadguide/database_context_test.py +++ b/tests/dbcog/database_context_test.py @@ -1,12 +1,12 @@ from tsutils.enums import Server -from dadguide.database_manager import DadguideDatabase -from dadguide.database_context import DbContext -from dadguide.dungeon_context import DungeonContext -from dadguide.monster_graph import MonsterGraph -from dadguide.models.enum_types import InternalEvoType +from dbcog.database_manager import DBCogDatabase +from dbcog.database_context import DbContext +from dbcog.dungeon_context import DungeonContext +from dbcog.monster_graph import MonsterGraph +from dbcog.models.enum_types import InternalEvoType -database = DadguideDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') +database = DBCogDatabase('S:\\Documents\\Games\\PAD\\dadguide.sqlite') graph = MonsterGraph(database) dungeon = DungeonContext(database) dungeon = DungeonContext(database) From 626612ecf00f781c66c72768d0707371390cec35 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Wed, 28 Jul 2021 14:33:29 -0400 Subject: [PATCH 0907/1220] Add toggles to buttoninfo (#1343) --- padinfo/core/button_info.py | 108 ++++++++++++++--- padinfo/menu/button_info.py | 140 ++++++++++++++++++++++ padinfo/menu/closable_embed.py | 2 - padinfo/menu/menu_map.py | 2 + padinfo/padinfo.py | 17 +-- padinfo/view/button_info.py | 209 +++++++++++++++++++++++++-------- 6 files changed, 407 insertions(+), 71 deletions(-) create mode 100644 padinfo/menu/button_info.py diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 0e2e4402..fdb03575 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -73,7 +73,7 @@ def get_info(self, dbcog, monster_model): sub_attr_multiplier = self._get_sub_attr_multiplier(monster_model) result = ButtonInfoResult() - result.main_damage = self._calculate_damage(dbcog, monster_model, max_level) + result.main_damage = self._calculate_coop_damage(dbcog, monster_model, max_level) result.sub_damage = result.main_damage * sub_attr_multiplier result.total_damage = result.main_damage + result.sub_damage if slb_level is None: @@ -81,11 +81,23 @@ def get_info(self, dbcog, monster_model): result.sub_slb_damage = None result.total_slb_damage = None else: - result.main_slb_damage = self._calculate_damage(dbcog, monster_model, slb_level) + result.main_slb_damage = self._calculate_coop_damage(dbcog, monster_model, slb_level) result.sub_slb_damage = result.main_slb_damage * sub_attr_multiplier result.total_slb_damage = result.main_slb_damage + result.sub_slb_damage - result.main_damage_with_atk_latent = self._calculate_damage( + result.main_solo_damage = self._calculate_solo_damage(dbcog, monster_model, max_level) + result.sub_solo_damage = result.main_solo_damage * sub_attr_multiplier + result.total_solo_damage = result.main_solo_damage + result.sub_solo_damage + if slb_level is None: + result.main_solo_slb_damage = None + result.sub_solo_slb_damage = None + result.total_solo_slb_damage = None + else: + result.main_solo_slb_damage = self._calculate_solo_damage(dbcog, monster_model, slb_level) + result.sub_solo_slb_damage = result.main_solo_slb_damage * sub_attr_multiplier + result.total_solo_slb_damage = result.main_solo_slb_damage + result.sub_solo_slb_damage + + result.main_damage_with_atk_latent = self._calculate_coop_damage( dbcog, monster_model, max_level, num_atkplus_latent=max_atk_latents) result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent @@ -94,22 +106,52 @@ def get_info(self, dbcog, monster_model): result.sub_damage_with_slb_atk_latent = None result.total_damage_with_slb_atk_latent = None else: - result.main_damage_with_slb_atk_latent = self._calculate_damage( + result.main_damage_with_slb_atk_latent = self._calculate_coop_damage( dbcog, monster_model, slb_level, num_atkplus2_latent=max_atk_latents) result.sub_damage_with_slb_atk_latent = result.main_damage_with_slb_atk_latent * sub_attr_multiplier result.total_damage_with_slb_atk_latent = (result.main_damage_with_slb_atk_latent + result.sub_damage_with_slb_atk_latent) - result.card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dbcog, monster_model) - result.team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dbcog, monster_model) + + result.main_solo_damage_with_atk_latent = self._calculate_solo_damage( + dbcog, monster_model, max_level, num_atkplus_latent=max_atk_latents) + result.sub_solo_damage_with_atk_latent = result.main_solo_damage_with_atk_latent * sub_attr_multiplier + result.total_solo_damage_with_atk_latent = (result.main_solo_damage_with_atk_latent + + result.sub_solo_damage_with_atk_latent) + if slb_level is None: + result.main_solo_damage_with_slb_atk_latent = None + result.sub_solo_damage_with_slb_atk_latent = None + result.total_solo_damage_with_slb_atk_latent = None + else: + result.main_solo_damage_with_slb_atk_latent = self._calculate_solo_damage( + dbcog, monster_model, slb_level, num_atkplus2_latent=max_atk_latents) + result.sub_solo_damage_with_slb_atk_latent = (result.main_solo_damage_with_slb_atk_latent + * sub_attr_multiplier) + result.total_solo_damage_with_slb_atk_latent = (result.main_solo_damage_with_slb_atk_latent + + result.sub_solo_damage_with_slb_atk_latent) + + result.card_btn_str = self._get_card_coop_damage(CARD_BUTTONS, dbcog, monster_model) + result.card_btn_solo_str = self._get_card_solo_damage(CARD_BUTTONS, dbcog, monster_model) + result.card_btn_slb_str = self._get_card_coop_slb_damage(CARD_BUTTONS, dbcog, monster_model) + result.card_btn_solo_slb_str = self._get_card_solo_slb_damage(CARD_BUTTONS, dbcog, monster_model) + result.team_btn_str = self._get_team_coop_damage(TEAM_BUTTONS, dbcog, monster_model) + result.team_btn_solo_str = self._get_team_solo_damage(TEAM_BUTTONS, dbcog, monster_model) + result.team_btn_slb_str = self._get_team_coop_slb_damage(TEAM_BUTTONS, dbcog, monster_model) + result.team_btn_solo_slb_str = self._get_team_solo_slb_damage(TEAM_BUTTONS, dbcog, monster_model) return result - def _calculate_damage(self, dbcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): + def _calculate_coop_damage(self, dbcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): + return self._calculate_damage(dbcog, monster_model, level, True, num_atkplus_latent, num_atkplus2_latent) + + def _calculate_solo_damage(self, dbcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): + return self._calculate_damage(dbcog, monster_model, level, False, num_atkplus_latent, num_atkplus2_latent) + + def _calculate_damage(self, dbcog, monster_model, level, multiplayer, num_atkplus_latent=0, num_atkplus2_latent=0): stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=num_atkplus_latent, num_atkplus2=num_atkplus2_latent) stat_latents.num_atk_awakening = len( [x for x in monster_model.awakenings if x.awoken_skill_id == 1]) dmg = dbcog.monster_stats.stat(monster_model, 'atk', level, - stat_latents=stat_latents, multiplayer=True) + stat_latents=stat_latents, multiplayer=multiplayer) return int(round(dmg)) @@ -129,32 +171,58 @@ def to_string(self, dbcog, monster, info): round(info.sub_damage_with_atk_latent)), int(round(info.total_damage_with_atk_latent)), card_btn_str, team_btn_str) - def _get_card_btn_damage(self, card_buttons, dbcog, monster): + def _get_card_coop_damage(self, card_buttons, dbcog, monster): + return self._get_card_btn_damage(card_buttons, dbcog, monster, True, LIMIT_BREAK_LEVEL) + + def _get_card_solo_damage(self, card_buttons, dbcog, monster): + return self._get_card_btn_damage(card_buttons, dbcog, monster, False, LIMIT_BREAK_LEVEL) + + def _get_card_coop_slb_damage(self, card_buttons, dbcog, monster): + return self._get_card_btn_damage(card_buttons, dbcog, monster, True, SUPER_LIMIT_BREAK_LEVEL) + + def _get_card_solo_slb_damage(self, card_buttons, dbcog, monster): + return self._get_card_btn_damage(card_buttons, dbcog, monster, False, SUPER_LIMIT_BREAK_LEVEL) + + def _get_card_btn_damage(self, card_buttons, dbcog, monster, multiplayer, limit_break): lines = [] card_buttons.sort(key=lambda x: x.mult) for card in card_buttons: inherit_model = dbcog.get_monster(card.id, server=monster.server_priority) - max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level + max_level = limit_break if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) dmg = int(round(dbcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, - inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level))) + inherited_monster=inherit_model, multiplayer=multiplayer, + inherited_monster_lvl=inherit_max_level))) oncolor = '*' if monster.attr1.value == inherit_model.attr1.value or monster.attr1.name == NIL_ATT else ' ' lines.append(CARD_BUTTON_FORMAT.format( card.id, card.name.format(oncolor), card.mult, round(dmg * card.mult, 2))) return "\n".join(lines) - def _get_team_btn_damage(self, team_buttons, dbcog, monster): + def _get_team_coop_damage(self, team_buttons, dbcog, monster): + return self._get_team_btn_damage(team_buttons, dbcog, monster, True, LIMIT_BREAK_LEVEL) + + def _get_team_solo_damage(self, team_buttons, dbcog, monster): + return self._get_team_btn_damage(team_buttons, dbcog, monster, False, LIMIT_BREAK_LEVEL) + + def _get_team_coop_slb_damage(self, team_buttons, dbcog, monster): + return self._get_team_btn_damage(team_buttons, dbcog, monster, True, SUPER_LIMIT_BREAK_LEVEL) + + def _get_team_solo_slb_damage(self, team_buttons, dbcog, monster): + return self._get_team_btn_damage(team_buttons, dbcog, monster, False, SUPER_LIMIT_BREAK_LEVEL) + + def _get_team_btn_damage(self, team_buttons, dbcog, monster, multiplayer, limit_break): lines = [] team_buttons.sort(key=lambda x: x.mult) for card in team_buttons: total_dmg = 0 inherit_model = dbcog.get_monster(card.id, server=monster.server_priority) - max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level + max_level = limit_break if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) dmg = dbcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, - inherited_monster=inherit_model, multiplayer=True, inherited_monster_lvl=inherit_max_level) + inherited_monster=inherit_model, multiplayer=multiplayer, + inherited_monster_lvl=inherit_max_level) if(monster.attr1.value in card.att): total_dmg += dmg if(monster.attr2.value in card.att): @@ -172,15 +240,27 @@ class ButtonInfoResult: main_damage: float total_damage: float sub_damage: float + main_solo_damage: float + total_solo_damage: float + sub_solo_damage: float main_slb_damage: float total_slb_damage: float sub_slb_damage: float + main_solo_slb_damage: float + total_solo_slb_damage: float + sub_solo_slb_damage: float main_damage_with_atk_latent: float total_damage_with_atk_latent: float sub_damage_with_atk_latent: float + main_solo_damage_with_atk_latent: float + total_solo_damage_with_atk_latent: float + sub_solo_damage_with_atk_latent: float main_damage_with_slb_atk_latent: float total_damage_with_slb_atk_latent: float sub_damage_with_slb_atk_latent: float + main_solo_damage_with_slb_atk_latent: float + total_solo_damage_with_slb_atk_latent: float + sub_solo_damage_with_slb_atk_latent: float button_info = ButtonInfo() diff --git a/padinfo/menu/button_info.py b/padinfo/menu/button_info.py new file mode 100644 index 00000000..8f44b513 --- /dev/null +++ b/padinfo/menu/button_info.py @@ -0,0 +1,140 @@ +from typing import Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from tsutils import char_to_emoji +from tsutils.menu.panes import emoji_buttons, MenuPanes + +from padinfo.view.button_info import ButtonInfoOptions, ButtonInfoToggles, ButtonInfoViewState, ButtonInfoView + + +class ButtonInfoEmoji: + delete = '\N{CROSS MARK}' + home = emoji_buttons['home'] + coop = '\N{BUSTS IN SILHOUETTE}' + solo = '\N{BUST IN SILHOUETTE}' + desktop = '\N{DESKTOP COMPUTER}' + mobile = '\N{MOBILE PHONE}' + limit_break = ('lv110', char_to_emoji('1')) + super_limit_break = ('lv120', char_to_emoji('2')) + + +class ButtonInfoMenu: + MENU_TYPE = 'ButtonInfo' + + @staticmethod + def menu(): + return EmbedMenu(ButtonInfoMenuPanes.transitions(), ButtonInfoMenu.button_info_control) + + @classmethod + async def _get_view_state(cls, ims: dict, **data) -> ButtonInfoViewState: + dbcog = data['dbcog'] + user_config = data['user_config'] + return await ButtonInfoViewState.deserialize(dbcog, user_config, ims) + + @staticmethod + async def respond_with_delete(message: Message, ims, **data): + return await message.delete() + + @staticmethod + async def respond_with_button_info(message: Optional[Message], ims, **data): + dbcog = data['dbcog'] + user_config = data['user_config'] + view_state = await ButtonInfoViewState.deserialize(dbcog, user_config, ims) + control = ButtonInfoMenu.button_info_control(view_state) + return control + + @classmethod + async def respond_with_coop(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.set_player_count(ButtonInfoOptions.coop) + return ButtonInfoMenu.button_info_control(view_state) + + @classmethod + async def respond_with_solo(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.set_player_count(ButtonInfoOptions.solo) + return ButtonInfoMenu.button_info_control(view_state) + + @classmethod + async def respond_with_desktop(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.set_device(ButtonInfoOptions.desktop) + return ButtonInfoMenu.button_info_control(view_state) + + @classmethod + async def respond_with_mobile(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.set_device(ButtonInfoOptions.mobile) + return ButtonInfoMenu.button_info_control(view_state) + + @classmethod + async def respond_with_limit_break(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.set_max_level(ButtonInfoOptions.limit_break) + return ButtonInfoMenu.button_info_control(view_state) + + @classmethod + async def respond_with_super_limit_break(cls, message: Optional[Message], ims, **data): + view_state = await cls._get_view_state(ims, **data) + view_state.set_max_level(ButtonInfoOptions.super_limit_break) + return ButtonInfoMenu.button_info_control(view_state) + + @staticmethod + def button_info_control(state: ButtonInfoViewState): + if state is None: + return None + reaction_list = ButtonInfoMenuPanes.get_user_reaction_list(state.display_options) + return EmbedControl( + [ButtonInfoView.embed(state)], + reaction_list + ) + + +class ButtonInfoMenuPanes(MenuPanes): + INITIAL_EMOJI = ButtonInfoEmoji.home + + DATA = { + ButtonInfoEmoji.delete: (ButtonInfoMenu.respond_with_delete, None), + ButtonInfoEmoji.home: (ButtonInfoMenu.respond_with_button_info, ButtonInfoView.VIEW_TYPE), + ButtonInfoEmoji.coop: (ButtonInfoMenu.respond_with_coop, ButtonInfoView.VIEW_TYPE), + ButtonInfoEmoji.solo: (ButtonInfoMenu.respond_with_solo, ButtonInfoView.VIEW_TYPE), + ButtonInfoEmoji.desktop: (ButtonInfoMenu.respond_with_desktop, ButtonInfoView.VIEW_TYPE), + ButtonInfoEmoji.mobile: (ButtonInfoMenu.respond_with_mobile, ButtonInfoView.VIEW_TYPE), + ButtonInfoEmoji.limit_break: (ButtonInfoMenu.respond_with_limit_break, ButtonInfoView.VIEW_TYPE), + ButtonInfoEmoji.super_limit_break: (ButtonInfoMenu.respond_with_super_limit_break, ButtonInfoView.VIEW_TYPE), + } + + HIDDEN_EMOJIS = [ + ButtonInfoEmoji.home, + ] + + OPTIONAL_EMOJIS = [ + ButtonInfoEmoji.coop, + ButtonInfoEmoji.solo, + ButtonInfoEmoji.desktop, + ButtonInfoEmoji.mobile, + ButtonInfoEmoji.limit_break, + ButtonInfoEmoji.super_limit_break, + ] + + @classmethod + def get_user_reaction_list(cls, current_options: ButtonInfoToggles): + other_toggle_reactions = [] + if current_options.players == ButtonInfoOptions.coop: + other_toggle_reactions.append(ButtonInfoEmoji.solo) + elif current_options.players == ButtonInfoOptions.solo: + other_toggle_reactions.append(ButtonInfoEmoji.coop) + + if current_options.device == ButtonInfoOptions.desktop: + other_toggle_reactions.append(ButtonInfoEmoji.mobile) + elif current_options.device == ButtonInfoOptions.mobile: + other_toggle_reactions.append(ButtonInfoEmoji.desktop) + + if current_options.max_level == ButtonInfoOptions.limit_break: + other_toggle_reactions.append(ButtonInfoEmoji.super_limit_break) + elif current_options.max_level == ButtonInfoOptions.super_limit_break: + other_toggle_reactions.append(ButtonInfoEmoji.limit_break) + + return [_ for _ in cls.emoji_names() if _ not in cls.OPTIONAL_EMOJIS] + other_toggle_reactions diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index 4ea9c57e..456e6539 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -3,13 +3,11 @@ from tsutils.menu.panes import MenuPanes from padinfo.view.awakening_help import AwakeningHelpView -from padinfo.view.button_info import ButtonInfoView from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.id_traceback import IdTracebackView view_types = { AwakeningHelpView.VIEW_TYPE: AwakeningHelpView, - ButtonInfoView.VIEW_TYPE: ButtonInfoView, IdTracebackView.VIEW_TYPE: IdTracebackView, } diff --git a/padinfo/menu/menu_map.py b/padinfo/menu/menu_map.py index 6739ce3c..037225b4 100644 --- a/padinfo/menu/menu_map.py +++ b/padinfo/menu/menu_map.py @@ -1,4 +1,5 @@ from padinfo.menu.awakening_list import AwakeningListMenu, AwakeningListMenuPanes +from padinfo.menu.button_info import ButtonInfoMenu, ButtonInfoMenuPanes from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes @@ -14,6 +15,7 @@ padinfo_menu_map = { AwakeningListMenu.MENU_TYPE: (AwakeningListMenu, AwakeningListMenuPanes), + ButtonInfoMenu.MENU_TYPE: (ButtonInfoMenu, ButtonInfoMenuPanes), ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), IdMenu.MENU_TYPE: (IdMenu, IdMenuPanes), LeaderSkillMenu.MENU_TYPE: (LeaderSkillMenu, LeaderSkillMenuPanes), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3c7f8852..b527f7c2 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -27,6 +27,7 @@ from padinfo.core.padinfo_settings import settings from padinfo.core.transforminfo import perform_transforminfo_query from padinfo.menu.awakening_list import AwakeningListMenu, AwakeningListMenuPanes +from padinfo.menu.button_info import ButtonInfoMenu, ButtonInfoMenuPanes from padinfo.menu.closable_embed import ClosableEmbedMenu from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu @@ -40,7 +41,7 @@ from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.awakening_help import AwakeningHelpView, AwakeningHelpViewProps from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListSortTypes -from padinfo.view.button_info import ButtonInfoView, ButtonInfoViewProps +from padinfo.view.button_info import ButtonInfoToggles, ButtonInfoViewState from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.common import invalid_monster_text from padinfo.view.components.monster.header import MonsterHeader @@ -580,13 +581,15 @@ async def buttoninfo(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return - info = button_info.get_info(dbcog, monster) - original_author_id = ctx.message.author.id - menu = ClosableEmbedMenu.menu() color = await self.get_user_embed_color(ctx) - props = ButtonInfoViewProps(monster=monster, info=info) - state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, - color, ButtonInfoView.VIEW_TYPE, props) + original_author_id = ctx.message.author.id + info = button_info.get_info(dbcog, monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + display_options = ButtonInfoToggles() + state = ButtonInfoViewState(original_author_id, ButtonInfoMenu.MENU_TYPE, query, color, display_options, + monster, info, query_settings, + reaction_list=ButtonInfoMenuPanes.get_user_reaction_list(display_options)) + menu = ButtonInfoMenu.menu() await menu.create(ctx, state) @commands.command() diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 290d2bbc..8cfdc518 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -1,91 +1,186 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedAuthor, EmbedField, EmbedMain from discordmenu.embed.text import Text, BlockText from discordmenu.embed.view import EmbedView from tsutils import embed_footer_with_state +from tsutils.query_settings import QuerySettings +from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx -from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL +from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage +from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel -class ButtonInfoViewProps: - def __init__(self, monster: "MonsterModel", info): +class ButtonInfoOptions: + coop = 'Multiplayer' + solo = 'Singleplayer' + desktop = 'Desktop' + mobile = 'Mobile' + limit_break = 'Level 110' + super_limit_break = 'Level 120' + + +class ButtonInfoToggles: + def __init__(self, players_setting=ButtonInfoOptions.coop, device_setting=ButtonInfoOptions.desktop, + max_level_setting=ButtonInfoOptions.limit_break): + self.players = players_setting + self.device = device_setting + self.max_level = max_level_setting + + +class ButtonInfoViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, raw_query, color, display_options: ButtonInfoToggles, + monster: "MonsterModel", info, query_settings: QuerySettings, reaction_list: List[str] = None): + super().__init__(original_author_id, menu_type, raw_query, extra_state=None, reaction_list=reaction_list) + self.color = color + self.display_options = display_options self.monster = monster self.info = info + self.query_settings = query_settings + + def serialize(self): + ret = super().serialize() + ret.update({ + # maybe serialize the object? look into how it's done for query settings + 'players_setting': self.display_options.players, + 'device_setting': self.display_options.device, + 'max_level_setting': self.display_options.max_level, + 'resolved_monster_id': self.monster.monster_id, + 'query_settings': self.query_settings.serialize() + }) + return ret - -def get_max_level(monster): - level_text = str(LIMIT_BREAK_LEVEL) if monster.limit_mult != 0 else 'Max ({})'.format(monster.level) + @staticmethod + async def deserialize(dbcog, user_config: UserConfig, ims: dict): + raw_query = ims['raw_query'] + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + query_settings = QuerySettings.deserialize(ims['query_settings']) + display_options = ButtonInfoToggles(ims['players_setting'], ims['device_setting'], ims['max_level_setting']) + monster = dbcog.get_monster(ims['resolved_monster_id']) + info = button_info.get_info(dbcog, monster) + reaction_list = ims['reaction_list'] + return ButtonInfoViewState(original_author_id, menu_type, raw_query, user_config.color, display_options, + monster, info, query_settings, reaction_list=reaction_list) + + def set_player_count(self, new_count): + self.display_options.players = new_count + + def set_device(self, new_device): + self.display_options.device = new_device + + def set_max_level(self, new_max_level): + self.display_options.max_level = new_max_level + + +def get_max_level(monster, max_110): + limit = str(LIMIT_BREAK_LEVEL) if max_110 else str(SUPER_LIMIT_BREAK_LEVEL) + level_text = limit if monster.limit_mult != 0 else 'Max ({})'.format(monster.level) return 'Lv. {}'.format(level_text) -def get_max_stats_without_latents(info): +def get_stat_block(main, sub, total): return BlockText( Box( - Text('Base: {}'.format(int(round(info.main_damage)))), - Text('Subattr: {}'.format(int(round(info.sub_damage)))), - Text('Total: {}'.format(int(round(info.total_damage)))) + Text('Base: {}'.format(int(round(main)))), + Text('Subattr: {}'.format(int(round(sub)))), + Text('Total: {}'.format(int(round(total)))) ) ) -def get_120_stats_without_latents(info): - return BlockText( - Box( - Text('Base: {}'.format(int(round(info.main_slb_damage)))), - Text('Subattr: {}'.format(int(round(info.sub_slb_damage)))), - Text('Total: {}'.format(int(round(info.total_slb_damage)))) - ) - ) +def get_max_stats_without_latents(info, is_coop): + main = info.main_damage if is_coop else info.main_solo_damage + sub = info.sub_damage if is_coop else info.sub_solo_damage + total = info.total_damage if is_coop else info.total_solo_damage + return get_stat_block(main, sub, total) -def get_max_stats_with_latents(info): - return BlockText( - Box( - Text('Base: {}'.format(int(round(info.main_damage_with_atk_latent)))), - Text('Subattr: {}'.format(int(round(info.sub_damage_with_atk_latent)))), - Text('Total: {}'.format(int(round(info.total_damage_with_atk_latent)))) - ) - ) +def get_120_stats_without_latents(info, is_coop): + main = info.main_slb_damage if is_coop else info.main_solo_slb_damage + sub = info.sub_slb_damage if is_coop else info.sub_solo_slb_damage + total = info.total_slb_damage if is_coop else info.total_solo_slb_damage + return get_stat_block(main, sub, total) -def get_120_stats_with_latents(info): - return BlockText( - Box( - Text('Base: {}'.format(int(round(info.main_damage_with_slb_atk_latent)))), - Text('Subattr: {}'.format(int(round(info.sub_damage_with_slb_atk_latent)))), - Text('Total: {}'.format(int(round(info.total_damage_with_slb_atk_latent)))) - ) - ) +def get_max_stats_with_latents(info, is_coop): + main = info.main_damage_with_atk_latent if is_coop else info.main_solo_damage_with_atk_latent + sub = info.sub_damage_with_atk_latent if is_coop else info.sub_solo_damage_with_atk_latent + total = info.total_damage_with_atk_latent if is_coop else info.total_solo_damage_with_atk_latent + return get_stat_block(main, sub, total) + + +def get_120_stats_with_latents(info, is_coop): + main = info.main_damage_with_slb_atk_latent if is_coop else info.main_solo_damage_with_slb_atk_latent + sub = info.sub_damage_with_slb_atk_latent if is_coop else info.sub_solo_damage_with_slb_atk_latent + total = info.total_damage_with_slb_atk_latent if is_coop else info.total_solo_damage_with_slb_atk_latent + return get_stat_block(main, sub, total) + + +def get_mobile_btn_str(btn_str): + output = [] + lines = btn_str.split('\n') + for line in lines: + partition = line.find('(') + name = line[:partition].strip() + damage = line[partition:] + output.append('{}\n {}'.format(name, damage)) + return '\n'.join(output) + + +def get_card_btn_str(info, is_coop, max_110): + if is_coop and max_110: + return info.card_btn_str + elif is_coop and not max_110: + return info.card_btn_slb_str + elif not is_coop and max_110: + return info.card_btn_solo_str + elif not is_coop and not max_110: + return info.card_btn_solo_slb_str + + +def get_team_btn_str(info, is_coop, max_110): + if is_coop and max_110: + return info.team_btn_str + elif is_coop and not max_110: + return info.team_btn_slb_str + elif not is_coop and max_110: + return info.team_btn_solo_str + elif not is_coop and not max_110: + return info.team_btn_solo_slb_str class ButtonInfoView: VIEW_TYPE = 'ButtonInfo' @staticmethod - def embed(state, props: ButtonInfoViewProps): - monster = props.monster - info = props.info + def embed(state: ButtonInfoViewState): + is_coop = state.display_options.players == ButtonInfoOptions.coop + is_desktop = state.display_options.device == ButtonInfoOptions.desktop + max_110 = state.display_options.max_level == ButtonInfoOptions.limit_break + monster = state.monster + info = state.info fields = [ EmbedField( - get_max_level(monster), + # this block does not change if the lv110/lv120 toggle is clicked + get_max_level(monster, True), Box( Text('Without Latents'), # avoid whitespace after code block Box( - get_max_stats_without_latents(info), + get_max_stats_without_latents(info, is_coop), Text('With Latents (Atk+)'), delimiter='' ), - get_max_stats_with_latents(info) + get_max_stats_with_latents(info, is_coop) ), inline=True ), @@ -95,16 +190,16 @@ def embed(state, props: ButtonInfoViewProps): Text('Without Latents'), # avoid whitespace after code block Box( - get_120_stats_without_latents(info), + get_120_stats_without_latents(info, is_coop), Text('With Latents (Atk++)'), delimiter='' ), - get_120_stats_with_latents(info) + get_120_stats_with_latents(info, is_coop) ), inline=True ) if monster.limit_mult != 0 else None, EmbedField( - 'Common Buttons - {}'.format(get_max_level(monster)), + 'Common Buttons - Base Card {}'.format(get_max_level(monster, max_110)), Box( Text('*Inherits are assumed to be the max possible level (up to 110) and +297.*'), # janky, but python gives DeprecationWarnings when using \* in a regular string @@ -112,19 +207,37 @@ def embed(state, props: ButtonInfoViewProps): Text('Card Button Damage'), # done this way to not have the whitespace after code block Box( - BlockText(info.card_btn_str), + BlockText(get_card_btn_str(info, is_coop, max_110)), Text('Team Button Contribution'), delimiter='' ), - BlockText(info.team_btn_str) + BlockText(get_team_btn_str(info, is_coop, max_110)) ) - ) + ) if is_desktop else None, + EmbedField( + 'Common Buttons - {}'.format(get_max_level(monster, max_110)), + Box( + Text('*Inherits are assumed to be the max possible level (up to 110) and +297.*'), + # janky, but python gives DeprecationWarnings when using \* in a regular string + Text(r'*\* = on-color stat bonus applied*') + ) + ) if not is_desktop else None, + EmbedField( + 'Card Button Damage', + BlockText(get_mobile_btn_str(get_card_btn_str(info, is_coop, max_110))), + inline=True + ) if not is_desktop else None, + EmbedField( + 'Team Button Contribution', + BlockText(get_mobile_btn_str(get_team_btn_str(info, is_coop, max_110))), + inline=True + ) if not is_desktop else None ] return EmbedView( EmbedMain( color=state.color, - description='(Co-op mode)' + description='(Co-op mode)' if is_coop else '(Singleplayer mode)' ), embed_author=EmbedAuthor( MonsterHeader.long_v2(monster).to_markdown(), From adbb064da14f69e7fd324c29e3f15ff8a36d48ce Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 29 Jul 2021 10:39:46 -0700 Subject: [PATCH 0908/1220] Use new series & name override tables (#1344) --- dbcog/models/monster_model.py | 11 ++++++++++- dbcog/monster_graph.py | 20 ++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index f95e040b..2738361c 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -42,7 +42,16 @@ def __init__(self, **m): else: # Remove annoying stuff from NA names, like Jörmungandr self.name_en = tsutils.rmdiacritics(self.name_en) - self.name_en_override = m['name_en_override'] + + self.name_en_override = None + # If the NA and JP names are the same, chances are the card isn't released in both reigions, and + # if we have an override, chances are it's JP only because we wouldn't need one for an NA only card. + # We aren't using the on_na flag here because that flag sucks and has false positives all the time + if self.name_en == self.name_ja: + self.name_en_override = m['name_en_override'] + if not m['name_is_translation']: + self.name_en_override = m['name_en_override'] + self.name_en = self.name_en_override or self.name_en self.type1 = enum_or_none(MonsterType, m['type_1_id']) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 2d87818e..67852385 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -23,7 +23,8 @@ MONSTER_QUERY = """SELECT monsters{0}.*, - COALESCE(monster_name_overrides.name_en, monsters{0}.name_en_override) AS name_en_override, + monster_name_overrides.name_en AS name_override, + monster_name_overrides.is_translation, leader_skills{0}.name_ja AS ls_name_ja, leader_skills{0}.name_en AS ls_name_en, leader_skills{0}.name_ko AS ls_name_ko, @@ -48,9 +49,10 @@ active_skills{0}.desc_ko AS as_desc_ko, active_skills{0}.turn_max, active_skills{0}.turn_min, - series.name_ja AS s_name_ja, - series.name_en AS s_name_en, - series.name_ko AS s_name_ko, + COALESCE(series.series_id, 0) AS s_series_id, + COALESCE(series.name_ja, 'Unsorted') AS s_name_ja, + COALESCE(series.name_en, 'Unsorted') AS s_name_en, + COALESCE(series.name_ko, 'Unsorted') AS s_name_ko, series.series_type AS s_series_type, exchanges.target_monster_id AS evo_gem_id, drops.drop_id @@ -58,7 +60,8 @@ monsters{0} LEFT OUTER JOIN leader_skills{0} ON monsters{0}.leader_skill_id = leader_skills{0}.leader_skill_id LEFT OUTER JOIN active_skills{0} ON monsters{0}.active_skill_id = active_skills{0}.active_skill_id - LEFT OUTER JOIN series ON monsters{0}.series_id = series.series_id + LEFT OUTER JOIN monster_series ON monsters{0}.monster_id = monster_series.monster_id AND priority = 1 + LEFT OUTER JOIN series ON monster_series.series_id = series.series_id LEFT OUTER JOIN monsters{0} AS target_monsters ON monsters{0}.name_ja || 'の希石' = target_monsters.name_ja LEFT OUTER JOIN exchanges ON target_monsters.monster_id = exchanges.target_monster_id LEFT OUTER JOIN drops ON monsters{0}.monster_id = drops.monster_id @@ -191,7 +194,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: turn_min=m.turn_min ) if m.active_skill_id != 0 else None - s_model = SeriesModel(series_id=m.series_id, + s_model = SeriesModel(series_id=m.s_series_id, name_ja=m.s_name_ja, name_en=m.s_name_en, name_ko=m.s_name_ko, @@ -203,17 +206,18 @@ def build_graph(self, server: Server) -> MultiDiGraph: monster_no_jp=m.monster_no_jp, monster_no_na=m.monster_no_na, monster_no_kr=m.monster_no_kr, + name_is_translation=m.is_translation, awakenings=mtoawo[m.monster_id], leader_skill=ls_model, active_skill=as_model, series=s_model, - series_id=m.series_id, + series_id=m.s_series_id, attribute_1_id=m.attribute_1_id, attribute_2_id=m.attribute_2_id, name_ja=m.name_ja, name_en=m.name_en, name_ko=m.name_ko, - name_en_override=m.name_en_override, + name_en_override=m.name_override, rarity=m.rarity, is_farmable=m.drop_id is not None, in_pem=mtoegg[m.monster_id]['pem'], From 633046b9a53a9c70cffd156438cd6472272dba6d Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Jul 2021 00:53:52 -0700 Subject: [PATCH 0909/1220] Begin updating DungeonCog (#1346) --- dbcog/models/encounter_model.py | 4 +- dungeon/__init__.py | 10 - dungeon/dungeon_monster.py | 31 -- dungeon/dungeoncog.py | 199 ---------- dungeon/enemy_skill.py | 53 --- dungeon/enemy_skills_pb2.py | 646 -------------------------------- dungeon/grouped_skillls.py | 41 -- dungeon/menu/__init__.py | 0 dungeon/menu/dungeon.py | 126 ------- dungeon/menu/menu_map.py | 7 - dungeon/menu/simple.py | 71 ---- dungeon/models/EnemySkill.py | 26 -- dungeon/models/__init__.py | 0 dungeon/processors.py | 271 -------------- dungeon/safe_dict.py | 3 - dungeon/view/__init__.py | 0 dungeon/view/dungeon.py | 288 -------------- dungeon/view/simple.py | 53 --- dungeon/view/test_view.py | 0 dungeoncog/dungeoncog.py | 5 +- dungeoncog/enemy_skill.py | 2 +- dungeoncog/grouped_skillls.py | 4 +- dungeoncog/processors.py | 58 ++- dungeoncog/view/dungeon.py | 25 +- 24 files changed, 43 insertions(+), 1880 deletions(-) delete mode 100644 dungeon/__init__.py delete mode 100644 dungeon/dungeon_monster.py delete mode 100644 dungeon/dungeoncog.py delete mode 100644 dungeon/enemy_skill.py delete mode 100644 dungeon/enemy_skills_pb2.py delete mode 100644 dungeon/grouped_skillls.py delete mode 100644 dungeon/menu/__init__.py delete mode 100644 dungeon/menu/dungeon.py delete mode 100644 dungeon/menu/menu_map.py delete mode 100644 dungeon/menu/simple.py delete mode 100644 dungeon/models/EnemySkill.py delete mode 100644 dungeon/models/__init__.py delete mode 100644 dungeon/processors.py delete mode 100644 dungeon/safe_dict.py delete mode 100644 dungeon/view/__init__.py delete mode 100644 dungeon/view/dungeon.py delete mode 100644 dungeon/view/simple.py delete mode 100644 dungeon/view/test_view.py diff --git a/dbcog/models/encounter_model.py b/dbcog/models/encounter_model.py index f3d20d5e..ad2b6e94 100644 --- a/dbcog/models/encounter_model.py +++ b/dbcog/models/encounter_model.py @@ -14,7 +14,7 @@ def __init__(self, enemy_data_model: EnemyDataModel = None, **kwargs): self.level = kwargs['level'] self.hp = kwargs['hp'] self.atk = kwargs['atk'] - self.defence = kwargs['defence'] + self.defense = kwargs['defence'] # We aren't Brittish self.enemy_data = enemy_data_model def to_dict(self): @@ -29,5 +29,5 @@ def to_dict(self): 'level': self.level, 'hp': self.hp, 'atk': self.atk, - 'defence': self.defence + 'defense': self.defense } diff --git a/dungeon/__init__.py b/dungeon/__init__.py deleted file mode 100644 index e65b9960..00000000 --- a/dungeon/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -import asyncio - -from .dungeoncog import DungeonCog - - -def setup(bot): - n = DungeonCog(bot) - bot.add_cog(n) - bot.loop.create_task(n.register_menu()) - asyncio.create_task(n.load_emojis()) diff --git a/dungeon/dungeon_monster.py b/dungeon/dungeon_monster.py deleted file mode 100644 index 32d1c418..00000000 --- a/dungeon/dungeon_monster.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List - -from dungeon.grouped_skillls import GroupedSkills - - -class DungeonMonster(object): - """ - A class that symbolizes an encounter in a dungeon. Why is it not a model? Good question. - """ - - def __init__(self, name: str, hp, atk, defense, turns, level, error=None): - self.name = name - self.hp = hp - self.atk = atk - self.defense = defense - self.defense = hp - self.turns = turns - self.level = level - self.groups: List[GroupedSkills] = [] - self.am_invade = False - - self.error = error - - def add_group(self, group: GroupedSkills): - self.groups.append(group) - - async def collect_skills(self): - skills = [] - for g in self.groups: - skills.extend(await g.collect_skills()) - return skills diff --git a/dungeon/dungeoncog.py b/dungeon/dungeoncog.py deleted file mode 100644 index e2e336f0..00000000 --- a/dungeon/dungeoncog.py +++ /dev/null @@ -1,199 +0,0 @@ -import logging -import os - -import discord -from discord import Color -from discordmenu.emoji.emoji_cache import emoji_cache -from redbot.core import commands, data_manager - -from dungeon.enemy_skills_pb2 import MonsterBehavior -from dungeon.menu.dungeon import DungeonMenu, DungeonMenuPanes -from dungeon.menu.menu_map import dungeon_menu_map -from dungeon.view.dungeon import DungeonViewState - -logger = logging.getLogger('red.padbot-cogs.dungeon') -EMBED_NOT_GENERATED = -1 - - -def _data_file(file_name: str) -> str: - return os.path.join(str(data_manager.cog_data_path(raw_name='dungeon')), file_name) - - -RAW_ENEMY_SKILLS_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/ilmina/download_enemy_skill_data.json' -RAW_ENEMY_SKILLS_DUMP = _data_file('enemy_skills.json') - - -class DungeonCog(commands.Cog): - """ - Contains commands that are display information about dungeons. - Right now only one command, dungeon_info which displays information such as spawns in a dungeon. - """ - menu_map = dungeon_menu_map - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def load_emojis(self): - await self.bot.wait_until_ready() - logging.debug('load_emojis, dungeon') - emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) - emoji_cache.refresh_from_discord_bot(self.bot) - - async def register_menu(self): - await self.bot.wait_until_ready() - menulistener = self.bot.get_cog("MenuListener") - if menulistener is None: - logger.warning("MenuListener is not loaded.") - return - await menulistener.register(self) - - async def get_menu_default_data(self, ims): - data = { - 'dbcog': await self.get_dbcog(), - 'color': Color.default() - } - return data - - async def get_dbcog(self): - dbcog = self.bot.get_cog("DBCog") - if dbcog is None: - raise ValueError("DBCog cog is not loaded") - await dbcog.wait_until_ready() - return dbcog - - async def find_dungeon_from_name2(self, ctx, name: str, database: "DungeonContext", difficulty: str = None): - """ - Gets the sub_dungeon model given the name of a dungeon and its difficulty. - """ - dungeon = database.get_dungeons_from_nickname(name.lower()) - if len(dungeon) == 0: - dungeons = database.get_dungeons_from_name(name) - if len(dungeons) == 0: - await ctx.send("No dungeons found!") - return None - if len(dungeons) > 1: - message = "Multiple Dungeons Found, please be more specific:" - for d in dungeons: - message += "\n{}".format(d.name_en) - await ctx.send(message) - return None - dungeon = dungeons[0] - sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty) - sub_dungeon_model = None - if sub_id is None: - sub_id = 0 - for sd in dungeon.sub_dungeons: - if sd.sub_dungeon_id > sub_id: - sub_id = sd.sub_dungeon_id - sub_dungeon_model = sd - else: - for sd in dungeon.sub_dungeons: - if sd.sub_dungeon_id == sub_id: - sub_dungeon_model = sd - break - dungeon.sub_dungeons = [sub_dungeon_model] - else: - dungeon = dungeon[0] - return dungeon - - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed, timeout=60): - if starting_menu_emoji not in emoji_to_embed.emoji_dict: - # Selected menu wasn't generated for this monster - return EMBED_NOT_GENERATED - - emoji_to_embed.emoji_dict[self.menu.emoji.get("no")] = self.menu.reaction_delete_message - - try: - result_msg, result_embed = await self.menu.custom_menu(ctx, emoji_to_embed, - starting_menu_emoji, timeout=timeout) - if result_msg and result_embed: - # Message is finished but not deleted, clear the footer - result_embed.set_footer(text=discord.Embed.Empty) - await result_msg.edit(embed=result_embed) - await result_msg.e - except Exception as ex: - logger.error('Menu failure', exc_info=True) - - @commands.command() - async def dgid(self, ctx, name: str, difficulty: str = None): - ''' - Name: Name of Dungeon - Difficulty: Difficulty level/name of floor (eg. for A1, "Bipolar Goddess") - ''' - # load dbcog cog for database access - dbcog = self.bot.get_cog('DBCog') - if not dbcog: - logger.warning("Cog 'DBCog' not loaded") - return - logger.info('Waiting until DG is ready') - await dbcog.wait_until_ready() - dungeon = await self.find_dungeon_from_name2(ctx=ctx, name=name, database=dbcog.database.dungeon, - difficulty=difficulty) - - if dungeon is not None: - # await ctx.send(formatOverview(test_result)) - current_stage = 0 - pm_dungeon = [] - # check for invades: - invades = [] - - # print(dungeon.sub_dungeons[0].encounter_models) - for enc_model in dungeon.sub_dungeons[0].encounter_models: - behavior_test = MonsterBehavior() - if (enc_model.enemy_data is not None) and (enc_model.enemy_data.behavior is not None): - behavior_test.ParseFromString(enc_model.enemy_data.behavior) - else: - behavior_test = None - - # await ctx.send(formatOverview(test_result)) - # pm = process_monster(behavior_test, enc_model, db_cog.database) - if enc_model.stage < 0: - # pm.am_invade = True - invades.append(enc_model) - elif enc_model.stage > current_stage: - current_stage = enc_model.stage - floor = [enc_model] - pm_dungeon.append(floor) - else: - pm_dungeon[current_stage - 1].append(enc_model) - for f in pm_dungeon: - if pm_dungeon.index(f) != (len(pm_dungeon) - 1): - f.extend(invades) - - menu = DungeonMenu.menu() - original_author_id = ctx.message.author.id - full_reaction_list = [emoji_cache.get_by_name(e) for e in DungeonMenuPanes.emoji_names()] - test_list = ['1', '2', '3', '4'] - # print(pm_dungeon[0]) - view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, Color.default(), pm_dungeon[0][0], - dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, - len(pm_dungeon[0]), 0, - int(dungeon.sub_dungeons[0].technical), dbcog.database, verbose=False, - reaction_list=full_reaction_list) - await ctx.send( - "EN: {}({})\nJP: {}({})".format(dungeon.name_en, dungeon.sub_dungeons[0].name_en, dungeon.name_ja, - dungeon.sub_dungeons[0].name_ja)) - message = await menu.create(ctx, view_state) - - -''' @commands.command() - async def spinner_help(self, ctx, spin_time, move_time): - """ - spin_time: The cycle the spinner is set at (typically changes once every second) - move_time: How much time you have to move orbs - """ - embed = discord.Embed(title="Spinner Helper {}s Rotation {}s Move Time".format(spin_time, move_time), - description="Assuming orbs are set and are NOT moved during movement:") - casino = ["🔥", "🌊", "🌿", "💡", "🌙", "🩹"] - cycles = float(move_time) / float(spin_time) - rounded = int(cycles) - casino_dict = OrderedDict() - for c in casino: - casino_dict.update({c: casino[(casino.index(c) + rounded) % 6]}) - casino_dict.update({"Other": casino[(rounded - 1) % 6]}) - output = "" - for k, v in casino_dict.items(): - output += "\n{} {} {}".format(k, "➡", v) - embed.add_field(name="Original -> Final", value=output) - await ctx.send(embed=embed)''' diff --git a/dungeon/enemy_skill.py b/dungeon/enemy_skill.py deleted file mode 100644 index 6e44c3fe..00000000 --- a/dungeon/enemy_skill.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import List - -from dungeon.grouped_skillls import GroupedSkills - - -class ProcessedSkill(object): - """ - This is a representation of a processed enemy skill. It contains the name, effect, - processed (emoji) version of the effect, condition requriements, parents, type (passive, preempt) - The class also contains a function called give_string. When called it returns the necessary strings needed to - display it in dungeon_info2 - """ - - def __init__(self, name: str, effect: str, processed: List[str], condition: str = None, - parent: GroupedSkills = None): - self.name = name - self.effect = effect - self.processed = processed - self.condition = condition - self.parent = parent - self.type = self.find_type() - self.is_passive_preempt = len(self.process_type()) != 0 - - def find_type(self): - """Helper function to determine whether the skill is a preempt/passive from the parent group""" - parent = self.parent - while parent is not None: - if parent.type is not None: - return parent.type - parent = parent.parent - return None - - def process_type(self): - if "Passive" in self.type or "Preemptive" in self.type: - return "({})".format(self.type) - return "" - - def give_string(self, indent: str = "", verbose: bool = False): - """When called returns the strings of a skill in the following format: [emoji, effect, condition]""" - components = [[], None, None] # skills (condensed), effect, condition - # if no skills/not processed just return (N/A) as the skill - if len(self.processed) == 0: - components[0].append("(N/A)") - return components - # For the emoji text we append the skill type to the beginning eg. (Emoji Effect) -> (Passive) (Emoji Effect) - components[0] = [self.process_type()] - components[0].extend(self.processed) - # If verbose information is requested (ie display Effect text) we provide it - if verbose: - components[1] = "E: {}".format(self.effect) - if self.condition is not None: - components[2] = "Cond: {}".format(self.condition) - return components diff --git a/dungeon/enemy_skills_pb2.py b/dungeon/enemy_skills_pb2.py deleted file mode 100644 index 64a724a7..00000000 --- a/dungeon/enemy_skills_pb2.py +++ /dev/null @@ -1,646 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: enemy_skills.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='enemy_skills.proto', - package='dadguide_proto', - syntax='proto3', - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x12\x65nemy_skills.proto\x12\x0e\x64\x61\x64guide_proto\"\xbf\x02\n\x1cMonsterBehaviorWithOverrides\x12\x12\n\nmonster_id\x18\x01 \x01(\x05\x12-\n\x06levels\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x36\n\x0flevel_overrides\x18\x03 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x43\n\x06status\x18\x04 \x01(\x0e\x32\x33.dadguide_proto.MonsterBehaviorWithOverrides.Status\"_\n\x06Status\x12\x10\n\x0cNOT_APPROVED\x10\x00\x12\x12\n\x0e\x41PPROVED_AS_IS\x10\x01\x12\x14\n\x10NEEDS_REAPPROVAL\x10\x02\x12\x19\n\x15\x41PPROVED_WITH_CHANGES\x10\x03\"f\n\x0fMonsterBehavior\x12\x12\n\nmonster_id\x18\x01 \x01(\x05\x12-\n\x06levels\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.LevelBehavior\x12\x10\n\x08\x61pproved\x18\x03 \x01(\x08\"M\n\rLevelBehavior\x12\r\n\x05level\x18\x01 \x01(\x05\x12-\n\x06groups\x18\x02 \x03(\x0b\x32\x1d.dadguide_proto.BehaviorGroup\"\xd9\x02\n\rBehaviorGroup\x12;\n\ngroup_type\x18\x01 \x01(\x0e\x32\'.dadguide_proto.BehaviorGroup.GroupType\x12,\n\tcondition\x18\x02 \x01(\x0b\x32\x19.dadguide_proto.Condition\x12.\n\x08\x63hildren\x18\x03 \x03(\x0b\x32\x1c.dadguide_proto.BehaviorItem\"\xac\x01\n\tGroupType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07PASSIVE\x10\x01\x12\x0b\n\x07PREEMPT\x10\x02\x12\x11\n\rDISPEL_PLAYER\x10\x03\x12\x12\n\x0eMONSTER_STATUS\x10\x04\x12\r\n\tREMAINING\x10\x05\x12\x0c\n\x08STANDARD\x10\x06\x12\t\n\x05\x44\x45\x41TH\x10\x07\x12\x0f\n\x0bUNKNOWN_USE\x10\x08\x12\x14\n\x10HIGHEST_PRIORITY\x10\t\"u\n\x0c\x42\x65haviorItem\x12.\n\x05group\x18\x02 \x01(\x0b\x32\x1d.dadguide_proto.BehaviorGroupH\x00\x12,\n\x08\x62\x65havior\x18\x03 \x01(\x0b\x32\x18.dadguide_proto.BehaviorH\x00\x42\x07\n\x05value\"c\n\x08\x42\x65havior\x12,\n\tcondition\x18\x01 \x01(\x0b\x32\x19.dadguide_proto.Condition\x12\x16\n\x0e\x65nemy_skill_id\x18\x02 \x01(\x05\x12\x11\n\tchild_ids\x18\x03 \x03(\x05\"\x80\x04\n\tCondition\x12\x14\n\x0chp_threshold\x18\x01 \x01(\x05\x12\x12\n\nuse_chance\x18\x02 \x01(\x05\x12\x15\n\rrepeats_every\x18\x03 \x01(\x05\x12\x17\n\x0fglobal_one_time\x18\x04 \x01(\x08\x12\x19\n\x11limited_execution\x18\r \x01(\x05\x12!\n\x19trigger_enemies_remaining\x18\x05 \x01(\x05\x12\x13\n\x0bif_defeated\x18\x06 \x01(\x08\x12\x1f\n\x17if_attributes_available\x18\x07 \x01(\x08\x12\x18\n\x10trigger_monsters\x18\x08 \x03(\x05\x12\x16\n\x0etrigger_combos\x18\t \x01(\x05\x12\x1a\n\x12if_nothing_matched\x18\n \x01(\x08\x12\x14\n\x0ctrigger_turn\x18\x0b \x01(\x05\x12\x18\n\x10trigger_turn_end\x18\x0c \x01(\x05\x12\x1c\n\x14\x61lways_trigger_above\x18\x0e \x01(\x05\x12\x14\n\x0c\x61lways_after\x18\x0f \x01(\x05\x12\x11\n\tskill_set\x18\x10 \x01(\x05\x12\x19\n\x11\x65rased_attributes\x18\x11 \x03(\x05\x12\x13\n\x0b\x64\x61mage_done\x18\x12 \x01(\x05\x12\x1b\n\x13\x61ttributes_attacked\x18\x13 \x03(\x05\x12\x13\n\x0bskills_used\x18\x14 \x01(\x05\x62\x06proto3' -) - - - -_MONSTERBEHAVIORWITHOVERRIDES_STATUS = _descriptor.EnumDescriptor( - name='Status', - full_name='dadguide_proto.MonsterBehaviorWithOverrides.Status', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='NOT_APPROVED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='APPROVED_AS_IS', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NEEDS_REAPPROVAL', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='APPROVED_WITH_CHANGES', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=263, - serialized_end=358, -) -_sym_db.RegisterEnumDescriptor(_MONSTERBEHAVIORWITHOVERRIDES_STATUS) - -_BEHAVIORGROUP_GROUPTYPE = _descriptor.EnumDescriptor( - name='GroupType', - full_name='dadguide_proto.BehaviorGroup.GroupType', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PASSIVE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PREEMPT', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DISPEL_PLAYER', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='MONSTER_STATUS', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='REMAINING', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='STANDARD', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEATH', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UNKNOWN_USE', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='HIGHEST_PRIORITY', index=9, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=717, - serialized_end=889, -) -_sym_db.RegisterEnumDescriptor(_BEHAVIORGROUP_GROUPTYPE) - - -_MONSTERBEHAVIORWITHOVERRIDES = _descriptor.Descriptor( - name='MonsterBehaviorWithOverrides', - full_name='dadguide_proto.MonsterBehaviorWithOverrides', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='monster_id', full_name='dadguide_proto.MonsterBehaviorWithOverrides.monster_id', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='levels', full_name='dadguide_proto.MonsterBehaviorWithOverrides.levels', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='level_overrides', full_name='dadguide_proto.MonsterBehaviorWithOverrides.level_overrides', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='status', full_name='dadguide_proto.MonsterBehaviorWithOverrides.status', index=3, - number=4, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _MONSTERBEHAVIORWITHOVERRIDES_STATUS, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=39, - serialized_end=358, -) - - -_MONSTERBEHAVIOR = _descriptor.Descriptor( - name='MonsterBehavior', - full_name='dadguide_proto.MonsterBehavior', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='monster_id', full_name='dadguide_proto.MonsterBehavior.monster_id', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='levels', full_name='dadguide_proto.MonsterBehavior.levels', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='approved', full_name='dadguide_proto.MonsterBehavior.approved', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=360, - serialized_end=462, -) - - -_LEVELBEHAVIOR = _descriptor.Descriptor( - name='LevelBehavior', - full_name='dadguide_proto.LevelBehavior', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='level', full_name='dadguide_proto.LevelBehavior.level', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='groups', full_name='dadguide_proto.LevelBehavior.groups', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=464, - serialized_end=541, -) - - -_BEHAVIORGROUP = _descriptor.Descriptor( - name='BehaviorGroup', - full_name='dadguide_proto.BehaviorGroup', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='group_type', full_name='dadguide_proto.BehaviorGroup.group_type', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='condition', full_name='dadguide_proto.BehaviorGroup.condition', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='children', full_name='dadguide_proto.BehaviorGroup.children', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _BEHAVIORGROUP_GROUPTYPE, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=544, - serialized_end=889, -) - - -_BEHAVIORITEM = _descriptor.Descriptor( - name='BehaviorItem', - full_name='dadguide_proto.BehaviorItem', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='group', full_name='dadguide_proto.BehaviorItem.group', index=0, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='behavior', full_name='dadguide_proto.BehaviorItem.behavior', index=1, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='dadguide_proto.BehaviorItem.value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=891, - serialized_end=1008, -) - - -_BEHAVIOR = _descriptor.Descriptor( - name='Behavior', - full_name='dadguide_proto.Behavior', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='condition', full_name='dadguide_proto.Behavior.condition', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='enemy_skill_id', full_name='dadguide_proto.Behavior.enemy_skill_id', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='child_ids', full_name='dadguide_proto.Behavior.child_ids', index=2, - number=3, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1010, - serialized_end=1109, -) - - -_CONDITION = _descriptor.Descriptor( - name='Condition', - full_name='dadguide_proto.Condition', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='hp_threshold', full_name='dadguide_proto.Condition.hp_threshold', index=0, - number=1, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='use_chance', full_name='dadguide_proto.Condition.use_chance', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='repeats_every', full_name='dadguide_proto.Condition.repeats_every', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='global_one_time', full_name='dadguide_proto.Condition.global_one_time', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='limited_execution', full_name='dadguide_proto.Condition.limited_execution', index=4, - number=13, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trigger_enemies_remaining', full_name='dadguide_proto.Condition.trigger_enemies_remaining', index=5, - number=5, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='if_defeated', full_name='dadguide_proto.Condition.if_defeated', index=6, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='if_attributes_available', full_name='dadguide_proto.Condition.if_attributes_available', index=7, - number=7, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trigger_monsters', full_name='dadguide_proto.Condition.trigger_monsters', index=8, - number=8, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trigger_combos', full_name='dadguide_proto.Condition.trigger_combos', index=9, - number=9, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='if_nothing_matched', full_name='dadguide_proto.Condition.if_nothing_matched', index=10, - number=10, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trigger_turn', full_name='dadguide_proto.Condition.trigger_turn', index=11, - number=11, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='trigger_turn_end', full_name='dadguide_proto.Condition.trigger_turn_end', index=12, - number=12, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='always_trigger_above', full_name='dadguide_proto.Condition.always_trigger_above', index=13, - number=14, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='always_after', full_name='dadguide_proto.Condition.always_after', index=14, - number=15, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='skill_set', full_name='dadguide_proto.Condition.skill_set', index=15, - number=16, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='erased_attributes', full_name='dadguide_proto.Condition.erased_attributes', index=16, - number=17, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='damage_done', full_name='dadguide_proto.Condition.damage_done', index=17, - number=18, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='attributes_attacked', full_name='dadguide_proto.Condition.attributes_attacked', index=18, - number=19, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='skills_used', full_name='dadguide_proto.Condition.skills_used', index=19, - number=20, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1112, - serialized_end=1624, -) - -_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['levels'].message_type = _LEVELBEHAVIOR -_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['level_overrides'].message_type = _LEVELBEHAVIOR -_MONSTERBEHAVIORWITHOVERRIDES.fields_by_name['status'].enum_type = _MONSTERBEHAVIORWITHOVERRIDES_STATUS -_MONSTERBEHAVIORWITHOVERRIDES_STATUS.containing_type = _MONSTERBEHAVIORWITHOVERRIDES -_MONSTERBEHAVIOR.fields_by_name['levels'].message_type = _LEVELBEHAVIOR -_LEVELBEHAVIOR.fields_by_name['groups'].message_type = _BEHAVIORGROUP -_BEHAVIORGROUP.fields_by_name['group_type'].enum_type = _BEHAVIORGROUP_GROUPTYPE -_BEHAVIORGROUP.fields_by_name['condition'].message_type = _CONDITION -_BEHAVIORGROUP.fields_by_name['children'].message_type = _BEHAVIORITEM -_BEHAVIORGROUP_GROUPTYPE.containing_type = _BEHAVIORGROUP -_BEHAVIORITEM.fields_by_name['group'].message_type = _BEHAVIORGROUP -_BEHAVIORITEM.fields_by_name['behavior'].message_type = _BEHAVIOR -_BEHAVIORITEM.oneofs_by_name['value'].fields.append( - _BEHAVIORITEM.fields_by_name['group']) -_BEHAVIORITEM.fields_by_name['group'].containing_oneof = _BEHAVIORITEM.oneofs_by_name['value'] -_BEHAVIORITEM.oneofs_by_name['value'].fields.append( - _BEHAVIORITEM.fields_by_name['behavior']) -_BEHAVIORITEM.fields_by_name['behavior'].containing_oneof = _BEHAVIORITEM.oneofs_by_name['value'] -_BEHAVIOR.fields_by_name['condition'].message_type = _CONDITION -DESCRIPTOR.message_types_by_name['MonsterBehaviorWithOverrides'] = _MONSTERBEHAVIORWITHOVERRIDES -DESCRIPTOR.message_types_by_name['MonsterBehavior'] = _MONSTERBEHAVIOR -DESCRIPTOR.message_types_by_name['LevelBehavior'] = _LEVELBEHAVIOR -DESCRIPTOR.message_types_by_name['BehaviorGroup'] = _BEHAVIORGROUP -DESCRIPTOR.message_types_by_name['BehaviorItem'] = _BEHAVIORITEM -DESCRIPTOR.message_types_by_name['Behavior'] = _BEHAVIOR -DESCRIPTOR.message_types_by_name['Condition'] = _CONDITION -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -MonsterBehaviorWithOverrides = _reflection.GeneratedProtocolMessageType('MonsterBehaviorWithOverrides', (_message.Message,), { - 'DESCRIPTOR' : _MONSTERBEHAVIORWITHOVERRIDES, - '__module__' : 'enemy_skills_pb2' - # @@protoc_insertion_point(class_scope:dadguide_proto.MonsterBehaviorWithOverrides) - }) -_sym_db.RegisterMessage(MonsterBehaviorWithOverrides) - -MonsterBehavior = _reflection.GeneratedProtocolMessageType('MonsterBehavior', (_message.Message,), { - 'DESCRIPTOR' : _MONSTERBEHAVIOR, - '__module__' : 'enemy_skills_pb2' - # @@protoc_insertion_point(class_scope:dadguide_proto.MonsterBehavior) - }) -_sym_db.RegisterMessage(MonsterBehavior) - -LevelBehavior = _reflection.GeneratedProtocolMessageType('LevelBehavior', (_message.Message,), { - 'DESCRIPTOR' : _LEVELBEHAVIOR, - '__module__' : 'enemy_skills_pb2' - # @@protoc_insertion_point(class_scope:dadguide_proto.LevelBehavior) - }) -_sym_db.RegisterMessage(LevelBehavior) - -BehaviorGroup = _reflection.GeneratedProtocolMessageType('BehaviorGroup', (_message.Message,), { - 'DESCRIPTOR' : _BEHAVIORGROUP, - '__module__' : 'enemy_skills_pb2' - # @@protoc_insertion_point(class_scope:dadguide_proto.BehaviorGroup) - }) -_sym_db.RegisterMessage(BehaviorGroup) - -BehaviorItem = _reflection.GeneratedProtocolMessageType('BehaviorItem', (_message.Message,), { - 'DESCRIPTOR' : _BEHAVIORITEM, - '__module__' : 'enemy_skills_pb2' - # @@protoc_insertion_point(class_scope:dadguide_proto.BehaviorItem) - }) -_sym_db.RegisterMessage(BehaviorItem) - -Behavior = _reflection.GeneratedProtocolMessageType('Behavior', (_message.Message,), { - 'DESCRIPTOR' : _BEHAVIOR, - '__module__' : 'enemy_skills_pb2' - # @@protoc_insertion_point(class_scope:dadguide_proto.Behavior) - }) -_sym_db.RegisterMessage(Behavior) - -Condition = _reflection.GeneratedProtocolMessageType('Condition', (_message.Message,), { - 'DESCRIPTOR' : _CONDITION, - '__module__' : 'enemy_skills_pb2' - # @@protoc_insertion_point(class_scope:dadguide_proto.Condition) - }) -_sym_db.RegisterMessage(Condition) - - -# @@protoc_insertion_point(module_scope) diff --git a/dungeon/grouped_skillls.py b/dungeon/grouped_skillls.py deleted file mode 100644 index 216a1ab6..00000000 --- a/dungeon/grouped_skillls.py +++ /dev/null @@ -1,41 +0,0 @@ -class GroupedSkills(object): - """This is a container class that holds the top level condition, and its processed children""" - - def __init__(self, condition: str, type, parent=None): - self.nested_groups = [] - self.skills = [] - self.condition = condition - self.type = type - self.parent = parent - - def add_group(self, group): - self.nested_groups.append(group) - - def add_skill(self, skill): - self.skills.append(skill) - - def give_string2(self, lines, level=0, verbose: bool = False): - """ - When called it stores the necessary text of it's child skills in lines. - @param lines: a list to store lines of text in the following format [number of indents needed, actual line of text] - @param level: how many indents do we need (essentially is this a nested group or not) - @param verbose: display skill effect text or not - """ - condition = self.condition - if condition is not None: - lines.append([level, ["**Condition: {}:**".format(condition)]]) - level += 1 - for g in self.nested_groups: - g.give_string2(lines, level, verbose) - for s in self.skills: - skill_string_components = s.give_string(verbose=verbose) - skill_string_components[0] = "".join(skill_string_components[0]) - skill_string_components[0] = "**{}**".format(skill_string_components[0]) - skill_string_components = [x for x in skill_string_components if x is not None] - lines.append([level, skill_string_components]) - - async def collect_skills(self): - skill_copy = self.skills.copy() - for g in self.nested_groups: - skill_copy.extend(await g.collect_skills()) - return skill_copy diff --git a/dungeon/menu/__init__.py b/dungeon/menu/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dungeon/menu/dungeon.py b/dungeon/menu/dungeon.py deleted file mode 100644 index c4d866d5..00000000 --- a/dungeon/menu/dungeon.py +++ /dev/null @@ -1,126 +0,0 @@ -from typing import Optional -from discord import Message -from discordmenu.embed.control import EmbedControl -from discordmenu.embed.menu import EmbedMenu -from tsutils.menu.panes import MenuPanes, emoji_buttons - -from dungeon.view.dungeon import DungeonViewState, DungeonView - - -class DungeonNames: - home = 'home' - - -class DungeonMenu: - MENU_TYPE = 'DungeonMenu' - message = None - - @staticmethod - def menu(): - embed = EmbedMenu(DungeonMenuPanes.transitions(), DungeonMenu.message_control, - delete_func=DungeonMenu.respond_with_delete) - return embed - - @staticmethod - async def respond_with_message(message: Optional[Message], ims, **data): - dbcog = data.get('dbcog') - color = data.get('color') - view_state = await DungeonViewState.deserialize(dbcog, color, ims) - control = DungeonMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_verbose(message: Optional[Message], ims, **data): - dbcog = data.get('dbcog') - color = data.get('color') - view_state = await DungeonViewState.deserialize(dbcog, color, ims, verbose_toggle=True) - control = DungeonMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_delete(message: Optional[Message], ims, **data): - return await message.delete() - - @staticmethod - async def respond_with_previous_monster(message: Optional[Message], ims, **data): - dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, -1) - control = DungeonMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_next_monster(message: Optional[Message], ims, **data): - dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, 1) - control = DungeonMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_previous_floor(message: Optional[Message], ims, **data): - dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, -1, 0, reset_spawn=True) - control = DungeonMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_next_floor(message: Optional[Message], ims, **data): - dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, 1, 0, reset_spawn=True) - control = DungeonMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_next_page(message: Optional[Message], ims, **data): - dbcog = data['dbcog'] - color = data['color'] - page = ims.get('page') - if page == 0: - page = 1 - else: - page = 0 - view_state = await DungeonViewState.deserialize(dbcog, color, ims, page=page) - control = DungeonMenu.message_control(view_state) - return control - - @staticmethod - def message_control(state: DungeonViewState): - if state is None: - return None - reaction_list = state.reaction_list - return EmbedControl( - [DungeonView.embed(state)], - reaction_list - ) - - - - - -class DungeonEmoji: - home = emoji_buttons['home'] - previous_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' - next_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' - next_floor = '\N{UPWARDS BLACK ARROW}' - previous_floor = '\N{DOWNWARDS BLACK ARROW}' - next_page = '📖' - verbose = '📜' - - -class DungeonMenuPanes(MenuPanes): - INITIAL_EMOJI = DungeonEmoji.home - DATA = { - DungeonEmoji.home: (DungeonMenu.respond_with_message, DungeonView.VIEW_TYPE), - DungeonEmoji.verbose: (DungeonMenu.respond_with_verbose, DungeonView.VIEW_TYPE), - DungeonEmoji.previous_monster_emoji: (DungeonMenu.respond_with_previous_monster, DungeonView.VIEW_TYPE), - DungeonEmoji.next_monster_emoji: (DungeonMenu.respond_with_next_monster, DungeonView.VIEW_TYPE), - DungeonEmoji.previous_floor: (DungeonMenu.respond_with_previous_floor, DungeonView.VIEW_TYPE), - DungeonEmoji.next_floor: (DungeonMenu.respond_with_next_floor, DungeonView.VIEW_TYPE), - DungeonEmoji.next_page: (DungeonMenu.respond_with_next_page, DungeonView.VIEW_TYPE) - } - HIDDEN_EMOJIS = [ - DungeonEmoji.home - ] diff --git a/dungeon/menu/menu_map.py b/dungeon/menu/menu_map.py deleted file mode 100644 index 1aac6176..00000000 --- a/dungeon/menu/menu_map.py +++ /dev/null @@ -1,7 +0,0 @@ -from dungeon.menu.dungeon import DungeonMenu, DungeonMenuPanes -from dungeon.menu.simple import SimpleMenu, SimpleMenuPanes - -dungeon_menu_map = { - SimpleMenu.MENU_TYPE: (SimpleMenu, SimpleMenuPanes), - DungeonMenu.MENU_TYPE: (DungeonMenu, DungeonMenuPanes) -} diff --git a/dungeon/menu/simple.py b/dungeon/menu/simple.py deleted file mode 100644 index 2a877136..00000000 --- a/dungeon/menu/simple.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Optional - -from discord import Message -from discordmenu.embed.control import EmbedControl -from discordmenu.embed.menu import EmbedMenu -from tsutils.menu.panes import MenuPanes, emoji_buttons - -from dungeon.view.simple import SimpleViewState, SimpleView - - -class SimpleNames: - home = 'home' - - -class SimpleMenu: - MENU_TYPE = 'SimpleMenu' - message = None - - @staticmethod - def menu(): - embed = EmbedMenu(SimpleMenuPanes.transitions(), SimpleMenu.message_control, - delete_func=SimpleMenu.respond_with_delete) - return embed - - @staticmethod - async def respond_with_message(message: Optional[Message], ims, **data): - dbcog = data.get('dbcog') - color = data.get('color') - view_state = await SimpleViewState.deserialize(dbcog, color, ims) - control = SimpleMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_delete(message: Optional[Message], ims, **data): - return await message.delete() - - @staticmethod - async def respond_with_right(message: Optional[Message], ims, **data): - dbcog = data.get('dbcog') - color = data.get('color') - print("test") - view_state = await SimpleViewState.deserialize(dbcog, color, ims, 1) - control = SimpleMenu.message_control(view_state) - return control - - @staticmethod - def message_control(state: SimpleViewState): - if state is None: - return None - reaction_list = state.reaction_list - return EmbedControl( - [SimpleView.embed(state)], - reaction_list - ) - - -class SimpleEmoji: - home = emoji_buttons['home'] - right = '\N{BLACK RIGHT-POINTING TRIANGLE}' - - -class SimpleMenuPanes(MenuPanes): - INITIAL_EMOJI = SimpleEmoji.home - DATA = { - SimpleEmoji.home: (SimpleMenu.respond_with_message, SimpleNames.home), - SimpleEmoji.right: (SimpleMenu.respond_with_right, SimpleView.VIEW_TYPE) - - } - HIDDEN_EMOJIS = [ - SimpleNames.home, - ] diff --git a/dungeon/models/EnemySkill.py b/dungeon/models/EnemySkill.py deleted file mode 100644 index 47048dfd..00000000 --- a/dungeon/models/EnemySkill.py +++ /dev/null @@ -1,26 +0,0 @@ -class EnemySkill(object): - def __init__(self, raw: "List[str]"): - self.enemy_skill_id = int(raw[0]) - self.name = raw[1].replace('\n', ' ') - self.type = int(raw[2]) - self.flags = int(raw[3], 16) # 16bitmap for params - self.params = [None] * 16 - offset = 0 - p_idx = 4 - while offset < self.flags.bit_length(): - if (self.flags >> offset) & 1 != 0: - p_value = raw[p_idx] - self.params[offset] = int(p_value) if p_value.lstrip('-').isdigit() else p_value - p_idx += 1 - offset += 1 - - def process(self): - return "Unknown: [{},{},{},{}]".format(self.id, self.name, self.type, self.params) - -class ESNone(EnemySkill): - def __init__(self, id = None): - self.enemy_skill_id = id - self.name = None - self.type = None - self.flags = None - self.params = None \ No newline at end of file diff --git a/dungeon/models/__init__.py b/dungeon/models/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dungeon/processors.py b/dungeon/processors.py deleted file mode 100644 index 6ecb0094..00000000 --- a/dungeon/processors.py +++ /dev/null @@ -1,271 +0,0 @@ -from discordmenu.emoji.emoji_cache import emoji_cache - -from dbcog.database_context import DbContext -from dbcog.models.encounter_model import EncounterModel -from dbcog.models.enemy_skill_model import EnemySkillModel -from dungeon.safe_dict import SafeDict -from dungeon.dungeon_monster import DungeonMonster -from dungeon.enemy_skill import ProcessedSkill -from dungeon.enemy_skills_pb2 import Condition, MonsterBehavior, Behavior, BehaviorGroup -from dungeon.grouped_skillls import GroupedSkills - -GroupType = { - 0: "Unspecified", - 1: "Passive", - 2: "Preemptive", - 3: "Dispel Player", - 4: "Monster Status", - 5: "Remaining", - 6: "Standard", - 7: "Death", - 8: "Unknown", - 9: "Highest Priority" -} - - -def format_condition(cond: Condition): - parts = [] - if cond.skill_set: - parts.append('SkillSet {}'.format(cond.skill_set)) - if cond.use_chance not in [0, 100]: - parts.append('{}% chance'.format(cond.use_chance)) - if cond.global_one_time: - parts.append('one time only') - if cond.limited_execution: - parts.append('at most {} times'.format(cond.limited_execution)) - if cond.trigger_enemies_remaining: - parts.append('when {} enemies remain'.format(cond.trigger_enemies_remaining)) - if cond.if_defeated: - parts.append('when defeated') - if cond.if_attributes_available: - parts.append('when required attributes on board') - if cond.trigger_monsters: - parts.append('when {} on team'.format(', '.join(map(str, cond.trigger_monsters)))) - if cond.trigger_combos: - parts.append('when {} combos last turn'.format(cond.trigger_combos)) - if cond.if_nothing_matched: - parts.append('if no other skills matched') - if cond.repeats_every: - if cond.trigger_turn: - if cond.trigger_turn_end: - parts.append('execute repeatedly, turn {}-{} of {}'.format(cond.trigger_turn, - cond.trigger_turn_end, - cond.repeats_every)) - else: - parts.append('execute repeatedly, turn {} of {}'.format(cond.trigger_turn, cond.repeats_every)) - else: - parts.append('repeats every {} turns'.format(cond.repeats_every)) - elif cond.trigger_turn_end: - turn_text = 'turns {}-{}'.format(cond.trigger_turn, cond.trigger_turn_end) - parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) - elif cond.trigger_turn: - turn_text = 'turn {}'.format(cond.trigger_turn) - parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) - - if not parts and cond.hp_threshold in [100, 0]: - return None - - if cond.hp_threshold == 101: - parts.append('when hp is full') - elif cond.hp_threshold: - parts.append('hp <= {}'.format(cond.hp_threshold)) - - return ', '.join(parts) - - -# From pad-data-pipeline -def _cond_hp_timed_text(always_trigger_above: int, turn_text: str) -> str: - text = turn_text - if always_trigger_above == 1: - text = 'always {}'.format(turn_text) - elif always_trigger_above: - text = '{} while HP > {}'.format(turn_text, always_trigger_above) - return text - - -def formatOverview(query): - output = "Dungeon Name: {}".format(query[0]["dungeon_name_en"]) - for q in query: - output += "\nEncounter ID: {} Stage: {} Monster Name: {} Level: {}".format(q["encounter_id"], q["stage"], - q["name_en"], q["level"]) - - return output - - -def behaviorForLevel(mb: MonsterBehavior, num: int): - lev = None - for level in mb.levels: - if level.level == num: lev = level - if lev is None: - return mb.levels[len(mb.levels) - 1] - else: - return lev - - -def process_enemy_skill2(encounter: EncounterModel, skill: EnemySkillModel, emoji_map): - effect = skill.desc_en_emoji - split_effects = effect.split("), ") - non_attack_effects = [] - for e in split_effects: - # print(encounter.monster_id) - e += ")" - if "Attack:" not in e: - non_attack_effects.append(e.format_map(emoji_map)) - - # Damage - atk = encounter.atk - if skill.min_hits != 0: - emoji = emoji_map['attack'] - if skill.min_hits > 1: - emoji = emoji_map['multi_attack'] - damage_per_hit = (int)(atk * (skill.atk_mult / 100.0)) - min_damage = skill.min_hits * damage_per_hit - max_damage = skill.max_hits * damage_per_hit - if min_damage != max_damage: - non_attack_effects.append("({}:{}~{})".format(emoji, f'{min_damage:,}', f'{max_damage:,}')) - else: - non_attack_effects.append("({}:{})".format(emoji, f'{min_damage:,}')) - - return non_attack_effects - - -""" -Process_[behavior, behavior_group, monster]: These functions take the behavior data and convert it to a easier to work -(for me) objects -""" - - -def process_behavior(behavior: Behavior, database, q: EncounterModel, emoji_map, parent: GroupedSkills = None): - skill = database.dungeon.get_enemy_skill(behavior.enemy_skill_id) - if skill is None: - return "Unknown" - skill_name = skill.name_en - skill_effect = skill.desc_en - # skill_processed_text = process_enemy_skill(skill_effect, q, skill) - skill_processed_texts = process_enemy_skill2(q, skill, emoji_map) - - condition = format_condition(behavior.condition) - processed_skill: ProcessedSkill = ProcessedSkill(skill_name, skill_effect, skill_processed_texts, condition, - parent) - # embed.add_field(name="{}Skill Name: {}{}".format(group_type, skill_name, process_enemy_skill(skill_effect, q, skill)), value="{}Effect: {}\n{}".format(indent, skill_effect, condition), inline=False) - return processed_skill - - -def process_behavior_group(group: BehaviorGroup, database, q: EncounterModel, emoji_map, - parent: GroupedSkills = None): - condition = format_condition(group.condition) - processed_group: GroupedSkills = GroupedSkills(condition, GroupType[group.group_type], parent) - for child in group.children: - if child.HasField('group'): - processed_group.add_group(process_behavior_group(child.group, database, q, emoji_map, processed_group)) - elif child.HasField('behavior'): - processed_group.add_skill(process_behavior(child.behavior, database, q, emoji_map, processed_group)) - return processed_group - - -# return output - -# Format: -# Skill Name Type:Preemptive/Passive/Etc -# Skill Effect -# Condition -def process_monster(mb: MonsterBehavior, q: EncounterModel, database: DbContext): - # output = "Behavior for: {} at Level: {}".format(q["name_en"], q["level"]) # TODO: Delete later after testing - """embed = discord.Embed(title="Behavior for: {} at Level: {}".format(q["name_en"], q["level"]), - description="HP:{} ATK:{} DEF:{} TURN:{}".format(q["hp"], q["atk"], q["defence"], q['turns']))""" - emoji_map = SafeDict(resolve_status=emoji_cache.get_by_name('resolve'), - fire_orb=emoji_cache.get_by_name('orb_fire'), - recover_health=emoji_cache.get_by_name('misc_autoheal'), - roulette=emoji_cache.get_by_name('spinner'), - unknown_type='Unknown', - defense_status=emoji_cache.get_by_name('defense'), - combo_absorb_status=emoji_cache.get_by_name('combo'), - absorb_status=emoji_cache.get_by_name('attribute_absorb'), - damage_absorb_status=emoji_cache.get_by_name('damage_absorb'), - damage_void_status=emoji_cache.get_by_name('vdp'), - status_shield_status=emoji_cache.get_by_name('status'), - movetime_buff_status=emoji_cache.get_by_name('time_buff'), - movetime_debuff_status=emoji_cache.get_by_name('time_debuff'), - dispel_status='Dispel', - leader_swap_status=emoji_cache.get_by_name('swap'), - skill_delay=emoji_cache.get_by_name('misc_sb'), - lock_orbs=emoji_cache.get_by_name('lock'), - tape_status=emoji_cache.get_by_name('res_seal'), - fixed_start=emoji_cache.get_by_name('orb_start'), - cloud_status=emoji_cache.get_by_name('res_cloud'), - gravity=emoji_cache.get_by_name('gravity'), - invincible_status=emoji_cache.get_by_name('invincible'), - invincible_off_status=emoji_cache.get_by_name('invincible'), # TODO - force_target_status=emoji_cache.get_by_name('force_target'), - leader_alter_status=emoji_cache.get_by_name('transform'), - board_size_status=emoji_cache.get_by_name('board_size'), - super_resolve_status=emoji_cache.get_by_name('super_resolve'), - turn_change=emoji_cache.get_by_name('turn_change'), - enrage_status=emoji_cache.get_by_name('enrage'), - active_skill_bind=emoji_cache.get_by_name('skill_bound'), - do_nothing="💤", - awoken_skill_bind=emoji_cache.get_by_name('awoken_skill_bind'), - no_skyfall_status=emoji_cache.get_by_name('no_skyfall'), - bind=emoji_cache.get_by_name('res_bind'), - skyfall_status='🌧', - blind='⚫', - super_blind=emoji_cache.get_by_name('blind_orb'), - to='➡️', - attack=emoji_cache.get_by_name('single_hit'), - multi_attack=emoji_cache.get_by_name('multi_hit'), - target_self='Self', - health=emoji_cache.get_by_name('health'), - combo_orb=emoji_cache.get_by_name('orb_combo'), - locked_bomb_orb=emoji_cache.get_by_name('locked_bomb_orb'), - random_attribute='[Random Att]', - water_orb=emoji_cache.get_by_name('orb_water'), - wood_orb=emoji_cache.get_by_name('orb_wood'), - light_orb=emoji_cache.get_by_name('orb_light'), - dark_orb=emoji_cache.get_by_name('orb_dark'), - healer_orb=emoji_cache.get_by_name('heal_orb'), - jammer_orb=emoji_cache.get_by_name('jammer_orb'), - poison_orb=emoji_cache.get_by_name('poison_orb'), - bomb_orb=emoji_cache.get_by_name('bomb_orb'), - mortal_poison_orb=emoji_cache.get_by_name('mortal_poison_orb'), - evo_material_type=emoji_cache.get_by_name('killer_evomat'), - balanced_type=emoji_cache.get_by_name('killer_balance'), - physical_type=emoji_cache.get_by_name('killer_physical'), - healer_type=emoji_cache.get_by_name('killer_healer'), - dragon_type=emoji_cache.get_by_name('killer_dragon'), - god_type=emoji_cache.get_by_name('killer_god'), - attacker_type=emoji_cache.get_by_name('killer_attacker'), - devil_type=emoji_cache.get_by_name('killer_devil'), - machine_type=emoji_cache.get_by_name('killer_machine'), - awakening_material_type=emoji_cache.get_by_name('killer_awoken'), - enhance_material_type=emoji_cache.get_by_name('killer_enhancemat'), - redeemable_material_type='🪙', - no_match_fire=emoji_cache.get_by_name('no_match_fire'), - no_match_wood=emoji_cache.get_by_name('no_match_wood'), - no_match_water=emoji_cache.get_by_name('no_match_water'), - no_match_dark=emoji_cache.get_by_name('no_match_dark'), - no_match_light=emoji_cache.get_by_name('no_match_light'), - no_match_heart=emoji_cache.get_by_name('no_match_heart'), - no_match_jammer=emoji_cache.get_by_name('no_match_jammer'), - no_match_poison=emoji_cache.get_by_name('no_match_poison'), - no_match_heal=emoji_cache.get_by_name('no_match_heal'), - recover_debuff_status=emoji_cache.get_by_name('recover_debuff_status'), - attack_debuff_status='⚔️''⬇️' - ) - monster_model = database.graph.get_monster(q.monster_id) - monster: DungeonMonster = DungeonMonster( - name=monster_model.name_en, - hp=q.hp, - atk=q.atk, - defense=q.defence, - turns=q.turns, - level=q.level - ) - if mb is None: - return monster - levelBehavior = behaviorForLevel(mb, q.level) - if levelBehavior is None: - return monster - for group in levelBehavior.groups: - monster.add_group(process_behavior_group(group, database, q, emoji_map)) - - return monster diff --git a/dungeon/safe_dict.py b/dungeon/safe_dict.py deleted file mode 100644 index 4f1007dc..00000000 --- a/dungeon/safe_dict.py +++ /dev/null @@ -1,3 +0,0 @@ -class SafeDict(dict): - def __missing__(self, key): - return '{' + key + '}' diff --git a/dungeon/view/__init__.py b/dungeon/view/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dungeon/view/dungeon.py b/dungeon/view/dungeon.py deleted file mode 100644 index 44756a67..00000000 --- a/dungeon/view/dungeon.py +++ /dev/null @@ -1,288 +0,0 @@ -import random -from typing import List - -import discord -from discord import Embed -from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField -from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state - -from dungeon.enemy_skills_pb2 import MonsterBehavior -from dungeon.processors import process_monster -from padinfo.view.components.view_state_base import ViewStateBase - - -class DungeonViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, pm, - sub_dungeon_id, num_floors, floor, num_spawns, floor_index, technical, database, page=0, verbose=False, - reaction_list: List[str] = None): - super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) - self.pm = pm - self.sub_dungeon_id = sub_dungeon_id - self.num_floors = num_floors - self.floor = floor - self.floor_index = floor_index - self.technical = technical - self.color = color - self.database = database - self.num_spawns = num_spawns - self.page = page - self.verbose = verbose - - def serialize(self): - ret = super().serialize() - ret.update({ - 'sub_dungeon_id': self.sub_dungeon_id, - 'num_floors': self.num_floors, - 'floor': self.floor, - 'floor_index': self.floor_index, - 'technical': self.technical, - 'pane_type': DungeonView.VIEW_TYPE, - 'verbose': self.verbose, - 'page': self.page - }) - return ret - - @classmethod - async def deserialize(cls, dbcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, - verbose_toggle: bool = False, page: int = 0, reset_spawn: bool = False): - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - raw_query = ims.get('raw_query') - sub_dungeon_id = ims.get('sub_dungeon_id') - num_floors = ims.get('num_floors') - floor = ims.get('floor') + inc_floor - floor_index = ims.get('floor_index') + inc_index - technical = ims.get('technical') - verbose = ims.get('verbose') - # check if we are on final floor/final monster of the floor - if floor > num_floors: - floor = 1 - - if floor < 1: - floor = num_floors - - # toggle verbose - if verbose_toggle: - verbose = not verbose - - # get encounter models for the floor - floor_models = dbcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) - - # check if we are on final monster of the floor - if floor_index >= len(floor_models): - floor_index = 0 - - if floor_index < 0: - floor_index = len(floor_models) + floor_index - - # check if we reset the floor_index - if reset_spawn: - floor_index = 0 - - encounter_model = floor_models[floor_index] - - return cls(original_author_id, menu_type, raw_query, color, encounter_model, sub_dungeon_id, - num_floors, floor, len(floor_models), floor_index, - technical, dbcog.database, verbose=verbose, - reaction_list=ims.get('reaction_list'), page=page) - - -class DungeonView: - VIEW_TYPE = 'DungeonText' - - egg_text = [ - "But nobody came...", - "Come again later", - "Tsubaki loves you", - "Remember 100 turn Hera stalling?", - "Odin x Mermaid are the OG heroes", - "Never forget 10 minute stamina", - "Another 3*...", - "Nice!", - "Maybe one day DBZ will come", - "Special thanks to the PADX team and tactical_retreat!" - ] - - @staticmethod - def indent(level): - """ - Helper function that indents text for the embed - """ - ret = "" - for l in range(level): - if l == 0: - ret += "> " - else: - ret += "\u200b \u200b \u200b \u200b \u200b " - return ret - - @staticmethod - def embed_helper(level, names, values, line): - """ - Adds a line in such a way to maximize discord embed limits. - - We first try to add it to the most recent name part of the name/value pair. - If that fails we try to add it to the corresponding value. If that fails, - we create another name/value pair. - @param level: how many indents the line needs - @param names: an existing list (a list of strings that will be placed in "name fields" - @param values: an existing list (a list of strings that will be placed in the value part of the name/value field - @param line: the line of text we want to add - """ - - current_index = len(names) - 1 - indents = DungeonView.indent(level) - if len(names[current_index]) + len(line) + len(indents) <= 255 and len(names[current_index]) == 0: - names[current_index] += "\n{}{}".format(indents, line) - elif len(values[current_index]) + len(line) + len(indents) <= 1023: - values[current_index] += "\n{}{}".format(indents, line) - else: - names.append("") - values.append("") - DungeonView.embed_helper(level, names, values, line) - - @staticmethod - def embed(state: DungeonViewState): - fields = [] - mb = MonsterBehavior() - encounter_model = state.pm - if (encounter_model.enemy_data is not None) and (encounter_model.enemy_data.behavior is not None): - mb.ParseFromString(encounter_model.enemy_data.behavior) - else: - mb = None - monster = process_monster(mb, encounter_model, state.database) - monster_embed: Embed = \ - DungeonView.make_embed(monster, verbose=state.verbose, spawn=[state.floor_index + 1, state.num_spawns], - floor=[state.floor, state.num_floors], technical=state.technical)[state.page] - hp = f'{monster.hp:,}' - atk = f'{monster.atk:,}' - defense = f'{monster.defense:,}' - turns = f'{monster.turns:,}' - - title = monster_embed.title - desc = monster_embed.description - me_fields = monster_embed.fields - for f in me_fields: - fields.append( - EmbedField(f.name, Box(*[f.value])) - ) - return EmbedView( - EmbedMain( - title=title, - description=desc, - ), - embed_fields=fields, - embed_footer=embed_footer_with_state(state) - ) - - @staticmethod - def make_embed(dungeon_monster, verbose: bool = False, spawn: "list[int]" = None, floor: "list[int]" = None, - technical: int = None): - """ - When called this generates an embed that displays the encounter (what is seen in dungeon_info). - @param dungeon_monster: the processed monster to make an embed for - @param verbose: whether or not to display effect text - @param spawn: used to show what spawn this is on a floor [spawn, max] -> "spawn/max" - @param floor: used to show what floor this is [current floor, number of floors] - @param technical: if the dungeon is a technical dungeon we don't display skills - """ - - embeds = [] - - desc = "" - - # We create two pages as monsters at max will only ever require two pages of embeds - if spawn is not None: - embed = discord.Embed( - title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{} Page:".format(dungeon_monster.name, dungeon_monster.level, spawn[0], - spawn[1], - floor[0], floor[1]), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc) - ) - else: - embed = discord.Embed( - title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc) - ) - - embeds.append(embed) - embeds.append(embed.copy()) - - if technical == 0: - embeds[0].title += " 1/1" - embeds[1].title += " 2/1" - return embeds - embeds[0].title += " 1/" - embeds[1].title += " 2/" - # We collect text from the skills and groups of skills - lines = [] - for group in dungeon_monster.groups: - group.give_string2(lines, 0, verbose) - - # We add the lines using embed_helper - names = [""] - values = [""] - fields = 0 - current_embed = 0 - length = len(embed.title) + len(embed.description) + 1 - first = None - for l in lines: - for comp in l[1]: - DungeonView.embed_helper(l[0], names, values, comp) - - # We add the name/value pairs to the actual embed. If needed we go to the second page - if len(values[len(values) - 1]) == 0: - values[len(values) - 1] = '\u200b' - for index in range(len(names)): - name = names[index] - value = values[index] - if len(name) > 0: - temp_length = length + len(name) + len(value) - if temp_length > 6000: - current_embed += 1 - length = len(embed.title) + len(embed.description) - embeds[current_embed].add_field(name=name, value=value, inline=False) - length += len(name) + len(value) - fields += 1 - # embed.add_field(name=k, value=content, inline=False) - - if current_embed == 0: - embeds[0].title += '1' - embeds[1].title += '1' - - # for fun - random_index = random.randint(0, len(DungeonView.egg_text) - 1) - embeds[1].add_field(name=DungeonView.egg_text[random_index], value="Come back when you see 1/2 for page!") - else: - embeds[0].title += '2' - embeds[1].title += '2' - return embeds - - @staticmethod - async def make_preempt_embed(dungeon_monster, spawn: "list[int]" = None, floor: "list[int]" = None, technical: int = None): - """ - Currently unused: when called it creates an embed that only contains embed information. - """ - skills = await dungeon_monster.collect_skills() - desc = "" - for s in skills: - if "Passive" in s.type or "Preemptive" in s.type: - desc += "\n{}".format(s.give_string(verbose=True)) - if technical == 0: - desc = "" - if spawn is not None: - embed = discord.Embed( - title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{}".format(dungeon_monster.name, dungeon_monster.level, spawn[0], spawn[1], - floor[0], floor[1]), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc)) - else: - embed = discord.Embed( - title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc)) - return [embed, discord.Embed(title="test", desc="test")] diff --git a/dungeon/view/simple.py b/dungeon/view/simple.py deleted file mode 100644 index d028277c..00000000 --- a/dungeon/view/simple.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import List - -from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField -from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state - -from padinfo.common.config import UserConfig -from padinfo.view.components.view_state_base import ViewStateBase - -class SimpleViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, - color, message_list, index, - reaction_list: List[str] = None): - super().__init__(original_author_id, menu_type, raw_query, reaction_list=reaction_list) - self.message_list = message_list - self.index = index - self.color = color - - def serialize(self): - ret = super().serialize() - ret.update({ - 'message_list': self.message_list, - 'index': self.index - }) - return ret - - @classmethod - async def deserialize(cls, dbcog, color, ims: dict, inc): - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - raw_query = ims.get('raw_query') - print("hi") - return cls(original_author_id, menu_type, raw_query, color, ims.get('message_list'), ims.get('index') + inc, - reaction_list=ims.get('reaction_list')) - - -class SimpleView: - VIEW_TYPE = 'SimpleText' - - @staticmethod - def embed(state: SimpleViewState): - fields=[ - EmbedField("Title", Box(*["1", '2'])) - ] - return EmbedView( - EmbedMain( - color=state.color, - description=state.message_list[state.index] - ), - embed_fields=fields, - embed_footer=embed_footer_with_state(state), - ) diff --git a/dungeon/view/test_view.py b/dungeon/view/test_view.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index f7db0b45..79be1993 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -31,7 +31,6 @@ def __init__(self, bot, *args, **kwargs): async def load_emojis(self): await self.bot.wait_until_ready() - logging.debug('load_emojis, dungeon') emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) @@ -57,7 +56,7 @@ async def get_dbcog(self): await dbcog.wait_until_ready() return dbcog - async def find_dungeon_from_name2(self, ctx, name, database: "DungeonContext", difficulty: str = None): + async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", difficulty: str = None): """ Gets the sub_dungeon model given the name of a dungeon and its difficulty. """ @@ -104,7 +103,7 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): # load dbcog cog for database access dbcog = await self.get_dbcog() - dungeon = await self.find_dungeon_from_name2(ctx, name, dbcog.database.dungeon, difficulty) + dungeon = await self.find_dungeon_from_name(ctx, name, dbcog.database.dungeon, difficulty) if dungeon is not None: # await ctx.send(format_overview(test_result)) diff --git a/dungeoncog/enemy_skill.py b/dungeoncog/enemy_skill.py index aaf8cb0d..4e6619b5 100644 --- a/dungeoncog/enemy_skill.py +++ b/dungeoncog/enemy_skill.py @@ -32,7 +32,7 @@ def find_type(self): def process_type(self): if "Passive" in self.type or "Preemptive" in self.type: - return "({})".format(self.type) + return "({}".format(self.type) return "" def give_string(self, indent: str = "", verbose: bool = False): diff --git a/dungeoncog/grouped_skillls.py b/dungeoncog/grouped_skillls.py index 73fa1ab1..be3e6459 100644 --- a/dungeoncog/grouped_skillls.py +++ b/dungeoncog/grouped_skillls.py @@ -1,11 +1,11 @@ class GroupedSkills(object): """This is a container class that holds the top level condition and its processed children""" - def __init__(self, condition: str, type, parent=None): + def __init__(self, condition: str, skill_type, parent=None): self.nested_groups = [] self.skills = [] self.condition = condition - self.type = type + self.type = skill_type self.parent = parent def add_group(self, group): diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index 7aa13e89..0c072bb2 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -69,9 +69,9 @@ def format_condition(cond: Condition): return None if cond.hp_threshold == 101: - parts.append('when hp is full') + parts.append('when HP is full') elif cond.hp_threshold: - parts.append('hp <= {}'.format(cond.hp_threshold)) + parts.append('HP <= {}'.format(cond.hp_threshold)) return ', '.join(parts) @@ -95,23 +95,22 @@ def format_overview(query): return output -def behavior_for_level(mb: MonsterBehavior, num: int): +def behavior_for_level(behavior: MonsterBehavior, num: int): lev = None - for level in mb.levels: + for level in behavior.levels: if level.level == num: lev = level if lev is None: - return mb.levels[len(mb.levels) - 1] + return behavior.levels[len(behavior.levels) - 1] else: return lev -def process_enemy_skill2(encounter: "EncounterModel", skill: "EnemySkillModel", emoji_map): +def process_enemy_skill(encounter: "EncounterModel", skill: "EnemySkillModel", emoji_map): effect = skill.desc_en_emoji split_effects = effect.split("), ") non_attack_effects = [] for e in split_effects: - # print(encounter.monster_id) e += ")" if "Attack:" not in e: non_attack_effects.append(e.format_map(emoji_map)) @@ -122,7 +121,7 @@ def process_enemy_skill2(encounter: "EncounterModel", skill: "EnemySkillModel", emoji = emoji_map['attack'] if skill.min_hits > 1: emoji = emoji_map['multi_attack'] - damage_per_hit = (int)(atk * (skill.atk_mult / 100.0)) + damage_per_hit = int(atk * (skill.atk_mult / 100)) min_damage = skill.min_hits * damage_per_hit max_damage = skill.max_hits * damage_per_hit if min_damage != max_damage: @@ -133,31 +132,29 @@ def process_enemy_skill2(encounter: "EncounterModel", skill: "EnemySkillModel", return non_attack_effects -def process_behavior(behavior: Behavior, database, q: "EncounterModel", emoji_map, parent: GroupedSkills = None): +def process_behavior(behavior: Behavior, database, encounter: "EncounterModel", emoji_map, parent: GroupedSkills = None): skill = database.dungeon.get_enemy_skill(behavior.enemy_skill_id) if skill is None: return "Unknown" skill_name = skill.name_en skill_effect = skill.desc_en - # skill_processed_text = process_enemy_skill(skill_effect, q, skill) - skill_processed_texts = process_enemy_skill2(q, skill, emoji_map) + skill_processed_texts = process_enemy_skill(encounter, skill, emoji_map) condition = format_condition(behavior.condition) processed_skill: ProcessedSkill = ProcessedSkill(skill_name, skill_effect, skill_processed_texts, condition, parent) - # embed.add_field(name="{}Skill Name: {}{}".format(group_type, skill_name, process_enemy_skill(skill_effect, q, skill)), value="{}Effect: {}\n{}".format(indent, skill_effect, condition), inline=False) return processed_skill -def process_behavior_group(group: BehaviorGroup, database, q: "EncounterModel", emoji_map, +def process_behavior_group(group: BehaviorGroup, database, encounter: "EncounterModel", emoji_map, parent: GroupedSkills = None): condition = format_condition(group.condition) processed_group: GroupedSkills = GroupedSkills(condition, GROUP_TYPES[group.group_type], parent) for child in group.children: if child.HasField('group'): - processed_group.add_group(process_behavior_group(child.group, database, q, emoji_map, processed_group)) + processed_group.add_group(process_behavior_group(child.group, database, encounter, emoji_map, processed_group)) elif child.HasField('behavior'): - processed_group.add_skill(process_behavior(child.behavior, database, q, emoji_map, processed_group)) + processed_group.add_skill(process_behavior(child.behavior, database, encounter, emoji_map, processed_group)) return processed_group @@ -166,16 +163,7 @@ def __missing__(self, key): return '<' + key + '>' -# return output - -# Format: -# Skill Name Type:Preemptive/Passive/Etc -# Skill Effect -# Condition -def process_monster(mb: MonsterBehavior, q: "EncounterModel", database: "DbContext"): - # output = "Behavior for: {} at Level: {}".format(q["name_en"], q["level"]) # TODO: Delete later after testing - """embed = discord.Embed(title="Behavior for: {} at Level: {}".format(q["name_en"], q["level"]), - description="HP:{} ATK:{} DEF:{} TURN:{}".format(q["hp"], q["atk"], q["defence"], q['turns']))""" +def process_monster(behavior: MonsterBehavior, encounter: "EncounterModel", database: "DbContext") -> DungeonMonster: emoji_map = SafeDgEmojiDict(resolve_status=emoji_cache.get_by_name('resolve'), fire_orb=emoji_cache.get_by_name('orb_fire'), recover_health=emoji_cache.get_by_name('misc_autoheal'), @@ -254,21 +242,21 @@ def process_monster(mb: MonsterBehavior, q: "EncounterModel", database: "DbConte recover_debuff_status=emoji_cache.get_by_name('recover_debuff_status'), attack_debuff_status='\N{CROSSED SWORDS}\N{DOWNWARDS BLACK ARROW}️' ) - monster_model = database.graph.get_monster(q.monster_id) - monster: DungeonMonster = DungeonMonster( + monster_model = database.graph.get_monster(encounter.monster_id) + monster = DungeonMonster( name=monster_model.name_en, - hp=q.hp, - atk=q.atk, - defense=q.defence, - turns=q.turns, - level=q.level + hp=encounter.hp, + atk=encounter.atk, + defense=encounter.defense, + turns=encounter.turns, + level=encounter.level ) - if mb is None: + if behavior is None: return monster - level_behavior = behavior_for_level(mb, q.level) + level_behavior = behavior_for_level(behavior, encounter.level) if level_behavior is None: return monster for group in level_behavior.groups: - monster.add_group(process_behavior_group(group, database, q, emoji_map)) + monster.add_group(process_behavior_group(group, database, encounter, emoji_map)) return monster diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index 839c75d0..63c24f07 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -1,14 +1,15 @@ import random -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List import discord -from discord import Embed +from discord import Color, Embed from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView from discordmenu.embed.view_state import ViewState from tsutils import embed_footer_with_state +from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skills_pb2 import MonsterBehavior from dungeoncog.processors import process_monster @@ -17,9 +18,10 @@ class DungeonViewState(ViewState): - def __init__(self, original_author_id, menu_type, raw_query, color, encounter: "EncounterModel", - sub_dungeon_id, num_floors, floor, num_spawns, floor_index, technical, database, - page=0, verbose=False): + def __init__(self, original_author_id: int, menu_type: str, raw_query: str, color: Color, + encounter: "EncounterModel", sub_dungeon_id: int, num_floors: int, floor: int, num_spawns: int, + floor_index: int, technical: int, database, + page: int = 0, verbose: bool = False): super().__init__(original_author_id, menu_type, raw_query) self.encounter = encounter self.sub_dungeon_id = sub_dungeon_id @@ -27,8 +29,8 @@ def __init__(self, original_author_id, menu_type, raw_query, color, encounter: " self.floor = floor self.floor_index = floor_index self.technical = technical - self.color = color self.database = database + self.color = color self.num_spawns = num_spawns self.page = page self.verbose = verbose @@ -183,8 +185,8 @@ def embed(state: DungeonViewState): ) @staticmethod - def make_embed(dungeon_monster, verbose: bool = False, spawn: "list[int]" = None, floor: "list[int]" = None, - technical: int = None): + def make_embed(dungeon_monster: DungeonMonster, verbose: bool = False, spawn: List[int] = None, + floor: List[int] = None, technical: int = None) -> List[discord.Embed]: """ When called this generates an embed that displays the encounter (what is seen in dungeon_info). @param dungeon_monster: the processed monster to make an embed for @@ -201,10 +203,9 @@ def make_embed(dungeon_monster, verbose: bool = False, spawn: "list[int]" = None # We create two pages as monsters at max will only ever require two pages of embeds if spawn is not None: embed = discord.Embed( - title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{} Page:".format(dungeon_monster.name, - dungeon_monster.level, spawn[0], - spawn[1], - floor[0], floor[1]), + title="{} Spawn:{}/{} Floor:{}/{} Page:".format(dungeon_monster.name, + spawn[0], spawn[1], + floor[0], floor[1]), description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', f'{dungeon_monster.atk:,}', f'{dungeon_monster.defense:,}', From da9c19096e54989a0c5713c12a2d35c0af3f044f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 30 Jul 2021 05:17:23 -0500 Subject: [PATCH 0910/1220] Fix dungeon emoji reactions (#1348) HIDDEN_EMOJI was being ignored also I cleaned up style/names --- dungeoncog/menu/dungeon.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/dungeoncog/menu/dungeon.py b/dungeoncog/menu/dungeon.py index 0e65db2e..3edc49a0 100644 --- a/dungeoncog/menu/dungeon.py +++ b/dungeoncog/menu/dungeon.py @@ -93,31 +93,33 @@ def message_control(state: DungeonViewState): return None return EmbedControl( [DungeonView.embed(state)], - [*DungeonMenuPanes.DATA.keys()] + DungeonMenuPanes.emoji_names() ) class DungeonEmoji: home = emoji_buttons['home'] - previous_monster_emoji = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' - next_monster_emoji = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' - next_floor = '\N{UPWARDS BLACK ARROW}' + verbose = '\N{SCROLL}' + previous_monster = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + next_monster = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' previous_floor = '\N{DOWNWARDS BLACK ARROW}' + next_floor = '\N{UPWARDS BLACK ARROW}' next_page = '\N{OPEN BOOK}' - verbose = '\N{SCROLL}' class DungeonMenuPanes(MenuPanes): INITIAL_EMOJI = DungeonEmoji.home + DATA = { DungeonEmoji.home: (DungeonMenu.respond_with_message, DungeonView.VIEW_TYPE), DungeonEmoji.verbose: (DungeonMenu.respond_with_verbose, DungeonView.VIEW_TYPE), - DungeonEmoji.previous_monster_emoji: (DungeonMenu.respond_with_previous_monster, DungeonView.VIEW_TYPE), - DungeonEmoji.next_monster_emoji: (DungeonMenu.respond_with_next_monster, DungeonView.VIEW_TYPE), + DungeonEmoji.previous_monster: (DungeonMenu.respond_with_previous_monster, DungeonView.VIEW_TYPE), + DungeonEmoji.next_monster: (DungeonMenu.respond_with_next_monster, DungeonView.VIEW_TYPE), DungeonEmoji.previous_floor: (DungeonMenu.respond_with_previous_floor, DungeonView.VIEW_TYPE), DungeonEmoji.next_floor: (DungeonMenu.respond_with_next_floor, DungeonView.VIEW_TYPE), DungeonEmoji.next_page: (DungeonMenu.respond_with_next_page, DungeonView.VIEW_TYPE) } + HIDDEN_EMOJIS = [ DungeonEmoji.home ] From 6e1399d5f614e2ec94280f5f8069c255e442ad2a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 30 Jul 2021 16:10:52 -0700 Subject: [PATCH 0911/1220] Use cloudfront url from tsutils (#1351) --- dbcog/dbcog.py | 2 +- padbuildimg/padbuildimg.py | 5 +++-- padglobal/padglobal.py | 2 +- padinfo/view/components/monster/image.py | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index b2f2c745..49fae753 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -54,7 +54,7 @@ def _data_file(file_name: str) -> str: TREENAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('treenames')) PANTHNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('panthnames')) - DB_DUMP_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/db/dadguide.sqlite' + DB_DUMP_URL = tsutils.CLOUDFRONT_URL + '/db/dadguide.sqlite' DB_DUMP_FILE = _data_file('dadguide.sqlite') except RuntimeError: pass diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 64b28f3a..98d7ce52 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -8,6 +8,7 @@ import aiohttp import discord +import tsutils from PIL import Image from PIL import ImageChops from PIL import ImageDraw @@ -152,7 +153,7 @@ AWK_STAR = 'star' DELAY_BUFFER = 'delay_buffer' REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/padbuildimg/assets/' -REMOTE_AWK_URL = 'https://d1kpnpud0qoyxf.cloudfront.net/media/awakenings/{0:03d}.png' +REMOTE_AWK_URL = tsutils.CLOUDFRONT_URL + '/media/awakenings/{0:03d}.png' # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' @@ -169,7 +170,7 @@ class PadBuildImgSettings(CogSettings): def make_default_build_img_params(self): build_img_params = DictWithAttributeAccess({ 'ASSETS_DIR': './assets/', - 'PORTRAIT_DIR': 'https://d1kpnpud0qoyxf.cloudfront.net/media/icons/{monster_id:05d}.png', + 'PORTRAIT_DIR': tsutils.CLOUDFRONT_URL + '/media/icons/{monster_id:05d}.png', # 'OUTPUT_DIR': './data/padbuildimg/output/', 'PORTRAIT_WIDTH': 100, 'PADDING': 10, diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 01ca2d67..cb5e620d 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -46,7 +46,7 @@ def _data_file(file_name: str) -> str: BLACKLISTED_CHARACTERS = '^[]*`~_' -PORTRAIT_TEMPLATE = 'https://d1kpnpud0qoyxf.cloudfront.net/media/portraits/{0:05d}.png' +PORTRAIT_TEMPLATE = tsutils.CLOUDFRONT_URL + '/media/portraits/{0:05d}.png' DISABLED_MSG = 'PAD Global info disabled on this server' diff --git a/padinfo/view/components/monster/image.py b/padinfo/view/components/monster/image.py index 301944d6..fd7c2078 100644 --- a/padinfo/view/components/monster/image.py +++ b/padinfo/view/components/monster/image.py @@ -1,11 +1,12 @@ from typing import TYPE_CHECKING +import tsutils from discordmenu.embed.components import EmbedThumbnail if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel -MEDIA_PATH = 'https://d1kpnpud0qoyxf.cloudfront.net/media/' +MEDIA_PATH = tsutils.CLOUDFRONT_URL + '/media/' ICON_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' From 5e22c5ef9971b8eb1bf5ea2b6cdc0e1c3753257f Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Wed, 4 Aug 2021 03:19:30 -0400 Subject: [PATCH 0912/1220] Fix bugs with buttoninfo and stat calculation (#1352) * Add nil checks, even though they seem to be wrong * Stop counting inherit's multiboosts * Only add stat awakenings from inherits that are equips * Calculate multiboosts from inherit iff it is an equip * Check that inherited monster exists --- dbcog/models/monster_stats.py | 17 +++++++++-------- padinfo/core/button_info.py | 6 ++++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/dbcog/models/monster_stats.py b/dbcog/models/monster_stats.py index 63f7ce41..1bc91e47 100644 --- a/dbcog/models/monster_stats.py +++ b/dbcog/models/monster_stats.py @@ -72,18 +72,19 @@ def stat(self, monster_model, key: StatType, lv, *, plus=99, inherit=False, is_p # inherit=True to calculate inherit stats and multiplayer=False in case the inherit has a multiboost awakening s_val += round(self.stat(inherited_monster, key, inherited_monster_lvl, inherit=True, multiplayer=False)) - # add bonus atk, hp, or rcv awakenings - inherit_bonus = MonsterStatModifierInput( - num_hp_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDHP.value), - num_atk_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDATK.value), - num_rcv_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDRCV.value) - ) - s_val += inherit_bonus.get_awakening_addition(key) + # add bonus atk, hp, or rcv awakenings iff the inherit has awoken assist + if inherited_monster.is_equip: + inherit_bonus = MonsterStatModifierInput( + num_hp_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDHP.value), + num_atk_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDATK.value), + num_rcv_awakening=inherited_monster.awakening_count(AwokenSkills.ENHANCEDRCV.value) + ) + s_val += inherit_bonus.get_awakening_addition(key) if multiplayer: num_multiboost = monster_model.awakening_count(AwokenSkills.MULTIBOOST.value) num_multiboost += inherited_monster.awakening_count( - AwokenSkills.MULTIBOOST.value) if inherited_monster else 0 + AwokenSkills.MULTIBOOST.value) if inherited_monster and inherited_monster.is_equip else 0 s_val = round(round(s_val) * (1.5 ** num_multiboost)) return s_val diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index fdb03575..c4ecdf02 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -223,9 +223,11 @@ def _get_team_btn_damage(self, team_buttons, dbcog, monster, multiplayer, limit_ dmg = dbcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, inherited_monster=inherit_model, multiplayer=multiplayer, inherited_monster_lvl=inherit_max_level) - if(monster.attr1.value in card.att): + # do count null main att monster damage as if it were the main att + if(monster.attr1.value in card.att or monster.attr1.name == NIL_ATT): total_dmg += dmg - if(monster.attr2.value in card.att): + # do not double-count null main att + if(monster.attr2.value in card.att and monster.attr1.name != NIL_ATT): total_dmg += dmg * self._get_sub_attr_multiplier(monster) colors_str = "" for i in card.att: From 40f53730245856a526df1fc119dd970a91965ece Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 4 Aug 2021 01:51:01 -0700 Subject: [PATCH 0913/1220] idtest refactor (#1355) --- dbcog/dbcog.py | 54 ++++++------------- dbcog/idtest_mixin.py | 119 +++++++++++++++++++++++++----------------- 2 files changed, 86 insertions(+), 87 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 49fae753..f92c00d9 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -10,24 +10,24 @@ import logging import os import shutil +import time from io import BytesIO -from typing import List, Dict, Optional +from typing import Dict, List, Optional import discord -import time import tsutils -from redbot.core import checks, data_manager, commands, Config -from redbot.core.utils.chat_formatting import pagify, box +from redbot.core import Config, checks, commands, data_manager +from redbot.core.utils.chat_formatting import box, pagify from tsutils import auth_check, safe_read_json from tsutils.enums import Server from . import token_mappings -from .find_monster import FindMonster from .database_loader import load_database +from .find_monster import FindMonster from .idtest_mixin import IdTest -from .models.monster_model import MonsterModel -from .models.monster_stats import monster_stats, MonsterStatModifierInput from .models.enum_types import DEFAULT_SERVER, SERVERS +from .models.monster_model import MonsterModel +from .models.monster_stats import MonsterStatModifierInput, monster_stats from .monster_index import MonsterIndex logger = logging.getLogger('red.padbot-cogs.dbcog') @@ -37,29 +37,6 @@ def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='dbcog')), file_name) -try: - # allow to run the tests locally without being inside a bot instance, which won't - # have any cog data path loaded - CSV_FILE_PATTERN = '{}.csv' - NAMES_EXPORT_PATH = _data_file('computed_names.json') - TREENAMES_EXPORT_PATH = _data_file('base_names.json') - TRANSLATEDNAMES_EXPORT_PATH = _data_file('translated_names.json') - - SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY/pub?gid={}&single=true&output=csv' - NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') - GROUP_TREENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') - PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') - - NICKNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('nicknames')) - TREENAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('treenames')) - PANTHNAME_FILE_PATTERN = _data_file(CSV_FILE_PATTERN.format('panthnames')) - - DB_DUMP_URL = tsutils.CLOUDFRONT_URL + '/db/dadguide.sqlite' - DB_DUMP_FILE = _data_file('dadguide.sqlite') -except RuntimeError: - pass - - class DBCog(commands.Cog, IdTest): """Database manager""" @@ -80,6 +57,7 @@ def __init__(self, bot, *args, **kwargs): debug_mode=False, debug_mode_monsters=[3260]) self.config.register_user(lastaction=None, fm_flags={}) + self.db_file_path = _data_file('dadguide.sqlite') self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') self.historic_lookups = safe_read_json(self.historic_lookups_file_path) self.monster_stats = monster_stats @@ -88,11 +66,11 @@ def __init__(self, bot, *args, **kwargs): self.token_maps = token_mappings self.DEFAULT_SERVER = DEFAULT_SERVER self.SERVERS = SERVERS + self.mon_finder = None # type: Optional[FindMonster] - GADMIN_COG = self.bot.get_cog("GlobalAdmin") - if GADMIN_COG: - GADMIN_COG.register_perm("contentadmin") + if (gadmin := self.bot.get_cog("GlobalAdmin")): + gadmin.register_perm("contentadmin") async def wait_until_ready(self) -> None: """Wait until the DBCog cog is ready. @@ -224,10 +202,12 @@ async def reload_data_task(self): async def download_and_refresh_nicknames(self): if await self.config.datafile(): logger.info('Copying dg data file') - shutil.copy2(await self.config.datafile(), DB_DUMP_FILE) + shutil.copy2(await self.config.datafile(), self.db_file_path) else: logger.info('Downloading dg data files') - await self._download_files() + await tsutils.async_cached_dadguide_request(self.db_file_path, + tsutils.CLOUDFRONT_URL + '/db/dadguide.sqlite', + 1 * 60 * 60) logger.info('Loading dg database') self.database = load_database(self.database, await self.get_debug_monsters()) @@ -236,10 +216,6 @@ async def download_and_refresh_nicknames(self): logger.info('Done refreshing dg data') - async def _download_files(self): - one_hour_secs = 1 * 60 * 60 - await tsutils.async_cached_dadguide_request(DB_DUMP_FILE, DB_DUMP_URL, one_hour_secs) - @commands.group() @checks.is_owner() async def dbcog(self, ctx): diff --git a/dbcog/idtest_mixin.py b/dbcog/idtest_mixin.py index 695e011a..71715217 100644 --- a/dbcog/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -1,23 +1,24 @@ import re from datetime import datetime -from typing import List, Dict, Optional +from typing import Callable, Coroutine, List, Mapping, Optional import tsutils -from tsutils.enums import Server -from redbot.core import commands, Config, checks +from redbot.core import Config, checks, commands +from redbot.core.bot import Red from redbot.core.utils.chat_formatting import box, pagify +from tsutils.enums import Server from dbcog.models.enum_types import DEFAULT_SERVER +from dbcog.models.monster_model import MonsterModel from dbcog.monster_index import MonsterIndex class IdTest: - bot = None - indexes: Dict[Server, MonsterIndex] - find_monster = None - - def __init__(self): - self.config = Config.get_conf(self, identifier=-1) + bot: Red + config: Config + indexes: Mapping[Server, MonsterIndex] + find_monster: Callable[[int], Coroutine[None, None, MonsterModel]] + wait_until_ready: Callable[[], Coroutine[None, None, None]] @commands.group() async def idtest(self, ctx): @@ -26,7 +27,7 @@ async def idtest(self, ctx): @idtest.command(name="add") @checks.is_owner() async def idt_add(self, ctx, mid: int, *, query): - """Add a test for the id3 test suite (Append `| reason` to add a reason)""" + """Add a test for the id3 test suite (Append `| ` to add a reason)""" query, *reason = query.split("|") query = query.strip() if await self.config.user(ctx.author).lastaction() != 'id3' and \ @@ -41,21 +42,31 @@ async def idt_add(self, ctx, mid: int, *, query): return suite[query] = { 'result': mid, - 'ts': datetime.now().timestamp(), + 'timestamp': datetime.now().timestamp(), 'reason': reason[0].strip() if reason else '' } - if await tsutils.get_user_reaction(ctx, f"Added test case `{mid}: {query}`" - f" with ref `{sorted(suite).index(query)}`", - "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): + try: + monster = await self.find_monster(query) + except Exception: + actual_mid = -2 + else: + actual_mid = -1 if monster is None else monster.monster_id + passing = actual_mid == mid + + if await tsutils.get_user_reaction(ctx, f"Added {'passing' if passing else 'failing'}" + f" test case `{mid}: {query}`" + f" with ref `{sorted(suite).index(query)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): if oldd: suite[query] = oldd else: del suite[query] await ctx.react_quietly("\N{CROSS MARK}") else: - await ctx.send( - f"Successfully added test case `{mid}: {query}` with ref `{sorted(suite).index(query)}`") + await ctx.send(f"Successfully added {'passing' if passing else 'failing'}" + f" test case `{mid}: {query}`" + f" with ref `{sorted(suite).index(query)}`") await ctx.tick() @idtest.group(name="name") @@ -68,7 +79,7 @@ async def idt_fluff(self, ctx): @idt_name.command(name="add") @checks.is_owner() - async def idtf_add(self, ctx, mid: int, token, server: Optional[Server] = DEFAULT_SERVER, *, reason=""): + async def idtn_add(self, ctx, mid: int, token, server: Optional[Server] = DEFAULT_SERVER, *, reason=""): """Add a name token test to the id3 test suite""" await self.norf_add(ctx, mid, token, server, reason, False) @@ -81,8 +92,8 @@ async def idtf_add(self, ctx, mid: int, token, server: Optional[Server] = DEFAUL async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.get_user_confirmation(ctx, - "Are you sure you want to add to the fluff/name test suite?"): + not await tsutils.get_user_confirmation(ctx, "Are you sure you want to add to the" + " fluff/name test suite?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -95,10 +106,10 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): if any(t['id'] == mid and t['token'] == token for t in suite): old = [t for t in suite if t['id'] == mid and t['token'] == token][0] if not await tsutils.get_user_confirmation(ctx, f"Are you sure you want to change" - f" the type of test case #{suite.index(old)}" - f" `{mid}: {token}` from " - f" **{'fluff' if fluffy else 'name'}** to" - f" **{'name' if fluffy else 'fluff'}**?"): + f" the type of test case #{suite.index(old)}" + f" `{mid}: {token}` from " + f" **{'fluff' if fluffy else 'name'}** to" + f" **{'name' if fluffy else 'fluff'}**?"): await ctx.react_quietly("\N{CROSS MARK}") return suite.remove(old) @@ -108,22 +119,30 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): 'token': token, 'fluff': fluffy, 'reason': reason, - 'server': server, - 'ts': datetime.now().timestamp() + 'server': server.name, + 'timestamp': datetime.now().timestamp() } suite.append(case) suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) - if await tsutils.get_user_reaction(ctx, f"Added {'fluff' if fluffy else 'name'} " - f"case `{mid}: {token}` with ref `{suite.index(case)}`", - "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): - suite.pop() + await self.wait_until_ready() + fluff = mid in [m.monster_id for m in self.indexes[server].fluff_tokens[token]] + name = mid in [m.monster_id for m in self.indexes[server].name_tokens[token]] + passing = (fluffy and fluff) or (not fluffy and name) + + if await tsutils.get_user_reaction(ctx, f"Added {'passing' if passing else 'failing'}" + f" {'fluff' if fluffy else 'name'} " + f"case `{mid}: {token}` with ref `{suite.index(case)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): + suite.remove(case) if old: suite.append(old) + suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) await ctx.react_quietly("\N{CROSS MARK}") else: - m = await ctx.send(f"Successfully added {'fluff' if fluffy else 'name'} " + m = await ctx.send(f"Successfully added {'passing' if passing else 'failing'}" + f" {'fluff' if fluffy else 'name'} " f"case `{mid}: {token}` with ref `{suite.index(case)}`") await m.add_reaction("\N{WHITE HEAVY CHECK MARK}") @@ -139,7 +158,7 @@ async def idt_import(self, ctx, *, queries): cases = re.findall(r'\s*(?:\d+. )?(.+?) +- (-?\d+) *(.*)', queries) async with self.config.test_suite() as suite: for query, result, reason in cases: - suite[query] = {'result': int(result), 'reason': reason, 'ts': datetime.now().timestamp()} + suite[query] = {'result': int(result), 'reason': reason, 'timestamp': datetime.now().timestamp()} await ctx.tick() @idt_name.command(name="import") @@ -170,10 +189,10 @@ async def norf_import(self, ctx, queries): 'fluff': fluff == 'fluff', 'reason': reason, 'server': "COMBINED", - 'ts': datetime.now().timestamp()}) + 'timestamp': datetime.now().timestamp()}) await ctx.tick() - @idtest.command(name="remove", aliases=["delete", "rm"]) + @idtest.command(name="remove", aliases=["rm", "delete", "del"]) @checks.is_owner() async def idt_remove(self, ctx, *, item): """Remove an id3 test""" @@ -192,16 +211,15 @@ async def idt_remove(self, ctx, *, item): return res = suite[case]['result'] del suite[case] - await ctx.send( - f"Removed test case `{case}: {res}` with ref") + await ctx.send(f"Removed test case `{case}: {res}` with ref") - @idt_name.command(name="remove") + @idt_name.command(name="remove", aliases=["rm", "delete", "del"]) @checks.is_owner() async def idtn_remove(self, ctx, *, item: int): """Remove a name/fluff test""" await self.norf_remove(ctx, item) - @idt_fluff.command(name="remove") + @idt_fluff.command(name="remove", aliases=["rm", "delete", "del"]) @checks.is_owner() async def idtf_remove(self, ctx, *, item: int): """Remove a name/fluff test""" @@ -254,7 +272,7 @@ async def idtf_setreason(self, ctx, number: int, *, reason): await self.norf_setreason(ctx, number, reason) async def norf_setreason(self, ctx, number, reason): - if reason == '""': + if reason == '""' or reason == "''": reason = "" if await self.config.user(ctx.author).lastaction() != 'name' and \ not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): @@ -356,7 +374,9 @@ async def idt_listrecent(self, ctx, count: int = 0): count = len(suite) o = "" ml = len(max(suite, key=len)) - for c, kv_tuple in enumerate(sorted(suite.items(), key=lambda kv: kv[1].get('ts', 0), reverse=True)[:count]): + for c, kv_tuple in enumerate(sorted(suite.items(), + key=lambda kv: kv[1].get('timestamp', 0), + reverse=True)[:count]): key, val = kv_tuple o += f"{key.ljust(ml)}: {str(val['result']).ljust(4)}\t{val.get('reason') or ''}\n" if not o: @@ -380,10 +400,11 @@ async def idt_run(self, ctx): for i, qr in enumerate(sorted(suite.items())): q, r = qr try: - m = await self.find_monster(q) or -1 + monster = await self.find_monster(q) except Exception: - m = -2 - mid = getattr(m, "monster_id", m) + mid = -2 + else: + mid = -1 if monster is None else monster.monster_id if mid != r['result']: reason = ' Reason: ' + r.get('reason') if 'reason' in r else '' @@ -450,10 +471,11 @@ async def idt_runall(self, ctx): async with ctx.typing(): for c, q in enumerate(sorted(qsuite)): try: - m = await self.find_monster(q) or -1 + monster = await self.find_monster(q) except Exception: - m = -2 - mid = getattr(m, "monster_id", m) + mid = -2 + else: + mid = -1 if monster is None else monster.monster_id if mid != qsuite[q]['result']: reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' @@ -501,10 +523,11 @@ async def run_tests(self) -> List[str]: ml = len(max(qsuite or [''], key=len)) + 2 for c, q in enumerate(sorted(qsuite)): try: - m = await self.find_monster(q) or -1 + monster = await self.find_monster(q) except Exception: - m = -2 - mid = getattr(m, "monster_id", m) + mid = -2 + else: + mid = -1 if monster is None else monster.monster_id if mid != qsuite[q]['result']: reason = ' Reason: ' + qsuite[q].get('reason') if qsuite[q].get('reason') else '' From 8388b1afd2f04249623adb1ba780042bfa845d3f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 5 Aug 2021 23:29:16 -0700 Subject: [PATCH 0914/1220] Fix whitespace issue in short_with_emoji header (#1357) --- padinfo/view/common.py | 6 +++--- padinfo/view/components/monster/header.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 80ed4dba..f1720e94 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -38,9 +38,9 @@ def get_awoken_skill_description(awoken_skill: "AwokenSkillModel"): ) -def invalid_monster_text(query: str, monster: "MonsterModel", append_text: str, link=False): - base_text = 'Your query `{}` found {}{}.' - return base_text.format(query, MonsterHeader.short_with_emoji(monster, link=link).to_markdown(), append_text) +def invalid_monster_text(query: str, monster: "MonsterModel", append_text: str, link: bool = False): + monster_name = MonsterHeader.short_with_emoji(monster, link=link).to_markdown() + return f'Your query `{query}` found {monster_name}{append_text}.' def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 44746bb1..7d45d072 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -68,10 +68,11 @@ def fmt_id_header(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed): @classmethod def short_with_emoji(cls, m: "MonsterModel", link=True, prefix=None): msg = f"{m.monster_no_na} - {m.name_en}" + suffix = cls.jp_suffix(m, False, False) return Box( prefix, Text(get_attribute_emoji_by_monster(m)), LinkedText(msg, puzzledragonx(m)) if link else Text(msg), - Text(cls.jp_suffix(m, False, False)), + Text(suffix) if suffix else None, delimiter=' ' ) From ca0d302eae31832fc6f630cd236caf2965eb336b Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 6 Aug 2021 14:01:06 -0700 Subject: [PATCH 0915/1220] Add Dungeon Type 10 (#1358) --- padevents/padevents.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index 49542fd2..6f78b667 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,22 +1,21 @@ import asyncio import datetime +import itertools import logging import re from collections import defaultdict from datetime import timedelta from enum import Enum from io import BytesIO -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional, TYPE_CHECKING, Union import discord -import itertools import prettytable import pytz -from redbot.core import checks -from redbot.core import commands, Config -from redbot.core.utils.chat_formatting import box, pagify, humanize_timedelta -from tsutils import CogSettings, get_user_confirmation, normalize_server_name, DummyObject, is_donor, rmdiacritics, \ - YES_EMOJI, NO_EMOJI, send_confirmation_message, send_cancellation_message +from redbot.core import Config, checks, commands +from redbot.core.utils.chat_formatting import box, humanize_timedelta, pagify +from tsutils import CogSettings, DummyObject, NO_EMOJI, YES_EMOJI, get_user_confirmation, is_donor, \ + normalize_server_name, rmdiacritics, send_cancellation_message, send_confirmation_message if TYPE_CHECKING: from dbcog.models.scheduled_event_model import ScheduledEventModel @@ -147,7 +146,7 @@ async def check_started(self): if aep['offset']: offsetstr = " in {} minute(s)".format(aep['offset']) try: - timestr = humanize_timedelta(timedelta=event.close_datetime-event.open_datetime) + timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) await channel.send("{} will be active for {}{}. {}".format(event.name_and_modifier, timestr, offsetstr, @@ -460,13 +459,14 @@ async def aep_remove_channel(self, ctx, key, group, f): if not await get_user_confirmation(ctx, "Are you sure you want to remove the {} channel for AEP `{}`?" .format(group, key)): - await send_cancellation_message(ctx, "No action was taken. You can set a channel with `{}aep set {}channel {} #channel_name`" + await send_cancellation_message(ctx, + "No action was taken. You can set a channel with `{}aep set {}channel {} #channel_name`" .format(ctx.prefix, group, key)) return else: await self.aepchange(ctx, key, 'channels', f) await send_confirmation_message(ctx, "Okay, I removed the {} channel for the AEP `{}`" - .format(group, key)) + .format(group, key)) return @aep_set.command(name="channel") @@ -476,7 +476,7 @@ async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): await ctx.tick() @aep_set.command(name="redchannel") - async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel=None): + async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel = None): """Sets channel to ping when event is red""" if channel is None: await self.aep_remove_channel(ctx, key, "red", lambda x: [None, x[1], x[2]]) @@ -485,7 +485,7 @@ async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel=None): await ctx.tick() @aep_set.command(name="bluechannel") - async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel=None): + async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel = None): """Sets channel to ping when event is blue""" if channel is None: await self.aep_remove_channel(ctx, key, "blue", lambda x: [x[0], None, x[2]]) @@ -494,7 +494,7 @@ async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel=None): await ctx.tick() @aep_set.command(name="greenchannel") - async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel=None): + async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel = None): """Sets channel to ping when event is green""" if channel is None: await self.aep_remove_channel(ctx, key, "green", lambda x: [x[0], x[1], None]) @@ -594,7 +594,7 @@ async def aed_add(self, ctx, server, searchstr, group=None, time_offset: int = 0 await ctx.send("Unsupported server, pick one of NA, KR, JP") return - #work around for 2 optional parameters + # work around for 2 optional parameters if group is not None and group.isdigit(): time_offset = int(group) group = None @@ -682,7 +682,7 @@ async def aed_purge(self, ctx): @autoeventdm.group(name="edit", aliases=["set"]) async def aed_e(self, ctx): """Edit a property of the autoeventdm""" - + async def _do_aed_add(self, ctx, item): """Add autoeventdm and return its index""" async with self.config.user(ctx.author).dmevents() as dmevents: @@ -701,7 +701,7 @@ async def aed_e_offset(self, ctx, index, offset: int): return async with self.config.user(ctx.author).dmevents() as dmevents: if not await get_user_confirmation(ctx, "Modify the offset for {}. `{}` to {} minutes?" - .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], offset)): + .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], offset)): return dmevents[corrected_index]['offset'] = offset await ctx.tick() @@ -741,7 +741,7 @@ async def aed_e_toggle3p(self, ctx, index): .format(corrected_index + 1, event)) return - async def _get_and_validate_aed_index(self, ctx, user_index): + async def _get_and_validate_aed_index(self, ctx, user_index: Union[str, int]): """Returns the corrected index for the autoeventdm of interest, based on Python list indexing""" async with self.config.user(ctx.author).dmevents() as dmevents: if isinstance(user_index, int) or user_index.isdigit(): @@ -1252,6 +1252,7 @@ class DungeonType(Enum): Unknown4 = 4 Unknown7 = 7 Unknown9 = 9 + Unknown10 = 10 def fmt_time(dt): From ba39d13ffdca5066756df2f91dfc4892e55217a1 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 6 Aug 2021 15:57:35 -0700 Subject: [PATCH 0916/1220] DungeonCog wording & readability fixes (#1359) --- dungeoncog/processors.py | 69 +++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index 0c072bb2..6c8f091d 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING +import re +from typing import Optional, TYPE_CHECKING from discordmenu.emoji.emoji_cache import emoji_cache @@ -26,72 +27,65 @@ } -def format_condition(cond: Condition): +def format_condition(cond: Condition) -> Optional[str]: parts = [] if cond.skill_set: - parts.append('SkillSet {}'.format(cond.skill_set)) + parts.append(f"SkillSet {cond.skill_set}") if cond.use_chance not in [0, 100]: - parts.append('{}% chance'.format(cond.use_chance)) + parts.append(f"{cond.use_chance}% chance") if cond.global_one_time: - parts.append('one time only') + parts.append(f"one time only") if cond.limited_execution: - parts.append('at most {} times'.format(cond.limited_execution)) + parts.append(f"at most {cond.limited_execution} times") if cond.trigger_enemies_remaining: - parts.append('when {} enemies remain'.format(cond.trigger_enemies_remaining)) + parts.append(f"when {cond.trigger_enemies_remaining} enemies remain") if cond.if_defeated: - parts.append('when defeated') + parts.append(f"when defeated") if cond.if_attributes_available: - parts.append('when required attributes on board') + parts.append(f"when required attributes on board") if cond.trigger_monsters: - parts.append('when {} on team'.format(', '.join(map(str, cond.trigger_monsters)))) + parts.append(f"when {', '.join(map(str, cond.trigger_monsters))} on team") if cond.trigger_combos: - parts.append('when {} combos last turn'.format(cond.trigger_combos)) + parts.append(f"when {cond.trigger_combos} combos last turn") if cond.if_nothing_matched: - parts.append('if no other skills matched') + parts.append(f"if no other skills matched") if cond.repeats_every: if cond.trigger_turn: if cond.trigger_turn_end: - parts.append('execute repeatedly, turn {}-{} of {}'.format(cond.trigger_turn, - cond.trigger_turn_end, - cond.repeats_every)) + parts.append(f"turns {cond.trigger_turn}-{cond.trigger_turn_end} of every {cond.repeats_every} turns") else: - parts.append('execute repeatedly, turn {} of {}'.format(cond.trigger_turn, cond.repeats_every)) + parts.append(f"turn {cond.trigger_turn} of every {cond.repeats_every} turns") else: - parts.append('repeats every {} turns'.format(cond.repeats_every)) + parts.append(f"repeats every {cond.repeats_every} turns") elif cond.trigger_turn_end: - turn_text = 'turns {}-{}'.format(cond.trigger_turn, cond.trigger_turn_end) + turn_text = f"turns {cond.trigger_turn}-{cond.trigger_turn_end}" parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) elif cond.trigger_turn: - turn_text = 'turn {}'.format(cond.trigger_turn) + turn_text = f"turn {cond.trigger_turn}" parts.append(_cond_hp_timed_text(cond.always_trigger_above, turn_text)) - if not parts and cond.hp_threshold in [100, 0]: - return None - if cond.hp_threshold == 101: - parts.append('when HP is full') - elif cond.hp_threshold: - parts.append('HP <= {}'.format(cond.hp_threshold)) + parts.append(f"when HP is full") + elif cond.hp_threshold not in (100, 0): + parts.append(f"HP <= {cond.hp_threshold}") - return ', '.join(parts) + return ', '.join(parts) or None # From pad-data-pipeline def _cond_hp_timed_text(always_trigger_above: int, turn_text: str) -> str: text = turn_text if always_trigger_above == 1: - text = 'always {}'.format(turn_text) + text = f"always {turn_text}" elif always_trigger_above: - text = '{} while HP > {}'.format(turn_text, always_trigger_above) + text = f"{turn_text} while HP > {always_trigger_above}" return text def format_overview(query): - output = "Dungeon Name: {}".format(query[0]["dungeon_name_en"]) + output = f"Dungeon Name: {query[0]['dungeon_name_en']}" for q in query: - output += "\nEncounter ID: {} Stage: {} Monster Name: {} Level: {}".format(q["encounter_id"], q["stage"], - q["name_en"], q["level"]) - + output += f"\nEncounter ID: {q['encounter_id']} Stage: {q['stage']} Monster Name: {q['name_en']}" return output @@ -107,11 +101,8 @@ def behavior_for_level(behavior: MonsterBehavior, num: int): def process_enemy_skill(encounter: "EncounterModel", skill: "EnemySkillModel", emoji_map): - effect = skill.desc_en_emoji - split_effects = effect.split("), ") non_attack_effects = [] - for e in split_effects: - e += ")" + for e in re.split(r'(?<=\)), ', skill.desc_en_emoji): if "Attack:" not in e: non_attack_effects.append(e.format_map(emoji_map)) @@ -125,9 +116,9 @@ def process_enemy_skill(encounter: "EncounterModel", skill: "EnemySkillModel", e min_damage = skill.min_hits * damage_per_hit max_damage = skill.max_hits * damage_per_hit if min_damage != max_damage: - non_attack_effects.append("({}:{}~{})".format(emoji, f'{min_damage:,}', f'{max_damage:,}')) + non_attack_effects.append(f"({emoji}:{min_damage:,}~{max_damage:,})") else: - non_attack_effects.append("({}:{})".format(emoji, f'{min_damage:,}')) + non_attack_effects.append(f"({emoji}:{min_damage:,})") return non_attack_effects @@ -199,7 +190,7 @@ def process_monster(behavior: MonsterBehavior, encounter: "EncounterModel", data no_skyfall_status=emoji_cache.get_by_name('no_skyfall'), bind=emoji_cache.get_by_name('res_bind'), skyfall_status='\N{CLOUD WITH RAIN}', - blind='\N{MEDIUM BLACK CIRCLE}', + blind=emoji_cache.get_by_name('black_circle_real'), super_blind=emoji_cache.get_by_name('blind_orb'), to='\N{BLACK RIGHTWARDS ARROW}️', attack=emoji_cache.get_by_name('single_hit'), From 4477b4eb1f034f41653f59469dc574891908a71d Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 6 Aug 2021 21:10:15 -0700 Subject: [PATCH 0917/1220] Allow adding email in CRUD for github pushes (#1362) --- crud/crud.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 8cad8f3f..3ccac836 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -1,6 +1,7 @@ import json import logging import os +import re from datetime import datetime from io import BytesIO @@ -8,7 +9,7 @@ import discord import pygit2 import pymysql -from redbot.core import checks, commands, Config, errors +from redbot.core import Config, checks, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils import auth_check, get_user_confirmation, send_cancellation_message @@ -54,6 +55,7 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.config = Config.get_conf(self, identifier=3270) self.config.register_global(config_file=None, pipeline_base=None, chan=None) + self.config.register_user(email=None) GADMIN_COG = self.bot.get_cog("GlobalAdmin") if GADMIN_COG: @@ -124,18 +126,22 @@ async def git_verify(self, ctx, filepath): await send_cancellation_message(ctx, f"Github credentials unset. Add via `{ctx.prefix}set api" f" github username token `") - index = repo.index - index.add(filepath) - index.write() - tree = index.write_tree() - author = pygit2.Signature(str(ctx.author), "famiel@tsubakibot.com") - commiter = pygit2.Signature("Famiel", "famiel@tsubakibot.com") - parent, ref = repo.resolve_refish(refish=repo.head.name) - repo.create_commit(ref.name, author, commiter, "Updating JSON", tree, [parent.oid]) - upcred = pygit2.UserPass(keys['username'], keys['token']) - remote = discord.utils.get(repo.remotes, name="origin") - remote.push(['refs/heads/master'], callbacks=pygit2.RemoteCallbacks(credentials=upcred)) - + try: + index = repo.index + index.add(filepath) + index.write() + tree = index.write_tree() + email = await self.bot.config.user(ctx.author).email() + author = pygit2.Signature(str(ctx.author), email or "famiel@tsubakibot.com") + commiter = pygit2.Signature("Famiel", "famiel@tsubakibot.com") + parent, ref = repo.resolve_refish(refish=repo.head.name) + repo.create_commit(ref.name, author, commiter, "Updating JSON", tree, [parent.oid]) + upcred = pygit2.UserPass(keys['username'], keys['token']) + remote = discord.utils.get(repo.remotes, name="origin") + remote.push(['refs/heads/master'], callbacks=pygit2.RemoteCallbacks(credentials=upcred)) + except Exception as e: + logger.exception("Failed to push.") + await send_cancellation_message(ctx, f"Failed to push to github. Please alert a sysadmin.") @commands.group() @auth_check('crud') @@ -520,3 +526,13 @@ async def setchan(self, ctx, channel: discord.TextChannel): async def rmchan(self, ctx): await self.config.chan.set(None) await ctx.tick() + + @crud.command() + async def setmyemail(self, ctx, email=None): + """Sets your email so GitHub commits can be properly attributed""" + if email is not None and \ + not re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email): + await ctx.send("Invalid email.") + return + await self.config.user(ctx.author).email.set(email) + await ctx.tick() From c6acedacc5f0529d39db430f0b3c808cb97a9e84 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 8 Aug 2021 02:10:35 -0500 Subject: [PATCH 0918/1220] add "box" as token for vdp (#1367) --- dbcog/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index ab3097c8..18131bad 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -135,7 +135,7 @@ class EvoTypes(Enum): AwokenSkills.FUA: ('fua',), AwokenSkills.ENHTEAMHP: ('teamhp', 'thp'), AwokenSkills.ENHTEAMRCV: ('teamrcv', 'trcv'), - AwokenSkills.VDP: ('vdp',), + AwokenSkills.VDP: ('vdp', 'box'), AwokenSkills.EQUIP: ('equip', 'assist', 'eq'), AwokenSkills.SUPERFUA: ('sfua',), AwokenSkills.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), From 3a77041979299c26d1be72defe25abb27b9fd306 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 12 Aug 2021 18:08:59 -0700 Subject: [PATCH 0919/1220] Add GlobalAdmin cog requirements (#1370) --- channelmirror/info.json | 4 +++- crud/info.json | 4 +++- dbcog/info.json | 4 +++- padglobal/info.json | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/channelmirror/info.json b/channelmirror/info.json index 698974f8..f31630e9 100644 --- a/channelmirror/info.json +++ b/channelmirror/info.json @@ -15,7 +15,9 @@ ], "hidden": false, "disabled": false, - "required_cogs": {}, + "required_cogs": { + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" + }, "requirements": [ "tsutils>=3.0.0", "aiohttp" diff --git a/crud/info.json b/crud/info.json index 73e5ac58..8efd37ee 100644 --- a/crud/info.json +++ b/crud/info.json @@ -15,7 +15,9 @@ ], "hidden": false, "disabled": false, - "required_cogs": {}, + "required_cogs": { + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" + }, "requirements": [ "tsutils", "pymysql", diff --git a/dbcog/info.json b/dbcog/info.json index e4666637..aacef102 100644 --- a/dbcog/info.json +++ b/dbcog/info.json @@ -15,7 +15,9 @@ ], "hidden": false, "disabled": false, - "required_cogs": {}, + "required_cogs": { + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" + }, "requirements": [ "tsutils>=4.0.0", "pytz", diff --git a/padglobal/info.json b/padglobal/info.json index 070d75e9..682b3ab3 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -17,6 +17,7 @@ "disabled": false, "required_cogs": { "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", + "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ From 43b8f5f46822bff327ae2ab3645c9c0c55cb2e2b Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 12 Aug 2021 22:21:45 -0700 Subject: [PATCH 0920/1220] Add converters for Server and StarterGroup (#1371) --- padevents/info.json | 2 +- padevents/padevents.py | 100 +++++++++++++---------------------------- 2 files changed, 32 insertions(+), 70 deletions(-) diff --git a/padevents/info.json b/padevents/info.json index c119b5dd..dd8565b2 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -19,7 +19,7 @@ "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ - "tsutils>=4.0.0", + "tsutils>=4.0.7", "pytz", "prettytable" ], diff --git a/padevents/padevents.py b/padevents/padevents.py index 6f78b667..5203c197 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -16,6 +16,7 @@ from redbot.core.utils.chat_formatting import box, humanize_timedelta, pagify from tsutils import CogSettings, DummyObject, NO_EMOJI, YES_EMOJI, get_user_confirmation, is_donor, \ normalize_server_name, rmdiacritics, send_cancellation_message, send_confirmation_message +from tsutils.enums import Server, StarterGroup if TYPE_CHECKING: from dbcog.models.scheduled_event_model import ScheduledEventModel @@ -231,11 +232,8 @@ async def padevents(self, ctx): @padevents.command() @checks.is_owner() - async def testevent(self, ctx, server, seconds: int = 0): - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + async def testevent(self, ctx, server: Server, seconds: int = 0): + server = server.value dbcog = self.bot.get_cog('DBCog') await dbcog.wait_until_ready() @@ -264,11 +262,8 @@ async def testevent(self, ctx, server, seconds: int = 0): @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addchannel(self, ctx, server): - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + async def addchannel(self, ctx, server: Server): + server = server.value channel_id = ctx.channel.id if self.settings.check_guerrilla_reg(channel_id, server): @@ -281,11 +276,8 @@ async def addchannel(self, ctx, server): @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmchannel(self, ctx, server): - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + async def rmchannel(self, ctx, server: Server): + server = server.value channel_id = ctx.channel.id if not self.settings.check_guerrilla_reg(channel_id, server): @@ -298,11 +290,8 @@ async def rmchannel(self, ctx, server): @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addchanneldaily(self, ctx, server): - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + async def addchanneldaily(self, ctx, server: Server): + server = server.value channel_id = ctx.channel.id if self.settings.check_daily_reg(channel_id, server): @@ -315,11 +304,8 @@ async def addchanneldaily(self, ctx, server): @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmchanneldaily(self, ctx, server): - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + async def rmchanneldaily(self, ctx, server: Server): + server = server.value channel_id = ctx.channel.id if not self.settings.check_daily_reg(channel_id, server): @@ -336,7 +322,7 @@ async def autoeventping(self, ctx): """Auto Event Pings""" @autoeventping.command(name="add", ignore_extra=False) - async def aep_add(self, ctx, key, server=None, searchstr=None, + async def aep_add(self, ctx, key, server: Server = None, searchstr=None, red_role: Optional[discord.Role] = None, blue_role: Optional[discord.Role] = None, green_role: Optional[discord.Role] = None, @@ -356,10 +342,7 @@ async def aep_add(self, ctx, key, server=None, searchstr=None, return if server: - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send(f"Unsupported server `{server}`, pick one of NA, KR, JP") - return + server = server.value if blue_role is None: blue_role = green_role = red_role @@ -527,12 +510,9 @@ async def aep_s_greenrole(self, ctx, key, role: discord.Role): await ctx.tick() @aep_set.command(name="server") - async def aep_s_server(self, ctx, key, server): + async def aep_s_server(self, ctx, key, server: Server): """Sets which server to listen to events in""" - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + server = server.value await self.aepset(ctx, key, 'server', server) await ctx.tick() @@ -587,25 +567,14 @@ async def autoeventdm(self, ctx): """Auto Event DMs""" @autoeventdm.command(name="add") - async def aed_add(self, ctx, server, searchstr, group=None, time_offset: int = 0): + async def aed_add(self, ctx, server: Server, searchstr, group: Optional[StarterGroup] = None, time_offset: int = 0): """Add a new autoeventdm""" - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return - - # work around for 2 optional parameters - if group is not None and group.isdigit(): - time_offset = int(group) - group = None + server = server.value if group is None: - group = "red" + group = 'red' else: - group = group.lower() - if group not in GROUPS: - await ctx.send("Unsupported group, pick one of red, blue, green") - return + group = GROUPS[group.value] if time_offset and not user_is_donor(ctx): await ctx.send("You must be a donor to set a time offset!") @@ -800,11 +769,8 @@ def filt(x): @padevents.command() @checks.mod_or_permissions(manage_guild=True) - async def active(self, ctx, server): - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + async def active(self, ctx, server: Server): + server = server.value msg = self.make_active_text(server) await self.page_output(ctx, msg) @@ -969,29 +935,25 @@ def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=Fa return header + tbl.get_string() + "\n" @commands.command(aliases=['events']) - async def eventsna(self, ctx, group=None): + async def eventsna(self, ctx, group: StarterGroup = None): """Display upcoming daily events for NA.""" - await self.do_partial(ctx, 'NA', group) + await self.do_partial(ctx, Server.NA, group) @commands.command() - async def eventsjp(self, ctx, group=None): + async def eventsjp(self, ctx, group: StarterGroup = None): """Display upcoming daily events for JP.""" - await self.do_partial(ctx, 'JP', group) + await self.do_partial(ctx, Server.JP, group) @commands.command() - async def eventskr(self, ctx, group=None): + async def eventskr(self, ctx, group: StarterGroup = None): """Display upcoming daily events for KR.""" - await self.do_partial(ctx, 'KR', group) + await self.do_partial(ctx, Server.KR, group) - async def do_partial(self, ctx, server, group=None): - server = normalize_server_name(server) - if server not in SUPPORTED_SERVERS: - await ctx.send("Unsupported server, pick one of NA, KR, JP") - return + async def do_partial(self, ctx, server: Server, group: StarterGroup = None): + server = server.value - if group is not None and group.lower() not in GROUPS: - await ctx.send("Unsupported group, pick one of red, blue, green") - return + if group is not None: + group = GROUPS[group.value] events = EventList(self.events) events = events.with_server(server) From e422e38b781516413f62b5ce6c2bb9f17f24e42d Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 12 Aug 2021 23:33:27 -0700 Subject: [PATCH 0921/1220] [crud] Return early if invalid git state (#1372) --- crud/crud.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 3ccac836..7f8e52ef 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -117,21 +117,21 @@ async def git_verify(self, ctx, filepath): f" was only **temporarily** made!") return if any(diff for diff in repo.diff().deltas if diff.old_file.path != filepath): - await send_cancellation_message(ctx, f"Hey {ctx.author.mention}, there are currently staged changes." - f" Please inform a sysadmin so that your changes can be" - f" manually committed.") + return await send_cancellation_message(ctx, f"Hey {ctx.author.mention}, there are currently staged changes." + f" Please inform a sysadmin so that your changes can be" + f" manually committed.") keys = await self.bot.get_shared_api_tokens("github") if "username" not in keys or "token" not in keys: - await send_cancellation_message(ctx, f"Github credentials unset. Add via `{ctx.prefix}set api" - f" github username token `") + return await send_cancellation_message(ctx, f"Github credentials unset. Add via `{ctx.prefix}set api" + f" github username token `") try: index = repo.index index.add(filepath) index.write() tree = index.write_tree() - email = await self.bot.config.user(ctx.author).email() + email = await self.config.user(ctx.author).email() author = pygit2.Signature(str(ctx.author), email or "famiel@tsubakibot.com") commiter = pygit2.Signature("Famiel", "famiel@tsubakibot.com") parent, ref = repo.resolve_refish(refish=repo.head.name) From a2337ca7a34893fdd07a3f72dca73d314f5782cc Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 22 Aug 2021 14:54:26 -0700 Subject: [PATCH 0922/1220] Updates to tsutils 5 (#1376) --- azurelane/azurelane.py | 6 +- azurelane/info.json | 4 +- channelmirror/channelmirror.py | 17 +++--- channelmirror/info.json | 2 +- crud/crud.py | 3 +- crud/info.json | 2 +- dbcog/dbcog.py | 35 ++++++----- dbcog/errors.py | 1 + dbcog/find_monster.py | 9 ++- dbcog/find_monster_tokens.py | 59 ++++++++++++++++++- dbcog/idtest_mixin.py | 46 +++++++-------- dbcog/info.json | 2 +- dbcog/models/monster_model.py | 6 +- dbcog/models/monster_stats.py | 8 ++- dbcog/monster_index.py | 11 ++-- dungeoncog/view/dungeon.py | 2 +- dungeoncog/view/simple.py | 6 +- feedback/feedback.py | 2 +- monidlistener/monidlistener.py | 8 +-- padboard/padboard.py | 4 +- padbuildimg/padbuildimg.py | 13 ++-- padbuilds/padbuilds.py | 4 +- padevents/padevents.py | 10 +++- padglobal/padglobal.py | 23 +++++--- padglobal/view/which.py | 2 +- padguidedb/padguidedb.py | 18 +++--- padinfo/common/emoji_map.py | 2 +- padinfo/common/external_links.py | 4 +- padinfo/core/padinfo_settings.py | 2 +- padinfo/menu/button_info.py | 3 +- padinfo/menu/id.py | 3 +- padinfo/menu/leader_skill.py | 3 +- padinfo/menu/monster_list.py | 12 ++-- padinfo/menu/series_scroll.py | 3 +- padinfo/menu/transforminfo.py | 2 +- padinfo/padinfo.py | 26 ++++---- padinfo/view/awakening_help.py | 2 +- padinfo/view/awakening_list.py | 2 +- padinfo/view/button_info.py | 3 +- padinfo/view/common.py | 2 +- padinfo/view/components/monster/image.py | 5 +- padinfo/view/evos.py | 3 +- padinfo/view/id.py | 3 +- padinfo/view/id_traceback.py | 2 +- padinfo/view/leader_skill.py | 3 +- padinfo/view/leader_skill_single.py | 3 +- padinfo/view/materials.py | 3 +- padinfo/view/monster_list/monster_list.py | 6 +- .../view/monster_list/static_monster_list.py | 3 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 3 +- padinfo/view/pic.py | 2 +- padinfo/view/series_scroll.py | 6 +- padinfo/view/simple_text.py | 2 +- padinfo/view/transforminfo.py | 3 +- padmonitor/padmonitor.py | 10 +++- padsearch/padsearch.py | 4 +- pricecheck/pricecheck.py | 2 +- profile/profile.py | 28 ++++----- schoolidol/schoolidol.py | 3 +- 60 files changed, 282 insertions(+), 186 deletions(-) diff --git a/azurelane/azurelane.py b/azurelane/azurelane.py index ab38d063..2b617451 100644 --- a/azurelane/azurelane.py +++ b/azurelane/azurelane.py @@ -7,7 +7,8 @@ from collections import OrderedDict from redbot.core import checks, commands from redbot.core.utils.chat_formatting import inline -from tsutils import EmojiUpdater, Menu, char_to_emoji +from tsutils.emoji import char_to_emoji +from tsutils.old_menu import EmojiUpdater, Menu logger = logging.getLogger('red.padbot-cogs.azurelane') @@ -24,6 +25,9 @@ def __init__(self, bot, *args, **kwargs): self.card_data = [] self.menu = Menu(bot) + self.id_to_card = None + self.names_to_card = None + async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) diff --git a/azurelane/info.json b/azurelane/info.json index fa131c3e..edb26126 100644 --- a/azurelane/info.json +++ b/azurelane/info.json @@ -17,8 +17,8 @@ "disabled": false, "required_cogs": {}, "requirements": [ - "tsutils", - "aiohttp" + "aiohttp", + "tsutils>=5.0.0" ], "tags": [ "azurlane", diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index d93a4d05..4feeec6a 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -6,11 +6,14 @@ from typing import Optional import discord -import tsutils -from redbot.core import checks, commands, Config -from redbot.core.utils.chat_formatting import inline, pagify, box -from tsutils import CogSettings, auth_check, replace_emoji_names_with_code, fix_emojis_for_server, \ - get_user_confirmation, send_confirmation_message, send_cancellation_message +from redbot.core import Config, checks, commands +from redbot.core.utils.chat_formatting import box, inline, pagify +from tsutils.cog_settings import CogSettings +from tsutils.cogs.globaladmin import auth_check +from tsutils.emoji import fix_emojis_for_server, replace_emoji_names_with_code +from tsutils.helper_classes import DummyObject +from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message, \ + send_repeated_consecutive_messages logger = logging.getLogger('red.misc-cogs.channelmirror') @@ -347,7 +350,7 @@ async def mirror_msg(self, message): fctx = await self.bot.get_context(message) fctx.send = dest_channel.guild.owner.send fctx.history = dest_channel.guild.owner.history - await tsutils.send_repeated_consecutive_messages(fctx, notify) + await send_repeated_consecutive_messages(fctx, notify) except Exception: logger.exception("Owner message failed.") except Exception as ex: @@ -370,7 +373,7 @@ async def mirror_msg_delete(self, payload): if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): return if not await self.config.channel(self.bot.get_channel(payload.channel_id)).nodeletion(): - fmessage = tsutils.DummyObject(id=payload.message_id, channel=self.bot.get_channel(payload.channel_id)) + fmessage = DummyObject(id=payload.message_id, channel=self.bot.get_channel(payload.channel_id)) await self.mirror_msg_mod(fmessage, delete_message_content=True) @commands.Cog.listener('on_raw_reaction_add') diff --git a/channelmirror/info.json b/channelmirror/info.json index f31630e9..d367786d 100644 --- a/channelmirror/info.json +++ b/channelmirror/info.json @@ -19,7 +19,7 @@ "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" }, "requirements": [ - "tsutils>=3.0.0", + "tsutils>=5.0.0", "aiohttp" ], "tags": [ diff --git a/crud/crud.py b/crud/crud.py index 7f8e52ef..02e5062c 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -11,7 +11,8 @@ import pymysql from redbot.core import Config, checks, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import auth_check, get_user_confirmation, send_cancellation_message +from tsutils.cogs.globaladmin import auth_check +from tsutils.user_interaction import get_user_confirmation, send_cancellation_message logger = logging.getLogger('red.padbot-cogs.crud') diff --git a/crud/info.json b/crud/info.json index 8efd37ee..11f525b4 100644 --- a/crud/info.json +++ b/crud/info.json @@ -19,7 +19,7 @@ "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" }, "requirements": [ - "tsutils", + "tsutils>=5.0.0", "pymysql", "aiofiles", "cryptography", diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index f92c00d9..0c498165 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -12,14 +12,16 @@ import shutil import time from io import BytesIO -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional import discord -import tsutils from redbot.core import Config, checks, commands, data_manager from redbot.core.utils.chat_formatting import box, pagify -from tsutils import auth_check, safe_read_json +from tsutils.cogs.globaladmin import auth_check from tsutils.enums import Server +from tsutils.json_utils import async_cached_dadguide_request, safe_read_json +from tsutils.tsubaki import CLOUDFRONT_URL +from tsutils.user_interaction import StatusManager, send_confirmation_message from . import token_mappings from .database_loader import load_database @@ -69,7 +71,8 @@ def __init__(self, bot, *args, **kwargs): self.mon_finder = None # type: Optional[FindMonster] - if (gadmin := self.bot.get_cog("GlobalAdmin")): + gadmin: Any = self.bot.get_cog("GlobalAdmin") + if gadmin: gadmin.register_perm("contentadmin") async def wait_until_ready(self) -> None: @@ -184,7 +187,7 @@ async def reload_data_task(self): while self == self.bot.get_cog('DBCog'): short_wait = False try: - async with tsutils.StatusManager(self.bot): + async with StatusManager(self.bot): await self.download_and_refresh_nicknames() logger.info('Done refreshing DBCog, triggering ready') self._is_ready.set() @@ -205,9 +208,9 @@ async def download_and_refresh_nicknames(self): shutil.copy2(await self.config.datafile(), self.db_file_path) else: logger.info('Downloading dg data files') - await tsutils.async_cached_dadguide_request(self.db_file_path, - tsutils.CLOUDFRONT_URL + '/db/dadguide.sqlite', - 1 * 60 * 60) + await async_cached_dadguide_request(self.db_file_path, + CLOUDFRONT_URL + '/db/dadguide.sqlite', + 1 * 60 * 60) logger.info('Loading dg database') self.database = load_database(self.database, await self.get_debug_monsters()) @@ -244,15 +247,15 @@ async def debug_enable(self, ctx, enabled: bool): """Sets tsubaki-only mode and reloads the index and graph""" await self.config.debug_mode.set(enabled) if enabled: - await tsutils.send_confirmation_message(ctx, f"You have set your bot to **Tsubaki mode** (aka debug mode)." - f" Only **Tsubaki** will be available in the graph and id" - f" queries, to allow for a faster restart time. To turn" - f" off **Tsubaki mode** run" - f" `{ctx.prefix}dbcog debugmode false`.") + await send_confirmation_message(ctx, f"You have set your bot to **Tsubaki mode** (aka debug mode)." + f" Only **Tsubaki** will be available in the graph and id" + f" queries, to allow for a faster restart time. To turn" + f" off **Tsubaki mode** run" + f" `{ctx.prefix}dbcog debugmode false`.") else: - await tsutils.send_confirmation_message(ctx, f"You have turned off **Tsubaki mode**. Your bot will behave" - f" normally. To return to **Tsubaki mode**, run" - f" `{ctx.prefix}dbcog debugmode true.`") + await send_confirmation_message(ctx, f"You have turned off **Tsubaki mode**. Your bot will behave" + f" normally. To return to **Tsubaki mode**, run" + f" `{ctx.prefix}dbcog debugmode true.`") await self.forceindexreload(ctx) @debugmode.group(name="monsters") diff --git a/dbcog/errors.py b/dbcog/errors.py index 93fe7422..e91d79b9 100644 --- a/dbcog/errors.py +++ b/dbcog/errors.py @@ -1,6 +1,7 @@ class InvalidGraphState(KeyError): pass + class InvalidMonsterId(KeyError): def __init__(self, monster_id: int): self.monster_id = monster_id diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index 11fe65a7..0674dbb2 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -4,7 +4,7 @@ from typing import Set, List, Tuple, Optional, Mapping, Union, Iterable, Any, Dict from Levenshtein import jaro_winkler -from tsutils import rmdiacritics +from tsutils.formatting import rmdiacritics from tsutils.query_settings import QuerySettings from tsutils.enums import Server @@ -102,10 +102,9 @@ def _string_to_token(self, value: str) -> Token: value = value.lstrip('-') if (exact := bool(re.fullmatch(r'".+"', value))): value = value[1:-1] - if re.fullmatch(r'.+-.+', value): - for special in SPECIAL_TOKEN_TYPES: - if re.fullmatch(special.RE_MATCH, value): - return special(value, negated=negated, exact=exact, database=self.dbcog.database) + for special in SPECIAL_TOKEN_TYPES: + if re.fullmatch(special.RE_MATCH, value): + return special(value, negated=negated, exact=exact, database=self.dbcog.database) return Token(value, negated=negated, exact=exact) def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index d2beb6ea..8636b9c2 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -1,8 +1,8 @@ import re +from dbcog.models.enum_types import AwokenSkills from dbcog.models.monster_model import MonsterModel from dbcog.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP -from dbcog.models.enum_types import AwokenSkills def regexlist(tokens): @@ -31,6 +31,7 @@ def __repr__(self): token = ("-" if self.negated else "") + (repr(self.full_value) if self.exact else self.full_value) return f"{self.__class__.__name__}<{token}>" + class SpecialToken(Token): RE_MATCH = r"" @@ -57,10 +58,10 @@ def matches(self, monster): for awakening in monster.awakenings: if awakening.is_super and not self.allows_super_awakenings: return False - + # Keep track of whether we matched this cycle for SA check at the end matched = True - + for awoken_skill in (self.database.awoken_skill_map[aws.value] for aws, tokens in AWOKEN_SKILL_MAP.items() if self.value in tokens): @@ -84,7 +85,59 @@ def matches(self, monster): return False +NUMERICAL_MONSTER_TOKENS = ['sell_mp'] + + +class MonsterAttributeNumeric(SpecialToken): + RE_MATCH = rf"({regexlist(NUMERICAL_MONSTER_TOKENS)}):([<>=]=?)?(\d+)" + + def __init__(self, fullvalue, *, negated=False, exact=False, database): + name, ineq, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() + self.monster_attribute = name + self.operator = ineq or "=" + self.rhs = int(value) + super().__init__('monster', negated=negated, exact=exact, database=database) + self.full_value = fullvalue + + def matches(self, monster): + attr = getattr(monster, self.monster_attribute) + print(attr) + if "<" in self.operator and attr < self.rhs: + return True + if ">" in self.operator and attr > self.rhs: + return True + if "=" in self.operator and attr == self.rhs: + return True + return False + + +STRING_MONSTER_TOKENS = {'asname': ('active_skill', 'name_en')} + + +class MonsterAttributeString(SpecialToken): + RE_MATCH = rf"({regexlist(STRING_MONSTER_TOKENS)}):\"(+)\"" + + def __init__(self, fullvalue, *, negated=False, exact=False, database): + name, ineq, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() + self.monster_attribute = name + self.operator = ineq or "=" + self.rhs = int(value) + super().__init__('monster', negated=negated, exact=exact, database=database) + self.full_value = fullvalue + + def matches(self, monster): + attr = getattr(monster, self.monster_attribute) + print(attr) + if "<" in self.operator and attr < self.rhs: + return True + if ">" in self.operator and attr > self.rhs: + return True + if "=" in self.operator and attr == self.rhs: + return True + return False + SPECIAL_TOKEN_TYPES = [ MultipleAwakeningToken, + MonsterAttributeNumeric, ] diff --git a/dbcog/idtest_mixin.py b/dbcog/idtest_mixin.py index 71715217..37dbe5b9 100644 --- a/dbcog/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -2,11 +2,11 @@ from datetime import datetime from typing import Callable, Coroutine, List, Mapping, Optional -import tsutils from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import box, pagify from tsutils.enums import Server +from tsutils.user_interaction import get_user_confirmation, get_user_reaction from dbcog.models.enum_types import DEFAULT_SERVER from dbcog.models.monster_model import MonsterModel @@ -31,7 +31,7 @@ async def idt_add(self, ctx, mid: int, *, query): query, *reason = query.split("|") query = query.strip() if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to add to the id3 test suite?"): + not await get_user_confirmation(ctx, "Are you sure you want to add to the id3 test suite?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -54,10 +54,10 @@ async def idt_add(self, ctx, mid: int, *, query): actual_mid = -1 if monster is None else monster.monster_id passing = actual_mid == mid - if await tsutils.get_user_reaction(ctx, f"Added {'passing' if passing else 'failing'}" - f" test case `{mid}: {query}`" - f" with ref `{sorted(suite).index(query)}`", - "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): + if await get_user_reaction(ctx, f"Added {'passing' if passing else 'failing'}" + f" test case `{mid}: {query}`" + f" with ref `{sorted(suite).index(query)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): if oldd: suite[query] = oldd else: @@ -92,8 +92,8 @@ async def idtf_add(self, ctx, mid: int, token, server: Optional[Server] = DEFAUL async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): reason = reason.lstrip("| ") if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to add to the" - " fluff/name test suite?"): + not await get_user_confirmation(ctx, "Are you sure you want to add to the" + " fluff/name test suite?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -105,11 +105,11 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): old = None if any(t['id'] == mid and t['token'] == token for t in suite): old = [t for t in suite if t['id'] == mid and t['token'] == token][0] - if not await tsutils.get_user_confirmation(ctx, f"Are you sure you want to change" - f" the type of test case #{suite.index(old)}" - f" `{mid}: {token}` from " - f" **{'fluff' if fluffy else 'name'}** to" - f" **{'name' if fluffy else 'fluff'}**?"): + if not await get_user_confirmation(ctx, f"Are you sure you want to change" + f" the type of test case #{suite.index(old)}" + f" `{mid}: {token}` from " + f" **{'fluff' if fluffy else 'name'}** to" + f" **{'name' if fluffy else 'fluff'}**?"): await ctx.react_quietly("\N{CROSS MARK}") return suite.remove(old) @@ -131,10 +131,10 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): name = mid in [m.monster_id for m in self.indexes[server].name_tokens[token]] passing = (fluffy and fluff) or (not fluffy and name) - if await tsutils.get_user_reaction(ctx, f"Added {'passing' if passing else 'failing'}" - f" {'fluff' if fluffy else 'name'} " - f"case `{mid}: {token}` with ref `{suite.index(case)}`", - "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): + if await get_user_reaction(ctx, f"Added {'passing' if passing else 'failing'}" + f" {'fluff' if fluffy else 'name'} " + f"case `{mid}: {token}` with ref `{suite.index(case)}`", + "\N{LEFTWARDS ARROW WITH HOOK}", timeout=5): suite.remove(case) if old: suite.append(old) @@ -151,7 +151,7 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): async def idt_import(self, ctx, *, queries): """Import id3 tests""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): + not await get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -175,7 +175,7 @@ async def idtf_import(self, ctx, *, queries): async def norf_import(self, ctx, queries): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): + not await get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -197,7 +197,7 @@ async def norf_import(self, ctx, queries): async def idt_remove(self, ctx, *, item): """Remove an id3 test""" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): + not await get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -227,7 +227,7 @@ async def idtf_remove(self, ctx, *, item: int): async def norf_remove(self, ctx, item): if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): + not await get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') @@ -248,7 +248,7 @@ async def idt_setreason(self, ctx, number: int, *, reason): if reason == '""': reason = "" if await self.config.user(ctx.author).lastaction() != 'id3' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): + not await get_user_confirmation(ctx, "Are you sure you want to edit **query**?"): return await self.config.user(ctx.author).lastaction.set('id3') @@ -275,7 +275,7 @@ async def norf_setreason(self, ctx, number, reason): if reason == '""' or reason == "''": reason = "" if await self.config.user(ctx.author).lastaction() != 'name' and \ - not await tsutils.get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): + not await get_user_confirmation(ctx, "Are you sure you want to edit **name/fluff**?"): return await self.config.user(ctx.author).lastaction.set('name') diff --git a/dbcog/info.json b/dbcog/info.json index aacef102..6e8332e3 100644 --- a/dbcog/info.json +++ b/dbcog/info.json @@ -19,7 +19,7 @@ "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" }, "requirements": [ - "tsutils>=4.0.0", + "tsutils>=5.0.0", "pytz", "romkan", "networkx" diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index 2738361c..a25573f1 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -1,4 +1,5 @@ from tsutils.enums import Server +from tsutils.formatting import contains_ja, rmdiacritics from .base_model import BaseModel from .enum_types import Attribute @@ -7,7 +8,6 @@ from .enum_types import enum_or_none from .active_skill_model import ActiveSkillModel from .leader_skill_model import LeaderSkillModel -import tsutils import re from collections import defaultdict import romkan @@ -41,7 +41,7 @@ def __init__(self, **m): self.roma_subname = self.make_roma_subname(self.name_ja) else: # Remove annoying stuff from NA names, like Jörmungandr - self.name_en = tsutils.rmdiacritics(self.name_en) + self.name_en = rmdiacritics(self.name_en) self.name_en_override = None # If the NA and JP names are the same, chances are the card isn't released in both reigions, and @@ -165,7 +165,7 @@ def make_roma_subname(name_ja): adjusted_subname = '' for part in subname.split('・'): roma_part = romkan.to_roma(part) - if part != roma_part and not tsutils.contains_ja(roma_part): + if part != roma_part and not contains_ja(roma_part): adjusted_subname += ' ' + roma_part.strip('-') return adjusted_subname.strip() diff --git a/dbcog/models/monster_stats.py b/dbcog/models/monster_stats.py index 1bc91e47..a6e7624e 100644 --- a/dbcog/models/monster_stats.py +++ b/dbcog/models/monster_stats.py @@ -1,6 +1,9 @@ -from typing import Literal +from typing import Literal, TYPE_CHECKING from dbcog.models.enum_types import AwokenSkills +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + StatType = Literal['hp', 'atk', 'rcv'] @@ -47,7 +50,8 @@ class MonsterStats: LV_120_MULT_DICT = {'hp': 10, 'atk': 5, 'rcv': 5} def stat(self, monster_model, key: StatType, lv, *, plus=99, inherit=False, is_plus_297=True, - stat_latents: MonsterStatModifierInput = None, multiplayer=False, inherited_monster: "MonsterModel" = None, inherited_monster_lvl=99): + stat_latents: MonsterStatModifierInput = None, multiplayer=False, inherited_monster: "MonsterModel" = None, + inherited_monster_lvl=99): # TODO: deal with atk-, rcv-, and hp- awakenings # PAD rounds stats from the start before any calculations s_val = round(self.base_stat(key, lv, monster_model)) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index ce723ada..9fe1012a 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -6,9 +6,10 @@ from typing import Dict, List import aiohttp -import tsutils from redbot.core.utils import AsyncIter from tsutils.enums import Server +from tsutils.formatting import contains_ja +from tsutils.helper_classes import aobject from .errors import InvalidGraphState from .models.monster_model import MonsterModel @@ -28,7 +29,7 @@ logger = logging.getLogger('red.pad-cogs.dbcog.monster_index') -class MonsterIndex(tsutils.aobject): +class MonsterIndex(aobject): async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.graph = graph self.server = server @@ -206,7 +207,7 @@ async def _build_monster_index(self, monsters): for t in self.monster_id_to_nametokens[me.monster_id]: if t in nametokens: self.add_name_token(self.name_tokens, t, m) - if me.is_equip or tsutils.contains_ja(me.name_en): + if me.is_equip or contains_ja(me.name_en): continue if last_token != me.name_en.split(',')[-1].strip(): autotoken = False @@ -303,7 +304,7 @@ def _get_important_tokens(cls, oname, treenames=None): if treenames is None: treenames = set() - if tsutils.contains_ja(oname): + if contains_ja(oname): return list(treenames) name = oname.split(", ") @@ -319,7 +320,7 @@ def _get_important_tokens(cls, oname, treenames=None): return cls._name_to_tokens(min(n1, n2, key=token_count)) async def get_modifiers(self, monster: MonsterModel): - modifiers = self.manual_prefixes[monster.monster_id].copy() + modifiers = self.manual_prefixes[monster.monster_id].union({'monster'}) basemon = self.graph.get_base_monster(monster) diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index 63c24f07..d801f654 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -7,7 +7,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView from discordmenu.embed.view_state import ViewState -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skills_pb2 import MonsterBehavior diff --git a/dungeoncog/view/simple.py b/dungeoncog/view/simple.py index 5da677cd..026c1315 100644 --- a/dungeoncog/view/simple.py +++ b/dungeoncog/view/simple.py @@ -1,10 +1,8 @@ -from typing import List - from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.view import EmbedView from discordmenu.embed.view_state import ViewState -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state class SimpleViewState(ViewState): diff --git a/feedback/feedback.py b/feedback/feedback.py index 3a989cb6..7f07fb36 100644 --- a/feedback/feedback.py +++ b/feedback/feedback.py @@ -3,7 +3,7 @@ import discord from redbot.core import checks, commands, Config from redbot.core.utils.chat_formatting import inline -from tsutils import CogSettings +from tsutils.cog_settings import CogSettings class Feedback(commands.Cog): diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 740d91e3..9f8a0c4d 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -1,13 +1,7 @@ -import asyncio -import os import re -import urllib.parse from io import BytesIO -from redbot.core import checks, commands, Config - -import discord -import tsutils +from redbot.core import Config, checks, commands class MonIdListener(commands.Cog): diff --git a/padboard/padboard.py b/padboard/padboard.py index dc79094a..c6455615 100644 --- a/padboard/padboard.py +++ b/padboard/padboard.py @@ -2,13 +2,15 @@ import cv2 import discord import numpy as np +from tsutils.formatting import extract_image_url + from .padvision import NeuralClassifierBoardExtractor from io import BytesIO from collections import defaultdict from collections import deque from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import inline -from tsutils import extract_image_url + DAWNGLARE_BOARD_TEMPLATE = "https://pad.dawnglare.com/?patt={}" CNINJA_BOARD_TEMPLATE = "https://candyninja001.github.io/Puzzled/?patt={}" diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 98d7ce52..150b744a 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -8,15 +8,12 @@ import aiohttp import discord -import tsutils -from PIL import Image -from PIL import ImageChops -from PIL import ImageDraw -from PIL import ImageFont +from PIL import Image, ImageChops, ImageDraw, ImageFont from ply import lex from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline -from tsutils import CogSettings +from tsutils.cog_settings import CogSettings +from tsutils.tsubaki import CLOUDFRONT_URL HELP_MSG = """ ^buildimg @@ -153,7 +150,7 @@ AWK_STAR = 'star' DELAY_BUFFER = 'delay_buffer' REMOTE_ASSET_URL = 'https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/padbuildimg/assets/' -REMOTE_AWK_URL = tsutils.CLOUDFRONT_URL + '/media/awakenings/{0:03d}.png' +REMOTE_AWK_URL = CLOUDFRONT_URL + '/media/awakenings/{0:03d}.png' # REMOTE_LAT_URL = 'https://pad.protic.site/wp-content/uploads/pad-latents/' @@ -170,7 +167,7 @@ class PadBuildImgSettings(CogSettings): def make_default_build_img_params(self): build_img_params = DictWithAttributeAccess({ 'ASSETS_DIR': './assets/', - 'PORTRAIT_DIR': tsutils.CLOUDFRONT_URL + '/media/icons/{monster_id:05d}.png', + 'PORTRAIT_DIR': CLOUDFRONT_URL + '/media/icons/{monster_id:05d}.png', # 'OUTPUT_DIR': './data/padbuildimg/output/', 'PORTRAIT_WIDTH': 100, 'PADDING': 10, diff --git a/padbuilds/padbuilds.py b/padbuilds/padbuilds.py index d7ecccc9..4c3f97f6 100644 --- a/padbuilds/padbuilds.py +++ b/padbuilds/padbuilds.py @@ -4,7 +4,7 @@ from redbot.core import checks from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify -from tsutils import CogSettings +from tsutils.cog_settings import CogSettings class PadBuilds(commands.Cog): @@ -160,7 +160,7 @@ async def on_message(self, message): await message.channel.send(cmd) def format_cc(self, command, message): - results = re.findall(r"\{([^}]+)\}", command) + results = re.findall(r"{([^}]+)}", command) for result in results: param = self.transform_parameter(result, message) command = command.replace("{" + result + "}", param) diff --git a/padevents/padevents.py b/padevents/padevents.py index 5203c197..74b195e5 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -14,8 +14,14 @@ import pytz from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import box, humanize_timedelta, pagify -from tsutils import CogSettings, DummyObject, NO_EMOJI, YES_EMOJI, get_user_confirmation, is_donor, \ - normalize_server_name, rmdiacritics, send_cancellation_message, send_confirmation_message +from tsutils.cog_settings import CogSettings +from tsutils.cogs.donations import is_donor +from tsutils.emoji import NO_EMOJI, YES_EMOJI +from tsutils.formatting import normalize_server_name, rmdiacritics +from tsutils.helper_classes import DummyObject +from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message + +normalize_server_name, rmdiacritics, send_cancellation_message, send_confirmation_message from tsutils.enums import Server, StarterGroup if TYPE_CHECKING: diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index cb5e620d..b128f3fd 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -11,12 +11,19 @@ import discord import prettytable import pytz -import tsutils + from discord import Color from redbot.core import checks, data_manager, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta, bold -from tsutils import CogSettings, clean_global_mentions, get_user_confirmation, replace_emoji_names_with_code, safe_read_json, \ - auth_check, get_user_reaction +from tsutils.cog_settings import CogSettings +from tsutils.cogs.globaladmin import auth_check +from tsutils.emoji import fix_emojis_for_server, replace_emoji_names_with_code +from tsutils.formatting import clean_global_mentions, strip_right_multiline +from tsutils.json_utils import safe_read_json +from tsutils.tsubaki import CLOUDFRONT_URL +from tsutils.user_interaction import get_user_confirmation, get_user_reaction + +auth_check, get_user_reaction from padglobal.menu.closable_embed import ClosableEmbedMenu from padglobal.menu.menu_map import padglobal_menu_map @@ -46,7 +53,7 @@ def _data_file(file_name: str) -> str: BLACKLISTED_CHARACTERS = '^[]*`~_' -PORTRAIT_TEMPLATE = tsutils.CLOUDFRONT_URL + '/media/portraits/{0:05d}.png' +PORTRAIT_TEMPLATE = CLOUDFRONT_URL + '/media/portraits/{0:05d}.png' DISABLED_MSG = 'PAD Global info disabled on this server' @@ -791,7 +798,7 @@ async def which_to_text(self): for grp in sorted(monsters.keys()): tbl.add_row([grp, ', '.join(sorted(monsters[grp]))]) - tbl_string = tsutils.strip_right_multiline(tbl.get_string()) + tbl_string = strip_right_multiline(tbl.get_string()) return tbl_string async def _do_send_which(self, ctx, to_user: discord.Member, name, definition, timestamp): @@ -986,7 +993,7 @@ async def pwhich_list(self, ctx): for mon in sorted(sorted(monsters), key=lambda x: x[1]): tbl.add_row(mon) - msg = tsutils.strip_right_multiline(tbl.get_string()) + msg = strip_right_multiline(tbl.get_string()) for page in pagify(msg): if destination == channel: @@ -1434,8 +1441,8 @@ def emojify(self, message): if guild.id in emoteservers: continue emojis.extend(guild.emojis) - message = tsutils.replace_emoji_names_with_code(emojis, message) - return tsutils.fix_emojis_for_server(emojis, message) + message = replace_emoji_names_with_code(emojis, message) + return fix_emojis_for_server(emojis, message) def check_simple_tree(monster, db_context): diff --git a/padglobal/view/which.py b/padglobal/view/which.py index a950a329..bba0967f 100644 --- a/padglobal/view/which.py +++ b/padglobal/view/which.py @@ -3,7 +3,7 @@ from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state STAR = '* ' BULLET_EMOJIS = ['db', 'dp', 'dg'] diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 00cd3f3d..695ff402 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -1,16 +1,14 @@ import asyncio -import csv -import discord import json import logging -import pymysql from io import BytesIO -from datetime import datetime -from redbot.core import checks -from redbot.core import commands + +import discord +import pymysql +from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import CogSettings -import tsutils +from tsutils.cog_settings import CogSettings +from tsutils.user_interaction import send_repeated_consecutive_messages logger = logging.getLogger('red.padbot-cogs.padguidedb') @@ -156,10 +154,10 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): if stderr: logger.error("Dungeon Load Error:\n{}\n\n{}".format(stdout.decode(), stderr.decode())) - await tsutils.send_repeated_consecutive_messages(ctx, inline( + await send_repeated_consecutive_messages(ctx, inline( 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) else: - await tsutils.send_repeated_consecutive_messages(ctx, inline( + await send_repeated_consecutive_messages(ctx, inline( 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) self.queue_size -= 1 diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index 99a24317..becd85ac 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -113,7 +113,7 @@ def get_attribute_emoji_by_monster(monster: "MonsterModel"): return get_emoji(emoji) -def get_attribute_emoji_by_enum(type1: Union[Enum, Literal[False]], type2: Optional[Union[Enum, Literal[bool]]] = None): +def get_attribute_emoji_by_enum(type1: Union[Enum, Literal[False]], type2: Optional[Union[Enum, Literal[False]]] = None): attr1 = 'nil' if type1 is False else type1.name.lower() attr2 = 'nil' if type2 is False else type2.name.lower() if type2 else attr1 emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) diff --git a/padinfo/common/external_links.py b/padinfo/common/external_links.py index 18ca7db7..9d933ac4 100644 --- a/padinfo/common/external_links.py +++ b/padinfo/common/external_links.py @@ -1,7 +1,7 @@ import urllib.parse from typing import TYPE_CHECKING -import tsutils +from tsutils.pad import get_pdx_id if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -13,7 +13,7 @@ def puzzledragonx(m: "MonsterModel"): - return INFO_PDX_TEMPLATE.format(tsutils.get_pdx_id(m)) + return INFO_PDX_TEMPLATE.format(get_pdx_id(m)) def youtube_search(m: "MonsterModel"): diff --git a/padinfo/core/padinfo_settings.py b/padinfo/core/padinfo_settings.py index 149535eb..95aeee88 100644 --- a/padinfo/core/padinfo_settings.py +++ b/padinfo/core/padinfo_settings.py @@ -1,4 +1,4 @@ -from tsutils import CogSettings +from tsutils.cog_settings import CogSettings class PadInfoSettings(CogSettings): diff --git a/padinfo/menu/button_info.py b/padinfo/menu/button_info.py index 8f44b513..bf43d6ab 100644 --- a/padinfo/menu/button_info.py +++ b/padinfo/menu/button_info.py @@ -3,7 +3,8 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from tsutils import char_to_emoji +from tsutils.emoji import char_to_emoji + from tsutils.menu.panes import emoji_buttons, MenuPanes from padinfo.view.button_info import ButtonInfoOptions, ButtonInfoToggles, ButtonInfoViewState, ButtonInfoView diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 4f62e8a0..01e421e2 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -2,7 +2,8 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from tsutils import char_to_emoji +from tsutils.emoji import char_to_emoji + from tsutils.enums import ChildMenuType from tsutils.menu.panes import MenuPanes diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 37f55b0c..250bf46b 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -2,7 +2,8 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from tsutils import char_to_emoji +from tsutils.emoji import char_to_emoji + from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 189debfa..b7e48722 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -1,12 +1,12 @@ -from typing import Optional, List +from typing import List, Optional from discord import Message -from discordmenu.embed.menu import EmbedMenu, EmbedControl -from tsutils import char_to_emoji -from tsutils.menu.panes import emoji_buttons, MenuPanes +from discordmenu.embed.menu import EmbedControl, EmbedMenu +from tsutils.emoji import char_to_emoji +from tsutils.menu.panes import MenuPanes, emoji_buttons -from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji -from padinfo.menu.na_diff import NaDiffMenu, NaDiffEmoji +from padinfo.menu.id import IdMenu, IdMenuEmoji +from padinfo.menu.na_diff import NaDiffEmoji, NaDiffMenu from padinfo.view.id import IdView from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 98024634..877f1274 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -2,7 +2,8 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from tsutils import char_to_emoji +from tsutils.emoji import char_to_emoji + from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 14875f73..96868da9 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -2,7 +2,7 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from tsutils import char_to_emoji +from tsutils.emoji import char_to_emoji from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index b527f7c2..6e3d5b53 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -5,23 +5,25 @@ import re import urllib.parse from io import BytesIO -from typing import TYPE_CHECKING, List, Optional, Type +from typing import List, Optional, TYPE_CHECKING, Type import discord -import tsutils from discord import Color from discordmenu.emoji.emoji_cache import emoji_cache -from redbot.core import checks, commands, data_manager, Config +from redbot.core import Config, checks, commands, data_manager from redbot.core.commands import Literal as LiteralConverter -from redbot.core.utils.chat_formatting import box, inline, bold, pagify, text_to_file +from redbot.core.utils.chat_formatting import bold, box, inline, pagify, text_to_file from tabulate import tabulate -from tsutils import char_to_emoji, is_donor, safe_read_json -from tsutils.enums import Server, AltEvoSort, LsMultiplier, CardPlusModifier +from tsutils.cogs.donations import is_donor +from tsutils.emoji import char_to_emoji +from tsutils.enums import AltEvoSort, CardPlusModifier, LsMultiplier, Server +from tsutils.json_utils import safe_read_json from tsutils.query_settings import QuerySettings +from tsutils.user_interaction import get_user_confirmation from padinfo.common.config import BotConfig -from padinfo.common.emoji_map import get_attribute_emoji_by_enum, get_awakening_emoji, get_type_emoji, \ - get_attribute_emoji_by_monster, AWAKENING_ID_TO_EMOJI_NAME_MAP +from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ + get_attribute_emoji_by_monster, get_awakening_emoji, get_type_emoji from padinfo.core.button_info import button_info from padinfo.core.leader_skills import leaderskill_query from padinfo.core.padinfo_settings import settings @@ -33,14 +35,14 @@ from padinfo.menu.leader_skill import LeaderSkillMenu from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu from padinfo.menu.menu_map import padinfo_menu_map -from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes, MonsterListEmoji +from padinfo.menu.monster_list import MonsterListEmoji, MonsterListMenu, MonsterListMenuPanes from padinfo.menu.na_diff import NaDiffMenu, NaDiffMenuPanes -from padinfo.menu.series_scroll import SeriesScrollMenuPanes, SeriesScrollMenu, SeriesScrollEmoji +from padinfo.menu.series_scroll import SeriesScrollEmoji, SeriesScrollMenu, SeriesScrollMenuPanes from padinfo.menu.simple_text import SimpleTextMenu from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.awakening_help import AwakeningHelpView, AwakeningHelpViewProps -from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListSortTypes +from padinfo.view.awakening_list import AwakeningListSortTypes, AwakeningListViewState from padinfo.view.button_info import ButtonInfoToggles, ButtonInfoViewState from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.common import invalid_monster_text @@ -291,7 +293,7 @@ async def send_survey_after(self, ctx, query, result_monster): 'entry.1787446565': str(ctx.author)}) url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) - userres = await tsutils.get_user_confirmation(ctx, "Was this the monster you were looking for?", + userres = await get_user_confirmation(ctx, "Was this the monster you were looking for?", yes_emoji=char_to_emoji('y'), no_emoji=char_to_emoji('n')) if userres is True: await self.config.good.set(await self.config.good() + 1) diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index d454503c..83674333 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedAuthor from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state from padinfo.common.emoji_map import get_awakening_emoji from padinfo.common.external_links import puzzledragonx diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index fd4482d1..f43cad4b 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -3,7 +3,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.view.common import get_awoken_skill_description diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 8cfdc518..1639c917 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -4,7 +4,8 @@ from discordmenu.embed.components import EmbedAuthor, EmbedField, EmbedMain from discordmenu.embed.text import Text, BlockText from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padinfo/view/common.py b/padinfo/view/common.py index f1720e94..f5be7c4e 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import Text, BoldText -from tsutils import char_to_emoji + from tsutils.enums import LsMultiplier from tsutils.query_settings import QuerySettings diff --git a/padinfo/view/components/monster/image.py b/padinfo/view/components/monster/image.py index fd7c2078..abf2ea6d 100644 --- a/padinfo/view/components/monster/image.py +++ b/padinfo/view/components/monster/image.py @@ -1,12 +1,13 @@ from typing import TYPE_CHECKING -import tsutils + from discordmenu.embed.components import EmbedThumbnail +from tsutils.tsubaki import CLOUDFRONT_URL if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel -MEDIA_PATH = tsutils.CLOUDFRONT_URL + '/media/' +MEDIA_PATH = CLOUDFRONT_URL + '/media/' ICON_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 9c340320..341f279d 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -3,7 +3,8 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 98eef5ca..b49baa72 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -4,8 +4,9 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state + from tsutils.enums import Server, CardPlusModifier +from tsutils.menu.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index 1d95634b..c6a4fb74 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.text import LabeledText from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state from padinfo.common.emoji_map import get_attribute_emoji_by_monster diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 3c34c150..61237c21 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -4,7 +4,8 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index 8cf25203..d93452ab 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -4,7 +4,8 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 5f7dfb55..444e9fc6 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -4,7 +4,8 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index aef36184..6dc9164e 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -4,7 +4,9 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView -from tsutils import char_to_emoji, embed_footer_with_state +from tsutils.emoji import char_to_emoji +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -189,7 +191,7 @@ def _monster_list(monsters): if not len(monsters): return [] return [ - MonsterHeader.short_with_emoji(mon, link=True, prefix=char_to_emoji(i)) + MonsterHeader.short_with_emoji(mon, link=True, prefix=char_to_emoji(str(i))) for i, mon in enumerate(monsters) ] diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index 5fe2ee33..ab3b89a9 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -1,6 +1,5 @@ -from typing import TYPE_CHECKING, List +from typing import List, TYPE_CHECKING -from tsutils.enums import Server from tsutils.query_settings import QuerySettings from padinfo.view.monster_list.monster_list import MonsterListViewState diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 70770855..ea08bc4b 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -6,7 +6,7 @@ from discordmenu.embed.text import LabeledText, Text from discordmenu.embed.view import EmbedView from redbot.core.utils.chat_formatting import box -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 1dd4f80d..57ffe94d 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -3,7 +3,8 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 5d2addd6..395fa907 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -2,7 +2,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage from discordmenu.embed.text import LinkedText, Text from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 85cde690..8d6a99de 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -4,7 +4,9 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView -from tsutils import char_to_emoji, embed_footer_with_state +from tsutils.emoji import char_to_emoji +from tsutils.menu.footers import embed_footer_with_state + from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -257,7 +259,7 @@ def _monster_list(monsters, current_index): MonsterHeader.short_with_emoji( mon, link=SeriesScrollView._is_linked(i, current_index), - prefix=char_to_emoji(i) + prefix=char_to_emoji(str(i)) ) for i, mon in enumerate(monsters) ] diff --git a/padinfo/view/simple_text.py b/padinfo/view/simple_text.py index 6b516ea3..686dc923 100644 --- a/padinfo/view/simple_text.py +++ b/padinfo/view/simple_text.py @@ -2,7 +2,7 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state +from tsutils.menu.footers import embed_footer_with_state from padinfo.common.config import UserConfig from padinfo.view.components.view_state_base import ViewStateBase diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index aed8798c..3ee2516a 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -4,8 +4,9 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText from discordmenu.embed.view import EmbedView -from tsutils import embed_footer_with_state + from tsutils.enums import LsMultiplier +from tsutils.menu.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index f3e5db0a..79d7464d 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,13 +1,17 @@ import asyncio import logging -import tsutils + import discord from io import BytesIO + from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, pagify from typing import TYPE_CHECKING +from tsutils.cog_settings import CogSettings +from tsutils.formatting import contains_ja + logger = logging.getLogger('red.padbot-cogs.padmonitor') if TYPE_CHECKING: from dbcog.database_context import DbContext @@ -69,7 +73,7 @@ def process(existing, new_map, name): msg = 'New monsters added to {}:'.format(name) for m in [new_map[x] for x in delta_set]: msg += '\n\t[{}] {}'.format(m.monster_id, m.unoverridden_name_en) - if tsutils.contains_ja(m.name_en) and m.name_en_override is not None: + if contains_ja(m.name_en) and m.name_en_override is not None: msg += ' ({})'.format(m.name_en_override) return msg else: @@ -126,7 +130,7 @@ async def rmchannel(self, ctx, channel: discord.TextChannel = None): await ctx.tick() -class PadMonitorSettings(tsutils.CogSettings): +class PadMonitorSettings(CogSettings): def make_default_settings(self): config = { 'jp_seen_ids': [], diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py index b515811f..d56971f4 100644 --- a/padsearch/padsearch.py +++ b/padsearch/padsearch.py @@ -6,7 +6,9 @@ from ply import lex from redbot.core import commands from redbot.core.utils.chat_formatting import box, pagify -from tsutils import timeout_after +from tsutils.helper_functions import timeout_after + +from dbcog.database_context import DbContext logger = logging.getLogger('red.padbot-cogs.padsearch') diff --git a/pricecheck/pricecheck.py b/pricecheck/pricecheck.py index 8d0cfd57..2e6474e9 100644 --- a/pricecheck/pricecheck.py +++ b/pricecheck/pricecheck.py @@ -2,7 +2,7 @@ from redbot.core import Config, commands from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import auth_check +from tsutils.cogs.globaladmin import auth_check DISCLAIMER = "**Disclaimer**: This is Lumon's data. Use at your own discretion." diff --git a/profile/profile.py b/profile/profile.py index a9281ecb..7ae3bac2 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -4,7 +4,8 @@ from io import BytesIO from redbot.core import commands from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils import CogSettings, normalize_server_name +from tsutils.cog_settings import CogSettings +from tsutils.formatting import normalize_server_name logger = logging.getLogger('red.padbot-cogs.profile') @@ -12,10 +13,10 @@ SUPPORTED_SERVERS = ["NA", "KR", "JP", "EU"] -def validate_and_clean_id(id): - id = id.replace('-', '').replace(' ', '').replace(',', '').replace('.', '').strip() - if re.match(r'^\d{9}$', id): - return id +def validate_and_clean_id(pad_id): + pad_id = pad_id.replace('-', '').replace(' ', '').replace(',', '').replace('.', '').strip() + if re.match(r'^\d{9}$', pad_id): + return pad_id else: return None @@ -25,8 +26,8 @@ def format_name_line(server, pad_name, pad_id): return "[{}]: '{}' : {} (Group {})".format(server, pad_name, format_id(pad_id), group) -def format_id(id): - return id[0:3] + "," + id[3:6] + "," + id[6:9] +def format_id(pad_id): + return pad_id[0:3] + "," + pad_id[3:6] + "," + pad_id[6:9] def compute_old_group(str_id): @@ -77,7 +78,7 @@ async def idme(self, ctx, server=None): await ctx.send(profile_msg) @commands.command() - async def idto(self, ctx, user: discord.Member, server=None): + async def idto(self, ctx, user: discord.User, server=None): """Whispers your profile to specified user If you do not provide a server, your default is used @@ -93,7 +94,7 @@ async def idto(self, ctx, user: discord.Member, server=None): await ctx.author.send(inline("Sent your profile to " + user.name)) @commands.command() - async def idfor(self, ctx, user: discord.Member, server=None): + async def idfor(self, ctx, user: discord.User, server=None): """Displays the profile of the specified user If you do not provide a server, your default is used. @@ -148,7 +149,7 @@ async def server(self, ctx, server): await ctx.send(inline('Set your default server to: ' + server)) @profile.command(name="id") - async def _id(self, ctx, server, *id): + async def _id(self, ctx, server, *, pad_id): """Sets your ID for a server ID must be 9 digits, can be space/comma/dash delimited. @@ -157,8 +158,7 @@ async def _id(self, ctx, server, *id): if server is None: return None - id = " ".join(id) - clean_id = validate_and_clean_id(id) + clean_id = validate_and_clean_id(pad_id) if clean_id is None: await ctx.send(inline('Your ID looks invalid, expected a 9 digit code, got: {}'.format(id))) return @@ -295,8 +295,8 @@ def getProfile(self, user, server): profile[server] = {} return profile[server] - def setId(self, user, server, id): - self.getProfile(user, server)['id'] = int(id) + def setId(self, user, server, pad_id): + self.getProfile(user, server)['id'] = int(pad_id) self.save_settings() def getId(self, user, server): diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py index 9e1a29ad..c9e063cd 100644 --- a/schoolidol/schoolidol.py +++ b/schoolidol/schoolidol.py @@ -7,7 +7,8 @@ from collections import OrderedDict from redbot.core import checks, commands from redbot.core.utils.chat_formatting import inline -from tsutils import EmojiUpdater, Menu, char_to_emoji +from tsutils.emoji import char_to_emoji +from tsutils.old_menu import EmojiUpdater, Menu logger = logging.getLogger('red.padbot-cogs.schoolidol') From 1aea53fc0a4ded2ecaa540c3ce265984bad26213 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 23 Aug 2021 12:03:53 -0700 Subject: [PATCH 0923/1220] Add blackmetal modifier (#1383) --- dbcog/monster_graph.py | 6 ++++++ dbcog/monster_index.py | 2 ++ dbcog/token_mappings.py | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 67852385..57e38d83 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -584,6 +584,12 @@ def monster_is_vendor_exchange(self, monster: MonsterModel) -> bool: ids = self.get_monster_exchange_mats(monster) return bool(ids) and all(15 in [t.value for t in m.types] for m in ids) + def monster_is_black_medal_exchange(self, monster: MonsterModel) -> bool: + return 5155 in [m.monster_id for m in self.get_monster_exchange_mats(monster)] + + def monster_is_black_medal_exchange_evo(self, monster: MonsterModel) -> bool: + return any(self.monster_is_black_medal_exchange(alt) for alt in self.get_alt_monsters(monster)) + def monster_is_new(self, monster: MonsterModel) -> bool: latest_time = max(am.reg_date for am in self.get_alt_monsters(monster)) return monster.reg_date == latest_time diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 9fe1012a..7861db92 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -458,6 +458,8 @@ def is_story(m, do_transform=True): try: if self.graph.monster_is_vendor_exchange(monster): modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) + if self.graph.monster_is_black_medal_exchange_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.BLACK_MEDAL]) except InvalidGraphState: pass if self.graph.monster_is_mp_evo(monster): diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 18131bad..cd1dd02e 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -191,6 +191,7 @@ class MiscModifiers(Enum): ORBSKIN = 'Grants an orb skin' ANIMATED = 'Animated monster' MEDAL_EXC = 'Exchangable for vendor mats' + BLACK_MEDAL = 'Exchangable for black medals' MISC_MAP = { @@ -211,13 +212,15 @@ class MiscModifiers(Enum): MiscModifiers.NEW: ('new',), MiscModifiers.ORBSKIN: ('orbskin',), MiscModifiers.ANIMATED: ('animated',), - MiscModifiers.MEDAL_EXC: ('medal', 'shop') + MiscModifiers.MEDAL_EXC: ('medal', 'shop'), + MiscModifiers.BLACK_MEDAL: ('blackmetal',), } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { 'super reincarnated', 'mega awoken', 'orb skin', + 'black metal', }} ALL_TOKEN_DICTS = { From a231fad55c98bf9aaec0a7da7238cc5651b8b191 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 23 Aug 2021 17:31:52 -0700 Subject: [PATCH 0924/1220] Add nowshop and permshop (#1384) --- dbcog/models/exchange_model.py | 29 +++++++++++++++++ dbcog/models/monster_model.py | 2 +- dbcog/monster_graph.py | 57 ++++++++++++++++++++++++++++++---- dbcog/monster_index.py | 7 +++++ dbcog/token_mappings.py | 6 ++++ 5 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 dbcog/models/exchange_model.py diff --git a/dbcog/models/exchange_model.py b/dbcog/models/exchange_model.py new file mode 100644 index 00000000..9f3d1121 --- /dev/null +++ b/dbcog/models/exchange_model.py @@ -0,0 +1,29 @@ +import re +from datetime import datetime + +from tsutils.enums import Server + +from .base_model import BaseModel + + +class ExchangeModel(BaseModel): + def __init__(self, **kwargs): + self.exchange_id = kwargs['exchange_id'] + self.trade_id = kwargs['trade_id'] + self.server = Server(('JP', 'NA', 'KR')[kwargs['server_id']]) + self.target_monster_id = kwargs['target_monster_id'] + self.required_monster_ids = [int(i) for i in re.findall(r'\d+', kwargs['required_monster_ids'])] + self.required_count = kwargs['required_count'] + self.start_timestamp = datetime.fromtimestamp(kwargs['start_timestamp']) + self.end_timestamp = datetime.fromtimestamp(kwargs['end_timestamp']) + self.permanent = bool(kwargs['permanent']) + self.menu_idx = kwargs['menu_idx'] + self.order_idx = kwargs['order_idx'] + self.flags = kwargs['flags'] + self.tstamp = datetime.fromtimestamp(kwargs['tstamp']) + + def to_dict(self): + return { + 'from': self.target_monster_id, + 'to': self.required_monster_ids, + } diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index a25573f1..69b47d4e 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -52,7 +52,7 @@ def __init__(self, **m): if not m['name_is_translation']: self.name_en_override = m['name_en_override'] - self.name_en = self.name_en_override or self.name_en + self.name_en = (self.name_en_override or self.name_en).strip() self.type1 = enum_or_none(MonsterType, m['type_1_id']) self.type2 = enum_or_none(MonsterType, m['type_2_id']) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 57e38d83..b8262e56 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -2,7 +2,8 @@ import logging import re from collections import defaultdict -from typing import Optional, List, Set, Dict, Tuple, Any +from datetime import datetime +from typing import Optional, List, Set, Dict, Tuple, Any, Type, TypeVar from networkx import MultiDiGraph from tsutils.enums import Server @@ -14,6 +15,7 @@ from .models.base_model import BaseModel from .models.enum_types import InternalEvoType, DEFAULT_SERVER, SERVERS from .models.evolution_model import EvolutionModel +from .models.exchange_model import ExchangeModel from .models.leader_skill_model import LeaderSkillModel from .models.monster_model import MonsterModel from .models.monster.monster_difference import MonsterDifference @@ -21,6 +23,8 @@ logger = logging.getLogger('red.padbot-cogs.dbcog') +M = TypeVar('M', bound=BaseModel) + MONSTER_QUERY = """SELECT monsters{0}.*, monster_name_overrides.name_en AS name_override, @@ -305,15 +309,18 @@ def build_graph(self, server: Server) -> MultiDiGraph: mat, evo_model.to_id, type="material_of", model=evo_model) already_used_in_this_evo.append(mat) + exchanges = defaultdict(set) for ex in exs: + model = ExchangeModel(**ex) for vendor_id in re.findall(r'\d+', ex.required_monster_ids): if self.debug_monster_ids is not None: - if ex.target_monster_id not in self.debug_monster_ids or int(vendor_id) not in self.debug_monster_ids: + if ex.target_monster_id not in self.debug_monster_ids \ + or int(vendor_id) not in self.debug_monster_ids: continue - graph.add_edge( - ex.target_monster_id, int(vendor_id), type='exchange_from') - graph.add_edge( - int(vendor_id), ex.target_monster_id, type='exchange_for') + exchanges[(int(vendor_id), ex.target_monster_id)].add(model) + for (sell_id, buy_id), models in exchanges.items(): + graph.add_edge(buy_id, sell_id, type='exchange_from', models=models) + graph.add_edge(sell_id, buy_id, type='exchange_for', models=models) return graph @@ -347,6 +354,14 @@ def _get_edge_model(self, monster: MonsterModel, etype) -> Optional[BaseModel]: return None return sorted(possible_results, key=lambda x: x.tstamp)[-1] + def _get_edge_model_set(self, monster: MonsterModel, etype, model_type: Type[M] = BaseModel) -> Set[M]: + possible_results = set() + for atlas in self.graph_dict[monster.server_priority][monster.monster_id].values(): + for edge in atlas.values(): + if edge.get('type') == etype: + possible_results.update(edge['models']) + return {model for model in possible_results} + def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER, do_logging: bool = False) \ -> Optional[MonsterModel]: if monster_id not in self.graph_dict[server].nodes: @@ -576,6 +591,9 @@ def monster_is_exchange_evo(self, monster: MonsterModel) -> bool: def get_monster_exchange_mat_ids(self, monster: MonsterModel) -> Set[int]: return self._get_edges(monster, 'exchange_from') + def get_monster_exchange_models(self, monster: MonsterModel) -> Set[ExchangeModel]: + return self._get_edge_model_set(monster, 'exchange_from') + def get_monster_exchange_mats(self, monster: MonsterModel) -> Set[MonsterModel]: return {self.get_monster(mid, server=monster.server_priority) for mid in self.get_monster_exchange_mat_ids(monster)} @@ -590,6 +608,33 @@ def monster_is_black_medal_exchange(self, monster: MonsterModel) -> bool: def monster_is_black_medal_exchange_evo(self, monster: MonsterModel) -> bool: return any(self.monster_is_black_medal_exchange(alt) for alt in self.get_alt_monsters(monster)) + def monster_is_currently_exchangable(self, monster: MonsterModel) -> bool: + models = self.get_monster_exchange_models(monster) + now = datetime.now() + for model in models: + if model.start_timestamp < now < model.end_timestamp: + return True + return False + + def monster_is_currently_exchangable_evo(self, monster: MonsterModel) -> bool: + return any(self.monster_is_currently_exchangable(alt) for alt in self.get_alt_monsters(monster)) + + def monster_is_permanent_exchange(self, monster: MonsterModel) -> bool: + return any(model.permanent or model.end_timestamp.year > 2030 + for model in self.get_monster_exchange_models(monster)) + + def monster_is_permanent_exchange_evo(self, monster: MonsterModel) -> bool: + return any(self.monster_is_permanent_exchange(alt) for alt in self.get_alt_monsters(monster)) + + def monster_is_temporary_exchange(self, monster: MonsterModel) -> bool: + """Not necessarily a current exchange""" + return any(not (model.permanent and model.end_timestamp.year < 2030) + for model in self.get_monster_exchange_models(monster)) + + def monster_is_temporary_exchange_evo(self, monster: MonsterModel) -> bool: + """Not necessarily a current exchange""" + return any(self.monster_is_temporary_exchange(alt) for alt in self.get_alt_monsters(monster)) + def monster_is_new(self, monster: MonsterModel) -> bool: latest_time = max(am.reg_date for am in self.get_alt_monsters(monster)) return monster.reg_date == latest_time diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 7861db92..9dc4fa76 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -460,6 +460,13 @@ def is_story(m, do_transform=True): modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) if self.graph.monster_is_black_medal_exchange_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.BLACK_MEDAL]) + if self.graph.monster_is_currently_exchangable_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.CURRENT_EXCHANGE]) + if self.graph.monster_is_permanent_exchange_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.PERMANENT_EXCHANGE]) + if self.graph.monster_is_temporary_exchange_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.TEMP_EXCHANGE]) + except InvalidGraphState: pass if self.graph.monster_is_mp_evo(monster): diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index cd1dd02e..67f532f3 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -192,6 +192,9 @@ class MiscModifiers(Enum): ANIMATED = 'Animated monster' MEDAL_EXC = 'Exchangable for vendor mats' BLACK_MEDAL = 'Exchangable for black medals' + CURRENT_EXCHANGE = 'Currently exchangable' + PERMANENT_EXCHANGE = 'Permanently exchangable' + TEMP_EXCHANGE = 'Temporarily exchangable at some point in time' MISC_MAP = { @@ -214,6 +217,9 @@ class MiscModifiers(Enum): MiscModifiers.ANIMATED: ('animated',), MiscModifiers.MEDAL_EXC: ('medal', 'shop'), MiscModifiers.BLACK_MEDAL: ('blackmetal',), + MiscModifiers.CURRENT_EXCHANGE: ('nowshop', 'shopnow'), + MiscModifiers.PERMANENT_EXCHANGE: ('permshop', 'shopperm'), + MiscModifiers.TEMP_EXCHANGE: ('tempshop', 'shoptemp'), } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { From ab19e3c6a4e825761228193ae61021ba6174905a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 23 Aug 2021 20:44:50 -0700 Subject: [PATCH 0925/1220] Fix temp exchange logic (#1385) --- dbcog/monster_graph.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index b8262e56..dec7db58 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -3,7 +3,7 @@ import re from collections import defaultdict from datetime import datetime -from typing import Optional, List, Set, Dict, Tuple, Any, Type, TypeVar +from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar from networkx import MultiDiGraph from tsutils.enums import Server @@ -13,12 +13,12 @@ from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel from .models.base_model import BaseModel -from .models.enum_types import InternalEvoType, DEFAULT_SERVER, SERVERS +from .models.enum_types import DEFAULT_SERVER, InternalEvoType, SERVERS from .models.evolution_model import EvolutionModel from .models.exchange_model import ExchangeModel from .models.leader_skill_model import LeaderSkillModel -from .models.monster_model import MonsterModel from .models.monster.monster_difference import MonsterDifference +from .models.monster_model import MonsterModel from .models.series_model import SeriesModel logger = logging.getLogger('red.padbot-cogs.dbcog') @@ -427,7 +427,7 @@ def get_id_order_key(self, monster: MonsterModel) -> Tuple[Any, ...]: '覚醒' in monster.name_ja or 'awoken' in monster.name_en.lower(), monster.monster_id, ) - + def get_alt_ids(self, monster: MonsterModel) -> List[int]: return self.graph_dict[monster.server_priority].nodes[monster.monster_id]['alt_versions'] @@ -628,8 +628,8 @@ def monster_is_permanent_exchange_evo(self, monster: MonsterModel) -> bool: def monster_is_temporary_exchange(self, monster: MonsterModel) -> bool: """Not necessarily a current exchange""" - return any(not (model.permanent and model.end_timestamp.year < 2030) - for model in self.get_monster_exchange_models(monster)) + return not all(model.permanent and model.end_timestamp.year > 2030 + for model in self.get_monster_exchange_models(monster)) def monster_is_temporary_exchange_evo(self, monster: MonsterModel) -> bool: """Not necessarily a current exchange""" From bf1a9e05cf331bedc3967942e01e9e9b8a9dd779 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 23 Aug 2021 21:50:28 -0700 Subject: [PATCH 0926/1220] Add Server-based shopnow tokens (#1386) --- dbcog/monster_graph.py | 9 +++++---- dbcog/monster_index.py | 10 +++++++--- dbcog/token_mappings.py | 8 ++++++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index dec7db58..54218633 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -608,16 +608,17 @@ def monster_is_black_medal_exchange(self, monster: MonsterModel) -> bool: def monster_is_black_medal_exchange_evo(self, monster: MonsterModel) -> bool: return any(self.monster_is_black_medal_exchange(alt) for alt in self.get_alt_monsters(monster)) - def monster_is_currently_exchangable(self, monster: MonsterModel) -> bool: + def monster_is_currently_exchangable(self, monster: MonsterModel, server: Optional[Server] = None) -> bool: models = self.get_monster_exchange_models(monster) now = datetime.now() for model in models: - if model.start_timestamp < now < model.end_timestamp: + if model.start_timestamp < now < model.end_timestamp \ + and (server is None or server == model.server): return True return False - def monster_is_currently_exchangable_evo(self, monster: MonsterModel) -> bool: - return any(self.monster_is_currently_exchangable(alt) for alt in self.get_alt_monsters(monster)) + def monster_is_currently_exchangable_evo(self, monster: MonsterModel, server: Optional[Server] = None) -> bool: + return any(self.monster_is_currently_exchangable(alt, server) for alt in self.get_alt_monsters(monster)) def monster_is_permanent_exchange(self, monster: MonsterModel) -> bool: return any(model.permanent or model.end_timestamp.year > 2030 diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 9dc4fa76..43c07b61 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -460,11 +460,15 @@ def is_story(m, do_transform=True): modifiers.update(MISC_MAP[MiscModifiers.MEDAL_EXC]) if self.graph.monster_is_black_medal_exchange_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.BLACK_MEDAL]) - if self.graph.monster_is_currently_exchangable_evo(monster): - modifiers.update(MISC_MAP[MiscModifiers.CURRENT_EXCHANGE]) + if self.graph.monster_is_currently_exchangable_evo(monster, Server.JP): + modifiers.update(MISC_MAP[MiscModifiers.CURRENT_EXCHANGE_JP]) + if self.graph.monster_is_currently_exchangable_evo(monster, Server.NA): + modifiers.update(MISC_MAP[MiscModifiers.CURRENT_EXCHANGE_NA]) + if self.graph.monster_is_currently_exchangable_evo(monster, Server.KR): + modifiers.update(MISC_MAP[MiscModifiers.CURRENT_EXCHANGE_KR]) if self.graph.monster_is_permanent_exchange_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.PERMANENT_EXCHANGE]) - if self.graph.monster_is_temporary_exchange_evo(monster): + else: modifiers.update(MISC_MAP[MiscModifiers.TEMP_EXCHANGE]) except InvalidGraphState: diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 67f532f3..199d75d3 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -192,7 +192,9 @@ class MiscModifiers(Enum): ANIMATED = 'Animated monster' MEDAL_EXC = 'Exchangable for vendor mats' BLACK_MEDAL = 'Exchangable for black medals' - CURRENT_EXCHANGE = 'Currently exchangable' + CURRENT_EXCHANGE_JP = 'Currently exchangable in JP' + CURRENT_EXCHANGE_NA = 'Currently exchangable in NA' + CURRENT_EXCHANGE_KR = 'Currently exchangable in KR' PERMANENT_EXCHANGE = 'Permanently exchangable' TEMP_EXCHANGE = 'Temporarily exchangable at some point in time' @@ -217,7 +219,9 @@ class MiscModifiers(Enum): MiscModifiers.ANIMATED: ('animated',), MiscModifiers.MEDAL_EXC: ('medal', 'shop'), MiscModifiers.BLACK_MEDAL: ('blackmetal',), - MiscModifiers.CURRENT_EXCHANGE: ('nowshop', 'shopnow'), + MiscModifiers.CURRENT_EXCHANGE_JP: ('nowshopjp', 'shopnowjp'), + MiscModifiers.CURRENT_EXCHANGE_NA: ('nowshopna', 'shopnowna'), + MiscModifiers.CURRENT_EXCHANGE_KR: ('nowshopkr', 'shopnowkr'), MiscModifiers.PERMANENT_EXCHANGE: ('permshop', 'shopperm'), MiscModifiers.TEMP_EXCHANGE: ('tempshop', 'shoptemp'), } From e60ab1d7e96dfae5ab8efe9a4e53225cbdf24c53 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 26 Aug 2021 21:26:21 -0500 Subject: [PATCH 0927/1220] Use custom 1p & 2p emojis for buttoninfo (#1389) --- padinfo/menu/button_info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/menu/button_info.py b/padinfo/menu/button_info.py index bf43d6ab..792c5eb8 100644 --- a/padinfo/menu/button_info.py +++ b/padinfo/menu/button_info.py @@ -13,8 +13,8 @@ class ButtonInfoEmoji: delete = '\N{CROSS MARK}' home = emoji_buttons['home'] - coop = '\N{BUSTS IN SILHOUETTE}' - solo = '\N{BUST IN SILHOUETTE}' + coop = ('coop_2p', '\N{BUSTS IN SILHOUETTE}') + solo = ('solo_1p', '\N{BUST IN SILHOUETTE}') desktop = '\N{DESKTOP COMPUTER}' mobile = '\N{MOBILE PHONE}' limit_break = ('lv110', char_to_emoji('1')) From 2512ad38b18f3686c8361f03f6f74850a08750cd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 26 Aug 2021 21:31:05 -0500 Subject: [PATCH 0928/1220] Use custom plus/minus emojis for dungeon floors (#1390) --- dungeoncog/menu/dungeon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dungeoncog/menu/dungeon.py b/dungeoncog/menu/dungeon.py index 3edc49a0..3e899518 100644 --- a/dungeoncog/menu/dungeon.py +++ b/dungeoncog/menu/dungeon.py @@ -102,8 +102,8 @@ class DungeonEmoji: verbose = '\N{SCROLL}' previous_monster = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' next_monster = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' - previous_floor = '\N{DOWNWARDS BLACK ARROW}' - next_floor = '\N{UPWARDS BLACK ARROW}' + previous_floor = ('minus', '\N{DOWNWARDS BLACK ARROW}') + next_floor = ('plus', '\N{UPWARDS BLACK ARROW}') next_page = '\N{OPEN BOOK}' From 65f5181dd088ddaf757dced70fd0470821c24f0a Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 26 Aug 2021 22:49:17 -0700 Subject: [PATCH 0929/1220] Add new monster attribute moddedfiers (#1388) * Add new monster attribute moddedfiers * Tuples of Tuples * Add comment * Update find_monster_tokens.py * Requested Changes * Search don't match * Use regex module * c_attr --- dbcog/find_monster.py | 2 + dbcog/find_monster_tokens.py | 103 ++++++++++++++++++++++++----------- dbcog/info.json | 1 + dbcog/token_mappings.py | 39 ++++++++++++- 4 files changed, 112 insertions(+), 33 deletions(-) diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index 0674dbb2..ed6812bf 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -305,7 +305,9 @@ async def find_monster_debug(self, query: str) -> \ query = rmdiacritics(query).lower().replace(",", "") query = re.sub(r'(\s|^)\'(\S+)\'(\s|$)', r'\1"\2"\3', query) # Replace ' with " around tokens + query = re.sub(r':=?r?([\'"])[^\1]+\1', lambda m: m.group(0).replace(' ', '\0'), query) # Keep modded spaces tokenized_query = self._process_settings(query).split() + tokenized_query = [token.replace('\0', ' ') for token in tokenized_query] mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) best_monster, matches_dict, valid_monsters = max( diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index 8636b9c2..bdedead7 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -1,8 +1,14 @@ -import re +from fnmatch import fnmatch + +import regex as re from dbcog.models.enum_types import AwokenSkills from dbcog.models.monster_model import MonsterModel -from dbcog.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, PLUS_AWOKENSKILL_MAP +from dbcog.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, BOOL_MONSTER_ATTRIBUTE_ALIASES, \ + BOOL_MONSTER_ATTRIBUTE_NAMES, \ + NUMERIC_MONSTER_ATTRIBUTE_ALIASES, \ + NUMERIC_MONSTER_ATTRIBUTE_NAMES, \ + PLUS_AWOKENSKILL_MAP, STRING_MONSTER_ATTRIBUTE_ALIASES, STRING_MONSTER_ATTRIBUTE_NAMES def regexlist(tokens): @@ -85,59 +91,92 @@ def matches(self, monster): return False -NUMERICAL_MONSTER_TOKENS = ['sell_mp'] - - class MonsterAttributeNumeric(SpecialToken): - RE_MATCH = rf"({regexlist(NUMERICAL_MONSTER_TOKENS)}):([<>=]=?)?(\d+)" + RE_MATCH = rf"({regexlist(NUMERIC_MONSTER_ATTRIBUTE_NAMES)}):([<>=])?(\d+)([kmb])?" def __init__(self, fullvalue, *, negated=False, exact=False, database): - name, ineq, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() - self.monster_attribute = name + c_attr, ineq, value, mult = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() + self.monster_class_attributes = {ats for ats, aliases in NUMERIC_MONSTER_ATTRIBUTE_ALIASES.items() + if c_attr in aliases}.pop() self.operator = ineq or "=" - self.rhs = int(value) + self.rhs = int(value) * (1e9 if mult == 'b' else 1e6 if mult == 'm' else 1e3 if mult == 'k' else 1) super().__init__('monster', negated=negated, exact=exact, database=database) self.full_value = fullvalue def matches(self, monster): - attr = getattr(monster, self.monster_attribute) - print(attr) - if "<" in self.operator and attr < self.rhs: - return True - if ">" in self.operator and attr > self.rhs: - return True - if "=" in self.operator and attr == self.rhs: - return True + for class_attrs in self.monster_class_attributes: + val: MonsterModel = monster + for class_attr in class_attrs: + val: int = getattr(val, class_attr, None) + if val is None: + continue + # Test each character of the equality operator because I don't want to use the eval function here + if "<" in self.operator and val < self.rhs: + return True + if ">" in self.operator and val > self.rhs: + return True + if "=" in self.operator and val == self.rhs: + return True return False -STRING_MONSTER_TOKENS = {'asname': ('active_skill', 'name_en')} +class MonsterAttributeString(SpecialToken): + RE_MATCH = rf"({regexlist(STRING_MONSTER_ATTRIBUTE_NAMES)}):([=rg]?)([\"']?)(.+)\3" + def __init__(self, fullvalue, *, negated=False, exact=False, database): + c_attr, match, _, string = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() + self.monster_class_attributes = {ats for ats, aliases in STRING_MONSTER_ATTRIBUTE_ALIASES.items() + if c_attr in aliases}.pop() + self.match = match + self.string = string + super().__init__('monster', negated=negated, exact=exact, database=database) + self.full_value = fullvalue -class MonsterAttributeString(SpecialToken): - RE_MATCH = rf"({regexlist(STRING_MONSTER_TOKENS)}):\"(+)\"" + def matches(self, monster): + for class_attrs in self.monster_class_attributes: + val: MonsterModel = monster + for class_attr in class_attrs: + val: str = getattr(val, class_attr, None) + if val is None: + continue + val: str = val.lower() + if self.match == "=" and val == self.string: # Exact match + return True + elif self.match == "r" and bool(re.search(self.string, val)): # Regex match + return True + elif self.match == "g" and fnmatch(val, '*' + self.string + '*'): # Glob match + return True + elif self.string in val: + return True + return False + + +class MonsterAttributeBool(SpecialToken): + RE_MATCH = rf"({regexlist(BOOL_MONSTER_ATTRIBUTE_NAMES)}):(.+)" def __init__(self, fullvalue, *, negated=False, exact=False, database): - name, ineq, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() - self.monster_attribute = name - self.operator = ineq or "=" - self.rhs = int(value) + c_attr, raw_bool_value = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() + self.monster_class_attributes = {ats for ats, aliases in BOOL_MONSTER_ATTRIBUTE_ALIASES.items() + if c_attr in aliases}.pop() + self.bool_value = raw_bool_value not in ('0', 'false', 'no') super().__init__('monster', negated=negated, exact=exact, database=database) self.full_value = fullvalue def matches(self, monster): - attr = getattr(monster, self.monster_attribute) - print(attr) - if "<" in self.operator and attr < self.rhs: - return True - if ">" in self.operator and attr > self.rhs: - return True - if "=" in self.operator and attr == self.rhs: - return True + for class_attrs in self.monster_class_attributes: + val: MonsterModel = monster + for class_attr in class_attrs: + val: str = getattr(val, class_attr, None) + if val is None: + continue + if val == self.bool_value: + return True return False SPECIAL_TOKEN_TYPES = [ MultipleAwakeningToken, MonsterAttributeNumeric, + MonsterAttributeString, + MonsterAttributeBool, ] diff --git a/dbcog/info.json b/dbcog/info.json index 6e8332e3..5b498c3d 100644 --- a/dbcog/info.json +++ b/dbcog/info.json @@ -21,6 +21,7 @@ "requirements": [ "tsutils>=5.0.0", "pytz", + "regex", "romkan", "networkx" ], diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 199d75d3..788447fb 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Mapping, Tuple, TypeVar -from .models.enum_types import Attribute, MonsterType, AwokenSkills +from .models.enum_types import Attribute, AwokenSkills, MonsterType K = TypeVar('K') V = TypeVar('V') @@ -294,3 +294,40 @@ class MiscModifiers(Enum): AwokenSkills.EXTMOVEPLUS: PlusAwakeningData(AwokenSkills.EXTMOVE, 2), AwokenSkills.SKILLBOOSTPLUS: PlusAwakeningData(AwokenSkills.SKILLBOOST, 2), } + +NUMERIC_MONSTER_ATTRIBUTE_ALIASES = { + (('monster_no_na',),): ('monsterid', 'monsterno'), + (('base_evo_id',),): ('baseid',), + (('superawakening_count',),): ('sacount',), + (('leader_skill', 'id'),): ('lsid',), + (('active_skill', 'id'),): ('asid',), + (('series', 'id'),): ('sid', 'seriesid'), + (('rarity',),): ('rarity',), + (('buy_mp',),): ('buymp',), + (('sell_mp',),): ('sellmp',), + (('sell_gold',),): ('gold',), + (('cost',),): ('cost',), + (('exp',),): ('exp',), + (('fodder_exp',),): ('fodderexp',), + (('level',),): ('maxlvl', 'maxlevel'), + (('latent_slots',),): ('latentslots',), + (('hp_max',),): ('hp', 'maxhp'), + (('atk_max',),): ('atk', 'maxatk'), + (('rcv_max',),): ('rcv', 'maxrcv'), + (('hp_min',),): ('minhp',), + (('atk_min',),): ('minatk',), + (('rcv_min',),): ('minrcv',), +} +NUMERIC_MONSTER_ATTRIBUTE_NAMES = {*sum(NUMERIC_MONSTER_ATTRIBUTE_ALIASES.values(), ())} + +STRING_MONSTER_ATTRIBUTE_ALIASES = { + (('leader_skill', 'name_en'),): ('lsname',), + (('leader_skill', 'desc_en'),): ('lsdesc',), + (('active_skill', 'name_en'),): ('asname',), + (('active_skill', 'desc_en'),): ('asdesc',), + (('series', 'name_en'),): ('seriesname',), +} +STRING_MONSTER_ATTRIBUTE_NAMES = {*sum(STRING_MONSTER_ATTRIBUTE_ALIASES.values(), ())} + +BOOL_MONSTER_ATTRIBUTE_ALIASES = {} +BOOL_MONSTER_ATTRIBUTE_NAMES = {*sum(BOOL_MONSTER_ATTRIBUTE_ALIASES.values(), ())} From 7f677c9bf934157aa05b261761cf59113d8db61f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 26 Aug 2021 23:10:18 -0700 Subject: [PATCH 0930/1220] Fix Monster Attribute Names moddedfier regex (#1396) --- dbcog/find_monster_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index bdedead7..fe6409b6 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -92,7 +92,7 @@ def matches(self, monster): class MonsterAttributeNumeric(SpecialToken): - RE_MATCH = rf"({regexlist(NUMERIC_MONSTER_ATTRIBUTE_NAMES)}):([<>=])?(\d+)([kmb])?" + RE_MATCH = rf"({regexlist(NUMERIC_MONSTER_ATTRIBUTE_NAMES)}):([<>=]+)?(\d+)([kmb])?" def __init__(self, fullvalue, *, negated=False, exact=False, database): c_attr, ineq, value, mult = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() From 400070ce8a3a15c0b138cfa1ea529a798a0330ec Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 27 Aug 2021 01:11:08 -0700 Subject: [PATCH 0931/1220] Add classattr tests on startup/fir (#1397) --- dbcog/dbcog.py | 15 ++++++++++++++- dbcog/token_mappings.py | 12 +++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 0c498165..027a285d 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -30,7 +30,7 @@ from .models.enum_types import DEFAULT_SERVER, SERVERS from .models.monster_model import MonsterModel from .models.monster_stats import MonsterStatModifierInput, monster_stats -from .monster_index import MonsterIndex +from .monster_index import MONSTER_CLASS_ATTRIBUTES, MonsterIndex logger = logging.getLogger('red.padbot-cogs.dbcog') @@ -151,6 +151,19 @@ async def check_index(self): issues.extend(self.database.graph.issues) for index in self.indexes.values(): issues.extend(index.issues) + for class_attributes in MONSTER_CLASS_ATTRIBUTES: + good = False + for monster in self.database.get_all_monsters(): + val: Any = monster + for ca in class_attributes: + if not hasattr(val, ca): + break + val = getattr(val, ca) + else: + break + else: + issues.append(f"Invalid class attribute: {'.'.join(class_attributes)}") + issues.extend(await self.run_tests()) if issues and self.database.graph.debug_monster_ids is None: diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 788447fb..4b334343 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -299,9 +299,9 @@ class MiscModifiers(Enum): (('monster_no_na',),): ('monsterid', 'monsterno'), (('base_evo_id',),): ('baseid',), (('superawakening_count',),): ('sacount',), - (('leader_skill', 'id'),): ('lsid',), - (('active_skill', 'id'),): ('asid',), - (('series', 'id'),): ('sid', 'seriesid'), + (('leader_skill', 'leader_skill_id'),): ('lsid',), + (('active_skill', 'active_skill_id'),): ('asid',), + (('series', 'series_id'),): ('sid', 'seriesid'), (('rarity',),): ('rarity',), (('buy_mp',),): ('buymp',), (('sell_mp',),): ('sellmp',), @@ -331,3 +331,9 @@ class MiscModifiers(Enum): BOOL_MONSTER_ATTRIBUTE_ALIASES = {} BOOL_MONSTER_ATTRIBUTE_NAMES = {*sum(BOOL_MONSTER_ATTRIBUTE_ALIASES.values(), ())} + +MONSTER_CLASS_ATTRIBUTES = { + *sum(NUMERIC_MONSTER_ATTRIBUTE_ALIASES, ()), + *sum(STRING_MONSTER_ATTRIBUTE_ALIASES, ()), + *sum(BOOL_MONSTER_ATTRIBUTE_NAMES, ()), +} From 41ff51ede66df979ab61dda440f77e804985ff56 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 31 Aug 2021 08:36:32 -0700 Subject: [PATCH 0932/1220] Add I'd as an alias for id (#1399) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6e3d5b53..63dd1a6b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -185,7 +185,7 @@ async def jpname(self, ctx, *, query: str): else: await self.send_id_failure_message(ctx, query) - @commands.command(name="id") + @commands.command(name="id", aliases=['I\'d']) @checks.bot_has_permissions(embed_links=True) async def _id(self, ctx, *, query: str): """Monster info (main tab)""" From e48c23c6bdd1e5c3f0b7e3eb208e368d0a693f29 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 2 Sep 2021 10:42:14 -0700 Subject: [PATCH 0933/1220] Fix CMod to allow 4k character messages (#1401) --- channelmirror/channelmirror.py | 334 +++++++++++++-------------------- 1 file changed, 127 insertions(+), 207 deletions(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index 4feeec6a..14b59a58 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -1,11 +1,10 @@ import logging import re -import time -from datetime import datetime from io import BytesIO -from typing import Optional +from typing import List, Optional import discord +import time from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils.cog_settings import CogSettings @@ -19,9 +18,10 @@ # Three hour cooldown ATTRIBUTION_TIME_SECONDS = 60 * 60 * 3 +MAX_MIRRORED_MESSAGES = 100 -frMESSAGE_LINK = r'(https://discordapp\.com/channels/{0.guild.id}/{0.id}/(\d+)/?)' -MESSAGE_LINK = 'https://discordapp.com/channels/{0.guild.id}/{0.channel.id}/{0.id}' +frMESSAGE_LINK = r'(https://discord\.com/channels/{0.guild.id}/{0.id}/(\d+)/?)' +MESSAGE_LINK = 'https://discord.com/channels/{0.guild.id}/{0.channel.id}/{0.id}' class ChannelMirror(commands.Cog): @@ -33,9 +33,10 @@ def __init__(self, bot, *args, **kwargs): self.settings = ChannelMirrorSettings("channelmirror") self.config = Config.get_conf(self, identifier=3747737700) - self.config.register_channel(multiedit=False, mirroredit_target=None, nodeletion=False) - self.config.init_custom("dest_message", 1) - self.config.register_custom("dest_message", small=False) + self.config.register_channel(last_spoke=0, last_spoke_timestamp=0, mirrored_channels=[], mirrored_messages={}, + multiedit=False, mirroredit_target=None, nodeletion=False) + self.config.init_custom("message", 1) + self.config.register_custom("message", attribute=False) GACOG = self.bot.get_cog("GlobalAdmin") if GACOG: @@ -62,25 +63,26 @@ async def channelmirror(self, ctx): async def add(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): """Set mirroring between two channels.""" if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): - await ctx.send(inline('Check your channel IDs, or maybe the bot is not in those servers')) + await ctx.send('Check your channel IDs, or maybe the bot is not in those servers') return - conf = await get_user_confirmation(ctx, "Set up mirror of <#{}> to mirror to <#{}>?" - .format(source_channel_id, dest_channel_id)) - if not conf: + if not await get_user_confirmation(ctx, f"Set <#{dest_channel_id}> to mirror message from " + f"<#{source_channel_id}>?"): await send_cancellation_message(ctx, "Action cancelled. No action was taken.") return - self.settings.add_mirrored_channel(source_channel_id, dest_channel_id) - await send_confirmation_message(ctx, "Okay, I set up a mirror of <#{}> to <#{}>" - .format(source_channel_id, dest_channel_id)) + async with self.config.channel_from_id(source_channel_id).mirrored_channels() as mirrored_channels: + if dest_channel_id not in mirrored_channels: + mirrored_channels.append(dest_channel_id) + await send_confirmation_message(ctx, f"Mirror from <#{source_channel_id}> to <#{dest_channel_id}> successful") @channelmirror.command(aliases=['rmmirror', 'rm', 'delete']) @checks.is_owner() async def remove(self, ctx, source_channel_id: int, dest_channel_id: int): """Remove mirroring between two channels.""" - success = self.settings.rm_mirrored_channel(source_channel_id, dest_channel_id) - if not success: - await ctx.send("That isn't an existing mirror.") - return + async with self.config.channel_from_id(source_channel_id).mirrored_channels() as mirrored_channels: + if dest_channel_id in mirrored_channels: + mirrored_channels.remove(dest_channel_id) + else: + return await ctx.send("That isn't an existing mirror.") await ctx.tick() @channelmirror.command() @@ -105,7 +107,6 @@ async def nodeletion(self, ctx, channel: Optional[discord.TextChannel], enable: @checks.is_owner() async def config(self, ctx, server_id: int = None): """List mirror config.""" - mirrored_channels = self.settings.mirrored_channels() gchs = set() if server_id: guild = self.bot.get_guild(server_id) @@ -114,14 +115,14 @@ async def config(self, ctx, server_id: int = None): return gchs = {c.id for c in self.bot.get_guild(server_id).channels} msg = 'Mirrored channels\n' - for mc_id, config in mirrored_channels.items(): - if server_id is not None and mc_id not in gchs and not gchs.intersection(config['channels']): + for mc_id, config in (await self.config.all_channels()).items(): + if server_id is not None and mc_id not in gchs and not gchs.intersection(config['mirrored_channels']): continue channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' multiedit = await self.config.channel_from_id(mc_id).multiedit() msg += '\n\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) - for channel_id in config['channels']: + for channel_id in config['mirrored_channels']: if server_id is not None and mc_id not in gchs and not channel_id not in gchs: continue channel = self.bot.get_channel(channel_id) @@ -135,7 +136,6 @@ async def config(self, ctx, server_id: int = None): @checks.is_owner() async def guildconfig(self, ctx, server_id: int): """List mirror config for a guild.""" - mirrored_channels = self.settings.mirrored_channels() gchs = set() if server_id: guild = self.bot.get_guild(server_id) @@ -143,15 +143,16 @@ async def guildconfig(self, ctx, server_id: int): await ctx.send("Invalid server id.") return gchs = {c.id for c in self.bot.get_guild(server_id).channels} + msg = 'From:' - for mc_id, config in mirrored_channels.items(): + for mc_id, config in (await self.config.all_channels()).items(): if mc_id not in gchs: continue channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' multiedit = await self.config.channel_from_id(mc_id).multiedit() msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) - for channel_id in config['channels']: + for channel_id in config['mirrored_channels']: channel = self.bot.get_channel(channel_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' msg += '\n\t{} ({})'.format(channel_id, channel_name) @@ -159,14 +160,14 @@ async def guildconfig(self, ctx, server_id: int): await ctx.send(box(page)) msg = 'To:' - for mc_id, config in mirrored_channels.items(): - if not gchs.intersection(config['channels']): + for mc_id, config in (await self.config.all_channels()).items(): + if not gchs.intersection(config['mirrored_channels']): continue channel = self.bot.get_channel(mc_id) channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' multiedit = await self.config.channel_from_id(mc_id).multiedit() msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) - for channel_id in config['channels']: + for channel_id in config['mirrored_channels']: if channel_id not in gchs: continue channel = self.bot.get_channel(channel_id) @@ -184,26 +185,27 @@ async def countreactions(self, ctx, message: discord.Message): The message can be a link, a message id (if used in the same channel as the message), or the channel_id and message_id separated by a dash (channel_id-message-id)""" - mirrored_messages = self.settings.get_mirrored_messages(message.channel.id, message.id) + mirrored_messages = (await self.config.channel(message.channel).mirrored_messages())[str(message.id)] if not mirrored_messages: await ctx.send("This message isn't mirrored!") reacts = {} for react in message.reactions: reacts[str(react)] = react.count - 1 - for (chid, mid) in mirrored_messages: + for (chid, mids) in mirrored_messages: dest_channel = self.bot.get_channel(chid) if not dest_channel: logger.warning('could not locate channel {}'.format(chid)) continue - dest_message = await dest_channel.fetch_message(mid) - if not dest_message: - logger.warning('could not locate message {}'.format(mid)) - continue - dest_reaction = discord.utils.find(lambda r: r == react, dest_message.reactions) - if not dest_reaction: - logger.warning('could not locate reaction {}'.format(react)) + dest_messages = [await dest_channel.fetch_message(mid) for mid in mids] + if not all(dest_messages): + logger.warning('could not locate messages {}'.format(mids)) continue - reacts[str(react)] += dest_reaction.count - 1 + for dest_message in dest_messages: + dest_reaction = discord.utils.find(lambda r: r == react, dest_message.reactions) + if not dest_reaction: + logger.warning('could not locate reaction {}'.format(react)) + continue + reacts[str(react)] += dest_reaction.count - 1 o = "" maxlen = len(str(max(reacts.values(), key=lambda x: len(str(x))))) for reaction, count in reacts.items(): @@ -239,7 +241,7 @@ async def catchup(self, ctx, channel, from_message, to_message=None): await self.mirror_msg(to_message) await ctx.tick() - @commands.Cog.listener('on_message_without_command') + @commands.Cog.listener('on_message') async def mirror_msg(self, message): author = message.author @@ -250,32 +252,29 @@ async def mirror_msg(self, message): return channel = message.channel - mirrored_channels = self.settings.get_mirrored_channels(channel.id) + mirrored_channels = await self.config.channel(channel).mirrored_channels() + multiedit = await self.config.channel(channel).multiedit() if not mirrored_channels: return - last_spoke, last_spoke_timestamp = self.settings.get_last_spoke(channel.id) - now_time = datetime.utcnow() - last_spoke_time = datetime.utcfromtimestamp( - last_spoke_timestamp) if last_spoke_timestamp else now_time - attribution_required = last_spoke != author.id - attribution_required |= (now_time - last_spoke_time).total_seconds() > ATTRIBUTION_TIME_SECONDS - attribution_required &= not await self.config.channel(message.channel).multiedit() + if multiedit and len(message.content) > 2000: + return await message.channel.send("I can't send this message as it's longer than 2000 characters.") - self.settings.set_last_spoke(channel.id, author.id) + last_spoke = await self.config.channel(channel).last_spoke() + last_spoke_timestamp = await self.config.channel(channel).last_spoke_timestamp() + attribution_required = last_spoke != author.id + attribution_required |= time.time() - last_spoke_timestamp > ATTRIBUTION_TIME_SECONDS + attribution_required &= not multiedit - attachment_bytes = None - filename = None + await self.config.custom('message', message.id).attribute.set(attribution_required) + await self.config.channel(channel).last_spoke.set(author.id) + await self.config.channel(channel).last_spoke_timestamp.set(time.time()) - if message.attachments: - # If we know we're copying a message and that message has an attachment, - # pre download it and reuse it for every upload. - attachment_bytes = [(BytesIO(await attachment.read()), attachment.filename) - for attachment in message.attachments - if hasattr(attachment, 'url') and hasattr(attachment, 'filename')] + attachment_bytes = [(BytesIO(await attachment.read()), attachment.filename) + for attachment in message.attachments] - if await self.config.channel(message.channel).multiedit(): + if multiedit: await message.delete() idmess = await channel.send("Pending...") attachments = message.attachments @@ -295,46 +294,35 @@ async def mirror_msg(self, message): await idmess.edit(content=str(message.id)) + linked_messages = [] + for dest_channel_id in mirrored_channels: dest_channel = self.bot.get_channel(dest_channel_id) if not dest_channel: continue try: - fmessage = await self.mformat(message.content, message.channel, dest_channel) - - small = False - if attribution_required: - msg = self.makeheader(message, author) - if len(fmessage) > 1000: - await dest_channel.send(msg) - else: - fmessage = msg + '\n' + fmessage - small = True - - if attachment_bytes: - try: - [b.seek(0) for b, fn in attachment_bytes] - dest_message = await dest_channel.send( - files=[discord.File(b, fn) for b, fn in attachment_bytes], - content=fmessage) - except discord.HTTPException: - try: - [b.seek(0) for b, fn in attachment_bytes] - dest_message = await dest_channel.send(file=discord.File(*attachment_bytes[0]), - content=fmessage) - for b, fn in attachment_bytes[1:]: - await dest_channel.send(file=discord.File(b, fn)) - except discord.HTTPException: - dest_message = await dest_channel.send(fmessage) - await dest_channel.send("") - elif message.content: - dest_message = await dest_channel.send(fmessage) - else: + if not (attachment_bytes or message.content): logger.warning('Failed to mirror message from {} no action to take'.format(channel.id)) continue - self.settings.add_mirrored_message(channel.id, message.id, dest_channel.id, dest_message.id) - await self.config.custom("dest_message", dest_message.id).small.set(small) + message.content = await self.mformat(message.content, message.channel, dest_channel) + fmessages = await self.split_message(message) + + [b.seek(0) for b, fn in attachment_bytes] + try: + dest_messages = [await dest_channel.send( + files=[discord.File(b, fn) for b, fn in attachment_bytes], + content=fmessage) for fmessage in fmessages] + except discord.HTTPException: + dest_messages = [await dest_channel.send(fmessage) for fmessage in fmessages] + try: + for b, fn in attachment_bytes: + await dest_channel.send(file=discord.File(b, fn)) + except discord.HTTPException: + await dest_channel.send("") + + linked_messages.append((dest_channel.id, [m.id for m in dest_messages])) + except discord.Forbidden: if dest_channel.guild.owner: try: @@ -357,12 +345,15 @@ async def mirror_msg(self, message): logger.exception( 'Failed to mirror message from {} to {}: {}'.format(channel.id, dest_channel_id, str(ex))) - if attachment_bytes: - [b.close() for b, fn in attachment_bytes] + async with self.config.channel(channel).mirrored_messages() as mirrored_messages: + mirrored_messages[str(message.id)] = linked_messages + if len(mirrored_messages) > MAX_MIRRORED_MESSAGES: + mirrored_messages.pop(min(mirrored_messages)) # This works because snowflakes sort by time + [b.close() for b, fn in attachment_bytes] @commands.Cog.listener('on_raw_message_edit') async def mirror_msg_edit(self, payload): - if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): return message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) if 'content' in payload.data: @@ -370,7 +361,7 @@ async def mirror_msg_edit(self, payload): @commands.Cog.listener('on_raw_message_delete') async def mirror_msg_delete(self, payload): - if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): return if not await self.config.channel(self.bot.get_channel(payload.channel_id)).nodeletion(): fmessage = DummyObject(id=payload.message_id, channel=self.bot.get_channel(payload.channel_id)) @@ -378,7 +369,7 @@ async def mirror_msg_delete(self, payload): @commands.Cog.listener('on_raw_reaction_add') async def mirror_reaction_add(self, payload): - if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): return message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): @@ -387,13 +378,27 @@ async def mirror_reaction_add(self, payload): @commands.Cog.listener('on_raw_reaction_remove') async def mirror_reaction_remove(self, payload): - if not self.settings.get_mirrored_messages(payload.channel_id, payload.message_id): + if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): return message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): return await self.mirror_msg_mod(message, delete_message_reaction=payload.emoji) + async def split_message(self, message: discord.Message, fit_in: Optional[int] = None) -> List[str]: + attribute = await self.config.custom('message', message.id).attribute() + content = (self.makeheader(message) if attribute else '') + message.content + if fit_in is None: + return list(pagify(content, delims=['\n\n', '\n'], shorten_by=0, page_length=1750)) + + messages = list(pagify(content, delims=['\n\n', '\n'], shorten_by=0, page_length=2000)) + if len(messages) > fit_in: + messages = messages[:fit_in] + messages[-1] = messages[-1][:1971] + "... *(Continued in original)*" + elif len(messages) < fit_in: + messages += ["[Placeholder - This message used to be longer!]"] * (fit_in - len(messages)) + return messages + async def mirror_msg_mod(self, message, new_message_content: str = None, delete_message_content: bool = False, @@ -403,45 +408,44 @@ async def mirror_msg_mod(self, message, return channel = message.channel - mirrored_messages = self.settings.get_mirrored_messages(channel.id, message.id) - for (dest_channel_id, dest_message_id) in mirrored_messages: + mirrored_messages = (await self.config.channel(channel).mirrored_messages())[str(message.id)] + for (dest_channel_id, dest_message_ids) in mirrored_messages: try: dest_channel = self.bot.get_channel(dest_channel_id) if not dest_channel: logger.warning('could not locate channel to mod') continue - dest_message = await dest_channel.fetch_message(dest_message_id) - if not dest_message: + dest_messages = [await dest_channel.fetch_message(m_id) for m_id in dest_message_ids] + if not all(dest_messages): logger.warning('could not locate message to mod') continue if new_message_content: - fcontent = await self.mformat(new_message_content, channel, dest_message.channel) - if await self.config.custom("dest_message", dest_message.id).small(): - fcontent = self.makeheader(message) + '\n' + fcontent - if len(fcontent) > 4000: - fcontent = fcontent[:3971] + "... *(Continued in original)*" - await dest_message.edit(content=fcontent) + message.content = await self.mformat(new_message_content, channel, dest_channel) + fcontents = await self.split_message(message, fit_in=len(dest_message_ids)) + for dest_message, content in zip(dest_messages, fcontents): + await dest_message.edit(content=content) elif new_message_reaction: try: - await dest_message.add_reaction(new_message_reaction) + await dest_messages[-1].add_reaction(new_message_reaction) except discord.HTTPException: pass elif delete_message_content: - await dest_message.delete() + for dest_message in dest_messages: + await dest_message.delete() elif delete_message_reaction: try: - await dest_message.remove_reaction(delete_message_reaction, dest_message.guild.me) + await dest_messages[-1].remove_reaction(delete_message_reaction, dest_channel.me) except discord.HTTPException: pass except Exception as ex: logger.exception('Failed to mirror message edit from {} to {}:'.format(channel.id, dest_channel_id)) - def makeheader(self, message, author=None): - return 'Posted by **{}** in *{} - #{}*:\n{}'.format(message.author.name if author is None else author.name, - message.guild.name, - message.channel.name, - message.jump_url) + def makeheader(self, message): + return 'Posted by **{}** in *{} - #{}*:\n{}\n'.format(message.author.name, + message.guild.name, + message.channel.name, + message.jump_url) async def mformat(self, text, from_channel, dest_channel): # LINKS @@ -450,16 +454,17 @@ async def mformat(self, text, from_channel, dest_channel): if not from_link: logger.warning('could not locate link to copy') continue - mirrored_messages = self.settings.get_mirrored_messages(from_link.channel.id, from_link.id) - to_link_id = [dmid for dcid, dmid in mirrored_messages if dcid == dest_channel.id] - if not to_link_id: + mirrored_messages = (await self.config.channel(from_channel).mirrored_messages())[str(from_link.id)] + to_link_ids = [dmids for dcid, dmids in mirrored_messages if dcid == dest_channel.id] + if not to_link_ids: logger.warning('could not locate link to mod') continue - to_link_id = to_link_id[0] + to_link_id = to_link_ids[0][0] dest_link = await dest_channel.fetch_message(to_link_id) newlink = MESSAGE_LINK.format(dest_link) text = text.replace(link, newlink) + # ROLES for rtext, rid in re.findall(r'(<@&(\d+)>)', text): target = from_channel.guild.get_role(int(rid)) @@ -472,6 +477,7 @@ async def mformat(self, text, from_channel, dest_channel): else: repl = "<@&{}>".format(dest.id) text = text.replace(rtext, repl) + # MENTIONS for utext, uid in re.findall(r'(<@!(\d+)>)', text): target = from_channel.guild.get_member(int(uid)) @@ -479,6 +485,7 @@ async def mformat(self, text, from_channel, dest_channel): logger.warning('could not locate user to mod') continue text = text.replace(utext, target.name) + # CHANNELS for ctext, cid in re.findall(r'(<#(\d+)>)', text): target = from_channel.guild.get_channel(int(cid)) @@ -486,9 +493,11 @@ async def mformat(self, text, from_channel, dest_channel): logger.warning('could not locate channel to mod') continue text = text.replace(ctext, "\\#" + target.name) + # EVERYONE text = re.sub(r"@everyone\b", "@\u200beveryone", text) text = re.sub(r"@here\b", "@\u200bhere", text) + # EMOJI text = self.emojify(text) return text @@ -554,92 +563,3 @@ def make_default_settings(self): 'max_mirrored_messages': 20, } return config - - def servers(self): - return self.bot_settings['servers'] - - def get_server(self, server_id: int): - servers = self.servers() - if server_id not in servers: - servers[server_id] = {} - return servers[server_id] - - def max_mirrored_messages(self): - return self.bot_settings['max_mirrored_messages'] - - def mirrored_channels(self): - # Mirrored channels looks like: - # : { - # 'last_spoke: '', - # 'channels': [dest_channel_id_1, dest_channel_id2], - # 'messages': { - # : [ - # (dest_channel_id, dest_channel_msg], - # ] - # } - # } - return self.bot_settings['mirrored_channels'] - - def get_mirrored_channels(self, source_channel: str): - return self.mirrored_channels().get(source_channel, {}).get('channels', []) - - def get_last_spoke(self, source_channel: str): - spoke_values = self.mirrored_channels().get(source_channel, {}) - return spoke_values.get('last_spoke', None), spoke_values.get('last_spoke_timestamp', None) - - def set_last_spoke(self, source_channel: str, last_spoke: str): - spoke_values = self.mirrored_channels().get(source_channel) - spoke_values['last_spoke'] = last_spoke - spoke_values['last_spoke_timestamp'] = time.time() - self.save_settings() - - def add_mirrored_channel(self, source_channel: int, dest_channel: int): - channels = self.mirrored_channels() - if source_channel == dest_channel: - raise commands.UserFeedbackCheckFailure('Cannot mirror a channel to itself') - if dest_channel in channels: - raise commands.UserFeedbackCheckFailure('Destination channel is already a source channel') - if source_channel not in channels: - channels[source_channel] = { - 'channels': [], - 'messages': {}, - } - if dest_channel in channels[source_channel]['channels']: - raise commands.UserFeedbackCheckFailure('This mirror already exists') - channels[source_channel]['channels'].append(dest_channel) - self.save_settings() - - def rm_mirrored_channel(self, source_channel: int, dest_channel: int): - channels = self.mirrored_channels() - config = channels.get(source_channel) - if config and dest_channel in config['channels']: - dest_channel_config = config['channels'] - dest_channel_config.remove(dest_channel) - if not dest_channel_config: - channels.pop(source_channel) - self.save_settings() - return True - else: - return False - - def add_mirrored_message(self, source_channel: int, source_message: str, dest_channel: int, dest_message: str): - channel_config = self.mirrored_channels()[source_channel] - messages = channel_config['messages'] - if source_message not in messages: - messages[source_message] = [] - - targets = messages[source_message] - new_entry = [dest_channel, dest_message] - if new_entry not in targets: - targets.append(new_entry) - self.save_settings() - - if len(messages) > self.max_mirrored_messages(): - oldest_msg = min(messages.keys()) - messages.pop(oldest_msg) - self.save_settings() - - def get_mirrored_messages(self, source_channel: int, source_message: str): - """Returns [(channel_id, message_id), ...]""" - channel_config = self.mirrored_channels().get(source_channel, {}) - return channel_config.get('messages', {}).get(source_message, []) From 37513ee777595e55ade99cc0ea5ed1056b620f40 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 2 Sep 2021 23:15:11 -0700 Subject: [PATCH 0934/1220] Add ref to idtest rm (#1403) Closes #1395 --- dbcog/idtest_mixin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dbcog/idtest_mixin.py b/dbcog/idtest_mixin.py index 37dbe5b9..1c5d5fa9 100644 --- a/dbcog/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -210,8 +210,9 @@ async def idt_remove(self, ctx, *, item): await ctx.react_quietly("\N{CROSS MARK}") return res = suite[case]['result'] + ref = suite.index(case) del suite[case] - await ctx.send(f"Removed test case `{case}: {res}` with ref") + await ctx.send(f"Removed test case `{case}: {res}` with ref {ref}") @idt_name.command(name="remove", aliases=["rm", "delete", "del"]) @checks.is_owner() From b40d263e76e0159459f9e6d8ff809c51d086d82c Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 3 Sep 2021 14:12:33 -0700 Subject: [PATCH 0935/1220] Fix AEP set searchstr (#1402) --- channelmirror/channelmirror.py | 8 ++++---- padevents/padevents.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py index 14b59a58..6574f2ff 100644 --- a/channelmirror/channelmirror.py +++ b/channelmirror/channelmirror.py @@ -1,10 +1,10 @@ import logging import re +import time from io import BytesIO from typing import List, Optional import discord -import time from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils.cog_settings import CogSettings @@ -443,9 +443,9 @@ async def mirror_msg_mod(self, message, def makeheader(self, message): return 'Posted by **{}** in *{} - #{}*:\n{}\n'.format(message.author.name, - message.guild.name, - message.channel.name, - message.jump_url) + message.guild.name, + message.channel.name, + message.jump_url) async def mformat(self, text, from_channel, dest_channel): # LINKS diff --git a/padevents/padevents.py b/padevents/padevents.py index 74b195e5..bec3c37b 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,6 +1,5 @@ import asyncio import datetime -import itertools import logging import re from collections import defaultdict @@ -10,20 +9,20 @@ from typing import List, Optional, TYPE_CHECKING, Union import discord +import itertools import prettytable import pytz from redbot.core import Config, checks, commands -from redbot.core.utils.chat_formatting import box, humanize_timedelta, pagify +from redbot.core.utils.chat_formatting import box, humanize_timedelta, inline, pagify from tsutils.cog_settings import CogSettings from tsutils.cogs.donations import is_donor from tsutils.emoji import NO_EMOJI, YES_EMOJI +from tsutils.enums import Server, StarterGroup +from tsutils.errors import ClientInlineTextException from tsutils.formatting import normalize_server_name, rmdiacritics from tsutils.helper_classes import DummyObject from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message -normalize_server_name, rmdiacritics, send_cancellation_message, send_confirmation_message -from tsutils.enums import Server, StarterGroup - if TYPE_CHECKING: from dbcog.models.scheduled_event_model import ScheduledEventModel @@ -437,14 +436,15 @@ async def aepset(self, ctx, key, k, v): async def aepget(self, ctx, key): pingroles = await self.config.guild(ctx.guild).pingroles() if key not in pingroles: - await ctx.send("That key does not exist.") - return + raise ClientInlineTextException(f"Key `{key}` does not exist. Available keys are:" + f" {', '.join(map(inline, pingroles))}") return pingroles[key] async def aep_remove_channel(self, ctx, key, group, f): - if key not in await self.config.guild(ctx.guild).pingroles(): - await ctx.send("That key does not exist.") - return + pingroles = await self.config.guild(ctx.guild).pingroles() + if key not in pingroles: + raise ClientInlineTextException(f"Key `{key}` does not exist. Available keys are:" + f" {', '.join(map(inline, pingroles))}") if not await get_user_confirmation(ctx, "Are you sure you want to remove the {} channel for AEP `{}`?" .format(group, key)): From e51796b82a6fa154564aca5e0907d0f17523050f Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 3 Sep 2021 14:16:21 -0700 Subject: [PATCH 0936/1220] Add collab as a modifier to ghcollab (#1405) --- dbcog/monster_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 43c07b61..fa847f80 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -425,7 +425,7 @@ async def get_modifiers(self, monster: MonsterModel): modifiers.update(MISC_MAP[MiscModifiers.EVENT]) if monster.series.series_type == 'seasonal': modifiers.update(MISC_MAP[MiscModifiers.SEASONAL]) - if monster.series.series_type == 'collab': + if monster.series.series_type in ('collab', 'ghcollab'): modifiers.update(MISC_MAP[MiscModifiers.COLLAB]) # Story From 71eff5564cd6a311f3afd1a7054155d322abdb5a Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 9 Sep 2021 22:56:37 -0700 Subject: [PATCH 0937/1220] Use Async Iterators for the idtest suite (#1409) --- dbcog/idtest_mixin.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/dbcog/idtest_mixin.py b/dbcog/idtest_mixin.py index 1c5d5fa9..eb75159a 100644 --- a/dbcog/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -4,6 +4,7 @@ from redbot.core import Config, checks, commands from redbot.core.bot import Red +from redbot.core.utils import AsyncIter from redbot.core.utils.chat_formatting import box, pagify from tsutils.enums import Server from tsutils.user_interaction import get_user_confirmation, get_user_reaction @@ -398,7 +399,7 @@ async def idt_run(self, ctx): ml = len(max(suite, key=len)) + 2 rcircle = '\N{LARGE RED CIRCLE}' async with ctx.typing(): - for i, qr in enumerate(sorted(suite.items())): + async for i, qr in AsyncIter(enumerate(sorted(suite.items()))): q, r = qr try: monster = await self.find_monster(q) @@ -441,7 +442,10 @@ async def norf_run(self, ctx): o = "" rcircle, ycircle = '\N{LARGE RED CIRCLE}', '\N{LARGE YELLOW CIRCLE}' async with ctx.typing(): - for i, case in enumerate(sorted(suite, key=lambda v: (v['id'], v['token'], v['fluff'], v['server']))): + async for i, case in AsyncIter(enumerate(sorted(suite, key=lambda v: (v['id'], + v['token'], + v['fluff'], + v['server'])))): fluff = case['id'] in [m.monster_id for m in self.indexes[Server(case['server'])].fluff_tokens[case['token']]] name = case['id'] in [m.monster_id for m in @@ -469,8 +473,12 @@ async def idt_runall(self, ctx): qo = "" qc = 0 ml = len(max(qsuite or [''], key=len)) + 2 + + fsuite = await self.config.fluff_suite() + fo = "" + fc = 0 async with ctx.typing(): - for c, q in enumerate(sorted(qsuite)): + async for c, q in AsyncIter(enumerate(sorted(qsuite))): try: monster = await self.find_monster(q) except Exception: @@ -486,11 +494,7 @@ async def idt_runall(self, ctx): else: qc += 1 - fsuite = await self.config.fluff_suite() - fo = "" - fc = 0 - async with ctx.typing(): - for c, v in enumerate(fsuite): + async for c, v in AsyncIter(enumerate(fsuite)): fluff = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].fluff_tokens[v['token']]] name = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].name_tokens[v['token']]] @@ -522,7 +526,7 @@ async def run_tests(self) -> List[str]: qsuite = await self.config.test_suite() ml = len(max(qsuite or [''], key=len)) + 2 - for c, q in enumerate(sorted(qsuite)): + async for c, q in AsyncIter(enumerate(sorted(qsuite))): try: monster = await self.find_monster(q) except Exception: @@ -537,7 +541,7 @@ async def run_tests(self) -> List[str]: f"Ex: {qsuite[q]['result']}, Ac: {mid}{reason}") fsuite = await self.config.fluff_suite() - for c, v in enumerate(fsuite): + async for c, v in AsyncIter(enumerate(fsuite)): fluff = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].fluff_tokens[v['token']]] name = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].name_tokens[v['token']]] From 5ad480393a6a93e513c2c0332e9bec21b99c19c8 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 9 Sep 2021 22:56:54 -0700 Subject: [PATCH 0938/1220] Add Attribute Awakening images (#1408) --- padbuildimg/assets/awk/79.png | Bin 0 -> 1943 bytes padbuildimg/assets/awk/80.png | Bin 0 -> 1289 bytes padbuildimg/assets/awk/80_old.png | Bin 0 -> 2005 bytes padbuildimg/assets/awk/81.png | Bin 0 -> 2010 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 padbuildimg/assets/awk/79.png create mode 100644 padbuildimg/assets/awk/80.png create mode 100644 padbuildimg/assets/awk/80_old.png create mode 100644 padbuildimg/assets/awk/81.png diff --git a/padbuildimg/assets/awk/79.png b/padbuildimg/assets/awk/79.png new file mode 100644 index 0000000000000000000000000000000000000000..c15e36ea5d71098e14ae931eeec37e3473629416 GIT binary patch literal 1943 zcmV;I2Wa?-P) z3s98T6~|fLvUG#&0t)g_U=0$L^4x%sAP}C*o6s0vHHsLgsJzrQxCjUcf;^3RnA#Yl z#zqoj0CA!tASh}&)28WUoH)@mip8xWi{A2jvU6cFtteAthqG62+l zMM7Y_H$1{^5xvbFsSUFyrKB3AFft*TN+ht9nIdVYr-;Rtd0$*TxH?aP`~D6%;{FWw&w7Ts;DCMtigK5V92b8X(sn+J zy52QlUjcA@s2GhGvyoBrERrhRCnVMK=`c2tz{koI|L|1+fzxoz!v+Tl;1B^Eaks&c zY8tN3S0K#AN09nYWrd?d-J|WqnVQsP!OsJFNY^SM*ZurEY`x2XeKGT%-RyIp1Y*jJ8VL;L5)%aOKSiK^};U|#`n z^ru|Z_ANnrsV9<)+$JRxQwfa8Kt}r^-o^qwvD0xn$q7s3mSWD5S}!3bZgCYpm0gR# zz6O9C0$7B!VtTF`mkBGGq%LsA+^DHAHJ8AejNWmRiH$hbMBjRu!VC$N!};t}*_8?Q zH2}PhnhP%@Z7a2Oqw}PdkUs;l>z!a>Wg<4Fx#77xdrJ|M<207XJ{4VQU|#|7=by4r z-Is<`dX|Y)z`cPEG4gkw=Q!taKCk2TT!-t8 z)@Gl|ZZ+6f035!%lmOI7EpWk#qWL)9uEG6Vr_kNrgo>@J&|JM91D8J$a~vCuAAGk6 zWpBJD#)o5^&+B-7XKTY)oa?jC_O3XvuK@Vy?jlt6#ULfu8OPf+xc$#IBt`^5^_&|D zH!MK*>Ok1qN|C!ZLlnC4%~80zIv^|~7%yzjgl|S99GxA-Jjd6q$%M?>2Fa={P@gM9_SfjjA_I3I=L`WR6lEp`F&!h?YmT40a_Ep$2!A|sU0 z37$s<@=q0Rc>xG4dC5j4`!qYPdbs8N`(b9kEmm@zkkLpH<`Xvo< zDD8{{`wD;$@1&sed<1&WR^#I7x8O6=1*q2Q0d$56q=A6~DCECEVPPb`#ave>tucuDQk+fb>S%K@kiWZ1&+0B z631oyF?Q>B-mVAo7@CEw{fOl>! zz@GjPEGu@wvTQFLJG$|K7g1Byy0I4#$4281zlg@iIG@**7ybMq;uzO|CEpQE*Ph28 z2gCFL$y=Q1O9&dj4MhB_3Ph*cLR%7zzRp_Q?Wb4qcP(f+l8=>Z=ZRQcx*eLE7UR3G z-p9S09b)9~JkN2?<9uGn>$wis<5*0(9Ccp@2*0KEv-AL_X2u6=FZ*KOpbE`b{js*0 ze(g}35?+x%F{xjbV@;hKny&Z@pFRBnklL7Z8W}E!sZQo7|I1wLy&XsjdWs4}?PVW0 z&Yr?|?;+fYh=*wY+a0#d4%+THBYSWA;Yq8s&-?zuRrcanG^=&=)lwb=alp@ dY3_K_{{xjX&VdVE;(!1E002ovPDHLkV1hrcuhRek literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/80.png b/padbuildimg/assets/awk/80.png new file mode 100644 index 0000000000000000000000000000000000000000..c21a9fecd7de779a49901bcc6b14cc5b97000f99 GIT binary patch literal 1289 zcmV+k1@`)hP)Vl&|000EgNklGN)lq+&V1NIoZ~YPEo_M@)E>BR%Td&F2uBCi9{wyF|xWafs$(c zfT6_Uj9L^Xe$d23oK5z{7Yb~CQnO~;H?uHg*(X@S^>6;CJh#22;9zT$lc)E2ZqM(3 z&hwmeZ#8ZHGltW)s2(04)x*f3UHT0J4#ku)stxr0z?j}Ut&Qrv z7@6usVfCXc0xF?(UAqnN8_q}Ie`cd`tpxC1at323L9W1ieryyl9jw$=KVT79<>U9^ zTG>-#M+&k{0_d9p+J_nv@bFmEZNZsNrn@Nj9>hwXFyItHFb%9fBr+D5l%4_MFAUj-7B0s0x$Ax7N-P{@3#wVc-LkD#|7lX1r%;Y z{<_t2FU5wUZ>q?mjE#ff&@yxb$k@0??1)SAPQD-Mru+kpQ392|dat$-OV zjRN9xIp85=^j5279^TCZXNEU)?Z(-&$1rz2jM1M?priFg^mf67P_W%S9 zqaX@k1cR|p@#Wk(oSM5}itL5;cVXgH2fnGu#es+KHmy~)*yX?xPOcWw+YC7Om8oXL zH%|K(z^A7Jl7XYXazym+40tdaLxrcq05N**_u5tQEnyEJ*bHw=z-wD}NV+>(4@=bP z;6R^j!Hs_hQs9$lAG(Pe&-aD^wa={Y3XjczlM|18QFw9M$o~(1)b|T)HXD4ta%n0~ zSGEbv$QOa%B&ApiibhgqsUVVp5)Z}cR1WQQqY8|J#K8u%8~{}pM;YWPkU12DlKXZ z1YR=QE7Qmb4i@LhVM;yic89~L|K|EX@cG*__`Piw)9=pWe8Zo{cF-sNC&9f4MJtUx zQSjt3x&C?L&G0Me9Jwg<&jS|-%$VR5&{)z8ueVHsiNh-q(Y@UB1iZ952kDIIX8p6( zE`(n|FggjR%ME|uEIL{j!q2_)D|T+N$sx=rJYD3s1z%3u(?y<|_RbNu=cKf!OShT} z1oqwXf%EzPHtqQp|J9@ew@oIVrnSrnTw00000NkvXXu0mjfX2*j8 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/80_old.png b/padbuildimg/assets/awk/80_old.png new file mode 100644 index 0000000000000000000000000000000000000000..078a4c8394b94bf848da265d836b5bd9150c0b48 GIT binary patch literal 2005 zcmV;`2P*i9P) zc~q3w8OB*t6h&k&$T}W-@Whq z-4C;#i8ixK6YXX_6URa>O}zFw=_v`%K>s2AiF{;Qzcw>*aieFVy{KoR1N4jEexpXm zja_KFRza%#5mbTp>y`_gWx4=9AV z&3zw#S_EqwOPG^+h13BDqZ|Z;rL{TIRRNQ6V%B)#&3k*mz5?Lmu}0MO{2Vz)gP~{+ zG$}TJ@}LFzibw>9dLXql1@WZ{c!v5SJ3|IvFGun8YhSQ~Cc*_{kuKO0ZZG=GEzFR9 zI6%b7Z4H6)H|xQ^T7dC-wB0O59t|jHxr|BGnIfKYb!G%gikD&tCvXOxsK!Mo@j3jY zQNn0zlsCQ(cg6?-3`~~q|VgWEn0A~rH;6gPltgR3}(+c;( zU4dvfeB^JBGX!v!0M1G5F(z}vR|}kw;5Ab~1g~(!kt;=DUjcA_Y(D`MVU>!qxJF`B zh5)T%1sK3y_7wmhK6w=_H#Z=sAqeT!enN-eNyeRJiwwIEc(Q zH#b9>$82b%u4tGy3svs3sO^G1?zW;D^zQ;`wi}xI*MWU?0F?x=9yxVXYN{}FUD{-_ z)bcVGQW7qiOhV<@dF;{j!;B2VW?18T)_nZZ&kjm5uO*0M-gf9);tP3@lQ3T(cSLq= zAR7DEf_(+RuOFAAd0;Ja4h5jAXOp(nQl$u!xT0*kjFw$SPwqGo_*o)5_$C zFVmKxrG*77$iU9h94T{tg1)GQ=#7cNT1OkvXG2SGX=#DDkpnzOGpL%%4{CGY zJih?!Zuu6rwk{$upVRGoL0ADo-2KIzJJ&0aeZUv>ec51N0r39gBB%zkr!xJ;WmGohD8$L!_wn7U-(g35+2nm$ zdwo#fp9%KW0c;?E%*jCi55OqFrj~jLO#)Axp^qq|iP!1rbC_C`a0 z8rW9=bdDAhKpO7S@)&GEx9Kf-^cUI!GG=j``#dFq1^}N-0|=WJB<9?{vJ+VqUa0L$ z0s9Jo_eS&4IFJn0@fF%qONp3DLXBY(av7Q`(M)*^;u5!gZv{#gZA0ElnV3`Gx*Upf zPt^4#fqezQ>5&}N-$+Deu?H@ExL#Xo1i@ukzC9M@WwD|RxdMLZG~_XbJQkkxw25)0 z$R&kRdv!{z7~|NuGHTt}m-sZB6A4F~@x2RI4240#UEH5G@ol6?06E&}W;0N(ibVjO=M zhjmq6NXl_Asq>wMCQm~}dW;z3IX@JG_3}=4wEry@Z)p;=0GWF|>5n+H4Msywdz6&r zU{v|9_~886LX)M&V?4*y{jB61N3`6F5`Kk8{j>m9))r@)??>QmjSL-MMq+CReHGC% zrSg6mQA2$KZqq*NU&Y+!K2!5qX$6kh+~R}wp-AC#{9zPq?JX~x=`Po~9?zlfi!hvc z6iteGhKfe>{cyMk&ETi^7w$yFKS_LcC#-XZvOi?v#3LzwY#ka1eY*T-F0jykq~z&* n4K5}+{Ve}EC4E2&n{N7FlE3os&Ybk500000NkvXXu0mjf%nz?$ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/81.png b/padbuildimg/assets/awk/81.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5412ceb5f2a38cfdfae6f88961b642f1fce47d GIT binary patch literal 2010 zcmV<02POE4P) zdr*|u8OB*I4vIkp1VxvNEUU00$S&-{g4T#Z*<~**0zpt?5?Z5Kc1cjdGiYCAqh`G|04Z?d{hp$Lp5qxiDr36tc{1KTL3g-9e zgS}sKfD^+FIPzy3;LQ%aDf7j6`}#I8 zUk>owp;}aR{1{pLB4KQhKAGsvz5G1*+ZQbGMqgY2h7o81%z z+ehobd>sJ8)o8wCMh*ZImYHuX;dnAu7nZ{wB;N_0n3;pp>5s2gA({On1G|ECC?h?@Qa4-H$ zu)-Ur`uS))zX8mb1N>&F49&ei!7BTF=&QpYOR9CV;OZtq>=bwWG0q=In2C=iKKOtD zJ|uwS;XW8r&BUD;e=H83!XYA4{jj&Y5X_eYd@{6?01C0Hj=H!q{E=$^Xcc-d6l35k zI~QBCd%hu_@SsL3l}y7?GTRgK4BiRz#@$6T@K@6N1W`46Dh@~ZV4Gq(CcBE@EB1uF z`$aHc4)Ci7Z=mtgdSunigRXq;IK5E65Z8K2fWI{3_ouePUTWlGYj*Fw_SNv6K81I_ zNaO;`tf}ae2cl-qRFnm=bpW;ndGTo&JqzTfnW*nt3+5{TY$kwp$f}aUP!={$Uy%8O zuNskOlp|Ue2&*NLluYX&$TiGda?%&#Y*rNWxz(}y9r`{h&Mh{2O~*H ze~iWtrrlE1b`^m6a)4jnFGfR80kU>Qz_2saske`t_*>>0;!tE(0kDzENY$i$c)^?s zlR*KaA)flc4X$M1P5q#yjC`rh$0?-}1%96Vo+qVVMAB^!;XXB;d0@T*fSCaDU@D_} zZwq#+x7*579UT=Yn2!gzg?5C?#0ZfEV|!mWR&1%jBu_6)C8L9(-qeUbnC|9C{oF8L zXiLOAF7v7B%mMQi0A7LpQZ`JbVbE<6J2i02&g*S`RW$~noK!<<;sCm0EmAB7+PQ)q zmv3Y1(L3;ZCV)5QB%$tc3&i;PN`*5n^QrDMf%$TPpWiP;U5^Qw+e4vyeU4L3fK87E zxG4Z6bAZdReQ^h+AO90OTJPgW%gUjQSp)TaEh;x_F-Vg$8khN0cNxHZ1%UMgU_j>9 z5NIv4ow{_+GC>JycnNL_B?zVxi~`s`>4WV6J-Hj$PmfFI%;Pb7JFU*R%%`R+1I(8L z935Ok02#cB(O0owkDEN_!wpz^!L7X{4(HkusLjbm{>k#Ii7$^eVn*(k5|$%U_{NuWzcO5!hu5>j@j49QM828caqZHZ*%rQkvS1cc?eyX@G1BZ zM^S-K`4p)0*CS@36zb)hoIP-cbdOX-<;bOsu-J#F9F^yu1@q+qKkLsx)rBR<*f0}4 zr;8ko7ZL>2*}ezBdf?@k5}Zxw065`nLgmVWaZPxd^mpM`qlJdNeAa@pvngP{0zf(e zq(DpQAGj%u-m;|=0+g068v}4m$bbW2N$wjH0=$-DMcobq25G#@&#J+EIl#Mp&!O^s z5;QLcJO)6MP=aGZ2Chs9aPN@-DllITaJWAeySkOot_wiV@67*m2}S|R$7Fz)pzXhx zp!bXgrDx*7d^x~-{Yz2txdQ3={y5O8bl`Er1Re@jN&T{YI01tTrX7uU`d^O4CV2sVbr=0raKYz*0X!1w43 zU}KnJ)gZ;{7(=5@$@h$!M8_DaEyyOZF=P|K$B<$_|9TKw{*;KH_APb*7`BV)2Q`{+ zCP1^=AFJ1g;_B(|4k9+&OwI%4|JciaDGZI*mI`TCCIVJsq6g}4T-$rE>BweK@2mk;807*qoM6N<$f*IJyeE Date: Sat, 11 Sep 2021 14:47:54 -0700 Subject: [PATCH 0939/1220] Add ^expcurve (#1410) * Add exp_curve * Move constants --- dbcog/dbcog.py | 2 +- dbcog/models/monster_model.py | 15 ++ padinfo/menu/closable_embed.py | 2 + padinfo/padinfo.py | 69 +++++++-- padinfo/view/components/monster/header.py | 5 +- padinfo/view/experience_curve.py | 166 ++++++++++++++++++++++ padinfo/view/id.py | 11 +- padinfo/view/id_traceback.py | 17 +-- 8 files changed, 258 insertions(+), 29 deletions(-) create mode 100644 padinfo/view/experience_curve.py diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 027a285d..66b3343c 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -310,7 +310,7 @@ async def get_debug_monsters(self) -> Optional[List[int]]: async def get_fm_flags(self, author_id): return {**self.fm_flags_default, **(await self.config.user_from_id(author_id).fm_flags())} - async def find_monster(self, query: str, author_id: int = 0) -> MonsterModel: + async def find_monster(self, query: str, author_id: int = 0) -> Optional[MonsterModel]: return await FindMonster(self, await self.get_fm_flags(author_id)).find_monster(query) async def find_monsters(self, query: str, author_id: int = 0) -> List[MonsterModel]: diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index 69b47d4e..a6f1a1c5 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -149,6 +149,21 @@ def awakening_restricted_latents(self): def history_us(self): return '[{}] New Added'.format(self.reg_date) + @property + def full_damage_attr(self) -> Attribute: + if self.attr1 == Attribute.Nil: + return self.attr2 + return self.attr1 + + @property + def exp_curve(self) -> int: + if self.level == 1: + return 0 + return int(round(self.exp / ((self.level - 1) / 98) ** 2.5)) + + def exp_to_level(self, target_level: int) -> int: + return int(self.exp_curve * ((target_level - 1) / 98) ** 2.5) + def awakening_count(self, awid): return len([x for x in self.awakenings if x.awoken_skill_id == awid]) diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index 456e6539..a745d289 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -4,11 +4,13 @@ from tsutils.menu.panes import MenuPanes from padinfo.view.awakening_help import AwakeningHelpView from padinfo.view.closable_embed import ClosableEmbedViewState +from padinfo.view.experience_curve import ExperienceCurveView from padinfo.view.id_traceback import IdTracebackView view_types = { AwakeningHelpView.VIEW_TYPE: AwakeningHelpView, IdTracebackView.VIEW_TYPE: IdTracebackView, + ExperienceCurveView.VIEW_TYPE: ExperienceCurveView, } diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 63dd1a6b..9fd7c0b8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -19,7 +19,7 @@ from tsutils.enums import AltEvoSort, CardPlusModifier, LsMultiplier, Server from tsutils.json_utils import safe_read_json from tsutils.query_settings import QuerySettings -from tsutils.user_interaction import get_user_confirmation +from tsutils.user_interaction import get_user_confirmation, send_cancellation_message from padinfo.common.config import BotConfig from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ @@ -48,6 +48,7 @@ from padinfo.view.common import invalid_monster_text from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.evos import EvosViewState +from padinfo.view.experience_curve import ExperienceCurveView, ExperienceCurveViewProps from padinfo.view.id import IdViewState from padinfo.view.id_traceback import IdTracebackView, IdTracebackViewProps from padinfo.view.leader_skill import LeaderSkillViewState @@ -118,14 +119,10 @@ def __init__(self, bot, *args, **kwargs): self.historic_lookups = safe_read_json(_data_file('historic_lookups_id3.json')) - self.id_menu = IdMenu # TODO: Support multi-menus for real self.awoken_emoji_names = {v: k for k, v in AWAKENING_ID_TO_EMOJI_NAME_MAP.items()} self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster self.settings = settings - def cog_unload(self): - """Manually nulling out database because the GC for cogs seems to be pretty shitty""" - async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) @@ -209,11 +206,9 @@ async def id3(self, ctx, *, query: str): """Monster info (main tab)""" await self._do_id(ctx, query) - async def _do_id(self, ctx, query: str): + async def _get_monster(self, ctx, query: str) -> Optional["MonsterModel"]: dbcog = await self.get_dbcog() raw_query = query - color = await self.get_user_embed_color(ctx) - original_author_id = ctx.message.author.id goodquery = None if query[0] in dbcog.token_maps.ID1_SUPPORTED \ @@ -245,8 +240,17 @@ async def send_message(): monster = await dbcog.find_monster(raw_query, ctx.author.id) - if not monster: + if monster is None: await self.send_id_failure_message(ctx, query) + return monster + + async def _do_id(self, ctx, query: str): + dbcog = await self.get_dbcog() + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + raw_query = query + + if (monster := await self._get_monster(ctx, query)) is None: return await self.log_id_result(ctx, monster.monster_id) @@ -294,7 +298,7 @@ async def send_survey_after(self, ctx, query, result_monster): url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params await asyncio.sleep(1) userres = await get_user_confirmation(ctx, "Was this the monster you were looking for?", - yes_emoji=char_to_emoji('y'), no_emoji=char_to_emoji('n')) + yes_emoji=char_to_emoji('y'), no_emoji=char_to_emoji('n')) if userres is True: await self.config.good.set(await self.config.good() + 1) elif userres is False: @@ -784,8 +788,7 @@ def user_color_to_discord_color(color): async def get_user_friends(self, original_author_id): friend_cog = self.bot.get_cog("Friend") - friends = (await friend_cog.get_friends(original_author_id)) if friend_cog else [] - return friends + return (await friend_cog.get_friends(original_author_id)) if friend_cog else [] @commands.command(aliases=['lssingle']) @checks.bot_has_permissions(embed_links=True) @@ -1384,6 +1387,48 @@ async def nadiffs(self, ctx, *, query): await self._do_idsearch(ctx, query, child_menu_type=NaDiffMenu.MENU_TYPE, child_reaction_list=NaDiffMenuPanes.emoji_names()) + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def expcurve(self, ctx, start, end: Optional[int], *, query=''): + if start.isdigit() and end is None and not query: + start, end, query = '', None, start + + if start.isdigit() and end is not None: + start, offset = int(start), 0 + elif (match := re.fullmatch(r'(\d+)\[(\d*\.?\d*)]', start)) and end is not None: + start, offset = int(match.group(1)), float(match.group(2) or '0') + elif (end is None and not start.isdigit()) or (query and not start and not end): + start, offset, query = 1, 0, start + ' ' + query + else: + return await ctx.send("Invalid syntax for argument `start`.") + + if (monster := await self._get_monster(ctx, query)) is None: + return + + if end is None: + end = monster.level + + if monster.exp_to_level(start + 1) - monster.exp_to_level(start) > offset and start <= 99 \ + or offset > 5e6 and start <= 110 or offset > 20e6: + return await send_cancellation_message(ctx, "Offset too large.") + if start <= 0 or end > 120 or end < start or (start == end and offset) or offset < 0: + return await send_cancellation_message(ctx, f"Invalid bounds ({start}[{offset}] - {end}).") + + header = MonsterHeader.fmt_id_header(monster, use_emoji=True).to_markdown() + if not monster.limit_mult and end > 99: + return await send_cancellation_message(ctx, f"{header} cannot limit break.") + if monster.level < end <= 99: + return await send_cancellation_message(ctx, f"{header} cannot get to level {end} " + f"(max level {monster.level}).") + + color = await self.get_user_embed_color(ctx) + original_author_id = ctx.message.author.id + menu = ClosableEmbedMenu.menu() + props = ExperienceCurveViewProps(monster=monster, low=start, high=end, offset=offset) + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, color, + ExperienceCurveView.VIEW_TYPE, props) + await menu.create(ctx, state) + async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): dbcog = await self.get_dbcog() diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 7d45d072..50d7205b 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -60,8 +60,9 @@ def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed=False): cls.jp_suffix(m, is_jp_buffed)) @classmethod - def fmt_id_header(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed): - return Text('{} {}'.strip().format( + def fmt_id_header(cls, m: "MonsterModel", is_tsubaki=False, is_jp_buffed=False, use_emoji=False): + return Text('{}{} {}'.strip().format( + get_attribute_emoji_by_monster(m) if use_emoji else '', '\N{EARTH GLOBE AMERICAS}' if m.server_priority == Server.NA else '', cls.long_maybe_tsubaki(m, is_tsubaki, bool(is_jp_buffed)))) diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py new file mode 100644 index 00000000..fa1e5da0 --- /dev/null +++ b/padinfo/view/experience_curve.py @@ -0,0 +1,166 @@ +from math import ceil +from typing import TYPE_CHECKING, Text, Union + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail +from discordmenu.embed.text import LabeledText +from discordmenu.embed.view import EmbedView +from discordmenu.emoji.emoji_cache import emoji_cache +from redbot.core.utils.chat_formatting import humanize_number +from tsutils.menu.footers import embed_footer_with_state + +from padinfo.common.external_links import puzzledragonx +from padinfo.view.components.monster.header import MonsterHeader +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + +SSG_EXP = 4_000_000 +GLOBE_EXP = { + 0: 2_600_000, + 1: 2_900_000, + 2: 3_250_000, + 3: 3_000_000, + 4: 3_000_000, +} +TA2_EXP = 12_000_000 +TA3_EXP = 50_000_000 + + +class ExperienceCurveViewProps: + def __init__(self, monster: "MonsterModel", low: int, high: int, offset: Union[float, int]): + self.monster = monster + self.low = low + self.high = high + + if offset <= 1: + if low <= 99: + self.offset = int((monster.exp_to_level(low + 1) - monster.exp_to_level(low)) * offset) + elif low <= 110: + self.offset = int(5e6 * offset) + else: + self.offset = int(20e6 * offset) + else: + self.offset = offset + + +def get_normal_exp_difference(monster: "MonsterModel", low: int, high: int, offset: int) -> int: + if low <= 99: + return 0 + total_low = monster.exp_to_level(low) + total_high = monster.exp_to_level(min(high, 99)) + total = int(total_high - total_low) + if low < 99: + return total - offset + return total + + +def get_lb_exp_difference(monster: "MonsterModel", low: int, high: int, offset: int) -> int: + if high <= 99 or low >= 110: + return 0 + total = int(5e6 * (min(high, 110) - max(low, 100))) + if 99 <= low < 110: + return total - offset + return total + + +def get_slb_exp_difference(monster: "MonsterModel", low: int, high: int, offset: int) -> int: + if high <= 110: + return 0 + total = int(20e6 * (high - max(low, 110))) + if 110 <= low < 120: + return total - offset + return total + + +def trunc_humanize(n: int) -> str: + def strip_float(f) -> str: + return str(float(f)).rstrip('0').rstrip('.') + + if n < 1e6: + return strip_float(round(n, 1)) + if n < 1e9: + return strip_float(round(n / 1e6, 1)) + 'm' + if n < 1e12: + return strip_float(round(n / 1e9, 1)) + 'b' + return str(n) + + +def get_total_needed(monster: "MonsterModel", low: int, high: int, offset: int, amount: int, is_light: bool) -> int: + ssg = SSG_EXP + if is_light: + ssg *= 1.5 + + total = ceil(get_normal_exp_difference(monster, low, high, offset) / amount) + + lb = get_lb_exp_difference(monster, low, high, offset) + if low <= 99: + lb -= ssg + total += ceil(max(0, lb) / amount) + + slb = get_slb_exp_difference(monster, low, high, offset) + if low <= 110: + slb -= ssg * 5 + total += ceil(max(0, slb) / amount) + + return int(total) + + +class ExperienceCurveView: + VIEW_TYPE = 'ExperienceCurve' + + @staticmethod + def embed(state, props: ExperienceCurveViewProps): + regular = get_normal_exp_difference(props.monster, props.low, props.high, props.offset) + lb = get_lb_exp_difference(props.monster, props.low, props.high, props.offset) + slb = get_slb_exp_difference(props.monster, props.low, props.high, props.offset) + is_light = props.monster.full_damage_attr.value == 3 + return EmbedView( + EmbedMain( + color=state.color, + title=MonsterHeader.fmt_id_header(props.monster, use_emoji=True), + url=puzzledragonx(props.monster), + description=Text(f'lv{props.low} -> lv{props.high} (' + + (trunc_humanize(props.monster.exp_curve) if props.monster.exp_curve else "no") + + f' curve)'), + ), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(props.monster)), + embed_fields=[ + EmbedField( + title='Exact', + body=Box( + LabeledText("Reg", humanize_number(regular)), + LabeledText("LB", humanize_number(lb)), + LabeledText("SLB", humanize_number(slb)), + LabeledText("Net", humanize_number(regular + lb + slb)) + ), + inline=True + ), + EmbedField( + title='Approx', + body=Box( + LabeledText("Reg", trunc_humanize(regular)), + LabeledText("LB", trunc_humanize(lb)), + LabeledText("SLB", trunc_humanize(slb)), + LabeledText("Net", trunc_humanize(regular + lb + slb)) + ), + inline=True + ), + EmbedField( + title='Resources', + body=Box( + LabeledText("Globes " + + emoji_cache.get_emoji(f'orb_{props.monster.full_damage_attr.name.lower()}'), + str(get_total_needed(props.monster, props.low, props.high, props.offset, + ceil(1.5 * GLOBE_EXP[props.monster.full_damage_attr.value]), + is_light))), + LabeledText("TA2", str(get_total_needed(props.monster, props.low, props.high, props.offset, + TA2_EXP, is_light))), + LabeledText("TA3", str(get_total_needed(props.monster, props.low, props.high, props.offset, + TA3_EXP, is_light))), + ), + inline=True + ) + ], + embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index b49baa72..07d79c60 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,11 +1,10 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional, TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.text import Text, BoldText, LabeledText, HighlightableLinks, LinkedText +from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail +from discordmenu.embed.text import BoldText, HighlightableLinks, LabeledText, LinkedText, Text from discordmenu.embed.view import EmbedView - -from tsutils.enums import Server, CardPlusModifier +from tsutils.enums import CardPlusModifier, Server from tsutils.menu.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings @@ -16,7 +15,7 @@ from padinfo.view.common import get_monster_from_ims, invalid_monster_text, leader_skill_header from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution +from padinfo.view.components.view_state_base_id import MonsterEvolution, ViewStateBaseId if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index c6a4fb74..15f9aa57 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -1,12 +1,13 @@ from typing import TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.text import LabeledText from discordmenu.embed.view import EmbedView from tsutils.menu.footers import embed_footer_with_state from padinfo.common.emoji_map import get_attribute_emoji_by_monster +from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -40,16 +41,16 @@ class IdTracebackView: @staticmethod def embed(state, props: IdTracebackViewProps): - fields = [ - EmbedField('Matched Name Tokens', Box(props.name_tokens)), - EmbedField('Matched Modifier Tokens', Box(props.modifier_tokens)), - EmbedField('Equally-scoring matches', Box(props.lower_priority_monsters)), - ] return EmbedView( EmbedMain( color=state.color, - title=get_title(props.monster), + title=MonsterHeader.fmt_id_header(props.monster, use_emoji=True), description=get_description(props.score) ), + embed_fields=[ + EmbedField('Matched Name Tokens', Box(props.name_tokens)), + EmbedField('Matched Modifier Tokens', Box(props.modifier_tokens)), + EmbedField('Equally-scoring matches', Box(props.lower_priority_monsters)), + ], embed_footer=embed_footer_with_state(state), - embed_fields=fields) + ) From 3198b94bf46f4a2887b1c67040effe677be63df2 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 11 Sep 2021 15:27:34 -0700 Subject: [PATCH 0940/1220] Fix inequalities in ^expcurve (#1411) --- padinfo/padinfo.py | 4 ++-- padinfo/view/experience_curve.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9fd7c0b8..5dd47416 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1408,10 +1408,10 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): if end is None: end = monster.level - if monster.exp_to_level(start + 1) - monster.exp_to_level(start) > offset and start <= 99 \ + if monster.exp_to_level(start + 1) - monster.exp_to_level(start) < offset and start <= 99 \ or offset > 5e6 and start <= 110 or offset > 20e6: return await send_cancellation_message(ctx, "Offset too large.") - if start <= 0 or end > 120 or end < start or (start == end and offset) or offset < 0: + if start <= 0 or end > 120 or end < start or (start == end and offset): return await send_cancellation_message(ctx, f"Invalid bounds ({start}[{offset}] - {end}).") header = MonsterHeader.fmt_id_header(monster, use_emoji=True).to_markdown() diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index fa1e5da0..589dd184 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -46,7 +46,7 @@ def __init__(self, monster: "MonsterModel", low: int, high: int, offset: Union[f def get_normal_exp_difference(monster: "MonsterModel", low: int, high: int, offset: int) -> int: - if low <= 99: + if low >= 99: return 0 total_low = monster.exp_to_level(low) total_high = monster.exp_to_level(min(high, 99)) From 345de96d25c773c349491579dbd07b35a87dd1ba Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 12 Sep 2021 01:23:50 -0500 Subject: [PATCH 0941/1220] Force show +0 stats if you can't feed plus to a card (#1412) --- dbcog/models/monster_model.py | 5 +++++ dbcog/monster_graph.py | 13 +++++++++++ padinfo/padinfo.py | 4 ++-- padinfo/view/id.py | 41 ++++++++++++++++++++++------------- requirements.txt | 1 + 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index a6f1a1c5..99196494 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -129,6 +129,11 @@ def killers(self): killers.update(type_to_killers_map.get(t, [])) return sorted(killers) + @property + def is_material(self): + return MonsterType.Evolve in self.types or MonsterType.Vendor in self.types or \ + MonsterType.Enhance in self.types or MonsterType.Awoken in self.types + @property def awakening_restricted_latents(self): monster_awakening_to_allowed_latent_map = [ diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 54218633..8596db9c 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -543,6 +543,19 @@ def get_prev_evolution(self, monster: MonsterModel) -> Optional[MonsterModel]: pe = self.get_prev_evolution_id(monster) return pe and self.get_monster(pe, server=monster.server_priority) + def get_all_prev_evolutions(self, monster: MonsterModel, *, include_self: bool = True) -> List[MonsterModel]: + ret = [] + if include_self: + ret.append(monster) + cur_mon = monster + while True: + pe = self.get_prev_evolution(cur_mon) + if pe is None: + break + ret.append(pe) + cur_mon = pe + return ret + def get_next_evolution_ids(self, monster: MonsterModel) -> Set[int]: return self._get_edges(monster, 'evolution') diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 5dd47416..e9b534d7 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -266,7 +266,7 @@ async def _do_id(self, ctx, query: str): asyncio.create_task(self.send_survey_after(ctx, query, monster)) alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ + transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters = \ await IdViewState.do_query(dbcog, monster) full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) @@ -275,7 +275,7 @@ async def _do_id(self, ctx, query: str): state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters, use_evo_scroll=settings.checkEvoID(ctx.author.id), reaction_list=initial_reaction_list) menu = IdMenu.menu() await menu.create(ctx, state) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 07d79c60..6939769b 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel from dbcog.models.awakening_model import AwakeningModel + from dbcog.database_context import DbContext def alt_fmt(monsterevo, state): @@ -39,7 +40,7 @@ class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, + transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters, fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, is_child: bool = False, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, @@ -51,6 +52,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.is_child = is_child self.acquire_raw = acquire_raw self.base_rarity = base_rarity + self.previous_monsters = previous_monsters self.transform_base: "MonsterModel" = transform_base self.true_evo_type_raw = true_evo_type_raw @@ -70,7 +72,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): return None monster = await get_monster_from_ims(dbcog, ims) alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ + transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters = \ await IdViewState.do_query(dbcog, monster) raw_query = ims['raw_query'] @@ -88,6 +90,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, transform_base, true_evo_type_raw, acquire_raw, base_rarity, + previous_monsters, fallback_message=fallback_message, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, @@ -107,18 +110,19 @@ async def set_server(self, dbcog, server: Server): @classmethod async def do_query(cls, dbcog, monster): db_context = dbcog.database - acquire_raw, base_rarity, transform_base, true_evo_type_raw = \ + acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_monsters = \ await IdViewState._get_monster_misc_info(db_context, monster) - return transform_base, true_evo_type_raw, acquire_raw, base_rarity + return transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters @classmethod - async def _get_monster_misc_info(cls, db_context, monster): + async def _get_monster_misc_info(cls, db_context: "DbContext", monster): transform_base = db_context.graph.get_transform_base(monster) true_evo_type_raw = db_context.graph.true_evo_type(monster).value acquire_raw = db_context.graph.monster_acquisition(monster) base_rarity = db_context.graph.get_base_monster(monster).rarity - return acquire_raw, base_rarity, transform_base, true_evo_type_raw + previous_monsters = db_context.graph.get_all_prev_evolutions(monster, include_self=True) + return acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_monsters def set_na_diff_invalid_message(self, ims: dict) -> bool: message = self.get_na_diff_invalid_message() @@ -253,11 +257,11 @@ def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_ return Box(rarity, cost, series, acquire, true_evo_type) - @staticmethod - def stats(m: "MonsterModel", cardplus: CardPlusModifier): - plus = 297 if cardplus == CardPlusModifier.plus297 else 0 + @classmethod + def stats(cls, m: "MonsterModel", previous_monsters, cardplus: CardPlusModifier): + plus = cls.get_plus_status(previous_monsters, cardplus) hp, atk, rcv, weighted = m.stats(plus=plus) - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(plus=297, lv=110) if m.limit_mult > 0 else (None, None, None, None) + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(plus=plus, lv=110) if m.limit_mult > 0 else (None, None, None, None) return Box( LabeledText('HP', _get_stat_text(hp, lb_hp, _get_awakening_emoji_for_stats(m, 1))), LabeledText('ATK', _get_stat_text(atk, lb_atk, _get_awakening_emoji_for_stats(m, 2))), @@ -266,10 +270,17 @@ def stats(m: "MonsterModel", cardplus: CardPlusModifier): ) @staticmethod - def stats_header(m: "MonsterModel", cardplus: CardPlusModifier): + def get_plus_status(previous_monsters: List["MonsterModel"], cardplus: CardPlusModifier): + if cardplus == CardPlusModifier.plus0: + return 0 + if all([m.level == 1 and m.is_material for m in previous_monsters]): + return 0 + return 297 if cardplus == CardPlusModifier.plus297 else 0 + + @classmethod + def stats_header(cls, m: "MonsterModel", previous_monsters, cardplus: CardPlusModifier): voice_emoji = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' - # TODO: Get a +0 emoji for the +0 setting - plus_297_emoji = get_emoji('plus_297') if cardplus == CardPlusModifier.plus297 else get_emoji('plus_0') + plus_297_emoji = get_emoji('plus_297') if cls.get_plus_status(previous_monsters, cardplus) == 297 else get_emoji('plus_0') header = Box( Text(voice_emoji), Text(plus_297_emoji), @@ -316,8 +327,8 @@ def embed(cls, state: IdViewState): inline=True ), EmbedField( - IdView.stats_header(m, state.query_settings.cardplus).to_markdown(), - IdView.stats(m, state.query_settings.cardplus), + IdView.stats_header(m, state.previous_monsters, state.query_settings.cardplus).to_markdown(), + IdView.stats(m, state.previous_monsters, state.query_settings.cardplus), inline=True ), EmbedField( diff --git a/requirements.txt b/requirements.txt index 55da826e..48852b9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ pymysql python-levenshtein pytz Red-DiscordBot[postgres] +regex setuptools tabulate tsutils From d51e901b9dc0ffbeabf204a0929f05555e9197bd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 12 Sep 2021 01:29:29 -0500 Subject: [PATCH 0942/1220] Add ID as alias for ^id (#1413) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e9b534d7..ec265bdc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -182,7 +182,7 @@ async def jpname(self, ctx, *, query: str): else: await self.send_id_failure_message(ctx, query) - @commands.command(name="id", aliases=['I\'d']) + @commands.command(name="id", aliases=['I\'d', 'ID']) @checks.bot_has_permissions(embed_links=True) async def _id(self, ctx, *, query: str): """Monster info (main tab)""" From a55c55b2a157794d83bbbf78e627e07f8c880792 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 13 Sep 2021 16:30:01 -0700 Subject: [PATCH 0943/1220] Fix Event Type (#1414) --- padevents/padevents.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index bec3c37b..d2388f32 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1068,7 +1068,11 @@ def __init__(self, scheduled_event: "ScheduledEventModel", db_context): if self.clean_event_name != '': self.name_and_modifier += ', ' + self.clean_event_name - self.event_type = EventType(scheduled_event.event_type_id) + self.event_type_id = scheduled_event.event_type_id + try: + self.event_type = EventType(self.event_type_id) + except ValueError: + self.event_type = EventType.Unknown self.dungeon_type = DungeonType(self.dungeon.dungeon_type) if self.dungeon else DungeonType.Unknown @@ -1203,6 +1207,7 @@ def items_by_close_time(self, reverse=False): class EventType(Enum): + Unknown = -1 Week = 0 Special = 1 SpecialWeek = 2 From 8366b6d608a0e60f58c72c6c8cc2c2456ff4781b Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 16 Sep 2021 10:13:43 -0700 Subject: [PATCH 0944/1220] Force AEP and AED to not repeat (#1415) --- dbcog/models/scheduled_event_model.py | 7 +- padevents/padevents.py | 135 +++++++++++++------------- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/dbcog/models/scheduled_event_model.py b/dbcog/models/scheduled_event_model.py index 712b25da..b87e233f 100644 --- a/dbcog/models/scheduled_event_model.py +++ b/dbcog/models/scheduled_event_model.py @@ -12,9 +12,12 @@ def __init__(self, **kwargs): self.start_timestamp = kwargs.get("start_timestamp") self.end_timestamp = kwargs.get("end_timestamp") self.group_name = kwargs.get("group_name") + self.url = kwargs.get("message") + self.url = kwargs.get("url") self.dungeon: DungeonModel = kwargs.get("dungeon_model") - self.dungeon_id = self.dungeon.dungeon_id if self.dungeon else None + self.dungeon_id = kwargs.get("dungeon_id") + self.sub_dungeon_id = kwargs.get("sub_dungeon_id") @property def open_datetime(self): @@ -34,5 +37,5 @@ def close_datetime(self, value): def to_dict(self): return { - 'dungeon_id': self.event_id, + 'dungeon_id': self.dungeon_id, } diff --git a/padevents/padevents.py b/padevents/padevents.py index d2388f32..cc1844f4 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -3,15 +3,17 @@ import logging import re from collections import defaultdict +from contextlib import suppress from datetime import timedelta from enum import Enum from io import BytesIO -from typing import List, Optional, TYPE_CHECKING, Union +from typing import List, NoReturn, Optional, TYPE_CHECKING, Union import discord import itertools import prettytable import pytz +import time from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import box, humanize_timedelta, inline, pagify from tsutils.cog_settings import CogSettings @@ -21,6 +23,7 @@ from tsutils.errors import ClientInlineTextException from tsutils.formatting import normalize_server_name, rmdiacritics from tsutils.helper_classes import DummyObject +from tsutils.helper_functions import repeating_timer from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message if TYPE_CHECKING: @@ -51,18 +54,18 @@ def __init__(self, bot, *args, **kwargs): self.settings = PadEventSettings("padevents") self.config = Config.get_conf(self, identifier=940373775) + self.config.register_global(sent={}) self.config.register_guild(pingroles={}) self.config.register_user(dmevents=[]) # Load event data self.events = list() self.started_events = set() - self.rolepinged_events = set() self.fake_uid = -999 self._event_loop = bot.loop.create_task(self.reload_padevents()) - self._refresh_loop = bot.loop.create_task(self.event_check_loop()) + self._refresh_loop = bot.loop.create_task(self.do_loop()) async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -83,16 +86,28 @@ def cog_unload(self): self._event_loop.cancel() self._refresh_loop.cancel() - async def reload_padevents(self): + async def reload_padevents(self) -> NoReturn: await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadEvents'): - try: - await self.refresh_data() - logger.info('Done refreshing PadEvents') - except Exception as ex: - logger.exception("reload padevents loop caught exception " + str(ex)) - - await asyncio.sleep(60 * 60 * 1) + with suppress(asyncio.CancelledError): + async for _ in repeating_timer(60 * 60): + try: + await self.refresh_data() + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Error in loop:") + + async def do_loop(self) -> NoReturn: + await self.bot.wait_until_ready() + with suppress(asyncio.CancelledError): + async for _ in repeating_timer(10): + try: + await self.do_autoevents() + await self.do_eventloop() + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Error in loop:") async def refresh_data(self): dbcog = self.bot.get_cog('DBCog') @@ -102,28 +117,18 @@ async def refresh_data(self): new_events = [] for se in scheduled_events: try: - db_context = self.bot.get_cog("DBCog").database - new_events.append(Event(se, db_context)) + new_events.append(Event(se)) except Exception as ex: logger.exception("Refresh error:") self.events = new_events self.started_events = {ev.key for ev in new_events if ev.is_started()} - self.rolepinged_events = set() + async with self.config.sent() as seen: + for key, value in [*seen.items()]: + if value < time.time() + 60 * 60: + del seen[key] - async def event_check_loop(self): - await self.bot.wait_until_ready() - while self == self.bot.get_cog('PadEvents'): - try: - await self.check_started() - except asyncio.CancelledError: - break - except Exception: - logger.exception("caught exception while checking guerrillas:") - await asyncio.sleep(10) - logger.info("done check_started (cog probably unloaded)") - - async def check_started(self): + async def do_autoevents(self): events = filter(lambda e: e.key not in self.started_events, self.events) for event in events: for gid, data in (await self.config.all_guilds()).items(): @@ -134,32 +139,34 @@ async def check_started(self): if event.start_from_now_sec() > aep['offset'] * 60 \ or not aep['enabled'] \ or event.server != aep['server'] \ - or (key, event.key) in self.rolepinged_events: + or (key, event.key, gid) in await self.config.sent(): continue elif aep['regex']: matches = re.search(aep['searchstr'], event.clean_dungeon_name) else: matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() - self.rolepinged_events.add((key, event.key)) - if matches: - index = GROUPS.index(event.group) - channel = guild.get_channel(aep['channels'][index]) - if channel is not None: - role = guild.get_role(aep['roles'][index]) - ment = role.mention if role else "" - offsetstr = "" - if aep['offset']: - offsetstr = " in {} minute(s)".format(aep['offset']) - try: - timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) - await channel.send("{} will be active for {}{}. {}".format(event.name_and_modifier, - timestr, - offsetstr, - ment), - allowed_mentions=discord.AllowedMentions(roles=True)) - except Exception: - logger.exception("Failed to send AEP in channel {}".format(channel.id)) + async with self.config.sent() as sent: + sent[(key, event.key, gid)] = time.time() + if not matches: + continue + + index = GROUPS.index(event.group) + channel = guild.get_channel(aep['channels'][index]) + if channel is None: + continue + role = guild.get_role(aep['roles'][index]) + ment = role.mention if role else "" + offsetstr = "now" + if aep['offset']: + offsetstr = f"" + try: + timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) + await channel.send(f"{event.name_and_modifier} starts {offsetstr}!" + f" It will be active for {timestr}. {ment}", + allowed_mentions=discord.AllowedMentions(roles=True)) + except Exception: + logger.exception("Failed to send AEP in channel {}".format(channel.id)) for uid, data in (await self.config.all_users()).items(): user = self.bot.get_user(uid) @@ -169,25 +176,27 @@ async def check_started(self): if event.start_from_now_sec() > aed['offset'] * 60 \ or (event.group not in (aed['group'], None)) \ or event.server != aed['server'] \ - or (aed['key'], event.key) in self.rolepinged_events: + or (aed['key'], event.key, uid) in await self.config.sent(): continue if aed.get('include3p') is None: # case of legacy configs aed['include3p'] = True if not aed['include3p'] and event.clean_dungeon_name.startswith("Multiplayer"): continue - self.rolepinged_events.add((aed['key'], event.key)) + async with self.config.sent() as sent: + sent[(aed['key'], event.key, uid)] = time.time() if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): offsetstr = "now" - timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) if aed['offset']: - offsetstr = "in {} minute(s)".format(aed['offset']) + offsetstr = f"" + timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) try: await user.send(f"{event.clean_dungeon_name} starts {offsetstr}!" f" It will be active for {timestr}.") except Exception: logger.exception("Failed to send AED to user {}".format(user.id)) + async def do_eventloop(self): events = filter(lambda e: e.is_started() and e.key not in self.started_events, self.events) daily_refresh_servers = set() for event in events: @@ -204,12 +213,10 @@ async def check_started(self): role_name = '{}_group_{}'.format(event.server, event.group_long_name()) role = channel.guild.get_role(role_name) if role and role.mentionable: - message = "{} `: {} is starting`".format(role.mention, event.name_and_modifier) + message = "{}`: {} is starting`".format(role.mention, event.name_and_modifier) else: - message = box( - "Server " + event.server + ", group " + event.group_long_name() + - " : " + event.name_and_modifier - ) + message = box(f"Server {event.server}, group {event.group_long_name()}:" + f" {event.name_and_modifier}") await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) except Exception as ex: @@ -226,8 +233,6 @@ async def check_started(self): msg, channel_id=daily_registration['channel_id']) logger.info("daily_reg server") except Exception as ex: - # self.settings.remove_daily_reg( - # daily_registration['channel_id'], daily_registration['server']) logger.exception("caught exception while sending daily msg:") @commands.group(aliases=['pde']) @@ -261,7 +266,7 @@ async def testevent(self, ctx, server: Server, seconds: int = 0): dungeon_id=1, ) ) - self.events.append(Event(te, self.bot.get_cog('DBCog').database)) + self.events.append(Event(te)) await ctx.tick() @padevents.command() @@ -590,7 +595,7 @@ async def aed_add(self, ctx, server: Server, searchstr, group: Optional[StarterG return default = { - 'key': datetime.datetime.now().timestamp(), + 'key': time.time(), 'server': server, 'group': group, 'searchstr': searchstr, @@ -1050,8 +1055,7 @@ def remove_daily_reg(self, channel_id, server): class Event: - def __init__(self, scheduled_event: "ScheduledEventModel", db_context): - self.db_context = db_context + def __init__(self, scheduled_event: "ScheduledEventModel"): self.key = scheduled_event.event_id self.server = SUPPORTED_SERVERS[scheduled_event.server_id] self.open_datetime = scheduled_event.open_datetime @@ -1069,10 +1073,7 @@ def __init__(self, scheduled_event: "ScheduledEventModel", db_context): self.name_and_modifier += ', ' + self.clean_event_name self.event_type_id = scheduled_event.event_type_id - try: - self.event_type = EventType(self.event_type_id) - except ValueError: - self.event_type = EventType.Unknown + self.event_type = None self.dungeon_type = DungeonType(self.dungeon.dungeon_type) if self.dungeon else DungeonType.Unknown From a3109ce0f3af98ad18015867420abff489b9fc7b Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 16 Sep 2021 10:14:23 -0700 Subject: [PATCH 0945/1220] Add VEM modifier (#1416) --- dbcog/models/monster_model.py | 1 + dbcog/monster_graph.py | 10 ++++++++-- dbcog/monster_index.py | 4 ++++ dbcog/token_mappings.py | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index 99196494..22eb7430 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -63,6 +63,7 @@ def __init__(self, **m): self.is_farmable = m['is_farmable'] self.in_rem = m['in_rem'] self.in_pem = m['in_pem'] + self.in_vem = m['in_vem'] self.in_mpshop = m['buy_mp'] is not None self.buy_mp = m['buy_mp'] self.sell_gold = m['sell_gold'] diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 8596db9c..747de7bd 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -158,10 +158,10 @@ def build_graph(self, server: Server) -> MultiDiGraph: awakening_model = AwakeningModel(awoken_skill_model=awoken_skill_model, **a) mtoawo[a.monster_id].append(awakening_model) - mtoegg = defaultdict(lambda: {'pem': False, 'rem': False}) + mtoegg = defaultdict(lambda: {'pem': False, 'rem': False, 'vem': False}) for e in ems: data = json.loads(e.contents) - e_type = 'pem' if e.type == "PEM" else 'rem' + e_type = 'pem' if e.type == "PEM" else 'vem' if e.type == "VEM" else 'rem' for m in data: idx = int(m[1:-1]) # Remove parentheses mtoegg[idx][e_type] = True @@ -226,6 +226,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: is_farmable=m.drop_id is not None, in_pem=mtoegg[m.monster_id]['pem'], in_rem=mtoegg[m.monster_id]['rem'], + in_vem=mtoegg[m.monster_id]['vem'], buy_mp=m.buy_mp, sell_mp=m.sell_mp, sell_gold=m.sell_gold, @@ -595,6 +596,9 @@ def monster_is_pem_evo(self, monster: MonsterModel) -> bool: def monster_is_rem_evo(self, monster: MonsterModel) -> bool: return any(alt.in_rem for alt in self.get_alt_monsters(monster)) + def monster_is_vem_evo(self, monster: MonsterModel) -> bool: + return any(alt.in_vem for alt in self.get_alt_monsters(monster)) + def monster_is_exchange(self, monster: MonsterModel) -> bool: return bool(self._get_edges(monster, 'exchange_from')) @@ -662,6 +666,8 @@ def monster_acquisition(self, monster: MonsterModel) -> str: return 'Farmable Card' elif self.monster_is_pem_evo(monster): return 'PEM Card' + elif self.monster_is_vem_evo(monster): + return 'VEM Card' def numeric_next_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: next_monster = None diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index fa847f80..ef3109db 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -451,6 +451,10 @@ def is_story(m, do_transform=True): # Method of Obtaining if self.graph.monster_is_farmable_evo(monster) or self.graph.monster_is_mp_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.FARMABLE]) + if self.graph.monster_is_pem_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.PEM]) + if self.graph.monster_is_vem_evo(monster): + modifiers.update(MISC_MAP[MiscModifiers.VEM]) if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 4b334343..76f4dc81 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -177,7 +177,9 @@ class MiscModifiers(Enum): STORY = 'Story' FARMABLE = 'Farmable' TRADEABLE = 'Tradeable' - REM = 'REM' + REM = 'In REM' + PEM = 'In PEM' + VEM = 'In VEM' MP = 'MP' INJP = 'In JP Server' ONLYJP = 'Only in JP Server' @@ -205,6 +207,8 @@ class MiscModifiers(Enum): MiscModifiers.FARMABLE: ('farmable',), MiscModifiers.TRADEABLE: ('tradeable', 'tradable'), MiscModifiers.REM: ('rem',), + MiscModifiers.PEM: ('pem',), + MiscModifiers.VEM: ('vem', 'adpem'), MiscModifiers.MP: ('mp',), MiscModifiers.INJP: ('injp',), MiscModifiers.INNA: ('inna',), From 0d8d529fb25a22c8dfdfb3d47287fbb6b779eed0 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 17 Sep 2021 09:37:33 -0700 Subject: [PATCH 0946/1220] Fix padevents (#1417) * Remove Padvision * Fix padevents * fake_uid * fake_uid 2 * Fix --- padevents/padevents.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index cc1844f4..36a12acb 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -62,7 +62,7 @@ def __init__(self, bot, *args, **kwargs): self.events = list() self.started_events = set() - self.fake_uid = -999 + self.fake_uid = -time.time() self._event_loop = bot.loop.create_task(self.reload_padevents()) self._refresh_loop = bot.loop.create_task(self.do_loop()) @@ -139,18 +139,19 @@ async def do_autoevents(self): if event.start_from_now_sec() > aep['offset'] * 60 \ or not aep['enabled'] \ or event.server != aep['server'] \ - or (key, event.key, gid) in await self.config.sent(): + or str((key, event.key, gid)) in await self.config.sent(): continue elif aep['regex']: matches = re.search(aep['searchstr'], event.clean_dungeon_name) else: matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() - async with self.config.sent() as sent: - sent[(key, event.key, gid)] = time.time() if not matches: continue + async with self.config.sent() as sent: + sent[str((key, event.key, gid))] = time.time() + index = GROUPS.index(event.group) channel = guild.get_channel(aep['channels'][index]) if channel is None: @@ -176,7 +177,7 @@ async def do_autoevents(self): if event.start_from_now_sec() > aed['offset'] * 60 \ or (event.group not in (aed['group'], None)) \ or event.server != aed['server'] \ - or (aed['key'], event.key, uid) in await self.config.sent(): + or str((aed['key'], event.key, uid)) in await self.config.sent(): continue if aed.get('include3p') is None: # case of legacy configs @@ -184,7 +185,7 @@ async def do_autoevents(self): if not aed['include3p'] and event.clean_dungeon_name.startswith("Multiplayer"): continue async with self.config.sent() as sent: - sent[(aed['key'], event.key, uid)] = time.time() + sent[str((aed['key'], event.key, uid))] = time.time() if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): offsetstr = "now" if aed['offset']: From b913a4e16a6473b8b53fae4bda564bedcc82c876 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 18 Sep 2021 17:41:43 -0700 Subject: [PATCH 0947/1220] Add AdPEM command (#1419) --- crowddata/__init__.py | 7 ++ crowddata/crowddata.py | 34 ++++++ crowddata/info.json | 15 +++ crowddata/mixins/__init__.py | 0 crowddata/mixins/vem.py | 210 +++++++++++++++++++++++++++++++++++ padinfo/padinfo.py | 1 + 6 files changed, 267 insertions(+) create mode 100644 crowddata/__init__.py create mode 100644 crowddata/crowddata.py create mode 100644 crowddata/info.json create mode 100644 crowddata/mixins/__init__.py create mode 100644 crowddata/mixins/vem.py diff --git a/crowddata/__init__.py b/crowddata/__init__.py new file mode 100644 index 00000000..693d1bc2 --- /dev/null +++ b/crowddata/__init__.py @@ -0,0 +1,7 @@ +from .crowddata import CrowdData + +__red_end_user_data_statement__ = "All explicitly stored user preferences are kept persistantly." + + +def setup(bot): + bot.add_cog(CrowdData(bot)) diff --git a/crowddata/crowddata.py b/crowddata/crowddata.py new file mode 100644 index 00000000..2730ad04 --- /dev/null +++ b/crowddata/crowddata.py @@ -0,0 +1,34 @@ +import logging +from io import BytesIO + +from redbot.core import Config +from redbot.core.bot import Red + +from crowddata.mixins.vem import VEM + +logger = logging.getLogger('red.misc-cogs.crowddata') + + +class CrowdData(VEM): + """Stores user preferences for users.""" + + def __init__(self, bot: Red, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + self.config = Config.get_conf(self, identifier=720700474) + + self.setup_mixins() + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = [] + + data = '\n'.join(await self.get_mixin_user_data(user_id)) + if not data: + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + await self.delete_mixin_user_data(requester, user_id) diff --git a/crowddata/info.json b/crowddata/info.json new file mode 100644 index 00000000..7c710acb --- /dev/null +++ b/crowddata/info.json @@ -0,0 +1,15 @@ +{ + "author": ["Aradia Megido#2552"], + "description": "Allows users to set up custom preferences to be used cross-cog.", + "end_user_data_statement": "All explicitly stored user preferences are kept persistantly.", + "install_msg": "See for a full list of commands.", + "short": "User Preferences", + "min_bot_version": "3.4.0", + "max_bot_version": "0.0.0", + "min_python_version": [3, 8, 1], + "hidden": false, + "disabled": false, + "required_cogs": {}, + "requirements": [], + "tags": [] +} diff --git a/crowddata/mixins/__init__.py b/crowddata/mixins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py new file mode 100644 index 00000000..a78810e5 --- /dev/null +++ b/crowddata/mixins/vem.py @@ -0,0 +1,210 @@ +import time +from datetime import datetime, timedelta, timezone +from typing import Any, TYPE_CHECKING + +from discordmenu.emoji.emoji_cache import emoji_cache +from redbot.core import Config, commands +from tsutils.cog_mixins import CogMixin +from tsutils.user_interaction import get_user_confirmation + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + + +def opted_in(is_opted): + async def check(ctx): + return is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in() + + return commands.check(check) + + +class VEM(CogMixin): + config: Config + + def setup_self(self): + self.config.register_global(pulls=[]) + self.config.register_user(opted_in=False, accounts=1) + + async def red_get_data_for_user(self, *, user_id): + num = len([p for p in await self.config.pulls() if p[0] == user_id]) + return f"You have {num} logged pulls." + + async def red_delete_data_for_user(self, *, requester, user_id): + async with self.config.pulls() as pulls: + for pull in pulls: + if pull[0] == user_id: + pulls.remove(pull) + await self.config.user_from_id(user_id).opted_in.set(False) + + @commands.group(aliases=['vem']) + async def adpem(self, ctx): + """Log data for data from the Video Egg Machine""" + + @adpem.group(usage=", , , ", invoke_without_command=True) + @opted_in(True) + async def report(self, ctx, *, pulls): + """Report a command or opt-in. """ + await self.report_at_time(ctx, pulls, self.midnight()) + + @adpem.group(usage=", , , ", invoke_without_command=True) + @opted_in(True) + async def reportyesterday(self, ctx, *, pulls): + """Report yesterday's rolls""" + await self.report_at_time(ctx, pulls, self.midnight() - (24 * 60 * 60)) + + async def report_at_time(self, ctx, pulls, midnight): + if not await self.config.user(ctx.author).opted_in(): + return await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") + + if len(pulls := pulls.split(',')) != 4: + return await ctx.send("Make sure to supply all 4 rolls, and make sure not to put commas in their names.") + + dbcog: Any = ctx.bot.get_cog("DBCog") + pdicog: Any = ctx.bot.get_cog("PadInfo") + if dbcog is None or pdicog is None: + return await ctx.send("Required cogs not loaded. Please alert a bot owner.") + await dbcog.wait_until_ready() + + if not await self.assert_ready(ctx, midnight): + return + + monsters = [await dbcog.find_monster('vem ' + pull.strip()) + for pull in pulls] + if not all(monsters): + unknown = '\n\t'.join(s for s, m in zip(pulls, monsters) if m is None) + return await ctx.send(f"Not all monsters were valid. The following could not be processed:\n\t{unknown}") + + def get_vem_evo(mon: "MonsterModel") -> "MonsterModel": + return {m for m in dbcog.database.graph.get_alt_monsters(mon) if m.in_vem}.pop() + + correct_evos = [get_vem_evo(m) for m in monsters] + + check = '\n\t'.join(pdicog.monster_header.fmt_id_header(m, use_emoji=True).to_markdown() + for m in correct_evos) + if not await get_user_confirmation(ctx, f"Are these monsters correct?\n\t{check}", + timeout=30, force_delete=False, show_feedback=True): + return + + await self.add_roll(ctx, [m.monster_id for m in correct_evos]) + + @report.command() + async def skip(self, ctx): + """Mark that you skipped your pulls today""" + if not await self.assert_ready(ctx, self.midnight()): + return + await self.add_roll(ctx, "Skipped") + + @report.command() + async def forgot(self, ctx): + """Mark that you forgot your pulls today""" + if not await self.assert_ready(ctx, self.midnight()): + return + await self.add_roll(ctx, "Forgot") + + @reportyesterday.command(name="skip") + async def y_skip(self, ctx): + """Mark that you skipped your pulls yesterday""" + if not await self.assert_ready(ctx, self.midnight() - (24 * 60 * 60)): + return + await self.add_roll(ctx, "Skipped") + + @reportyesterday.command(name="forgot") + async def y_forgot(self, ctx): + """Mark that you forgot your pulls yesterday""" + if not await self.assert_ready(ctx, self.midnight() - (24 * 60 * 60)): + return + await self.add_roll(ctx, "Forgot") + + @adpem.command() + @opted_in(True) + async def remove(self, ctx): + """Remove all of today's pulls""" + await self.remove_at_time(ctx, self.midnight()) + + @adpem.command() + @opted_in(True) + async def removeyesterday(self, ctx): + """Remove all of yesterday's pulls""" + await self.remove_at_time(ctx, self.midnight() - (24 * 60 * 60)) + + async def remove_at_time(self, ctx, midnight): + dbcog: Any = ctx.bot.get_cog("DBCog") + pdicog: Any = ctx.bot.get_cog("PadInfo") + if dbcog is None or pdicog is None: + return await ctx.send("Required cogs not loaded. Please alert a bot owner.") + await dbcog.wait_until_ready() + + pulls = [v for uid, v, ts in await self.config.pulls() + if midnight < ts < midnight + (24 * 60 * 60) and uid == ctx.author.id] + if not pulls: + return await ctx.send("There are no logged pulls during the given time period.") + + pulltext = [] + for pull in pulls: + if isinstance(pull, str): + pulltext.append(pull) + else: + pulltext.append('\t' + '\n\t'.join( + pdicog.monster_header.fmt_id_header(dbcog.get_monster(mid), use_emoji=True).to_markdown() + for mid in pull) + ) + pulltext = '\n\n'.join(pulltext) + + if not await get_user_confirmation(ctx, f"Are you sure you want to remove the following pulls:\n\n{pulltext}", + force_delete=False, show_feedback=True): + return + + async with self.config.pulls() as pulls: + for pull in pulls: + if midnight < pull[2] < midnight + (24 * 60 * 60) and pull[0] == ctx.author.id: + pulls.remove(pull) + await ctx.send("Your pulls have been deleted.") + + @adpem.command() + @opted_in(False) + async def optin(self, ctx): + """Opt in to the data collection system""" + if not await get_user_confirmation(ctx, f"Thanks for participating in our data collection! We take selection" + f" bias VERY seriously." + f" (See: )" + f" By participating, you are agreeing to report **every single roll" + f" that you make, every single day**. If you only report SOME rolls," + f" then you are likely to forget your worse rolls and report only the" + f" better rolls, which would bias our statistics towards the better" + f" rolls and make us sad. {emoji_cache.get_by_name('blobsad')}" + f"\n\nSo report all rolls and give us nice," + f" unbiased data! {emoji_cache.get_by_name('blobcheer')}" + f"\n\nDo you agree?", force_delete=False, show_feedback=True): + return + await self.config.user(ctx.author).opted_in.set(True) + + @adpem.command() + @opted_in(True) + async def setaccounts(self, ctx, number_of_accounts: int): + """Set how many accounts you'll be using with this cog""" + if not (1 <= number_of_accounts <= 10): + await ctx.send("You must have at most 10 accounts.") + await self.config.user(ctx.author).accounts.set(number_of_accounts) + await ctx.tick() + + async def assert_ready(self, ctx, midnight: float) -> bool: + pulls = await self.config.pulls() + numtoday = len([1 for uid, v, ts in pulls + if midnight < ts < midnight + (24 * 60 * 60) and uid == ctx.author.id]) + if numtoday >= await self.config.user(ctx.author).accounts(): + await ctx.send(f"You already reported {numtoday} times today, which is your current number of" + f" accounts for reporting. If you're rolling daily on more accounts than this," + f" you may increase your number of accounts via" + f" `{ctx.prefix}{ctx.invoked_parents[0]} setaccounts`, however please keep in" + f" mind we're very worried about selection bias, so please only do this if you" + f" are actually going to report daily!") + return False + return True + + async def add_roll(self, ctx, values: Any): + async with self.config.pulls() as pulls: + pulls.append((ctx.author.id, values, time.time())) + + def midnight(self) -> float: + return datetime.now(timezone(timedelta(hours=-8))).replace(hour=0, minute=0, second=0, microsecond=0) \ + .astimezone(timezone.utc).timestamp() diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index ec265bdc..8246ea58 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -121,6 +121,7 @@ def __init__(self, bot, *args, **kwargs): self.awoken_emoji_names = {v: k for k, v in AWAKENING_ID_TO_EMOJI_NAME_MAP.items()} self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster + self.monster_header = MonsterHeader self.settings = settings async def red_get_data_for_user(self, *, user_id): From e002ec36b310d1010a61a4c9339b0b133450706a Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 18 Sep 2021 22:58:46 -0700 Subject: [PATCH 0948/1220] [adpem] Confirm for skip and forget (#1420) --- crowddata/mixins/vem.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index a78810e5..9cc1f409 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -92,6 +92,9 @@ async def skip(self, ctx): """Mark that you skipped your pulls today""" if not await self.assert_ready(ctx, self.midnight()): return + if not await get_user_confirmation(ctx, f"Are you sure you want to report that you skipped today's" + f" pulls on an account", force_delete=False, show_feedback=True): + return await self.add_roll(ctx, "Skipped") @report.command() @@ -99,6 +102,9 @@ async def forgot(self, ctx): """Mark that you forgot your pulls today""" if not await self.assert_ready(ctx, self.midnight()): return + if not await get_user_confirmation(ctx, f"Are you sure you want to report that you forgot today's" + f" pulls on an account", force_delete=False, show_feedback=True): + return await self.add_roll(ctx, "Forgot") @reportyesterday.command(name="skip") @@ -106,6 +112,10 @@ async def y_skip(self, ctx): """Mark that you skipped your pulls yesterday""" if not await self.assert_ready(ctx, self.midnight() - (24 * 60 * 60)): return + + if not await get_user_confirmation(ctx, f"Are you sure you want to report that you skipped yesterday's" + f" pulls on an account", force_delete=True, show_feedback=True): + return await self.add_roll(ctx, "Skipped") @reportyesterday.command(name="forgot") @@ -113,6 +123,9 @@ async def y_forgot(self, ctx): """Mark that you forgot your pulls yesterday""" if not await self.assert_ready(ctx, self.midnight() - (24 * 60 * 60)): return + if not await get_user_confirmation(ctx, f"Are you sure you want to report that you forgot yesterday's" + f" pulls on an account", force_delete=False, show_feedback=True): + return await self.add_roll(ctx, "Forgot") @adpem.command() From f5dbafcc3531a5d193b875bb53ff46b89b0cff77 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 19 Sep 2021 03:24:13 -0700 Subject: [PATCH 0949/1220] [adpem] UX fixes & fix remove iterator (#1421) --- crowddata/mixins/vem.py | 49 +++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 9cc1f409..ab27a1ec 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -44,15 +44,15 @@ async def adpem(self, ctx): @opted_in(True) async def report(self, ctx, *, pulls): """Report a command or opt-in. """ - await self.report_at_time(ctx, pulls, self.midnight()) + await self.report_at_time(ctx, pulls, 0) @adpem.group(usage=", , , ", invoke_without_command=True) @opted_in(True) async def reportyesterday(self, ctx, *, pulls): """Report yesterday's rolls""" - await self.report_at_time(ctx, pulls, self.midnight() - (24 * 60 * 60)) + await self.report_at_time(ctx, pulls, 24 * 60 * 60) - async def report_at_time(self, ctx, pulls, midnight): + async def report_at_time(self, ctx, pulls, offset): if not await self.config.user(ctx.author).opted_in(): return await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") @@ -65,7 +65,7 @@ async def report_at_time(self, ctx, pulls, midnight): return await ctx.send("Required cogs not loaded. Please alert a bot owner.") await dbcog.wait_until_ready() - if not await self.assert_ready(ctx, midnight): + if not await self.assert_ready(ctx, self.midnight() - offset): return monsters = [await dbcog.find_monster('vem ' + pull.strip()) @@ -85,7 +85,7 @@ def get_vem_evo(mon: "MonsterModel") -> "MonsterModel": timeout=30, force_delete=False, show_feedback=True): return - await self.add_roll(ctx, [m.monster_id for m in correct_evos]) + await self.add_roll(ctx, [m.monster_id for m in correct_evos], time.time() - offset) @report.command() async def skip(self, ctx): @@ -93,9 +93,9 @@ async def skip(self, ctx): if not await self.assert_ready(ctx, self.midnight()): return if not await get_user_confirmation(ctx, f"Are you sure you want to report that you skipped today's" - f" pulls on an account", force_delete=False, show_feedback=True): + f" pulls on an account?", force_delete=False, show_feedback=True): return - await self.add_roll(ctx, "Skipped") + await self.add_roll(ctx, "Skipped", self.midnight()) @report.command() async def forgot(self, ctx): @@ -103,30 +103,30 @@ async def forgot(self, ctx): if not await self.assert_ready(ctx, self.midnight()): return if not await get_user_confirmation(ctx, f"Are you sure you want to report that you forgot today's" - f" pulls on an account", force_delete=False, show_feedback=True): + f" pulls on an account?", force_delete=False, show_feedback=True): return - await self.add_roll(ctx, "Forgot") + await self.add_roll(ctx, "Forgot", self.midnight()) @reportyesterday.command(name="skip") async def y_skip(self, ctx): """Mark that you skipped your pulls yesterday""" - if not await self.assert_ready(ctx, self.midnight() - (24 * 60 * 60)): + if not await self.assert_ready(ctx, time.time() - (24 * 60 * 60)): return if not await get_user_confirmation(ctx, f"Are you sure you want to report that you skipped yesterday's" - f" pulls on an account", force_delete=True, show_feedback=True): + f" pulls on an account?", force_delete=True, show_feedback=True): return - await self.add_roll(ctx, "Skipped") + await self.add_roll(ctx, "Skipped", time.time() - (24 * 60 * 60)) @reportyesterday.command(name="forgot") async def y_forgot(self, ctx): """Mark that you forgot your pulls yesterday""" - if not await self.assert_ready(ctx, self.midnight() - (24 * 60 * 60)): + if not await self.assert_ready(ctx, time.time() - (24 * 60 * 60)): return if not await get_user_confirmation(ctx, f"Are you sure you want to report that you forgot yesterday's" - f" pulls on an account", force_delete=False, show_feedback=True): + f" pulls on an account?", force_delete=False, show_feedback=True): return - await self.add_roll(ctx, "Forgot") + await self.add_roll(ctx, "Forgot", time.time() - (24 * 60 * 60)) @adpem.command() @opted_in(True) @@ -148,7 +148,7 @@ async def remove_at_time(self, ctx, midnight): await dbcog.wait_until_ready() pulls = [v for uid, v, ts in await self.config.pulls() - if midnight < ts < midnight + (24 * 60 * 60) and uid == ctx.author.id] + if midnight <= ts < midnight + (24 * 60 * 60) and uid == ctx.author.id] if not pulls: return await ctx.send("There are no logged pulls during the given time period.") @@ -168,8 +168,8 @@ async def remove_at_time(self, ctx, midnight): return async with self.config.pulls() as pulls: - for pull in pulls: - if midnight < pull[2] < midnight + (24 * 60 * 60) and pull[0] == ctx.author.id: + for pull in pulls[:]: + if midnight <= pull[2] < midnight + (24 * 60 * 60) and pull[0] == ctx.author.id: pulls.remove(pull) await ctx.send("Your pulls have been deleted.") @@ -203,20 +203,21 @@ async def setaccounts(self, ctx, number_of_accounts: int): async def assert_ready(self, ctx, midnight: float) -> bool: pulls = await self.config.pulls() numtoday = len([1 for uid, v, ts in pulls - if midnight < ts < midnight + (24 * 60 * 60) and uid == ctx.author.id]) + if midnight <= ts < midnight + (24 * 60 * 60) and uid == ctx.author.id]) if numtoday >= await self.config.user(ctx.author).accounts(): - await ctx.send(f"You already reported {numtoday} times today, which is your current number of" - f" accounts for reporting. If you're rolling daily on more accounts than this," - f" you may increase your number of accounts via" + await ctx.send(f"You already reported {numtoday} times" + f" {'today' if midnight > time.time() - 24 * 60 * 60 else 'yesterday'}, which " + f"is your current number of accounts for reporting. If you're rolling daily on more" + f" accounts than this, you may increase your number of accounts via" f" `{ctx.prefix}{ctx.invoked_parents[0]} setaccounts`, however please keep in" f" mind we're very worried about selection bias, so please only do this if you" f" are actually going to report daily!") return False return True - async def add_roll(self, ctx, values: Any): + async def add_roll(self, ctx, values: Any, at_time: float): async with self.config.pulls() as pulls: - pulls.append((ctx.author.id, values, time.time())) + pulls.append((ctx.author.id, values, at_time)) def midnight(self) -> float: return datetime.now(timezone(timedelta(hours=-8))).replace(hour=0, minute=0, second=0, microsecond=0) \ From 1b997b46f1057ad6cc2e3c8e436bba366cd8dc72 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 19 Sep 2021 07:19:28 -0500 Subject: [PATCH 0950/1220] Increase timeout of adpem optin to 30s (#1423) --- crowddata/mixins/vem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index ab27a1ec..9c7c85cd 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -187,7 +187,8 @@ async def optin(self, ctx): f" rolls and make us sad. {emoji_cache.get_by_name('blobsad')}" f"\n\nSo report all rolls and give us nice," f" unbiased data! {emoji_cache.get_by_name('blobcheer')}" - f"\n\nDo you agree?", force_delete=False, show_feedback=True): + f"\n\nDo you agree?", + force_delete=False, show_feedback=True, timeout=30): return await self.config.user(ctx.author).opted_in.set(True) From f2468faa035fbc34a2ca4792f43a402865c22bd6 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 19 Sep 2021 07:49:51 -0500 Subject: [PATCH 0951/1220] Clarify adpem reporting help text (#1424) --- crowddata/mixins/vem.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 9c7c85cd..109e3d7f 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -40,13 +40,13 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def adpem(self, ctx): """Log data for data from the Video Egg Machine""" - @adpem.group(usage=", , , ", invoke_without_command=True) + @adpem.group(usage="pull1, pull2, pull3, pull4", invoke_without_command=True) @opted_in(True) async def report(self, ctx, *, pulls): """Report a command or opt-in. """ await self.report_at_time(ctx, pulls, 0) - @adpem.group(usage=", , , ", invoke_without_command=True) + @adpem.group(usage="pull1, pull2, pull3, pull4", invoke_without_command=True) @opted_in(True) async def reportyesterday(self, ctx, *, pulls): """Report yesterday's rolls""" @@ -57,7 +57,10 @@ async def report_at_time(self, ctx, pulls, offset): return await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") if len(pulls := pulls.split(',')) != 4: - return await ctx.send("Make sure to supply all 4 rolls, and make sure not to put commas in their names.") + return await ctx.send(f"Please supply all 4 rolls with commas in between them. You can use names or" + f" numbers.\n\t" + f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013`\n\t" + f"Valid input: `{ctx.prefix}adpem report Enoch, Facet, D Globe, B Super King`") dbcog: Any = ctx.bot.get_cog("DBCog") pdicog: Any = ctx.bot.get_cog("PadInfo") @@ -72,7 +75,7 @@ async def report_at_time(self, ctx, pulls, offset): for pull in pulls] if not all(monsters): unknown = '\n\t'.join(s for s, m in zip(pulls, monsters) if m is None) - return await ctx.send(f"Not all monsters were valid. The following could not be processed:\n\t{unknown}") + return await ctx.send(f"Not all monsters were valid. The following could not be processed:\n\t{unknown}") def get_vem_evo(mon: "MonsterModel") -> "MonsterModel": return {m for m in dbcog.database.graph.get_alt_monsters(mon) if m.in_vem}.pop() @@ -220,6 +223,7 @@ async def add_roll(self, ctx, values: Any, at_time: float): async with self.config.pulls() as pulls: pulls.append((ctx.author.id, values, at_time)) - def midnight(self) -> float: + @staticmethod + def midnight() -> float: return datetime.now(timezone(timedelta(hours=-8))).replace(hour=0, minute=0, second=0, microsecond=0) \ .astimezone(timezone.utc).timestamp() From 3c73f01727038cf5c50438d80145f87af08a1420 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 19 Sep 2021 05:57:28 -0700 Subject: [PATCH 0952/1220] [adpem] Add toggleskipconfirm (#1422) * toggleskipconfirm * skipconf --- crowddata/mixins/vem.py | 59 ++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 109e3d7f..c490ab23 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -13,7 +13,11 @@ def opted_in(is_opted): async def check(ctx): - return is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in() + if is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in(): + return True + if is_opted: + await ctx.send(f"Use `{ctx.prefix}{ctx.invoked_parents[0]} optin` to opt in.") + return False return commands.check(check) @@ -23,7 +27,7 @@ class VEM(CogMixin): def setup_self(self): self.config.register_global(pulls=[]) - self.config.register_user(opted_in=False, accounts=1) + self.config.register_user(opted_in=False, accounts=1, skipconf=False) async def red_get_data_for_user(self, *, user_id): num = len([p for p in await self.config.pulls() if p[0] == user_id]) @@ -93,43 +97,34 @@ def get_vem_evo(mon: "MonsterModel") -> "MonsterModel": @report.command() async def skip(self, ctx): """Mark that you skipped your pulls today""" - if not await self.assert_ready(ctx, self.midnight()): - return - if not await get_user_confirmation(ctx, f"Are you sure you want to report that you skipped today's" - f" pulls on an account?", force_delete=False, show_feedback=True): - return - await self.add_roll(ctx, "Skipped", self.midnight()) + await self.skipforgot(ctx, 'Skipped', 0) @report.command() async def forgot(self, ctx): """Mark that you forgot your pulls today""" - if not await self.assert_ready(ctx, self.midnight()): - return - if not await get_user_confirmation(ctx, f"Are you sure you want to report that you forgot today's" - f" pulls on an account?", force_delete=False, show_feedback=True): - return - await self.add_roll(ctx, "Forgot", self.midnight()) + await self.skipforgot(ctx, 'Forgot', 0) @reportyesterday.command(name="skip") async def y_skip(self, ctx): """Mark that you skipped your pulls yesterday""" - if not await self.assert_ready(ctx, time.time() - (24 * 60 * 60)): - return - - if not await get_user_confirmation(ctx, f"Are you sure you want to report that you skipped yesterday's" - f" pulls on an account?", force_delete=True, show_feedback=True): - return - await self.add_roll(ctx, "Skipped", time.time() - (24 * 60 * 60)) + await self.skipforgot(ctx, 'Skipped', 24 * 60 * 60) @reportyesterday.command(name="forgot") async def y_forgot(self, ctx): """Mark that you forgot your pulls yesterday""" - if not await self.assert_ready(ctx, time.time() - (24 * 60 * 60)): + await self.skipforgot(ctx, 'Forgot', 24 * 60 * 60) + + async def skipforgot(self, ctx, ptype: str, offset: int): + if not await self.assert_ready(ctx, time.time() - offset): return - if not await get_user_confirmation(ctx, f"Are you sure you want to report that you forgot yesterday's" - f" pulls on an account?", force_delete=False, show_feedback=True): + if await self.config.user(ctx.author).skipconf() or \ + not await get_user_confirmation(ctx, f"Are you sure you want to report that you {ptype.lower()}" + f" {'yesterday' if offset else 'today'}'s pulls on an account?" + f"", force_delete=False, show_feedback=True): return - await self.add_roll(ctx, "Forgot", time.time() - (24 * 60 * 60)) + await self.add_roll(ctx, ptype, time.time() - offset) + if await self.config.user(ctx.author).skipconf(): + await ctx.tick() @adpem.command() @opted_in(True) @@ -204,6 +199,20 @@ async def setaccounts(self, ctx, number_of_accounts: int): await self.config.user(ctx.author).accounts.set(number_of_accounts) await ctx.tick() + @report.command() + @opted_in(True) + async def toggleskipconfirm(self, ctx): + """Force you to confirm when skipping or forgetting a days rolls.""" + await self.config.user(ctx.author).skipconf.set(not await self.config.user(ctx.author).skipconf()) + await ctx.send(f"You have {'dis' if await self.config.user(ctx.author).skipconf() else 'en'}abled confirmation" + f" for `skip` and `forgot`.") + + @reportyesterday.command(name="toggleskipconfirm") + @opted_in(True) + async def y_toggleskipconfirm(self, ctx): + """Force you to confirm when skipping or forgetting a days rolls.""" + await self.toggleskipconfirm(ctx) + async def assert_ready(self, ctx, midnight: float) -> bool: pulls = await self.config.pulls() numtoday = len([1 for uid, v, ts in pulls From 4d532416de9fd415a20de3f58c8390b9565b5f51 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 19 Sep 2021 08:03:43 -0500 Subject: [PATCH 0953/1220] [adpem] Update toggleskipconfirm docstring (#1425) --- crowddata/mixins/vem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index c490ab23..c6a039f0 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -202,7 +202,7 @@ async def setaccounts(self, ctx, number_of_accounts: int): @report.command() @opted_in(True) async def toggleskipconfirm(self, ctx): - """Force you to confirm when skipping or forgetting a days rolls.""" + """Toggle if you want to confirm on skip/forgot""" await self.config.user(ctx.author).skipconf.set(not await self.config.user(ctx.author).skipconf()) await ctx.send(f"You have {'dis' if await self.config.user(ctx.author).skipconf() else 'en'}abled confirmation" f" for `skip` and `forgot`.") @@ -210,7 +210,7 @@ async def toggleskipconfirm(self, ctx): @reportyesterday.command(name="toggleskipconfirm") @opted_in(True) async def y_toggleskipconfirm(self, ctx): - """Force you to confirm when skipping or forgetting a days rolls.""" + """Toggle if you want to confirm on skip/forgot""" await self.toggleskipconfirm(ctx) async def assert_ready(self, ctx, midnight: float) -> bool: From ba32b39044c46a05e144e6ca828b2006920485d7 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 19 Sep 2021 08:16:10 -0500 Subject: [PATCH 0954/1220] [adpem] Fix multiple sends for opted-out users (#1426) --- crowddata/mixins/vem.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index c6a039f0..1818ac84 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -13,11 +13,7 @@ def opted_in(is_opted): async def check(ctx): - if is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in(): - return True - if is_opted: - await ctx.send(f"Use `{ctx.prefix}{ctx.invoked_parents[0]} optin` to opt in.") - return False + return is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in() return commands.check(check) From 55aa4096de55b3302faa77e29ba1079680b434a5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 19 Sep 2021 08:43:44 -0500 Subject: [PATCH 0955/1220] add 'option' as alias for 'optin' (#1427) --- crowddata/mixins/vem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 1818ac84..07e4c813 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -167,7 +167,7 @@ async def remove_at_time(self, ctx, midnight): pulls.remove(pull) await ctx.send("Your pulls have been deleted.") - @adpem.command() + @adpem.command(aliases=['option']) @opted_in(False) async def optin(self, ctx): """Opt in to the data collection system""" From 4c5d8750b031c0b35c3aa51c3a4e62bec219398e Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 20 Sep 2021 00:56:01 -0500 Subject: [PATCH 0956/1220] Add support for coop/solo & 110/120 flags (#1428) * Add support for coop/solo & 110/120 flags * Format padinfo.py * fix docstring * more docstring --- dbcog/models/monster_model.py | 4 +-- dbcog/models/monster_stats.py | 16 ++++++---- padinfo/padinfo.py | 60 +++++++++++++++++++++++++++++++---- padinfo/view/id.py | 41 +++++++++++++++++------- 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index 22eb7430..d5624994 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -176,8 +176,8 @@ def awakening_count(self, awid): def stat(self, key, lv, plus=99, inherit=False, is_plus_297=True): return monster_stats.stat(self, key, lv, plus=plus, inherit=inherit, is_plus_297=is_plus_297) - def stats(self, lv=99, plus=0, inherit=False): - return monster_stats.stats(self, lv, plus=plus, inherit=inherit) + def stats(self, lv=99, plus=0, inherit=False, multiplayer: bool = False): + return monster_stats.stats(self, lv, plus=plus, inherit=inherit, multiplayer=multiplayer) @staticmethod def make_roma_subname(name_ja): diff --git a/dbcog/models/monster_stats.py b/dbcog/models/monster_stats.py index a6e7624e..4ca20b13 100644 --- a/dbcog/models/monster_stats.py +++ b/dbcog/models/monster_stats.py @@ -75,7 +75,7 @@ def stat(self, monster_model, key: StatType, lv, *, plus=99, inherit=False, is_p # recursion is not possible because inherits do not have inherits on top of them # inherit=True to calculate inherit stats and multiplayer=False in case the inherit has a multiboost awakening s_val += round(self.stat(inherited_monster, key, inherited_monster_lvl, - inherit=True, multiplayer=False)) + inherit=True, multiplayer=False)) # add bonus atk, hp, or rcv awakenings iff the inherit has awoken assist if inherited_monster.is_equip: inherit_bonus = MonsterStatModifierInput( @@ -106,11 +106,12 @@ def base_stat(self, key, lv, monster_model): if 99 < lv: s_val *= 1 + (monster_model.limit_mult / 11 * (min(lv, 110) - 99)) / 100 if 110 < lv: - slb_bonus = self.LV_120_MULT_DICT[key] * val_at_99 * (lv-110) / 1000 + slb_bonus = self.LV_120_MULT_DICT[key] * val_at_99 * (lv - 110) / 1000 s_val += slb_bonus return s_val - def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatModifierInput = None): + def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: MonsterStatModifierInput = None, + multiplayer: bool = False): is_plus_297 = False if plus == 297: plus = (99, 99, 99) @@ -127,11 +128,14 @@ def stats(self, monster_model, lv=99, plus=0, inherit=False, stat_latents: Monst ) # TODO: Fix rounding hp = int(round(self.stat(monster_model, 'hp', lv, - plus=plus[0], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents))) + plus=plus[0], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents, + multiplayer=multiplayer))) atk = int(round(self.stat(monster_model, 'atk', lv, - plus=plus[1], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents))) + plus=plus[1], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents, + multiplayer=multiplayer))) rcv = int(round(self.stat(monster_model, 'rcv', lv, - plus=plus[2], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents))) + plus=plus[2], inherit=inherit, is_plus_297=is_plus_297, stat_latents=stat_latents, + multiplayer=multiplayer))) weighted = int(round(hp / 10 + atk / 5 + rcv / 3)) return hp, atk, rcv, weighted diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8246ea58..a740c88d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -16,7 +16,7 @@ from tabulate import tabulate from tsutils.cogs.donations import is_donor from tsutils.emoji import char_to_emoji -from tsutils.enums import AltEvoSort, CardPlusModifier, LsMultiplier, Server +from tsutils.enums import AltEvoSort, CardPlusModifier, LsMultiplier, Server, CardLevelModifier, CardModeModifier from tsutils.json_utils import safe_read_json from tsutils.query_settings import QuerySettings from tsutils.user_interaction import get_user_confirmation, send_cancellation_message @@ -213,10 +213,10 @@ async def _get_monster(self, ctx, query: str) -> Optional["MonsterModel"]: goodquery = None if query[0] in dbcog.token_maps.ID1_SUPPORTED \ - and query[1:] in dbcog.indexes[Server.COMBINED].all_name_tokens: + and query[1:] in dbcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] elif query[:2] in dbcog.token_maps.ID1_SUPPORTED \ - and query[2:] in dbcog.indexes[Server.COMBINED].all_name_tokens: + and query[2:] in dbcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: @@ -923,7 +923,7 @@ async def padsay(self, ctx, server, *, query: str = None): @commands.group(aliases=['idmode']) async def idset(self, ctx): - """id settings configuration""" + """`[p]id` settings configuration""" @idset.command() async def scroll(self, ctx, value): @@ -961,7 +961,7 @@ async def survey(self, ctx, value): @is_donor() @idset.command() async def embedcolor(self, ctx, *, color): - """(DONOR ONLY) Change the color of all your ID embeds! + """(DONOR ONLY) Change the color of all your `[p]id` embeds! Examples: [p]idset embedcolor green @@ -1078,6 +1078,54 @@ async def cardplus(self, ctx, value: str): await ctx.send( f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + @idset.command() + async def cardmode(self, ctx, value: str): + """Change mode between solo and coop in your `[p]id` queries + + `[p]idset mode solo`: [Default] Show cards with stats in solo + `[p]idset mode coop`: Show cards with stats in coop (i.e. multiboost) + """ + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: + value = value.lower() + if value == "solo": + fm_flags['cardmode'] = CardModeModifier.solo.value + not_value = 'coop' + not_value_flag = 'coop' + elif value == "coop": + fm_flags['cardmode'] = CardModeModifier.coop.value + not_value = 'solo' + not_value_flag = 'solo' + else: + await ctx.send( + f'Please input an allowed value, either `coop` or `solo`.') + return + await ctx.send( + f"Your default `{ctx.prefix}id` cardmode preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + + @idset.command() + async def cardlevel(self, ctx, value: str): + """Change the limitbreak level in your your `[p]id` queries + + `[p]idset cardlevel 110`: [Default] Show LB stats at 110. + `[p]idset cardlevel 120`: Show LB stats at 120. + """ + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: + value = value.lower() + if value == "120": + fm_flags['cardlevel'] = CardLevelModifier.lv120.value + not_value = '110' + not_value_flag = 'lv110' + elif value == "110": + fm_flags['cardlevel'] = CardLevelModifier.lv110.value + not_value = '120' + not_value_flag = 'lv120' + else: + await ctx.send( + f'Please input an allowed value, either `297` or `0`.') + return + await ctx.send( + f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + @commands.group() @checks.is_owner() async def padinfo(self, ctx): @@ -1410,7 +1458,7 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): end = monster.level if monster.exp_to_level(start + 1) - monster.exp_to_level(start) < offset and start <= 99 \ - or offset > 5e6 and start <= 110 or offset > 20e6: + or offset > 5e6 and start <= 110 or offset > 20e6: return await send_cancellation_message(ctx, "Offset too large.") if start <= 0 or end > 120 or end < start or (start == end and offset): return await send_cancellation_message(ctx, f"Invalid bounds ({start}[{offset}] - {end}).") diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 6939769b..54d33e2f 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.text import BoldText, HighlightableLinks, LabeledText, LinkedText, Text from discordmenu.embed.view import EmbedView -from tsutils.enums import CardPlusModifier, Server +from tsutils.enums import CardPlusModifier, Server, CardModeModifier, CardLevelModifier from tsutils.menu.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings @@ -258,10 +258,16 @@ def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_ return Box(rarity, cost, series, acquire, true_evo_type) @classmethod - def stats(cls, m: "MonsterModel", previous_monsters, cardplus: CardPlusModifier): - plus = cls.get_plus_status(previous_monsters, cardplus) - hp, atk, rcv, weighted = m.stats(plus=plus) - lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(plus=plus, lv=110) if m.limit_mult > 0 else (None, None, None, None) + def stats(cls, m: "MonsterModel", previous_monsters, query_settings: QuerySettings): + plus = cls.get_plus_status(previous_monsters, query_settings.cardplus) + multiplayer = query_settings.cardmode == CardModeModifier.coop + lb_level = 110 if query_settings.cardlevel == CardLevelModifier.lv110 else 120 + hp, atk, rcv, weighted = m.stats(plus=plus, multiplayer=multiplayer) + + lb_hp, lb_atk, lb_rcv, lb_weighted = (None, None, None, None) + if m.limit_mult > 0: + lb_hp, lb_atk, lb_rcv, lb_weighted = m.stats(plus=plus, lv=lb_level, multiplayer=multiplayer) + return Box( LabeledText('HP', _get_stat_text(hp, lb_hp, _get_awakening_emoji_for_stats(m, 1))), LabeledText('ATK', _get_stat_text(atk, lb_atk, _get_awakening_emoji_for_stats(m, 2))), @@ -278,14 +284,27 @@ def get_plus_status(previous_monsters: List["MonsterModel"], cardplus: CardPlusM return 297 if cardplus == CardPlusModifier.plus297 else 0 @classmethod - def stats_header(cls, m: "MonsterModel", previous_monsters, cardplus: CardPlusModifier): + def stats_header(cls, m: "MonsterModel", previous_monsters, query_settings: QuerySettings): voice_emoji = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' - plus_297_emoji = get_emoji('plus_297') if cls.get_plus_status(previous_monsters, cardplus) == 297 else get_emoji('plus_0') + + multiboost_emoji = None + if m.awakening_count(30) and query_settings.cardmode == CardModeModifier.coop: + multiboost_emoji = get_emoji('misc_multiboost') + + plus_emoji = get_emoji('plus_297') + if cls.get_plus_status(previous_monsters, query_settings.cardplus) != 297: + plus_emoji = get_emoji('plus_0') + + lb_emoji = get_emoji('lv110') + if m.limit_mult > 0 and query_settings.cardlevel == CardLevelModifier.lv120: + lb_emoji = get_emoji('lv120') + header = Box( Text(voice_emoji), - Text(plus_297_emoji), + Text(plus_emoji), + Text(multiboost_emoji) if multiboost_emoji else None, Text('Stats'), - Text('(LB, +{}%)'.format(m.limit_mult)) if m.limit_mult else None, + Text('({}, +{}%)'.format(lb_emoji, m.limit_mult)) if m.limit_mult else None, delimiter=' ' ) return header @@ -327,8 +346,8 @@ def embed(cls, state: IdViewState): inline=True ), EmbedField( - IdView.stats_header(m, state.previous_monsters, state.query_settings.cardplus).to_markdown(), - IdView.stats(m, state.previous_monsters, state.query_settings.cardplus), + IdView.stats_header(m, state.previous_monsters, state.query_settings).to_markdown(), + IdView.stats(m, state.previous_monsters, state.query_settings), inline=True ), EmbedField( From 999aa47e13e16a6a84650020620a3af0ecdfdf50 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 20 Sep 2021 04:18:58 -0500 Subject: [PATCH 0957/1220] Show all previous AS cds for transform cards (#1429) * get_all_prev_transforms * IdQueriedProps * autoformat * Fix bugs & finish feature * autoformat --- dbcog/monster_graph.py | 68 +++++++++++++++++++++------- padinfo/padinfo.py | 6 +-- padinfo/view/id.py | 100 ++++++++++++++++++++++++----------------- 3 files changed, 114 insertions(+), 60 deletions(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 747de7bd..286c8866 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -316,7 +316,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: for vendor_id in re.findall(r'\d+', ex.required_monster_ids): if self.debug_monster_ids is not None: if ex.target_monster_id not in self.debug_monster_ids \ - or int(vendor_id) not in self.debug_monster_ids: + or int(vendor_id) not in self.debug_monster_ids: continue exchanges[(int(vendor_id), ex.target_monster_id)].add(model) for (sell_id, buy_id), models in exchanges.items(): @@ -364,7 +364,7 @@ def _get_edge_model_set(self, monster: MonsterModel, etype, model_type: Type[M] return {model for model in possible_results} def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER, do_logging: bool = False) \ - -> Optional[MonsterModel]: + -> Optional[MonsterModel]: if monster_id not in self.graph_dict[server].nodes: return None if 'model' not in self.graph_dict[server].nodes[monster_id]: @@ -396,7 +396,9 @@ def get_transform_monsters(self, monster: MonsterModel) -> Set[MonsterModel]: continue if (next_transform := self.get_next_transform(mon)): to_check.add(next_transform) - to_check.update(self.get_prev_transforms(mon)) + prev = self.get_prev_transform(mon) + if prev is not None: + to_check.add(prev) mons.add(mon) return mons @@ -440,9 +442,13 @@ def get_base_id(self, monster) -> int: if monster.base_evo_id == 5802: return 5810 - while (prevs := self.get_prev_transforms(monster)) \ - and list(prevs)[0].monster_id < monster.monster_id: - monster = prevs.pop() + while True: + prev = self.get_prev_transform(monster) + if prev is None: + break + if prev.monster_id >= monster.monster_id: + break + monster = prev if self.debug_monster_ids is not None: while (prev := self.get_prev_evolution(monster)): @@ -466,9 +472,9 @@ def get_transform_base(self, monster: MonsterModel) -> MonsterModel: curr = monster while curr not in seen: seen.add(curr) - next_mons = self.get_prev_transforms(curr) - if next_mons: - curr = next_mons.pop() + prev_mon = self.get_prev_transform(curr) + if prev_mon is not None: + curr = prev_mon else: break else: @@ -557,6 +563,33 @@ def get_all_prev_evolutions(self, monster: MonsterModel, *, include_self: bool = cur_mon = pe return ret + def get_all_prev_transforms(self, monster: MonsterModel, *, include_self: bool = True, + cyclical_wrap: bool = False) -> List[MonsterModel]: + ret = [] + if include_self: + ret.append(monster) + cur_mon = monster + is_cycle = False + while True: + pt = self.get_prev_transform(cur_mon) + if pt is None: + break + if pt.monster_id == monster.monster_id: + is_cycle = True + break + ret.append(pt) + cur_mon = pt + if is_cycle and not cyclical_wrap: + # remove everything that comes "after" monster in the cycle (i.e. has a higher monster id) + to_pop = [] + for i, mon in enumerate(ret): + if mon.monster_id > monster.monster_id: + to_pop.append(i) + to_pop.reverse() + for i in to_pop: + ret.pop(i) + return ret + def get_next_evolution_ids(self, monster: MonsterModel) -> Set[int]: return self._get_edges(monster, 'evolution') @@ -564,12 +597,17 @@ def get_next_evolutions(self, monster: MonsterModel) -> Set[MonsterModel]: return {self.get_monster(mid, server=monster.server_priority) for mid in self.get_next_evolution_ids(monster)} - def get_prev_transform_ids(self, monster: MonsterModel) -> Set[int]: - return self._get_edges(monster, 'back_transformation') + def get_prev_transform_id(self, monster: MonsterModel) -> Optional[int]: + transforms = list(self._get_edges(monster, 'back_transformation')) + if len(transforms) > 0: + return transforms[0] + return None - def get_prev_transforms(self, monster: MonsterModel) -> Set[MonsterModel]: - return {self.get_monster(mid, server=monster.server_priority) - for mid in self.get_prev_transform_ids(monster)} + def get_prev_transform(self, monster: MonsterModel) -> Optional[MonsterModel]: + prev = self.get_prev_transform_id(monster) + if prev is None: + return None + return self.get_monster(prev, server=monster.server_priority) def get_next_transform_id(self, monster: MonsterModel) -> Optional[int]: return self._get_edge_or_none(monster, 'transformation') @@ -630,7 +668,7 @@ def monster_is_currently_exchangable(self, monster: MonsterModel, server: Option now = datetime.now() for model in models: if model.start_timestamp < now < model.end_timestamp \ - and (server is None or server == model.server): + and (server is None or server == model.server): return True return False diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a740c88d..819768f9 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -267,16 +267,14 @@ async def _do_id(self, ctx, query: str): asyncio.create_task(self.send_survey_after(ctx, query, monster)) alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters = \ - await IdViewState.do_query(dbcog, monster) + id_queried_props = await IdViewState.do_query(dbcog, monster) full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, is_jp_buffed, query_settings, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters, + alt_monsters, is_jp_buffed, query_settings, id_queried_props, use_evo_scroll=settings.checkEvoID(ctx.author.id), reaction_list=initial_reaction_list) menu = IdMenu.menu() await menu.create(ctx, state) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 54d33e2f..41b17f32 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -33,6 +33,17 @@ def alt_fmt(monsterevo, state): return fmt.format(monsterevo.monster.monster_no_na) +class IdQueriedProps: + def __init__(self, acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_evolutions, + previous_transforms): + self.previous_evolutions = previous_evolutions + self.true_evo_type_raw = true_evo_type_raw + self.transform_base = transform_base + self.previous_transforms = previous_transforms + self.base_rarity = base_rarity + self.acquire_raw = acquire_raw + + class IdViewState(ViewStateBaseId): nadiff_na_only_text = ', which is only in NA' nadiff_jp_only_text = ', which is only in JP' @@ -40,7 +51,7 @@ class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters, + id_queried_props: IdQueriedProps, fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, is_child: bool = False, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, @@ -50,11 +61,12 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst extra_state=extra_state) self.fallback_message = fallback_message self.is_child = is_child - self.acquire_raw = acquire_raw - self.base_rarity = base_rarity - self.previous_monsters = previous_monsters - self.transform_base: "MonsterModel" = transform_base - self.true_evo_type_raw = true_evo_type_raw + self.acquire_raw = id_queried_props.acquire_raw + self.base_rarity = id_queried_props.base_rarity + self.previous_evolutions = id_queried_props.previous_evolutions + self.previous_transforms = id_queried_props.previous_transforms + self.transform_base: "MonsterModel" = id_queried_props.transform_base + self.true_evo_type_raw = id_queried_props.true_evo_type_raw def serialize(self): ret = super().serialize() @@ -72,8 +84,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): return None monster = await get_monster_from_ims(dbcog, ims) alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters = \ - await IdViewState.do_query(dbcog, monster) + id_queried_props = await IdViewState.do_query(dbcog, monster) raw_query = ims['raw_query'] # This is to support the 2 vs 1 monster query difference between ^ls and ^id @@ -89,8 +100,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, - previous_monsters, + id_queried_props, fallback_message=fallback_message, use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, @@ -101,28 +111,28 @@ async def set_server(self, dbcog, server: Server): self.query_settings.server = server self.monster = dbcog.database.graph.get_monster(self.monster.monster_id, server=server) self.alt_monsters = self.get_alt_monsters_and_evos(dbcog, self.monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity = await self.do_query(dbcog, self.monster) - self.transform_base = transform_base - self.true_evo_type_raw = true_evo_type_raw - self.acquire_raw = acquire_raw - self.base_rarity = base_rarity + id_queried_props = await self.do_query(dbcog, self.monster) + self.transform_base = id_queried_props.transform_base + self.true_evo_type_raw = id_queried_props.true_evo_type_raw + self.acquire_raw = id_queried_props.acquire_raw + self.base_rarity = id_queried_props.base_rarity @classmethod - async def do_query(cls, dbcog, monster): + async def do_query(cls, dbcog, monster) -> IdQueriedProps: db_context = dbcog.database - acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_monsters = \ - await IdViewState._get_monster_misc_info(db_context, monster) - - return transform_base, true_evo_type_raw, acquire_raw, base_rarity, previous_monsters + id_queried_props = await IdViewState._get_monster_misc_info(db_context, monster) + return id_queried_props @classmethod - async def _get_monster_misc_info(cls, db_context: "DbContext", monster): + async def _get_monster_misc_info(cls, db_context: "DbContext", monster) -> IdQueriedProps: transform_base = db_context.graph.get_transform_base(monster) true_evo_type_raw = db_context.graph.true_evo_type(monster).value acquire_raw = db_context.graph.monster_acquisition(monster) base_rarity = db_context.graph.get_base_monster(monster).rarity - previous_monsters = db_context.graph.get_all_prev_evolutions(monster, include_self=True) - return acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_monsters + previous_evolutions = db_context.graph.get_all_prev_evolutions(monster, include_self=True) + previous_transforms = db_context.graph.get_all_prev_transforms(monster, include_self=False) + return IdQueriedProps(acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_evolutions, + previous_transforms) def set_na_diff_invalid_message(self, ims: dict) -> bool: message = self.get_na_diff_invalid_message() @@ -258,8 +268,8 @@ def misc_info(m: "MonsterModel", true_evo_type_raw: str, acquire_raw: str, base_ return Box(rarity, cost, series, acquire, true_evo_type) @classmethod - def stats(cls, m: "MonsterModel", previous_monsters, query_settings: QuerySettings): - plus = cls.get_plus_status(previous_monsters, query_settings.cardplus) + def stats(cls, m: "MonsterModel", previous_evolutions, query_settings: QuerySettings): + plus = cls.get_plus_status(previous_evolutions, query_settings.cardplus) multiplayer = query_settings.cardmode == CardModeModifier.coop lb_level = 110 if query_settings.cardlevel == CardLevelModifier.lv110 else 120 hp, atk, rcv, weighted = m.stats(plus=plus, multiplayer=multiplayer) @@ -276,15 +286,15 @@ def stats(cls, m: "MonsterModel", previous_monsters, query_settings: QuerySettin ) @staticmethod - def get_plus_status(previous_monsters: List["MonsterModel"], cardplus: CardPlusModifier): + def get_plus_status(previous_evolutions: List["MonsterModel"], cardplus: CardPlusModifier): if cardplus == CardPlusModifier.plus0: return 0 - if all([m.level == 1 and m.is_material for m in previous_monsters]): + if all([m.level == 1 and m.is_material for m in previous_evolutions]): return 0 return 297 if cardplus == CardPlusModifier.plus297 else 0 @classmethod - def stats_header(cls, m: "MonsterModel", previous_monsters, query_settings: QuerySettings): + def stats_header(cls, m: "MonsterModel", previous_evolutions, query_settings: QuerySettings): voice_emoji = get_awakening_emoji(63) if m.awakening_count(63) and not m.is_equip else '' multiboost_emoji = None @@ -292,7 +302,7 @@ def stats_header(cls, m: "MonsterModel", previous_monsters, query_settings: Quer multiboost_emoji = get_emoji('misc_multiboost') plus_emoji = get_emoji('plus_297') - if cls.get_plus_status(previous_monsters, query_settings.cardplus) != 297: + if cls.get_plus_status(previous_evolutions, query_settings.cardplus) != 297: plus_emoji = get_emoji('plus_0') lb_emoji = get_emoji('lv110') @@ -310,19 +320,27 @@ def stats_header(cls, m: "MonsterModel", previous_monsters, query_settings: Quer return header @staticmethod - def active_skill_header(m: "MonsterModel", transform_base): + def active_skill_header(m: "MonsterModel", previous_transforms: List["MonsterModel"]): + transform_emoji = ['\N{DOWN-POINTING RED TRIANGLE}', get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), + get_emoji('downb'), get_emoji('downp')] + up_emoji = get_emoji('upgr') + active_skill = m.active_skill - if m == transform_base: + if len(previous_transforms) == 0: active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) \ if active_skill else 'None' else: - base_skill = transform_base.active_skill - base_cd = ' (\N{DOWN-POINTING RED TRIANGLE} {} -> {})'.format(base_skill.turn_max, - base_skill.turn_min) \ - if base_skill else 'None' - - active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' - active_cd += base_cd + skill_texts = [] + previous_transforms.reverse() + for i, mon in enumerate(previous_transforms): + skill = mon.active_skill + # we can assume skill is not None because the monster transforms + cooldown_text = '({}cd)'.format(str(skill.turn_max)) + if skill.turn_min != skill.turn_max: + cooldown_text = '{} -> {}'.format(skill.turn_min, skill.turn_max) + skill_texts.append('{}{}'.format(transform_emoji[i % len(transform_emoji)], cooldown_text)) + skill_texts.append('{} ({} cd)'.format(up_emoji, m.active_skill.turn_max)) + active_cd = ' '.join(skill_texts) return Box( BoldText('Active Skill'), BoldText(active_cd), @@ -346,12 +364,12 @@ def embed(cls, state: IdViewState): inline=True ), EmbedField( - IdView.stats_header(m, state.previous_monsters, state.query_settings).to_markdown(), - IdView.stats(m, state.previous_monsters, state.query_settings), + IdView.stats_header(m, state.previous_evolutions, state.query_settings).to_markdown(), + IdView.stats(m, state.previous_evolutions, state.query_settings), inline=True ), EmbedField( - IdView.active_skill_header(m, state.transform_base).to_markdown(), + IdView.active_skill_header(m, state.previous_transforms).to_markdown(), Text(m.active_skill.desc if m.active_skill else 'None') ), EmbedField( From 86a289397d875a5fa0212aa3950ea2ef780876ce Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 20 Sep 2021 02:28:57 -0700 Subject: [PATCH 0958/1220] [adpem] Remove just one set at a time (#1430) * Remove Padvision * Allow single remove * Reuqested Changes * Remove Colon --- crowddata/mixins/vem.py | 56 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 07e4c813..6e9b0d4c 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -5,7 +5,8 @@ from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import Config, commands from tsutils.cog_mixins import CogMixin -from tsutils.user_interaction import get_user_confirmation +from tsutils.emoji import NO_EMOJI, char_to_emoji +from tsutils.user_interaction import get_user_confirmation, get_user_reaction if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -43,13 +44,13 @@ async def adpem(self, ctx): @adpem.group(usage="pull1, pull2, pull3, pull4", invoke_without_command=True) @opted_in(True) async def report(self, ctx, *, pulls): - """Report a command or opt-in. """ + """Report a set of pulls or opt-in.""" await self.report_at_time(ctx, pulls, 0) @adpem.group(usage="pull1, pull2, pull3, pull4", invoke_without_command=True) @opted_in(True) async def reportyesterday(self, ctx, *, pulls): - """Report yesterday's rolls""" + """Report yesterday's pulls""" await self.report_at_time(ctx, pulls, 24 * 60 * 60) async def report_at_time(self, ctx, pulls, offset): @@ -57,7 +58,7 @@ async def report_at_time(self, ctx, pulls, offset): return await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") if len(pulls := pulls.split(',')) != 4: - return await ctx.send(f"Please supply all 4 rolls with commas in between them. You can use names or" + return await ctx.send(f"Please supply all 4 pulls with commas in between them. You can use names or" f" numbers.\n\t" f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013`\n\t" f"Valid input: `{ctx.prefix}adpem report Enoch, Facet, D Globe, B Super King`") @@ -88,7 +89,7 @@ def get_vem_evo(mon: "MonsterModel") -> "MonsterModel": timeout=30, force_delete=False, show_feedback=True): return - await self.add_roll(ctx, [m.monster_id for m in correct_evos], time.time() - offset) + await self.add_pull(ctx, [m.monster_id for m in correct_evos], time.time() - offset) @report.command() async def skip(self, ctx): @@ -111,14 +112,14 @@ async def y_forgot(self, ctx): await self.skipforgot(ctx, 'Forgot', 24 * 60 * 60) async def skipforgot(self, ctx, ptype: str, offset: int): - if not await self.assert_ready(ctx, time.time() - offset): + if not await self.assert_ready(ctx, self.midnight() - offset): return - if await self.config.user(ctx.author).skipconf() or \ + if not await self.config.user(ctx.author).skipconf() and \ not await get_user_confirmation(ctx, f"Are you sure you want to report that you {ptype.lower()}" f" {'yesterday' if offset else 'today'}'s pulls on an account?" f"", force_delete=False, show_feedback=True): return - await self.add_roll(ctx, ptype, time.time() - offset) + await self.add_pull(ctx, ptype, time.time() - offset) if await self.config.user(ctx.author).skipconf(): await ctx.tick() @@ -141,31 +142,32 @@ async def remove_at_time(self, ctx, midnight): return await ctx.send("Required cogs not loaded. Please alert a bot owner.") await dbcog.wait_until_ready() - pulls = [v for uid, v, ts in await self.config.pulls() + pulls = [[uid, v, ts] for uid, v, ts in await self.config.pulls() if midnight <= ts < midnight + (24 * 60 * 60) and uid == ctx.author.id] + pulls = {char_to_emoji(str(c)): vs for c, vs in enumerate(pulls, 1)} if not pulls: return await ctx.send("There are no logged pulls during the given time period.") pulltext = [] - for pull in pulls: + for c, (uid, pull, ts) in pulls.items(): if isinstance(pull, str): - pulltext.append(pull) + pulltext.append(c + ' ' + pull) else: - pulltext.append('\t' + '\n\t'.join( + pulltext.append(c + '\n\t' + '\n\t'.join( pdicog.monster_header.fmt_id_header(dbcog.get_monster(mid), use_emoji=True).to_markdown() for mid in pull) ) pulltext = '\n\n'.join(pulltext) - if not await get_user_confirmation(ctx, f"Are you sure you want to remove the following pulls:\n\n{pulltext}", - force_delete=False, show_feedback=True): + chosen = await get_user_reaction(ctx, f"Which set of pulls would you like to remove?\n\n{pulltext}", + *pulls.keys(), NO_EMOJI, force_delete=False, show_feedback=True) + + if chosen is None or chosen == NO_EMOJI: return - async with self.config.pulls() as pulls: - for pull in pulls[:]: - if midnight <= pull[2] < midnight + (24 * 60 * 60) and pull[0] == ctx.author.id: - pulls.remove(pull) - await ctx.send("Your pulls have been deleted.") + async with self.config.pulls() as c_pulls: + c_pulls.remove(pulls[chosen]) + await ctx.send("Your pulls have been removed.") @adpem.command(aliases=['option']) @opted_in(False) @@ -174,12 +176,12 @@ async def optin(self, ctx): if not await get_user_confirmation(ctx, f"Thanks for participating in our data collection! We take selection" f" bias VERY seriously." f" (See: )" - f" By participating, you are agreeing to report **every single roll" - f" that you make, every single day**. If you only report SOME rolls," - f" then you are likely to forget your worse rolls and report only the" - f" better rolls, which would bias our statistics towards the better" - f" rolls and make us sad. {emoji_cache.get_by_name('blobsad')}" - f"\n\nSo report all rolls and give us nice," + f" By participating, you are agreeing to report **every single pull" + f" that you make, every single day**. If you only report SOME pulls," + f" then you are likely to forget your worse pulls and report only the" + f" better pulls, which would bias our statistics towards the better" + f" pulls and make us sad. {emoji_cache.get_by_name('blobsad')}" + f"\n\nSo report all pulls and give us nice," f" unbiased data! {emoji_cache.get_by_name('blobcheer')}" f"\n\nDo you agree?", force_delete=False, show_feedback=True, timeout=30): @@ -216,7 +218,7 @@ async def assert_ready(self, ctx, midnight: float) -> bool: if numtoday >= await self.config.user(ctx.author).accounts(): await ctx.send(f"You already reported {numtoday} times" f" {'today' if midnight > time.time() - 24 * 60 * 60 else 'yesterday'}, which " - f"is your current number of accounts for reporting. If you're rolling daily on more" + f"is your current number of accounts for reporting. If you're pulling daily on more" f" accounts than this, you may increase your number of accounts via" f" `{ctx.prefix}{ctx.invoked_parents[0]} setaccounts`, however please keep in" f" mind we're very worried about selection bias, so please only do this if you" @@ -224,7 +226,7 @@ async def assert_ready(self, ctx, midnight: float) -> bool: return False return True - async def add_roll(self, ctx, values: Any, at_time: float): + async def add_pull(self, ctx, values: Any, at_time: float): async with self.config.pulls() as pulls: pulls.append((ctx.author.id, values, at_time)) From 3684d2593e604ab03a28d99f880ee1f70b75b04d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 20 Sep 2021 04:55:36 -0500 Subject: [PATCH 0959/1220] Use gray up arrow for awakenings (#1431) --- padinfo/view/id.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 41b17f32..3ad566a7 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -201,6 +201,9 @@ def evos_embed_field(state: ViewStateBaseId): class IdView(BaseIdView): VIEW_TYPE = 'Id' + transform_emojis = ['\N{DOWN-POINTING RED TRIANGLE}', get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), + get_emoji('downb'), get_emoji('downp')] + up_emoji = get_emoji('upgr') @staticmethod def normal_awakenings_row(m: "MonsterModel"): @@ -217,14 +220,14 @@ def super_awakenings_row(m: "MonsterModel"): *[Text(e) for e in super_awakenings_emojis], delimiter=' ') if len(super_awakenings_emojis) > 0 else None - @staticmethod - def all_awakenings_row(m: "MonsterModel", transform_base): + @classmethod + def all_awakenings_row(cls, m: "MonsterModel", transform_base): if len(m.awakenings) == 0: return Box(Text('No Awakenings')) return Box( Box( - '\N{UP-POINTING RED TRIANGLE}' if m != transform_base else '', + cls.up_emoji if m != transform_base else '', IdView.normal_awakenings_row(m), delimiter=' ' ), @@ -319,11 +322,8 @@ def stats_header(cls, m: "MonsterModel", previous_evolutions, query_settings: Qu ) return header - @staticmethod - def active_skill_header(m: "MonsterModel", previous_transforms: List["MonsterModel"]): - transform_emoji = ['\N{DOWN-POINTING RED TRIANGLE}', get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), - get_emoji('downb'), get_emoji('downp')] - up_emoji = get_emoji('upgr') + @classmethod + def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["MonsterModel"]): active_skill = m.active_skill if len(previous_transforms) == 0: @@ -338,8 +338,8 @@ def active_skill_header(m: "MonsterModel", previous_transforms: List["MonsterMod cooldown_text = '({}cd)'.format(str(skill.turn_max)) if skill.turn_min != skill.turn_max: cooldown_text = '{} -> {}'.format(skill.turn_min, skill.turn_max) - skill_texts.append('{}{}'.format(transform_emoji[i % len(transform_emoji)], cooldown_text)) - skill_texts.append('{} ({} cd)'.format(up_emoji, m.active_skill.turn_max)) + skill_texts.append('{}{}'.format(cls.transform_emojis[i % len(cls.transform_emojis)], cooldown_text)) + skill_texts.append('{} ({} cd)'.format(cls.up_emoji, m.active_skill.turn_max)) active_cd = ' '.join(skill_texts) return Box( BoldText('Active Skill'), From 2ef9d22c8c5ade60979c37a133a0fab5b556c45c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 20 Sep 2021 06:11:17 -0500 Subject: [PATCH 0960/1220] Refactor tfinfo/id to use a superclass (#1432) --- padinfo/menu/transforminfo.py | 6 +- padinfo/padinfo.py | 4 +- padinfo/view/common.py | 16 +- padinfo/view/components/base_id_main_view.py | 101 ++++++++++ padinfo/view/id.py | 79 +------- padinfo/view/transforminfo.py | 188 ++++++++++--------- 6 files changed, 213 insertions(+), 181 deletions(-) create mode 100644 padinfo/view/components/base_id_main_view.py diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 96868da9..263bdec0 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -82,7 +82,7 @@ def id_control(state: IdViewState, reaction_list): class TransformInfoEmoji: home = emoji_buttons['home'] down = '\N{DOWN-POINTING RED TRIANGLE}' - up = '\N{UP-POINTING RED TRIANGLE}' + up = ('upgr', '\N{UP-POINTING RED TRIANGLE}') one = char_to_emoji('1') two = char_to_emoji('2') three = char_to_emoji('3') @@ -116,9 +116,9 @@ class TransformInfoMenuPanes(MenuPanes): @classmethod def get_user_reaction_list(cls, number_of_monsters: int): if number_of_monsters > 2: - cls.HIDDEN_EMOJIS = TransformInfoEmoji.up + cls.HIDDEN_EMOJIS = list(TransformInfoEmoji.up) else: - cls.HIDDEN_EMOJIS = TransformInfoEmoji.one + cls.HIDDEN_EMOJIS = [TransformInfoEmoji.one] # add 1 for the home emoji return cls.emoji_names()[:number_of_monsters + 1] diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 819768f9..7621dabb 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -824,13 +824,13 @@ async def transforminfo(self, ctx, *, query): color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - acquire_raw = await TransformInfoViewState.do_query(dbcog, base_mon, transformed_mon) + tfinfo_queried_props = await TransformInfoViewState.do_query(dbcog, transformed_mon) reaction_list = TransformInfoMenuPanes.get_user_reaction_list(len(monster_ids)) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(base_mon) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, - color, base_mon, transformed_mon, acquire_raw, monster_ids, is_jp_buffed, + color, base_mon, transformed_mon, tfinfo_queried_props, monster_ids, is_jp_buffed, query_settings, reaction_list=reaction_list) menu = TransformInfoMenu.menu() diff --git a/padinfo/view/common.py b/padinfo/view/common.py index f5be7c4e..5f6e52c6 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -2,12 +2,9 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import Text, BoldText - -from tsutils.enums import LsMultiplier from tsutils.query_settings import QuerySettings -from padinfo.common.emoji_map import get_awakening_emoji, get_emoji -from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text +from padinfo.common.emoji_map import get_awakening_emoji from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: @@ -41,14 +38,3 @@ def get_awoken_skill_description(awoken_skill: "AwokenSkillModel"): def invalid_monster_text(query: str, monster: "MonsterModel", append_text: str, link: bool = False): monster_name = MonsterHeader.short_with_emoji(monster, link=link).to_markdown() return f'Your query `{query}` found {monster_name}{append_text}.' - - -def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): - return Box( - BoldText('Leader Skill'), - BoldText(ls_multiplier_text(m.leader_skill) if lsmultiplier == LsMultiplier.lsdouble - else get_emoji('1x') + ' ' + ls_single_multiplier_text(m.leader_skill)), - BoldText('(' + get_emoji( - '\N{DOWN-POINTING RED TRIANGLE}') + '7x6)') if m != transform_base and transform_base.leader_skill.is_7x6 else None, - delimiter=' ' - ) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py new file mode 100644 index 00000000..a3e79361 --- /dev/null +++ b/padinfo/view/components/base_id_main_view.py @@ -0,0 +1,101 @@ +from abc import abstractmethod +from typing import TYPE_CHECKING, List + +from discordmenu.embed.base import Box +from discordmenu.embed.text import Text, BoldText +from discordmenu.embed.view import EmbedView +from tsutils.enums import LsMultiplier + +from padinfo.common.emoji_map import get_awakening_emoji, get_emoji +from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text +from padinfo.view.base import BaseIdView +from padinfo.view.components.view_state_base_id import ViewStateBaseId + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + from dbcog.models.awakening_model import AwakeningModel + + +def _get_awakening_text(awakening: "AwakeningModel"): + return get_awakening_emoji(awakening.awoken_skill_id, awakening.name) + + +def _killer_latent_emoji(latent_name: str): + return get_emoji('latent_killer_{}'.format(latent_name.lower())) + + +class BaseIdMainView(BaseIdView): + transform_emojis = ['\N{DOWN-POINTING RED TRIANGLE}', get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), + get_emoji('downb'), get_emoji('downp')] + up_emoji = get_emoji('upgr') + down_emoji = transform_emojis[0] + + @staticmethod + def normal_awakenings_row(m: "MonsterModel"): + normal_awakenings = len(m.awakenings) - m.superawakening_count + normal_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[:normal_awakenings]] + return Box(*[Text(e) for e in normal_awakenings_emojis], delimiter=' ') + + @staticmethod + def super_awakenings_row(m: "MonsterModel"): + normal_awakenings = len(m.awakenings) - m.superawakening_count + super_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[normal_awakenings:]] + return Box( + Text(get_emoji('sa_questionmark')), + *[Text(e) for e in super_awakenings_emojis], + delimiter=' ') if len(super_awakenings_emojis) > 0 else None + + @staticmethod + def killers_row(m: "MonsterModel", transform_base): + killers = m.killers if m == transform_base else transform_base.killers + killers_text = 'Any' if 'Any' in killers else \ + ' '.join(_killer_latent_emoji(k) for k in killers) + return Box( + BoldText('Available killers:'), + Text('\N{DOWN-POINTING RED TRIANGLE}' if m != transform_base else ''), + Text('[{} slots]'.format(m.latent_slots if m == transform_base + else transform_base.latent_slots)), + Text(killers_text), + delimiter=' ' + ) + + @classmethod + def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["MonsterModel"]): + + active_skill = m.active_skill + if len(previous_transforms) == 0: + active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) \ + if active_skill else 'None' + else: + skill_texts = [] + previous_transforms.reverse() + for i, mon in enumerate(previous_transforms): + skill = mon.active_skill + # we can assume skill is not None because the monster transforms + cooldown_text = '({}cd)'.format(str(skill.turn_max)) + if skill.turn_min != skill.turn_max: + cooldown_text = '{} -> {}'.format(skill.turn_min, skill.turn_max) + skill_texts.append('{}{}'.format(cls.transform_emojis[i % len(cls.transform_emojis)], cooldown_text)) + skill_texts.append('{} ({} cd)'.format(cls.up_emoji, m.active_skill.turn_max)) + active_cd = ' '.join(skill_texts) + return Box( + BoldText('Active Skill'), + BoldText(active_cd), + delimiter=' ' + ) + + @staticmethod + def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): + return Box( + BoldText('Leader Skill'), + BoldText(ls_multiplier_text(m.leader_skill) if lsmultiplier == LsMultiplier.lsdouble + else get_emoji('1x') + ' ' + ls_single_multiplier_text(m.leader_skill)), + BoldText('(' + get_emoji( + '\N{DOWN-POINTING RED TRIANGLE}') + '7x6)') if m != transform_base and transform_base.leader_skill.is_7x6 else None, + delimiter=' ' + ) + + @classmethod + @abstractmethod + def embed(cls, state: ViewStateBaseId) -> EmbedView: + ... diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 3ad566a7..8383aa97 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -11,8 +11,8 @@ from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx -from padinfo.view.base import BaseIdView -from padinfo.view.common import get_monster_from_ims, invalid_monster_text, leader_skill_header +from padinfo.view.common import get_monster_from_ims, invalid_monster_text +from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import MonsterEvolution, ViewStateBaseId @@ -156,10 +156,6 @@ def _get_awakening_text(awakening: "AwakeningModel"): return get_awakening_emoji(awakening.awoken_skill_id, awakening.name) -def _killer_latent_emoji(latent_name: str): - return get_emoji('latent_killer_{}'.format(latent_name.lower())) - - def _get_awakening_emoji_for_stats(m: "MonsterModel", i: int): return get_awakening_emoji(i) if m.awakening_count(i) and not m.is_equip else '' @@ -181,7 +177,7 @@ def evos_embed_field(state: ViewStateBaseId): field_text = "**Evos**" help_text = "" # this isn't used right now, but maybe later if discord changes the api for embed titles...? - help_link = "https://github.com/TsubakiBotPad/pad-cogs/wiki/Evolutions-mini-view" + _help_link = "https://github.com/TsubakiBotPad/pad-cogs/wiki/Evolutions-mini-view" legend_parts = [] if any(not alt_evo.evolution.reversible for alt_evo in state.alt_monsters if alt_evo.evolution): legend_parts.append("⌊Irreversible⌋") @@ -199,26 +195,8 @@ def evos_embed_field(state: ViewStateBaseId): ) -class IdView(BaseIdView): +class IdView(BaseIdMainView): VIEW_TYPE = 'Id' - transform_emojis = ['\N{DOWN-POINTING RED TRIANGLE}', get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), - get_emoji('downb'), get_emoji('downp')] - up_emoji = get_emoji('upgr') - - @staticmethod - def normal_awakenings_row(m: "MonsterModel"): - normal_awakenings = len(m.awakenings) - m.superawakening_count - normal_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[:normal_awakenings]] - return Box(*[Text(e) for e in normal_awakenings_emojis], delimiter=' ') - - @staticmethod - def super_awakenings_row(m: "MonsterModel"): - normal_awakenings = len(m.awakenings) - m.superawakening_count - super_awakenings_emojis = [_get_awakening_text(a) for a in m.awakenings[normal_awakenings:]] - return Box( - Text(get_emoji('sa_questionmark')), - *[Text(e) for e in super_awakenings_emojis], - delimiter=' ') if len(super_awakenings_emojis) > 0 else None @classmethod def all_awakenings_row(cls, m: "MonsterModel", transform_base): @@ -228,7 +206,7 @@ def all_awakenings_row(cls, m: "MonsterModel", transform_base): return Box( Box( cls.up_emoji if m != transform_base else '', - IdView.normal_awakenings_row(m), + cls.normal_awakenings_row(m), delimiter=' ' ), Box( @@ -236,21 +214,7 @@ def all_awakenings_row(cls, m: "MonsterModel", transform_base): IdView.all_awakenings_row(transform_base, transform_base), delimiter=' ' ) if m != transform_base else None, - IdView.super_awakenings_row(m), - ) - - @staticmethod - def killers_row(m: "MonsterModel", transform_base): - killers = m.killers if m == transform_base else transform_base.killers - killers_text = 'Any' if 'Any' in killers else \ - ' '.join(_killer_latent_emoji(k) for k in killers) - return Box( - BoldText('Available killers:'), - Text('\N{DOWN-POINTING RED TRIANGLE}' if m != transform_base else ''), - Text('[{} slots]'.format(m.latent_slots if m == transform_base - else transform_base.latent_slots)), - Text(killers_text), - delimiter=' ' + cls.super_awakenings_row(m), ) @staticmethod @@ -322,31 +286,6 @@ def stats_header(cls, m: "MonsterModel", previous_evolutions, query_settings: Qu ) return header - @classmethod - def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["MonsterModel"]): - - active_skill = m.active_skill - if len(previous_transforms) == 0: - active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) \ - if active_skill else 'None' - else: - skill_texts = [] - previous_transforms.reverse() - for i, mon in enumerate(previous_transforms): - skill = mon.active_skill - # we can assume skill is not None because the monster transforms - cooldown_text = '({}cd)'.format(str(skill.turn_max)) - if skill.turn_min != skill.turn_max: - cooldown_text = '{} -> {}'.format(skill.turn_min, skill.turn_max) - skill_texts.append('{}{}'.format(cls.transform_emojis[i % len(cls.transform_emojis)], cooldown_text)) - skill_texts.append('{} ({} cd)'.format(cls.up_emoji, m.active_skill.turn_max)) - active_cd = ' '.join(skill_texts) - return Box( - BoldText('Active Skill'), - BoldText(active_cd), - delimiter=' ' - ) - @classmethod def embed(cls, state: IdViewState): m = state.monster @@ -355,7 +294,7 @@ def embed(cls, state: IdViewState): '/'.join(['{}'.format(t.name) for t in m.types]), Box( IdView.all_awakenings_row(m, state.transform_base), - IdView.killers_row(m, state.transform_base) + cls.killers_row(m, state.transform_base) ) ), EmbedField( @@ -369,11 +308,11 @@ def embed(cls, state: IdViewState): inline=True ), EmbedField( - IdView.active_skill_header(m, state.previous_transforms).to_markdown(), + cls.active_skill_header(m, state.previous_transforms).to_markdown(), Text(m.active_skill.desc if m.active_skill else 'None') ), EmbedField( - leader_skill_header(m, state.query_settings.lsmultiplier, state.transform_base).to_markdown(), + cls.leader_skill_header(m, state.query_settings.lsmultiplier, state.transform_base).to_markdown(), Text(m.leader_skill.desc if m.leader_skill else 'None') ), evos_embed_field(state) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 3ee2516a..03854bd1 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -4,35 +4,38 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import Text, BoldText, LabeledText from discordmenu.embed.view import EmbedView - from tsutils.enums import LsMultiplier from tsutils.menu.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx -from padinfo.view.common import leader_skill_header +from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base import ViewStateBase -from padinfo.view.id import IdView if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel + from dbcog.database_context import DbContext + -BASE_EMOJI = '\N{DOWN-POINTING RED TRIANGLE}' -TRANSFORM_EMOJI = '\N{UP-POINTING RED TRIANGLE}' +class TransformInfoQueriedProps: + def __init__(self, acquire_raw, previous_transforms): + self.acquire_raw = acquire_raw + self.previous_transforms = previous_transforms class TransformInfoViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, - acquire_raw, monster_ids, is_jp_buffed, query_settings, + tfinfo_queried_props: TransformInfoQueriedProps, monster_ids, is_jp_buffed, query_settings, reaction_list): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) self.color = color self.base_mon = base_mon self.transformed_mon = transformed_mon - self.acquire_raw = acquire_raw + self.acquire_raw = tfinfo_queried_props.acquire_raw + self.previous_transforms = tfinfo_queried_props.previous_transforms self.monster_ids = monster_ids self.is_jp_buffed = is_jp_buffed self.query_settings: QuerySettings = query_settings @@ -60,21 +63,23 @@ async def deserialize(dbcog, user_config: UserConfig, ims: dict): base_mon = dbcog.get_monster(base_mon_id, server=query_settings.server) transformed_mon = dbcog.get_monster(transformed_mon_id, server=query_settings.server) - acquire_raw = await TransformInfoViewState.do_query(dbcog, base_mon, transformed_mon) + tfinfo_queried_props = await TransformInfoViewState.do_query(dbcog, transformed_mon) reaction_list = ims['reaction_list'] - is_jp_buffed = dbcog.database.graph.monster_is_discrepant(base_mon) \ - or dbcog.database.graph.monster_is_discrepant(transformed_mon) + is_jp_buffed = dbcog.database.graph.monster_is_discrepant( + base_mon) or dbcog.database.graph.monster_is_discrepant(transformed_mon) return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, - base_mon, transformed_mon, acquire_raw, monster_ids, is_jp_buffed, + base_mon, transformed_mon, tfinfo_queried_props, monster_ids, is_jp_buffed, query_settings, reaction_list=reaction_list) @staticmethod - async def do_query(dbcog, base_mon, transformed_mon): + async def do_query(dbcog, transformed_mon) -> TransformInfoQueriedProps: db_context = dbcog.database + db_context: "DbContext" acquire_raw = db_context.graph.monster_acquisition(transformed_mon) - return acquire_raw + previous_transforms = db_context.graph.get_all_prev_transforms(transformed_mon) + return TransformInfoQueriedProps(acquire_raw, previous_transforms) def _get_tf_stat_diff_text(stat, tf_stat): @@ -89,80 +94,80 @@ def transformat(text: str): return '__{}__'.format(text) -def base_info(m: "MonsterModel"): - return Box( - Box( - BASE_EMOJI, - IdView.normal_awakenings_row(m) if len(m.awakenings) != 0 - else Box(Text('No Awakenings')), - delimiter=' ' - ), - IdView.super_awakenings_row(m) - ) - - -def card_info(base_mon: "MonsterModel", transformed_mon: "MonsterModel", acquire_raw): - rarity = Box( - LabeledText('Rarity', '{} -> {}'.format(base_mon.rarity, transformed_mon.rarity)), - Text("" if base_mon.orb_skin_id is None else "(Orb Skin)"), - delimiter=' ' - ) - cost = LabeledText('Cost', '{} -> {}'.format(base_mon.cost, transformed_mon.cost)) - acquire = BoldText(acquire_raw) if acquire_raw else None - - return Box(rarity, cost, acquire) - - -def stats(base_mon: "MonsterModel", transformed_mon: "MonsterModel"): - base_hp, base_atk, base_rcv, base_weighted = base_mon.stats() - tf_hp, tf_atk, tf_rcv, tf_weighed = transformed_mon.stats() - return Box( - LabeledText('HP', _get_tf_stat_diff_text(base_hp, tf_hp)), - LabeledText('ATK', _get_tf_stat_diff_text(base_atk, tf_atk)), - LabeledText('RCV', _get_tf_stat_diff_text(base_rcv, tf_rcv)) - ) - - -def transform_active_header(m: "MonsterModel"): - active_skill = m.active_skill - active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' - return Box( - TRANSFORM_EMOJI, - BoldText('Transform Active Skill {}'.format(active_cd)), - delimiter=' ' - ) - - -def base_active_header(m: "MonsterModel"): - return Box( - BASE_EMOJI, - BoldText('Base'), - IdView.active_skill_header(m, m), - delimiter=' ' - ) +class TransformInfoView(BaseIdMainView): + VIEW_TYPE = 'TransformInfo' + @classmethod + def base_info(cls, m: "MonsterModel"): + return Box( + Box( + cls.down_emoji, + cls.normal_awakenings_row(m) if len(m.awakenings) != 0 + else Box(Text('No Awakenings')), + delimiter=' ' + ), + cls.super_awakenings_row(m) + ) -def leader_header(m: "MonsterModel", is_base: bool, lsmultiplier: LsMultiplier, base_mon: "MonsterModel"): - if is_base: - emoji = BASE_EMOJI - label = 'Base' - else: - emoji = TRANSFORM_EMOJI - label = 'Transform' + @classmethod + def card_info(cls, base_mon: "MonsterModel", transformed_mon: "MonsterModel", acquire_raw): + rarity = Box( + LabeledText('Rarity', '{} -> {}'.format(base_mon.rarity, transformed_mon.rarity)), + Text("" if base_mon.orb_skin_id is None else "(Orb Skin)"), + delimiter=' ' + ) + cost = LabeledText('Cost', '{} -> {}'.format(base_mon.cost, transformed_mon.cost)) + acquire = BoldText(acquire_raw) if acquire_raw else None + + return Box(rarity, cost, acquire) + + @classmethod + def stats(cls, base_mon: "MonsterModel", transformed_mon: "MonsterModel"): + base_hp, base_atk, base_rcv, base_weighted = base_mon.stats() + tf_hp, tf_atk, tf_rcv, tf_weighed = transformed_mon.stats() + return Box( + LabeledText('HP', _get_tf_stat_diff_text(base_hp, tf_hp)), + LabeledText('ATK', _get_tf_stat_diff_text(base_atk, tf_atk)), + LabeledText('RCV', _get_tf_stat_diff_text(base_rcv, tf_rcv)) + ) - return Box( - emoji, - BoldText(label), - leader_skill_header(m, lsmultiplier, base_mon).to_markdown(), - delimiter=' ' - ) + @classmethod + def transform_active_header(cls, m: "MonsterModel"): + active_skill = m.active_skill + active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' + return Box( + cls.up_emoji, + BoldText('Transform Active Skill {}'.format(active_cd)), + delimiter=' ' + ) + @classmethod + def base_active_header(cls, m: "MonsterModel"): + return Box( + cls.down_emoji, + BoldText('Base'), + cls.active_skill_header(m, []), + delimiter=' ' + ) -class TransformInfoView: - VIEW_TYPE = 'TransformInfo' + @classmethod + def leader_header(cls, m: "MonsterModel", is_base: bool, lsmultiplier: LsMultiplier, base_mon: "MonsterModel"): + if is_base: + emoji = cls.down_emoji + label = 'Base' + else: + emoji = cls.up_emoji + label = 'Transform' + + return Box( + emoji, + BoldText(label), + cls.leader_skill_header(m, lsmultiplier, base_mon).to_markdown(), + delimiter=' ' + ) - @staticmethod - def embed(state: TransformInfoViewState): + @classmethod + def embed(cls, state: TransformInfoViewState): base_mon = state.base_mon transformed_mon = state.transformed_mon lsmultiplier = state.query_settings.lsmultiplier @@ -171,40 +176,41 @@ def embed(state: TransformInfoViewState): '/'.join(['{}'.format(t.name) for t in transformed_mon.types]), Box( Box( - TRANSFORM_EMOJI, - IdView.normal_awakenings_row(transformed_mon) + cls.up_emoji, + cls.normal_awakenings_row(transformed_mon) if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), delimiter=' ' ), - base_info(base_mon), - IdView.killers_row(base_mon, base_mon) + TransformInfoView.base_info(base_mon), + cls.killers_row(base_mon, base_mon) ), ), EmbedField( BoldText(transformat('Card info')), - card_info(base_mon, transformed_mon, state.acquire_raw), + TransformInfoView.card_info(base_mon, transformed_mon, state.acquire_raw), inline=True ), EmbedField( BoldText('Stats -> ' + transformat('Transform')), - stats(base_mon, transformed_mon), + TransformInfoView.stats(base_mon, transformed_mon), inline=True ), EmbedField( - transformat(transform_active_header(transformed_mon).to_markdown()), + transformat(TransformInfoView.transform_active_header(transformed_mon).to_markdown()), Box( Text(transformed_mon.active_skill.desc if transformed_mon.active_skill else 'None'), - base_active_header(base_mon).to_markdown(), + TransformInfoView.base_active_header(base_mon).to_markdown(), Text(base_mon.active_skill.desc if base_mon.active_skill else 'None') ) ), EmbedField( - transformat(leader_header(transformed_mon, False, lsmultiplier, base_mon).to_markdown()), + transformat( + TransformInfoView.leader_header(transformed_mon, False, lsmultiplier, base_mon).to_markdown()), Box( Text(transformed_mon.leader_skill.desc if transformed_mon.leader_skill else 'None'), - leader_header(base_mon, True, lsmultiplier, base_mon).to_markdown(), + TransformInfoView.leader_header(base_mon, True, lsmultiplier, base_mon).to_markdown(), Text(base_mon.leader_skill.desc if base_mon.leader_skill else 'None') ) ) From f9741d92987a8f51301b4af746f31e765a40c371 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 20 Sep 2021 06:15:41 -0500 Subject: [PATCH 0961/1220] Fix hidden emojis (#1433) --- padinfo/menu/transforminfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 263bdec0..f6292d14 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -116,7 +116,7 @@ class TransformInfoMenuPanes(MenuPanes): @classmethod def get_user_reaction_list(cls, number_of_monsters: int): if number_of_monsters > 2: - cls.HIDDEN_EMOJIS = list(TransformInfoEmoji.up) + cls.HIDDEN_EMOJIS = [TransformInfoEmoji.up] else: cls.HIDDEN_EMOJIS = [TransformInfoEmoji.one] From 1da58e09ce110f4286e2f830b2b96915f20fc7b1 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 20 Sep 2021 04:25:54 -0700 Subject: [PATCH 0962/1220] Use downr instead of DOWN-POINTING RED TRIANGLE (#1434) --- padinfo/menu/transforminfo.py | 2 +- padinfo/view/components/base_id_main_view.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index f6292d14..2a98c009 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -81,7 +81,7 @@ def id_control(state: IdViewState, reaction_list): class TransformInfoEmoji: home = emoji_buttons['home'] - down = '\N{DOWN-POINTING RED TRIANGLE}' + down = ('downr', '\N{DOWN-POINTING RED TRIANGLE}') up = ('upgr', '\N{UP-POINTING RED TRIANGLE}') one = char_to_emoji('1') two = char_to_emoji('2') diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index a3e79361..a0e9f434 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -25,7 +25,7 @@ def _killer_latent_emoji(latent_name: str): class BaseIdMainView(BaseIdView): - transform_emojis = ['\N{DOWN-POINTING RED TRIANGLE}', get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), + transform_emojis = [get_emoji('downr'), get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), get_emoji('downb'), get_emoji('downp')] up_emoji = get_emoji('upgr') down_emoji = transform_emojis[0] From a6476d3cad7b6e71e391bd0c0cc89b6ed0db8ce0 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 20 Sep 2021 06:35:13 -0500 Subject: [PATCH 0963/1220] Add a couple awakening modifiers (#1435) --- dbcog/token_mappings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 76f4dc81..f1e598ef 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -98,9 +98,9 @@ class EvoTypes(Enum): AwokenSkills.REDUCEDARK: ('elresd', 'elres'), AwokenSkills.AUTOHEAL: ('autoheal',), AwokenSkills.BINDRES: ('unbindable', 'bindres'), - AwokenSkills.BLINDRES: ('resb',), - AwokenSkills.JAMMERRES: ('resj',), - AwokenSkills.POISONRES: ('resp',), + AwokenSkills.BLINDRES: ('resb', 'bres'), + AwokenSkills.JAMMERRES: ('resj', 'jres'), + AwokenSkills.POISONRES: ('resp', 'pres'), AwokenSkills.ENHANCEDRED: ('oer', 'oe'), AwokenSkills.ENHANCEDBLUE: ('oeb', 'oe'), AwokenSkills.ENHANCEDGREEN: ('oeg', 'oe'), @@ -131,7 +131,7 @@ class EvoTypes(Enum): AwokenSkills.FODDERKILLER: ('enhancekiller', 'enhk', 'a2killer'), AwokenSkills.REDEEMKILLER: ('vendorkiller', 'vendork', 'a2killer'), AwokenSkills.ENHCOMBO7C: ('7c',), - AwokenSkills.GUARDBREAK: ('gb',), + AwokenSkills.GUARDBREAK: ('gb', 'guardbreak'), AwokenSkills.FUA: ('fua',), AwokenSkills.ENHTEAMHP: ('teamhp', 'thp'), AwokenSkills.ENHTEAMRCV: ('teamrcv', 'trcv'), From 3909ace6d6af87dd2e85875995884433724aa7d7 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 20 Sep 2021 09:56:24 -0500 Subject: [PATCH 0964/1220] Split/Grouped evos for results in ^ids (#1437) --- padinfo/padinfo.py | 69 +++++++++++++++++--------- padinfo/view/monster_list/id_search.py | 12 ++++- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 7621dabb..c5a04f3a 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -16,7 +16,8 @@ from tabulate import tabulate from tsutils.cogs.donations import is_donor from tsutils.emoji import char_to_emoji -from tsutils.enums import AltEvoSort, CardPlusModifier, LsMultiplier, Server, CardLevelModifier, CardModeModifier +from tsutils.enums import AltEvoSort, CardPlusModifier, LsMultiplier, Server, CardLevelModifier, CardModeModifier, \ + EvoGrouping from tsutils.json_utils import safe_read_json from tsutils.query_settings import QuerySettings from tsutils.user_interaction import get_user_confirmation, send_cancellation_message @@ -151,7 +152,6 @@ async def reload_nicknames(self): try: emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) emoji_cache.refresh_from_discord_bot(self.bot) - dbcog = await self.get_dbcog() except Exception as ex: wait_time = 5 logger.exception("reload padinfo loop caught exception " + str(ex)) @@ -212,11 +212,10 @@ async def _get_monster(self, ctx, query: str) -> Optional["MonsterModel"]: raw_query = query goodquery = None - if query[0] in dbcog.token_maps.ID1_SUPPORTED \ - and query[1:] in dbcog.indexes[Server.COMBINED].all_name_tokens: + ID1_SUPPORTED = dbcog.token_maps.ID1_SUPPORTED + if query[0] in ID1_SUPPORTED and query[1:] in dbcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[0], query[1:]] - elif query[:2] in dbcog.token_maps.ID1_SUPPORTED \ - and query[2:] in dbcog.indexes[Server.COMBINED].all_name_tokens: + elif query[:2] in ID1_SUPPORTED and query[2:] in dbcog.indexes[Server.COMBINED].all_name_tokens: goodquery = [query[:2], query[2:]] if goodquery: @@ -830,7 +829,8 @@ async def transforminfo(self, ctx, *, query): query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, - color, base_mon, transformed_mon, tfinfo_queried_props, monster_ids, is_jp_buffed, + color, base_mon, transformed_mon, tfinfo_queried_props, monster_ids, + is_jp_buffed, query_settings, reaction_list=reaction_list) menu = TransformInfoMenu.menu() @@ -925,7 +925,7 @@ async def idset(self, ctx): @idset.command() async def scroll(self, ctx, value): - """Switch between number scroll and evo scroll + """Switch between number scroll & evo scroll [p]idset scroll number [p]idset scroll evo""" @@ -944,7 +944,7 @@ async def scroll(self, ctx, value): @idset.command() async def survey(self, ctx, value): - """Change how often you see the id survey + """How often you see the id survey [p]idset survey always (Always see survey after using id) [p]idset survey sometimes (See survey some of the time after using id) @@ -959,7 +959,7 @@ async def survey(self, ctx, value): @is_donor() @idset.command() async def embedcolor(self, ctx, *, color): - """(DONOR ONLY) Change the color of all your `[p]id` embeds! + """(DONOR ONLY) The color of all your `[p]id` embeds! Examples: [p]idset embedcolor green @@ -979,7 +979,7 @@ async def embedcolor(self, ctx, *, color): @idset.command(usage="") async def naprio(self, ctx, value: bool): - """Change whether [p]id will default away from new evos of monsters that aren't in NA yet""" + """Whether `[p]id` will default away from new evos of monsters that aren't in NA yet""" async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: # The current na_prio enum has 0 and 1 instead of False and True as its values fm_flags['na_prio'] = int(value) @@ -987,7 +987,7 @@ async def naprio(self, ctx, value: bool): @idset.command() async def server(self, ctx, server: str): - """Change the server to be used for your `[p]id` queries + """The server used for your `[p]id` queries `[p]idset server default`: Include both NA & JP cards. You can still use `--na` as needed. `[p]idset server na`: Include only NA cards. You can still use `--allservers` as needed. @@ -1008,12 +1008,11 @@ async def server(self, ctx, server: str): @idset.command() async def evosort(self, ctx, value: str): - """Change the order for scrolling alt evos in your `[p]id` queries + """The order for scrolling alt evos in your `[p]id` queries `[p]idset evosort numerical`: Show alt evos sorted by card ID number. `[p]idset evosort dfs`: Show alt evos in a depth-first-sort order, starting with the base of the tree. """ - dbcog = await self.get_dbcog() async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: value = value.lower() if value == "dfs": @@ -1028,12 +1027,11 @@ async def evosort(self, ctx, value: str): @idset.command() async def lsmultiplier(self, ctx, value: str): - """Change the display of leader skill multipliers in your `[p]id` queries + """The display of LS multipliers in your `[p]id` queries `[p]idset lsmultiplier double`: [Default] Show leader skill multipliers assuming the card is paired with itself. `[p]idset lsmultiplier single`: Show leader skill multipliers of just the single card. """ - dbcog = await self.get_dbcog() async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: value = value.lower() if value == "double": @@ -1053,12 +1051,11 @@ async def lsmultiplier(self, ctx, value: str): @idset.command() async def cardplus(self, ctx, value: str): - """Change the monster stat plus points amount in your your `[p]id` queries + """The monster stat plus points amount in your `[p]id` queries `[p]idset cardplus 297`: [Default] Show cards with +297 stats. `[p]idset cardplus 0`: Show cards with +0 stats. """ - dbcog = await self.get_dbcog() async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: value = value.lower() if value == "297": @@ -1078,7 +1075,7 @@ async def cardplus(self, ctx, value: str): @idset.command() async def cardmode(self, ctx, value: str): - """Change mode between solo and coop in your `[p]id` queries + """Switch between solo and coop `[p]idset mode solo`: [Default] Show cards with stats in solo `[p]idset mode coop`: Show cards with stats in coop (i.e. multiboost) @@ -1102,7 +1099,7 @@ async def cardmode(self, ctx, value: str): @idset.command() async def cardlevel(self, ctx, value: str): - """Change the limitbreak level in your your `[p]id` queries + """The limitbreak level in your `[p]id` queries `[p]idset cardlevel 110`: [Default] Show LB stats at 110. `[p]idset cardlevel 120`: Show LB stats at 120. @@ -1119,7 +1116,31 @@ async def cardlevel(self, ctx, value: str): not_value_flag = 'lv120' else: await ctx.send( - f'Please input an allowed value, either `297` or `0`.') + f'Please input an allowed value, either `110` or `120`.') + return + await ctx.send( + f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + + @idset.command() + async def evogrouping(self, ctx, value: str): + """If trees are combined in your `[p]ids` queries + + `[p]idset evogrouping grouped`: [Default] Show trees grouped. + `[p]idset evogrouping split`: Show individual evos of trees split apart. + """ + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: + value = value.lower() + if value == "split": + fm_flags['evogrouping'] = EvoGrouping.splitevos.value + not_value = 'grouped' + not_value_flag = 'groupevos' + elif value == "grouped": + fm_flags['evogrouping'] = EvoGrouping.groupevos.value + not_value = 'split' + not_value_flag = 'splitevos' + else: + await ctx.send( + f'Please input an allowed value, either `grouped` or `split`.') return await ctx.send( f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @@ -1456,7 +1477,7 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): end = monster.level if monster.exp_to_level(start + 1) - monster.exp_to_level(start) < offset and start <= 99 \ - or offset > 5e6 and start <= 110 or offset > 20e6: + or offset > 5e6 and start <= 110 or offset > 20e6: return await send_cancellation_message(ctx, "Offset too large.") if start <= 0 or end > 120 or end < start or (start == end and offset): return await send_cancellation_message(ctx, f"Invalid bounds ({start}[{offset}] - {end}).") @@ -1479,8 +1500,8 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): dbcog = await self.get_dbcog() - - monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id, query_settings) if monster_list is None: await ctx.send("No monster matched.") diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py index 1ae4911c..c0d59e98 100644 --- a/padinfo/view/monster_list/id_search.py +++ b/padinfo/view/monster_list/id_search.py @@ -1,5 +1,8 @@ from typing import TYPE_CHECKING, List, Optional +from tsutils.enums import EvoGrouping +from tsutils.query_settings import QuerySettings + from padinfo.view.monster_list.monster_list import MonsterListViewState if TYPE_CHECKING: @@ -10,12 +13,16 @@ class IdSearchViewState(MonsterListViewState): VIEW_STATE_TYPE = "IdSearch" @classmethod - async def do_query(cls, dbcog, query, original_author_id) -> Optional[List["MonsterModel"]]: + async def do_query(cls, dbcog, query, original_author_id, query_settings: QuerySettings) \ + -> Optional[List["MonsterModel"]]: found_monsters = await dbcog.find_monsters(query, original_author_id) if not found_monsters: return None + print(query_settings.serialize()) + if query_settings.evogrouping == EvoGrouping.splitevos: + return found_monsters used = set() monster_list = [] for mon in found_monsters: @@ -28,5 +35,6 @@ async def do_query(cls, dbcog, query, original_author_id) -> Optional[List["Mons @classmethod async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: - monster_list = await cls.do_query(dbcog, ims['raw_query'], ims['original_author_id']) + monster_list = await cls.do_query(dbcog, ims['raw_query'], ims['original_author_id'], + QuerySettings.deserialize(ims['query_settings'])) return monster_list From e4f541f01ac10e7f8612a042ab3d1b9516925aa5 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 20 Sep 2021 15:24:43 -0700 Subject: [PATCH 0965/1220] Fix NADiff (#1438) --- padinfo/padinfo.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index c5a04f3a..8e6e51bf 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -16,8 +16,8 @@ from tabulate import tabulate from tsutils.cogs.donations import is_donor from tsutils.emoji import char_to_emoji -from tsutils.enums import AltEvoSort, CardPlusModifier, LsMultiplier, Server, CardLevelModifier, CardModeModifier, \ - EvoGrouping +from tsutils.enums import AltEvoSort, CardLevelModifier, CardModeModifier, CardPlusModifier, EvoGrouping, LsMultiplier, \ + Server from tsutils.json_utils import safe_read_json from tsutils.query_settings import QuerySettings from tsutils.user_interaction import get_user_confirmation, send_cancellation_message @@ -350,16 +350,13 @@ async def nadiff(self, ctx, *, query: str): return alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) - transform_base, true_evo_type_raw, acquire_raw, base_rarity = \ - await IdViewState.do_query(dbcog, monster) + id_queried_props = await IdViewState.do_query(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) state = IdViewState(original_author_id, NaDiffMenu.MENU_TYPE, raw_query, query, color, monster, - alt_monsters, is_jp_buffed, query_settings, - transform_base, true_evo_type_raw, acquire_raw, base_rarity, - ) + alt_monsters, is_jp_buffed, query_settings, id_queried_props) menu = NaDiffMenu.menu() message = state.get_na_diff_invalid_message() if message: From 333555b2467091a69f1f5cf464d7f6b8d3f7cb77 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 21 Sep 2021 00:09:49 -0500 Subject: [PATCH 0966/1220] Refactor buttoninfo a bit (#1439) --- padinfo/core/button_info.py | 128 ++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 72 deletions(-) diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index c4ecdf02..143301e4 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -47,7 +47,7 @@ class ButtonInfo: - def get_info(self, dbcog, monster_model): + def get_info(self, dbcog, monster): """ Usage: ^buttoninfo Vajrayasaka @@ -66,54 +66,57 @@ def get_info(self, dbcog, monster_model): with subattr: 10990.863 just subattr: whatever """ - max_level = LIMIT_BREAK_LEVEL if monster_model.limit_mult != 0 else monster_model.level - slb_level = SUPER_LIMIT_BREAK_LEVEL if monster_model.limit_mult != 0 else None - max_atk_latents = monster_model.latent_slots / 2 + max_level = LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else monster.level + slb_level = SUPER_LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else None + max_atk_latents = monster.latent_slots / 2 - sub_attr_multiplier = self._get_sub_attr_multiplier(monster_model) + sub_attr_multiplier = self._get_sub_attr_multiplier(monster) result = ButtonInfoResult() - result.main_damage = self._calculate_coop_damage(dbcog, monster_model, max_level) + result.main_damage = self._calculate_coop_damage(dbcog, monster, max_level) result.sub_damage = result.main_damage * sub_attr_multiplier result.total_damage = result.main_damage + result.sub_damage + if slb_level is None: result.main_slb_damage = None result.sub_slb_damage = None result.total_slb_damage = None else: - result.main_slb_damage = self._calculate_coop_damage(dbcog, monster_model, slb_level) + result.main_slb_damage = self._calculate_coop_damage(dbcog, monster, slb_level) result.sub_slb_damage = result.main_slb_damage * sub_attr_multiplier result.total_slb_damage = result.main_slb_damage + result.sub_slb_damage - result.main_solo_damage = self._calculate_solo_damage(dbcog, monster_model, max_level) + result.main_solo_damage = self._calculate_solo_damage(dbcog, monster, max_level) result.sub_solo_damage = result.main_solo_damage * sub_attr_multiplier result.total_solo_damage = result.main_solo_damage + result.sub_solo_damage + if slb_level is None: result.main_solo_slb_damage = None result.sub_solo_slb_damage = None result.total_solo_slb_damage = None else: - result.main_solo_slb_damage = self._calculate_solo_damage(dbcog, monster_model, slb_level) + result.main_solo_slb_damage = self._calculate_solo_damage(dbcog, monster, slb_level) result.sub_solo_slb_damage = result.main_solo_slb_damage * sub_attr_multiplier result.total_solo_slb_damage = result.main_solo_slb_damage + result.sub_solo_slb_damage result.main_damage_with_atk_latent = self._calculate_coop_damage( - dbcog, monster_model, max_level, num_atkplus_latent=max_atk_latents) + dbcog, monster, max_level, num_atkplus_latent=max_atk_latents) result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent + if slb_level is None: result.main_damage_with_slb_atk_latent = None result.sub_damage_with_slb_atk_latent = None result.total_damage_with_slb_atk_latent = None else: result.main_damage_with_slb_atk_latent = self._calculate_coop_damage( - dbcog, monster_model, slb_level, num_atkplus2_latent=max_atk_latents) + dbcog, monster, slb_level, num_atkplus2_latent=max_atk_latents) result.sub_damage_with_slb_atk_latent = result.main_damage_with_slb_atk_latent * sub_attr_multiplier result.total_damage_with_slb_atk_latent = (result.main_damage_with_slb_atk_latent - + result.sub_damage_with_slb_atk_latent) + + result.sub_damage_with_slb_atk_latent) result.main_solo_damage_with_atk_latent = self._calculate_solo_damage( - dbcog, monster_model, max_level, num_atkplus_latent=max_atk_latents) + dbcog, monster, max_level, num_atkplus_latent=max_atk_latents) result.sub_solo_damage_with_atk_latent = result.main_solo_damage_with_atk_latent * sub_attr_multiplier result.total_solo_damage_with_atk_latent = (result.main_solo_damage_with_atk_latent + result.sub_solo_damage_with_atk_latent) @@ -123,67 +126,60 @@ def get_info(self, dbcog, monster_model): result.total_solo_damage_with_slb_atk_latent = None else: result.main_solo_damage_with_slb_atk_latent = self._calculate_solo_damage( - dbcog, monster_model, slb_level, num_atkplus2_latent=max_atk_latents) + dbcog, monster, slb_level, num_atkplus2_latent=max_atk_latents) result.sub_solo_damage_with_slb_atk_latent = (result.main_solo_damage_with_slb_atk_latent * sub_attr_multiplier) result.total_solo_damage_with_slb_atk_latent = (result.main_solo_damage_with_slb_atk_latent + result.sub_solo_damage_with_slb_atk_latent) - result.card_btn_str = self._get_card_coop_damage(CARD_BUTTONS, dbcog, monster_model) - result.card_btn_solo_str = self._get_card_solo_damage(CARD_BUTTONS, dbcog, monster_model) - result.card_btn_slb_str = self._get_card_coop_slb_damage(CARD_BUTTONS, dbcog, monster_model) - result.card_btn_solo_slb_str = self._get_card_solo_slb_damage(CARD_BUTTONS, dbcog, monster_model) - result.team_btn_str = self._get_team_coop_damage(TEAM_BUTTONS, dbcog, monster_model) - result.team_btn_solo_str = self._get_team_solo_damage(TEAM_BUTTONS, dbcog, monster_model) - result.team_btn_slb_str = self._get_team_coop_slb_damage(TEAM_BUTTONS, dbcog, monster_model) - result.team_btn_solo_slb_str = self._get_team_solo_slb_damage(TEAM_BUTTONS, dbcog, monster_model) + result.card_btn_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=LIMIT_BREAK_LEVEL) + result.card_btn_solo_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, + multiplayer=False, limit_break=LIMIT_BREAK_LEVEL) + result.card_btn_slb_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=SUPER_LIMIT_BREAK_LEVEL) + result.card_btn_solo_slb_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, + multiplayer=False, + limit_break=SUPER_LIMIT_BREAK_LEVEL) + + result.team_btn_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=LIMIT_BREAK_LEVEL) + result.team_btn_solo_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=False, limit_break=LIMIT_BREAK_LEVEL) + result.team_btn_slb_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=SUPER_LIMIT_BREAK_LEVEL) + result.team_btn_solo_slb_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=False, + limit_break=SUPER_LIMIT_BREAK_LEVEL) return result - def _calculate_coop_damage(self, dbcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): - return self._calculate_damage(dbcog, monster_model, level, True, num_atkplus_latent, num_atkplus2_latent) + def _calculate_coop_damage(self, dbcog, monster, level, num_atkplus_latent=0, num_atkplus2_latent=0): + return self._calculate_damage(dbcog, monster, level, True, num_atkplus_latent, num_atkplus2_latent) - def _calculate_solo_damage(self, dbcog, monster_model, level, num_atkplus_latent=0, num_atkplus2_latent=0): - return self._calculate_damage(dbcog, monster_model, level, False, num_atkplus_latent, num_atkplus2_latent) + def _calculate_solo_damage(self, dbcog, monster, level, num_atkplus_latent=0, num_atkplus2_latent=0): + return self._calculate_damage(dbcog, monster, level, False, num_atkplus_latent, num_atkplus2_latent) - def _calculate_damage(self, dbcog, monster_model, level, multiplayer, num_atkplus_latent=0, num_atkplus2_latent=0): + @staticmethod + def _calculate_damage(dbcog, monster, level, multiplayer, num_atkplus_latent=0, num_atkplus2_latent=0): stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=num_atkplus_latent, num_atkplus2=num_atkplus2_latent) stat_latents.num_atk_awakening = len( - [x for x in monster_model.awakenings if x.awoken_skill_id == 1]) + [x for x in monster.awakenings if x.awoken_skill_id == 1]) - dmg = dbcog.monster_stats.stat(monster_model, 'atk', level, + dmg = dbcog.monster_stats.stat(monster, 'atk', level, stat_latents=stat_latents, multiplayer=multiplayer) return int(round(dmg)) - def _get_sub_attr_multiplier(self, monster_model): - if monster_model.attr2.value == 6 or monster_model.attr1.value == 6: + @staticmethod + def _get_sub_attr_multiplier(monster): + if monster.attr2.value == 6 or monster.attr1.value == 6: return 0 - if monster_model.attr2.value == monster_model.attr1.value: + if monster.attr2.value == monster.attr1.value: return 1 / 10 return 1 / 3 - def to_string(self, dbcog, monster, info): - card_btn_str = self._get_card_btn_damage(CARD_BUTTONS, dbcog, monster) - team_btn_str = self._get_team_btn_damage(TEAM_BUTTONS, dbcog, monster) - return INFO_STRING.format(monster.monster_id, monster.name_en, int(round(info.main_damage)), int(round(info.sub_damage)), - int(round(info.total_damage)), - int(round(info.main_damage_with_atk_latent)), int( - round(info.sub_damage_with_atk_latent)), - int(round(info.total_damage_with_atk_latent)), card_btn_str, team_btn_str) - - def _get_card_coop_damage(self, card_buttons, dbcog, monster): - return self._get_card_btn_damage(card_buttons, dbcog, monster, True, LIMIT_BREAK_LEVEL) - - def _get_card_solo_damage(self, card_buttons, dbcog, monster): - return self._get_card_btn_damage(card_buttons, dbcog, monster, False, LIMIT_BREAK_LEVEL) - - def _get_card_coop_slb_damage(self, card_buttons, dbcog, monster): - return self._get_card_btn_damage(card_buttons, dbcog, monster, True, SUPER_LIMIT_BREAK_LEVEL) - - def _get_card_solo_slb_damage(self, card_buttons, dbcog, monster): - return self._get_card_btn_damage(card_buttons, dbcog, monster, False, SUPER_LIMIT_BREAK_LEVEL) - - def _get_card_btn_damage(self, card_buttons, dbcog, monster, multiplayer, limit_break): + @staticmethod + def _get_card_btn_damage_text(card_buttons, dbcog, monster, multiplayer, limit_break): lines = [] card_buttons.sort(key=lambda x: x.mult) for card in card_buttons: @@ -192,26 +188,14 @@ def _get_card_btn_damage(self, card_buttons, dbcog, monster, multiplayer, limit_ inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) dmg = int(round(dbcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, - inherited_monster=inherit_model, multiplayer=multiplayer, - inherited_monster_lvl=inherit_max_level))) + inherited_monster=inherit_model, multiplayer=multiplayer, + inherited_monster_lvl=inherit_max_level))) oncolor = '*' if monster.attr1.value == inherit_model.attr1.value or monster.attr1.name == NIL_ATT else ' ' lines.append(CARD_BUTTON_FORMAT.format( card.id, card.name.format(oncolor), card.mult, round(dmg * card.mult, 2))) return "\n".join(lines) - def _get_team_coop_damage(self, team_buttons, dbcog, monster): - return self._get_team_btn_damage(team_buttons, dbcog, monster, True, LIMIT_BREAK_LEVEL) - - def _get_team_solo_damage(self, team_buttons, dbcog, monster): - return self._get_team_btn_damage(team_buttons, dbcog, monster, False, LIMIT_BREAK_LEVEL) - - def _get_team_coop_slb_damage(self, team_buttons, dbcog, monster): - return self._get_team_btn_damage(team_buttons, dbcog, monster, True, SUPER_LIMIT_BREAK_LEVEL) - - def _get_team_solo_slb_damage(self, team_buttons, dbcog, monster): - return self._get_team_btn_damage(team_buttons, dbcog, monster, False, SUPER_LIMIT_BREAK_LEVEL) - - def _get_team_btn_damage(self, team_buttons, dbcog, monster, multiplayer, limit_break): + def _get_team_btn_damage_text(self, team_buttons, dbcog, monster, multiplayer, limit_break): lines = [] team_buttons.sort(key=lambda x: x.mult) for card in team_buttons: @@ -224,17 +208,17 @@ def _get_team_btn_damage(self, team_buttons, dbcog, monster, multiplayer, limit_ inherited_monster=inherit_model, multiplayer=multiplayer, inherited_monster_lvl=inherit_max_level) # do count null main att monster damage as if it were the main att - if(monster.attr1.value in card.att or monster.attr1.name == NIL_ATT): + if monster.attr1.value in card.att or monster.attr1.name == NIL_ATT: total_dmg += dmg # do not double-count null main att - if(monster.attr2.value in card.att and monster.attr1.name != NIL_ATT): + if monster.attr2.value in card.att and monster.attr1.name != NIL_ATT: total_dmg += dmg * self._get_sub_attr_multiplier(monster) colors_str = "" for i in card.att: colors_str += COLORS[i] oncolor = '*' if monster.attr1.value == inherit_model.attr1.value or monster.attr1.name == NIL_ATT else ' ' lines.append(TEAM_BUTTON_FORMAT.format(card.id, card.name.format(oncolor), - card.mult, colors_str, round(total_dmg * card.mult, 2))) + card.mult, colors_str, round(total_dmg * card.mult, 2))) return "\n".join(lines) From 28a9b9babb922f9ee9275d4885ac571a591c0739 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 21 Sep 2021 01:05:54 -0500 Subject: [PATCH 0967/1220] Show atk++ instead of atk+ for 120 buttons (#1440) --- padinfo/core/button_info.py | 16 ++++++++++++++-- padinfo/view/button_info.py | 26 ++++++++++++++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 143301e4..fe6e3316 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -1,4 +1,8 @@ from collections import namedtuple +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel LIMIT_BREAK_LEVEL = 110 SUPER_LIMIT_BREAK_LEVEL = 120 @@ -179,14 +183,22 @@ def _get_sub_attr_multiplier(monster): return 1 / 3 @staticmethod - def _get_card_btn_damage_text(card_buttons, dbcog, monster, multiplayer, limit_break): + def _get_card_btn_damage_text(card_buttons, dbcog, monster: "MonsterModel", multiplayer, limit_break): lines = [] card_buttons.sort(key=lambda x: x.mult) for card in card_buttons: inherit_model = dbcog.get_monster(card.id, server=monster.server_priority) max_level = limit_break if monster.limit_mult != 0 else monster.level inherit_max_level = LIMIT_BREAK_LEVEL if inherit_model.limit_mult != 0 else inherit_model.level - stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=monster.latent_slots / 2) + + num_latent_slots = monster.latent_slots / 2 + num_atkplus = num_latent_slots + num_atkplus2 = 0 + if limit_break == SUPER_LIMIT_BREAK_LEVEL and monster.limit_mult != 0: + num_atkplus = 0 + num_atkplus2 = num_latent_slots + + stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=num_atkplus, num_atkplus2=num_atkplus2) dmg = int(round(dbcog.monster_stats.stat(monster, 'atk', max_level, stat_latents=stat_latents, inherited_monster=inherit_model, multiplayer=multiplayer, inherited_monster_lvl=inherit_max_level))) diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 1639c917..cf564a1b 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -5,7 +5,6 @@ from discordmenu.embed.text import Text, BlockText from discordmenu.embed.view import EmbedView from tsutils.menu.footers import embed_footer_with_state - from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -81,12 +80,6 @@ def set_max_level(self, new_max_level): self.display_options.max_level = new_max_level -def get_max_level(monster, max_110): - limit = str(LIMIT_BREAK_LEVEL) if max_110 else str(SUPER_LIMIT_BREAK_LEVEL) - level_text = limit if monster.limit_mult != 0 else 'Max ({})'.format(monster.level) - return 'Lv. {}'.format(level_text) - - def get_stat_block(main, sub, total): return BlockText( Box( @@ -161,6 +154,19 @@ def get_team_btn_str(info, is_coop, max_110): class ButtonInfoView: VIEW_TYPE = 'ButtonInfo' + @staticmethod + def get_max_level_text(monster, is_max_110): + limit = str(LIMIT_BREAK_LEVEL) if is_max_110 else str(SUPER_LIMIT_BREAK_LEVEL) + level_text = limit if monster.limit_mult != 0 else 'Max ({})'.format(monster.level) + return 'Lv. {}'.format(level_text) + + @staticmethod + def get_common_buttons_title_text(monster, is_max_110): + return 'Common Buttons - Base Card {} {}'.format( + ButtonInfoView.get_max_level_text(monster, is_max_110), + '(Atk+ Latents)' if is_max_110 or monster.limit_mult == 0 else '(Atk++ Latents)' + ) + @staticmethod def embed(state: ButtonInfoViewState): is_coop = state.display_options.players == ButtonInfoOptions.coop @@ -172,7 +178,7 @@ def embed(state: ButtonInfoViewState): fields = [ EmbedField( # this block does not change if the lv110/lv120 toggle is clicked - get_max_level(monster, True), + ButtonInfoView.get_max_level_text(monster, True), Box( Text('Without Latents'), # avoid whitespace after code block @@ -200,7 +206,7 @@ def embed(state: ButtonInfoViewState): inline=True ) if monster.limit_mult != 0 else None, EmbedField( - 'Common Buttons - Base Card {}'.format(get_max_level(monster, max_110)), + ButtonInfoView.get_common_buttons_title_text(monster, max_110), Box( Text('*Inherits are assumed to be the max possible level (up to 110) and +297.*'), # janky, but python gives DeprecationWarnings when using \* in a regular string @@ -216,7 +222,7 @@ def embed(state: ButtonInfoViewState): ) ) if is_desktop else None, EmbedField( - 'Common Buttons - {}'.format(get_max_level(monster, max_110)), + ButtonInfoView.get_common_buttons_title_text(monster, max_110), Box( Text('*Inherits are assumed to be the max possible level (up to 110) and +297.*'), # janky, but python gives DeprecationWarnings when using \* in a regular string From 7495bc79350fd33b14a9aea9e9e3d456a2320884 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 21 Sep 2021 03:10:36 -0500 Subject: [PATCH 0968/1220] Refactor buttoninfo (#1441) --- padinfo/core/button_info.py | 217 +++++++++++++++++------------------- padinfo/view/button_info.py | 80 +++---------- 2 files changed, 116 insertions(+), 181 deletions(-) diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index fe6e3316..48e90a31 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -1,5 +1,5 @@ from collections import namedtuple -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -50,6 +50,14 @@ NIL_ATT = 'Nil' +def _get_sub_attr_multiplier(monster: "MonsterModel"): + if monster.attr2.value == 6 or monster.attr1.value == 6: + return 0 + if monster.attr2.value == monster.attr1.value: + return 1 / 10 + return 1 / 3 + + class ButtonInfo: def get_info(self, dbcog, monster): """ @@ -74,87 +82,44 @@ def get_info(self, dbcog, monster): slb_level = SUPER_LIMIT_BREAK_LEVEL if monster.limit_mult != 0 else None max_atk_latents = monster.latent_slots / 2 - sub_attr_multiplier = self._get_sub_attr_multiplier(monster) - result = ButtonInfoResult() - result.main_damage = self._calculate_coop_damage(dbcog, monster, max_level) - result.sub_damage = result.main_damage * sub_attr_multiplier - result.total_damage = result.main_damage + result.sub_damage - - if slb_level is None: - result.main_slb_damage = None - result.sub_slb_damage = None - result.total_slb_damage = None - else: - result.main_slb_damage = self._calculate_coop_damage(dbcog, monster, slb_level) - result.sub_slb_damage = result.main_slb_damage * sub_attr_multiplier - result.total_slb_damage = result.main_slb_damage + result.sub_slb_damage - - result.main_solo_damage = self._calculate_solo_damage(dbcog, monster, max_level) - result.sub_solo_damage = result.main_solo_damage * sub_attr_multiplier - result.total_solo_damage = result.main_solo_damage + result.sub_solo_damage - - if slb_level is None: - result.main_solo_slb_damage = None - result.sub_solo_slb_damage = None - result.total_solo_slb_damage = None - else: - result.main_solo_slb_damage = self._calculate_solo_damage(dbcog, monster, slb_level) - result.sub_solo_slb_damage = result.main_solo_slb_damage * sub_attr_multiplier - result.total_solo_slb_damage = result.main_solo_slb_damage + result.sub_solo_slb_damage - - result.main_damage_with_atk_latent = self._calculate_coop_damage( - dbcog, monster, max_level, num_atkplus_latent=max_atk_latents) - result.sub_damage_with_atk_latent = result.main_damage_with_atk_latent * sub_attr_multiplier - result.total_damage_with_atk_latent = result.main_damage_with_atk_latent + result.sub_damage_with_atk_latent + result.coop = ButtonInfoStatSet(monster, self._calculate_coop_damage(dbcog, monster, max_level)) + result.solo = ButtonInfoStatSet(monster, self._calculate_solo_damage(dbcog, monster, max_level)) + result.coop_latent = ButtonInfoStatSet( + monster, self._calculate_coop_damage(dbcog, monster, max_level, num_atkplus_latent=max_atk_latents)) + result.solo_latent = ButtonInfoStatSet( + monster, self._calculate_solo_damage(dbcog, monster, max_level, num_atkplus_latent=max_atk_latents)) if slb_level is None: - result.main_damage_with_slb_atk_latent = None - result.sub_damage_with_slb_atk_latent = None - result.total_damage_with_slb_atk_latent = None + result.coop_slb = ButtonInfoStatSet(monster) + result.solo_slb = ButtonInfoStatSet(monster) + result.coop_slb_latent = ButtonInfoStatSet(monster) + result.solo_slb_latent = ButtonInfoStatSet(monster) else: - result.main_damage_with_slb_atk_latent = self._calculate_coop_damage( - dbcog, monster, slb_level, num_atkplus2_latent=max_atk_latents) - result.sub_damage_with_slb_atk_latent = result.main_damage_with_slb_atk_latent * sub_attr_multiplier - result.total_damage_with_slb_atk_latent = (result.main_damage_with_slb_atk_latent - + result.sub_damage_with_slb_atk_latent) - - result.main_solo_damage_with_atk_latent = self._calculate_solo_damage( - dbcog, monster, max_level, num_atkplus_latent=max_atk_latents) - result.sub_solo_damage_with_atk_latent = result.main_solo_damage_with_atk_latent * sub_attr_multiplier - result.total_solo_damage_with_atk_latent = (result.main_solo_damage_with_atk_latent - + result.sub_solo_damage_with_atk_latent) - if slb_level is None: - result.main_solo_damage_with_slb_atk_latent = None - result.sub_solo_damage_with_slb_atk_latent = None - result.total_solo_damage_with_slb_atk_latent = None - else: - result.main_solo_damage_with_slb_atk_latent = self._calculate_solo_damage( - dbcog, monster, slb_level, num_atkplus2_latent=max_atk_latents) - result.sub_solo_damage_with_slb_atk_latent = (result.main_solo_damage_with_slb_atk_latent - * sub_attr_multiplier) - result.total_solo_damage_with_slb_atk_latent = (result.main_solo_damage_with_slb_atk_latent - + result.sub_solo_damage_with_slb_atk_latent) - - result.card_btn_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, - multiplayer=True, limit_break=LIMIT_BREAK_LEVEL) - result.card_btn_solo_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, - multiplayer=False, limit_break=LIMIT_BREAK_LEVEL) - result.card_btn_slb_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, - multiplayer=True, limit_break=SUPER_LIMIT_BREAK_LEVEL) - result.card_btn_solo_slb_str = self._get_card_btn_damage_text(CARD_BUTTONS, dbcog, monster, - multiplayer=False, - limit_break=SUPER_LIMIT_BREAK_LEVEL) - - result.team_btn_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, - multiplayer=True, limit_break=LIMIT_BREAK_LEVEL) - result.team_btn_solo_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, - multiplayer=False, limit_break=LIMIT_BREAK_LEVEL) - result.team_btn_slb_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, - multiplayer=True, limit_break=SUPER_LIMIT_BREAK_LEVEL) - result.team_btn_solo_slb_str = self._get_team_btn_damage_text(TEAM_BUTTONS, dbcog, monster, - multiplayer=False, - limit_break=SUPER_LIMIT_BREAK_LEVEL) + result.coop_slb = ButtonInfoStatSet(monster, self._calculate_coop_damage(dbcog, monster, slb_level)) + result.solo_slb = ButtonInfoStatSet(monster, self._calculate_solo_damage(dbcog, monster, slb_level)) + result.coop_slb_latent = ButtonInfoStatSet( + monster, self._calculate_coop_damage(dbcog, monster, slb_level, num_atkplus2_latent=max_atk_latents)) + result.solo_slb_latent = ButtonInfoStatSet( + monster, self._calculate_solo_damage(dbcog, monster, slb_level, num_atkplus2_latent=max_atk_latents)) + + result.card_btn_str = self._get_card_text(CARD_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=LIMIT_BREAK_LEVEL) + result.card_btn_solo_str = self._get_card_text(CARD_BUTTONS, dbcog, monster, + multiplayer=False, limit_break=LIMIT_BREAK_LEVEL) + result.card_btn_slb_str = self._get_card_text(CARD_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=SUPER_LIMIT_BREAK_LEVEL) + result.card_btn_solo_slb_str = self._get_card_text(CARD_BUTTONS, dbcog, monster, + multiplayer=False, limit_break=SUPER_LIMIT_BREAK_LEVEL) + + result.team_btn_str = self._get_team_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=LIMIT_BREAK_LEVEL) + result.team_btn_solo_str = self._get_team_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=False, limit_break=LIMIT_BREAK_LEVEL) + result.team_btn_slb_str = self._get_team_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=True, limit_break=SUPER_LIMIT_BREAK_LEVEL) + result.team_btn_solo_slb_str = self._get_team_text(TEAM_BUTTONS, dbcog, monster, + multiplayer=False, limit_break=SUPER_LIMIT_BREAK_LEVEL) return result def _calculate_coop_damage(self, dbcog, monster, level, num_atkplus_latent=0, num_atkplus2_latent=0): @@ -166,24 +131,14 @@ def _calculate_solo_damage(self, dbcog, monster, level, num_atkplus_latent=0, nu @staticmethod def _calculate_damage(dbcog, monster, level, multiplayer, num_atkplus_latent=0, num_atkplus2_latent=0): stat_latents = dbcog.MonsterStatModifierInput(num_atkplus=num_atkplus_latent, num_atkplus2=num_atkplus2_latent) - stat_latents.num_atk_awakening = len( - [x for x in monster.awakenings if x.awoken_skill_id == 1]) + stat_latents.num_atk_awakening = len([x for x in monster.awakenings if x.awoken_skill_id == 1]) - dmg = dbcog.monster_stats.stat(monster, 'atk', level, - stat_latents=stat_latents, multiplayer=multiplayer) + dmg = dbcog.monster_stats.stat(monster, 'atk', level, stat_latents=stat_latents, multiplayer=multiplayer) return int(round(dmg)) @staticmethod - def _get_sub_attr_multiplier(monster): - if monster.attr2.value == 6 or monster.attr1.value == 6: - return 0 - if monster.attr2.value == monster.attr1.value: - return 1 / 10 - return 1 / 3 - - @staticmethod - def _get_card_btn_damage_text(card_buttons, dbcog, monster: "MonsterModel", multiplayer, limit_break): + def _get_card_text(card_buttons, dbcog, monster: "MonsterModel", multiplayer, limit_break): lines = [] card_buttons.sort(key=lambda x: x.mult) for card in card_buttons: @@ -207,7 +162,8 @@ def _get_card_btn_damage_text(card_buttons, dbcog, monster: "MonsterModel", mult card.id, card.name.format(oncolor), card.mult, round(dmg * card.mult, 2))) return "\n".join(lines) - def _get_team_btn_damage_text(self, team_buttons, dbcog, monster, multiplayer, limit_break): + @staticmethod + def _get_team_text(team_buttons, dbcog, monster, *, multiplayer, limit_break): lines = [] team_buttons.sort(key=lambda x: x.mult) for card in team_buttons: @@ -224,7 +180,7 @@ def _get_team_btn_damage_text(self, team_buttons, dbcog, monster, multiplayer, l total_dmg += dmg # do not double-count null main att if monster.attr2.value in card.att and monster.attr1.name != NIL_ATT: - total_dmg += dmg * self._get_sub_attr_multiplier(monster) + total_dmg += dmg * _get_sub_attr_multiplier(monster) colors_str = "" for i in card.att: colors_str += COLORS[i] @@ -234,31 +190,58 @@ def _get_team_btn_damage_text(self, team_buttons, dbcog, monster, multiplayer, l return "\n".join(lines) +class ButtonInfoStatSet: + def __init__(self, monster: "MonsterModel", main: Optional[float] = None): + self.main = main + if main is None: + self.sub = None + self.total = None + return + sub_attr_multiplier = _get_sub_attr_multiplier(monster) + self.sub = main * sub_attr_multiplier + self.total = self.main + self.sub + + class ButtonInfoResult: - main_damage: float - total_damage: float - sub_damage: float - main_solo_damage: float - total_solo_damage: float - sub_solo_damage: float - main_slb_damage: float - total_slb_damage: float - sub_slb_damage: float - main_solo_slb_damage: float - total_solo_slb_damage: float - sub_solo_slb_damage: float - main_damage_with_atk_latent: float - total_damage_with_atk_latent: float - sub_damage_with_atk_latent: float - main_solo_damage_with_atk_latent: float - total_solo_damage_with_atk_latent: float - sub_solo_damage_with_atk_latent: float - main_damage_with_slb_atk_latent: float - total_damage_with_slb_atk_latent: float - sub_damage_with_slb_atk_latent: float - main_solo_damage_with_slb_atk_latent: float - total_solo_damage_with_slb_atk_latent: float - sub_solo_damage_with_slb_atk_latent: float + coop: ButtonInfoStatSet + solo: ButtonInfoStatSet + coop_slb: ButtonInfoStatSet + solo_slb: ButtonInfoStatSet + + coop_latent: ButtonInfoStatSet + solo_latent: ButtonInfoStatSet + coop_slb_latent: ButtonInfoStatSet + solo_slb_latent: ButtonInfoStatSet + + card_btn_str: str + card_btn_solo_str: str + card_btn_slb_str: str + card_btn_solo_slb_str: str + + team_btn_str: str + team_btn_solo_str: str + team_btn_slb_str: str + team_btn_solo_slb_str: str + + def get_card_btn_str(self, is_coop, max_110): + if is_coop and max_110: + return self.card_btn_str + elif is_coop and not max_110: + return self.card_btn_slb_str + elif not is_coop and max_110: + return self.card_btn_solo_str + elif not is_coop and not max_110: + return self.card_btn_solo_slb_str + + def get_team_btn_str(self, is_coop, max_110): + if is_coop and max_110: + return self.team_btn_str + elif is_coop and not max_110: + return self.team_btn_slb_str + elif not is_coop and max_110: + return self.team_btn_solo_str + elif not is_coop and not max_110: + return self.team_btn_solo_slb_str button_info = ButtonInfo() diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index cf564a1b..1fbc0ce9 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -9,7 +9,8 @@ from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx -from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL +from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL, ButtonInfoStatSet, \ + ButtonInfoResult from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base import ViewStateBase @@ -37,7 +38,8 @@ def __init__(self, players_setting=ButtonInfoOptions.coop, device_setting=Button class ButtonInfoViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, raw_query, color, display_options: ButtonInfoToggles, - monster: "MonsterModel", info, query_settings: QuerySettings, reaction_list: List[str] = None): + monster: "MonsterModel", info: ButtonInfoResult, query_settings: QuerySettings, + reaction_list: List[str] = None): super().__init__(original_author_id, menu_type, raw_query, extra_state=None, reaction_list=reaction_list) self.color = color self.display_options = display_options @@ -80,44 +82,16 @@ def set_max_level(self, new_max_level): self.display_options.max_level = new_max_level -def get_stat_block(main, sub, total): +def get_stat_block(bi_stat_set: ButtonInfoStatSet): return BlockText( Box( - Text('Base: {}'.format(int(round(main)))), - Text('Subattr: {}'.format(int(round(sub)))), - Text('Total: {}'.format(int(round(total)))) + Text('Base: {}'.format(int(round(bi_stat_set.main)))), + Text('Subattr: {}'.format(int(round(bi_stat_set.sub)))), + Text('Total: {}'.format(int(round(bi_stat_set.total)))) ) ) -def get_max_stats_without_latents(info, is_coop): - main = info.main_damage if is_coop else info.main_solo_damage - sub = info.sub_damage if is_coop else info.sub_solo_damage - total = info.total_damage if is_coop else info.total_solo_damage - return get_stat_block(main, sub, total) - - -def get_120_stats_without_latents(info, is_coop): - main = info.main_slb_damage if is_coop else info.main_solo_slb_damage - sub = info.sub_slb_damage if is_coop else info.sub_solo_slb_damage - total = info.total_slb_damage if is_coop else info.total_solo_slb_damage - return get_stat_block(main, sub, total) - - -def get_max_stats_with_latents(info, is_coop): - main = info.main_damage_with_atk_latent if is_coop else info.main_solo_damage_with_atk_latent - sub = info.sub_damage_with_atk_latent if is_coop else info.sub_solo_damage_with_atk_latent - total = info.total_damage_with_atk_latent if is_coop else info.total_solo_damage_with_atk_latent - return get_stat_block(main, sub, total) - - -def get_120_stats_with_latents(info, is_coop): - main = info.main_damage_with_slb_atk_latent if is_coop else info.main_solo_damage_with_slb_atk_latent - sub = info.sub_damage_with_slb_atk_latent if is_coop else info.sub_solo_damage_with_slb_atk_latent - total = info.total_damage_with_slb_atk_latent if is_coop else info.total_solo_damage_with_slb_atk_latent - return get_stat_block(main, sub, total) - - def get_mobile_btn_str(btn_str): output = [] lines = btn_str.split('\n') @@ -129,28 +103,6 @@ def get_mobile_btn_str(btn_str): return '\n'.join(output) -def get_card_btn_str(info, is_coop, max_110): - if is_coop and max_110: - return info.card_btn_str - elif is_coop and not max_110: - return info.card_btn_slb_str - elif not is_coop and max_110: - return info.card_btn_solo_str - elif not is_coop and not max_110: - return info.card_btn_solo_slb_str - - -def get_team_btn_str(info, is_coop, max_110): - if is_coop and max_110: - return info.team_btn_str - elif is_coop and not max_110: - return info.team_btn_slb_str - elif not is_coop and max_110: - return info.team_btn_solo_str - elif not is_coop and not max_110: - return info.team_btn_solo_slb_str - - class ButtonInfoView: VIEW_TYPE = 'ButtonInfo' @@ -183,11 +135,11 @@ def embed(state: ButtonInfoViewState): Text('Without Latents'), # avoid whitespace after code block Box( - get_max_stats_without_latents(info, is_coop), + get_stat_block(info.coop if is_coop else info.solo), Text('With Latents (Atk+)'), delimiter='' ), - get_max_stats_with_latents(info, is_coop) + get_stat_block(info.coop_latent if is_coop else info.solo_latent) ), inline=True ), @@ -197,11 +149,11 @@ def embed(state: ButtonInfoViewState): Text('Without Latents'), # avoid whitespace after code block Box( - get_120_stats_without_latents(info, is_coop), + get_stat_block(info.coop_slb if is_coop else info.solo_slb), Text('With Latents (Atk++)'), delimiter='' ), - get_120_stats_with_latents(info, is_coop) + get_stat_block(info.coop_slb_latent if is_coop else info.solo_slb_latent) ), inline=True ) if monster.limit_mult != 0 else None, @@ -214,11 +166,11 @@ def embed(state: ButtonInfoViewState): Text('Card Button Damage'), # done this way to not have the whitespace after code block Box( - BlockText(get_card_btn_str(info, is_coop, max_110)), + BlockText(info.get_card_btn_str(is_coop, max_110)), Text('Team Button Contribution'), delimiter='' ), - BlockText(get_team_btn_str(info, is_coop, max_110)) + BlockText(info.get_team_btn_str(is_coop, max_110)) ) ) if is_desktop else None, EmbedField( @@ -231,12 +183,12 @@ def embed(state: ButtonInfoViewState): ) if not is_desktop else None, EmbedField( 'Card Button Damage', - BlockText(get_mobile_btn_str(get_card_btn_str(info, is_coop, max_110))), + BlockText(get_mobile_btn_str(info.get_card_btn_str(is_coop, max_110))), inline=True ) if not is_desktop else None, EmbedField( 'Team Button Contribution', - BlockText(get_mobile_btn_str(get_team_btn_str(info, is_coop, max_110))), + BlockText(get_mobile_btn_str(info.get_team_btn_str(is_coop, max_110))), inline=True ) if not is_desktop else None ] From 3a5bdc83589d8eb68b135b7a43d11d70cc4222c1 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 23 Sep 2021 00:25:10 -0500 Subject: [PATCH 0969/1220] Show evo scroll in ^buttoninfo (#1443) --- padinfo/menu/button_info.py | 20 ++-- padinfo/menu/components/evo_scroll_mixin.py | 50 +++++++++ padinfo/menu/id.py | 36 ++----- padinfo/padinfo.py | 3 +- padinfo/view/button_info.py | 30 ++++-- padinfo/view/components/evo_scroll_mixin.py | 100 ++++++++++++++++++ padinfo/view/components/view_state_base_id.py | 46 +------- padinfo/view/evos.py | 3 +- padinfo/view/id.py | 41 +------ padinfo/view/materials.py | 11 +- padinfo/view/otherinfo.py | 6 +- padinfo/view/pantheon.py | 9 +- padinfo/view/pic.py | 6 +- 13 files changed, 217 insertions(+), 144 deletions(-) create mode 100644 padinfo/menu/components/evo_scroll_mixin.py create mode 100644 padinfo/view/components/evo_scroll_mixin.py diff --git a/padinfo/menu/button_info.py b/padinfo/menu/button_info.py index 792c5eb8..32301a57 100644 --- a/padinfo/menu/button_info.py +++ b/padinfo/menu/button_info.py @@ -4,14 +4,15 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu from tsutils.emoji import char_to_emoji - from tsutils.menu.panes import emoji_buttons, MenuPanes +from padinfo.menu.components.evo_scroll_mixin import EvoScrollMenu from padinfo.view.button_info import ButtonInfoOptions, ButtonInfoToggles, ButtonInfoViewState, ButtonInfoView class ButtonInfoEmoji: - delete = '\N{CROSS MARK}' + left = '\N{BLACK LEFT-POINTING TRIANGLE}' + right = '\N{BLACK RIGHT-POINTING TRIANGLE}' home = emoji_buttons['home'] coop = ('coop_2p', '\N{BUSTS IN SILHOUETTE}') solo = ('solo_1p', '\N{BUST IN SILHOUETTE}') @@ -21,8 +22,14 @@ class ButtonInfoEmoji: super_limit_break = ('lv120', char_to_emoji('2')) -class ButtonInfoMenu: +class ButtonInfoMenu(EvoScrollMenu): MENU_TYPE = 'ButtonInfo' + VIEW_STATE_TYPE = ButtonInfoViewState + + @staticmethod + def get_panes_type(): + # TODO: change this to a property & classmethod once we update to Python 3.9 + return ButtonInfoMenuPanes @staticmethod def menu(): @@ -34,10 +41,6 @@ async def _get_view_state(cls, ims: dict, **data) -> ButtonInfoViewState: user_config = data['user_config'] return await ButtonInfoViewState.deserialize(dbcog, user_config, ims) - @staticmethod - async def respond_with_delete(message: Message, ims, **data): - return await message.delete() - @staticmethod async def respond_with_button_info(message: Optional[Message], ims, **data): dbcog = data['dbcog'] @@ -97,7 +100,8 @@ class ButtonInfoMenuPanes(MenuPanes): INITIAL_EMOJI = ButtonInfoEmoji.home DATA = { - ButtonInfoEmoji.delete: (ButtonInfoMenu.respond_with_delete, None), + ButtonInfoEmoji.left: (ButtonInfoMenu.respond_with_left, None), + ButtonInfoEmoji.right: (ButtonInfoMenu.respond_with_right, None), ButtonInfoEmoji.home: (ButtonInfoMenu.respond_with_button_info, ButtonInfoView.VIEW_TYPE), ButtonInfoEmoji.coop: (ButtonInfoMenu.respond_with_coop, ButtonInfoView.VIEW_TYPE), ButtonInfoEmoji.solo: (ButtonInfoMenu.respond_with_solo, ButtonInfoView.VIEW_TYPE), diff --git a/padinfo/menu/components/evo_scroll_mixin.py b/padinfo/menu/components/evo_scroll_mixin.py new file mode 100644 index 00000000..ede5c370 --- /dev/null +++ b/padinfo/menu/components/evo_scroll_mixin.py @@ -0,0 +1,50 @@ +from abc import abstractmethod +from typing import Optional + +from discord import Message +from tsutils.menu.panes import MenuPanes + +from padinfo.view.components.evo_scroll_mixin import EvoScrollViewState + + +class EvoScrollMenu: + VIEW_STATE_TYPE: EvoScrollViewState + + @staticmethod + @abstractmethod + def get_panes_type() -> MenuPanes: + ... + + @classmethod + async def respond_with_left(cls, message: Optional[Message], ims, **data): + dbcog = data['dbcog'] + user_config = data['user_config'] + # Figure out the new monster before doing all of the queries necessary for + # The specific pane type. For now just deserialize as the base Id ViewState. + view_state = await cls.VIEW_STATE_TYPE.deserialize(dbcog, user_config, ims) + view_state.decrement_monster(dbcog, ims) + + response_func = cls._get_response_func(ims) + return await response_func(message, ims, **data) + + @classmethod + async def respond_with_right(cls, message: Optional[Message], ims, **data): + dbcog = data['dbcog'] + user_config = data['user_config'] + view_state = await cls.VIEW_STATE_TYPE.deserialize(dbcog, user_config, ims) + view_state.increment_monster(dbcog, ims) + + response_func = cls._get_response_func(ims) + return await response_func(message, ims, **data) + + @classmethod + def _get_response_func(cls, ims): + panes = cls.get_panes_type() + + pane_type = ims.get('pane_type') + if pane_type is not None: + pane_type_to_func_map = panes.pane_types() + return pane_type_to_func_map[pane_type] + + home = panes.INITIAL_EMOJI + return panes.transitions()[home] diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 01e421e2..3c9e1797 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -3,10 +3,10 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils.emoji import char_to_emoji - from tsutils.enums import ChildMenuType from tsutils.menu.panes import MenuPanes +from padinfo.menu.components.evo_scroll_mixin import EvoScrollMenu from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.components.view_state_base_id import ViewStateBaseId from padinfo.view.evos import EvosView, EvosViewState @@ -17,8 +17,14 @@ from padinfo.view.pic import PicView, PicViewState -class IdMenu: +class IdMenu(EvoScrollMenu): MENU_TYPE = ChildMenuType.IdMenu.name + VIEW_STATE_TYPE = ViewStateBaseId + + @staticmethod + def get_panes_type(): + # TODO: change this to a property & classmethod once we update to Python 3.9 + return IdMenuPanes @staticmethod def menu(initial_control=None): @@ -29,32 +35,6 @@ def menu(initial_control=None): delete_func=IdMenu.respond_with_delete) return embed - @staticmethod - async def respond_with_left(message: Optional[Message], ims, **data): - dbcog = data['dbcog'] - user_config = data['user_config'] - # Figure out the new monster before doing all of the queries necessary for - # The specific pane type. For now just deserialize as the base Id ViewState. - view_state = await ViewStateBaseId.deserialize(dbcog, user_config, ims) - view_state.decrement_monster(dbcog, ims) - - pane_type = ims['pane_type'] - pane_type_to_func_map = IdMenuPanes.pane_types() - response_func = pane_type_to_func_map[pane_type] - return await response_func(message, ims, **data) - - @staticmethod - async def respond_with_right(message: Optional[Message], ims, **data): - dbcog = data['dbcog'] - user_config = data['user_config'] - view_state = await ViewStateBaseId.deserialize(dbcog, user_config, ims) - view_state.increment_monster(dbcog, ims) - - pane_type = ims.get('pane_type') - pane_type_to_func_map = IdMenuPanes.pane_types() - response_func = pane_type_to_func_map[pane_type] - return await response_func(message, ims, **data) - @staticmethod async def respond_with_refresh(message: Optional[Message], ims, **data): # This is used by disambig screen & other multi-message embeds, where we need to deserialize & then diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8e6e51bf..8eaa7497 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -587,8 +587,9 @@ async def buttoninfo(self, ctx, *, query: str): info = button_info.get_info(dbcog, monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) display_options = ButtonInfoToggles() + alt_monsters = ButtonInfoViewState.get_alt_monsters_and_evos(dbcog, monster) state = ButtonInfoViewState(original_author_id, ButtonInfoMenu.MENU_TYPE, query, color, display_options, - monster, info, query_settings, + monster, alt_monsters, info, query_settings, reaction_list=ButtonInfoMenuPanes.get_user_reaction_list(display_options)) menu = ButtonInfoMenu.menu() await menu.create(ctx, state) diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 1fbc0ce9..76f695e3 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -11,6 +11,7 @@ from padinfo.common.external_links import puzzledragonx from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL, ButtonInfoStatSet, \ ButtonInfoResult +from padinfo.view.components.evo_scroll_mixin import EvoScrollView, EvoScrollViewState, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base import ViewStateBase @@ -36,9 +37,10 @@ def __init__(self, players_setting=ButtonInfoOptions.coop, device_setting=Button self.max_level = max_level_setting -class ButtonInfoViewState(ViewStateBase): +class ButtonInfoViewState(ViewStateBase, EvoScrollViewState): def __init__(self, original_author_id, menu_type, raw_query, color, display_options: ButtonInfoToggles, - monster: "MonsterModel", info: ButtonInfoResult, query_settings: QuerySettings, + monster: "MonsterModel", alt_monsters: List[MonsterEvolution], + info: ButtonInfoResult, query_settings: QuerySettings, reaction_list: List[str] = None): super().__init__(original_author_id, menu_type, raw_query, extra_state=None, reaction_list=reaction_list) self.color = color @@ -47,6 +49,12 @@ def __init__(self, original_author_id, menu_type, raw_query, color, display_opti self.info = info self.query_settings = query_settings + self.alt_monsters = alt_monsters + self.alt_monster_ids = [m.monster.monster_id for m in self.alt_monsters] + + # numerical scroll makes no sense for ButtonInfo + self.use_evo_scroll = True + def serialize(self): ret = super().serialize() ret.update({ @@ -59,18 +67,19 @@ def serialize(self): }) return ret - @staticmethod - async def deserialize(dbcog, user_config: UserConfig, ims: dict): + @classmethod + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): raw_query = ims['raw_query'] original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] query_settings = QuerySettings.deserialize(ims['query_settings']) display_options = ButtonInfoToggles(ims['players_setting'], ims['device_setting'], ims['max_level_setting']) - monster = dbcog.get_monster(ims['resolved_monster_id']) + monster = dbcog.get_monster(int(ims['resolved_monster_id'])) + alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) info = button_info.get_info(dbcog, monster) reaction_list = ims['reaction_list'] return ButtonInfoViewState(original_author_id, menu_type, raw_query, user_config.color, display_options, - monster, info, query_settings, reaction_list=reaction_list) + monster, alt_monsters, info, query_settings, reaction_list=reaction_list) def set_player_count(self, new_count): self.display_options.players = new_count @@ -103,7 +112,7 @@ def get_mobile_btn_str(btn_str): return '\n'.join(output) -class ButtonInfoView: +class ButtonInfoView(EvoScrollView): VIEW_TYPE = 'ButtonInfo' @staticmethod @@ -119,8 +128,8 @@ def get_common_buttons_title_text(monster, is_max_110): '(Atk+ Latents)' if is_max_110 or monster.limit_mult == 0 else '(Atk++ Latents)' ) - @staticmethod - def embed(state: ButtonInfoViewState): + @classmethod + def embed(cls, state: ButtonInfoViewState): is_coop = state.display_options.players == ButtonInfoOptions.coop is_desktop = state.display_options.device == ButtonInfoOptions.desktop max_110 = state.display_options.max_level == ButtonInfoOptions.limit_break @@ -190,7 +199,8 @@ def embed(state: ButtonInfoViewState): 'Team Button Contribution', BlockText(get_mobile_btn_str(info.get_team_btn_str(is_coop, max_110))), inline=True - ) if not is_desktop else None + ) if not is_desktop else None, + cls.evos_embed_field(state) ] return EmbedView( diff --git a/padinfo/view/components/evo_scroll_mixin.py b/padinfo/view/components/evo_scroll_mixin.py new file mode 100644 index 00000000..d6fc28ba --- /dev/null +++ b/padinfo/view/components/evo_scroll_mixin.py @@ -0,0 +1,100 @@ +from abc import abstractmethod +from typing import List, TYPE_CHECKING, NamedTuple, Optional + +from discordmenu.embed.components import EmbedField +from discordmenu.embed.text import HighlightableLinks, LinkedText + +from padinfo.common.external_links import puzzledragonx + +if TYPE_CHECKING: + from dbcog.database_context import DbContext + from dbcog.models.monster_model import MonsterModel + from dbcog.models.evolution_model import EvolutionModel + + +class MonsterEvolution(NamedTuple): + monster: "MonsterModel" + evolution: Optional["EvolutionModel"] + + +class EvoScrollViewState: + use_evo_scroll: bool + alt_monster_ids: List[int] + monster: "MonsterModel" + alt_monsters: List["MonsterEvolution"] + + def decrement_monster(self, dbcog, ims: dict): + + # if this ever has a side effect in the actual class, it cannot remain as a mixin! + + db_context: "DbContext" = dbcog.database + if self.use_evo_scroll: + index = self.alt_monster_ids.index(self.monster.monster_id) + prev_monster_id = self.alt_monster_ids[index - 1] + else: + prev_monster = db_context.graph.numeric_prev_monster(self.monster) + prev_monster_id = prev_monster.monster_id if prev_monster else None + if prev_monster_id is None: + ims['unsupported_transition'] = True + ims['resolved_monster_id'] = str(prev_monster_id) + + def increment_monster(self, dbcog, ims: dict): + db_context: "DbContext" = dbcog.database + if self.use_evo_scroll: + index = self.alt_monster_ids.index(self.monster.monster_id) + if index == len(self.alt_monster_ids) - 1: + # cycle back to the beginning of the evos list + next_monster_id = self.alt_monster_ids[0] + else: + next_monster_id = self.alt_monster_ids[index + 1] + else: + next_monster = db_context.graph.numeric_next_monster(self.monster) + next_monster_id = next_monster.monster_id if next_monster else None + if next_monster_id is None: + ims['unsupported_transition'] = True + ims['resolved_monster_id'] = str(next_monster_id) + + @classmethod + def get_alt_monsters_and_evos(cls, dbcog, monster) -> List[MonsterEvolution]: + graph = dbcog.database.graph + alt_monsters = graph.get_alt_monsters(monster) + return [MonsterEvolution(m, graph.get_evolution(m)) for m in alt_monsters] + + @abstractmethod + def deserialize(self, dbcog, user_config, ims): + ... + + +class EvoScrollView: + @staticmethod + def evos_embed_field(state: EvoScrollViewState): + field_text = "**Evos**" + help_text = "" + # this isn't used right now, but maybe later if discord changes the api for embed titles...? + _help_link = "https://github.com/TsubakiBotPad/pad-cogs/wiki/Evolutions-mini-view" + legend_parts = [] + if any(not alt_evo.evolution.reversible for alt_evo in state.alt_monsters if alt_evo.evolution): + legend_parts.append("⌊Irreversible⌋") + if any(alt_evo.monster.is_equip for alt_evo in state.alt_monsters): + legend_parts.append("⌈Equip⌉") + if legend_parts: + help_text = ' – Help: {}'.format(" ".join(legend_parts)) + return EmbedField( + field_text + help_text, + HighlightableLinks( + links=[LinkedText(EvoScrollView.alt_fmt(evo), puzzledragonx(evo.monster)) for evo in + state.alt_monsters], + highlighted=next(i for i, me in enumerate(state.alt_monsters) + if state.monster.monster_id == me.monster.monster_id) + ) + ) + + @staticmethod + def alt_fmt(evo: MonsterEvolution): + if evo.monster.is_equip: + fmt = "⌈{}⌉" + elif not evo.evolution or evo.evolution.reversible: + fmt = "{}" + else: + fmt = "⌊{}⌋" + return fmt.format(evo.monster.monster_no_na) diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 8667f3f4..05af06f3 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, NamedTuple, Optional +from typing import TYPE_CHECKING, List from discordmenu.embed.view_state import ViewState from tsutils.enums import AltEvoSort @@ -6,19 +6,13 @@ from padinfo.common.config import UserConfig from padinfo.view.common import get_monster_from_ims +from padinfo.view.components.evo_scroll_mixin import EvoScrollViewState, MonsterEvolution if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel - from dbcog.models.evolution_model import EvolutionModel - from dbcog.database_context import DbContext -class MonsterEvolution(NamedTuple): - monster: "MonsterModel" - evolution: Optional["EvolutionModel"] - - -class ViewStateBaseId(ViewState): +class ViewStateBaseId(ViewState, EvoScrollViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, use_evo_scroll: bool = True, @@ -75,37 +69,3 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) - - @classmethod - def get_alt_monsters_and_evos(cls, dbcog, monster) -> List[MonsterEvolution]: - graph = dbcog.database.graph - alt_monsters = graph.get_alt_monsters(monster) - return [MonsterEvolution(m, graph.get_evolution(m)) for m in alt_monsters] - - def decrement_monster(self, dbcog, ims: dict): - db_context: "DbContext" = dbcog.database - if self.use_evo_scroll: - index = self.alt_monster_ids.index(self.monster.monster_id) - prev_monster_id = self.alt_monster_ids[index - 1] - else: - prev_monster = db_context.graph.numeric_prev_monster(self.monster) - prev_monster_id = prev_monster.monster_id if prev_monster else None - if prev_monster_id is None: - ims['unsupported_transition'] = True - ims['resolved_monster_id'] = str(prev_monster_id) - - def increment_monster(self, dbcog, ims: dict): - db_context: "DbContext" = dbcog.database - if self.use_evo_scroll: - index = self.alt_monster_ids.index(self.monster.monster_id) - if index == len(self.alt_monster_ids) - 1: - # cycle back to the beginning of the evos list - next_monster_id = self.alt_monster_ids[0] - else: - next_monster_id = self.alt_monster_ids[index + 1] - else: - next_monster = db_context.graph.numeric_next_monster(self.monster) - next_monster_id = next_monster.monster_id if next_monster else None - if next_monster_id is None: - ims['unsupported_transition'] = True - ims['resolved_monster_id'] = str(next_monster_id) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 341f279d..5d3dfffb 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -13,7 +13,8 @@ from padinfo.view.common import get_monster_from_ims from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution +from padinfo.view.components.view_state_base_id import ViewStateBaseId +from padinfo.view.components.evo_scroll_mixin import MonsterEvolution if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 8383aa97..120277b9 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -2,7 +2,7 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail -from discordmenu.embed.text import BoldText, HighlightableLinks, LabeledText, LinkedText, Text +from discordmenu.embed.text import BoldText, LabeledText, Text from discordmenu.embed.view import EmbedView from tsutils.enums import CardPlusModifier, Server, CardModeModifier, CardLevelModifier from tsutils.menu.footers import embed_footer_with_state @@ -13,9 +13,10 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.common import get_monster_from_ims, invalid_monster_text from padinfo.view.components.base_id_main_view import BaseIdMainView +from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import MonsterEvolution, ViewStateBaseId +from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -23,16 +24,6 @@ from dbcog.database_context import DbContext -def alt_fmt(monsterevo, state): - if monsterevo.monster.is_equip: - fmt = "⌈{}⌉" - elif not monsterevo.evolution or monsterevo.evolution.reversible: - fmt = "{}" - else: - fmt = "⌊{}⌋" - return fmt.format(monsterevo.monster.monster_no_na) - - class IdQueriedProps: def __init__(self, acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_evolutions, previous_transforms): @@ -173,29 +164,7 @@ def _monster_is_enhance(m: "MonsterModel"): return any(x if x.name == 'Enhance' else None for x in m.types) -def evos_embed_field(state: ViewStateBaseId): - field_text = "**Evos**" - help_text = "" - # this isn't used right now, but maybe later if discord changes the api for embed titles...? - _help_link = "https://github.com/TsubakiBotPad/pad-cogs/wiki/Evolutions-mini-view" - legend_parts = [] - if any(not alt_evo.evolution.reversible for alt_evo in state.alt_monsters if alt_evo.evolution): - legend_parts.append("⌊Irreversible⌋") - if any(alt_evo.monster.is_equip for alt_evo in state.alt_monsters): - legend_parts.append("⌈Equip⌉") - if legend_parts: - help_text = ' – Help: {}'.format(" ".join(legend_parts)) - return EmbedField( - field_text + help_text, - HighlightableLinks( - links=[LinkedText(alt_fmt(me, state), puzzledragonx(me.monster)) for me in state.alt_monsters], - highlighted=next(i for i, me in enumerate(state.alt_monsters) - if state.monster.monster_id == me.monster.monster_id) - ) - ) - - -class IdView(BaseIdMainView): +class IdView(BaseIdMainView, EvoScrollView): VIEW_TYPE = 'Id' @classmethod @@ -315,7 +284,7 @@ def embed(cls, state: IdViewState): cls.leader_skill_header(m, state.query_settings.lsmultiplier, state.transform_base).to_markdown(), Text(m.leader_skill.desc if m.leader_skill else 'None') ), - evos_embed_field(state) + cls.evos_embed_field(state) ] return EmbedView( diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 444e9fc6..dd4941db 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -5,18 +5,17 @@ from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView from tsutils.menu.footers import embed_footer_with_state - from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView +from padinfo.view.common import get_monster_from_ims +from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution -from padinfo.view.common import get_monster_from_ims -from padinfo.view.id import evos_embed_field +from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -152,7 +151,7 @@ def skillup_field(mons, sec, link): Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:MAX_MONS_TO_SHOW]), text, text2)) -class MaterialsView(BaseIdView): +class MaterialsView(BaseIdView, EvoScrollView): VIEW_TYPE = 'Materials' @classmethod @@ -178,5 +177,5 @@ def embed(cls, state: MaterialsViewState): if state.gemusedin else None, skillup_field(state.skillups, state.skillup_evo_count, state.link) if not (state.monster.is_stackable or state.gem_override) else None - ] if f is not None] + [evos_embed_field(state)] + ] if f is not None] + [cls.evos_embed_field(state)] ) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index ea08bc4b..6aa867da 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -10,9 +10,9 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView +from padinfo.view.components.evo_scroll_mixin import EvoScrollView from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId -from padinfo.view.id import evos_embed_field from padinfo.view.links import LinksView if TYPE_CHECKING: @@ -44,7 +44,7 @@ def statsbox(m, plus: int): return box(tbl.get_string()) -class OtherInfoView(BaseIdView): +class OtherInfoView(BaseIdView, EvoScrollView): VIEW_TYPE = 'OtherInfo' @classmethod @@ -91,4 +91,4 @@ def embed(cls, state: OtherInfoViewState): LabeledText("Rarity", str(m.rarity)), LabeledText("Cost", str(m.cost)), delimiter=' '))), - evos_embed_field(state)]) + cls.evos_embed_field(state)]) diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 57ffe94d..ca3cf239 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -4,7 +4,6 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.view import EmbedView from tsutils.menu.footers import embed_footer_with_state - from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -13,10 +12,10 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims +from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base_id import ViewStateBaseId, MonsterEvolution -from padinfo.view.id import evos_embed_field +from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -163,7 +162,7 @@ def _pantheon_lines(monsters, base_monster): ] -class PantheonView(BaseIdView): +class PantheonView(BaseIdView, EvoScrollView): VIEW_TYPE = 'Pantheon' @classmethod @@ -172,7 +171,7 @@ def embed(cls, state: PantheonViewState): 'Pantheon: {}'.format(state.series_name), Box(*_pantheon_lines(state.pantheon_list, state.base_monster)) ), - evos_embed_field(state)] + cls.evos_embed_field(state)] return EmbedView( EmbedMain( diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 395fa907..12557ab4 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -6,10 +6,10 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView +from padinfo.view.components.evo_scroll_mixin import EvoScrollView from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId -from padinfo.view.id import evos_embed_field class PicViewState(ViewStateBaseId): @@ -22,7 +22,7 @@ def serialize(self): return ret -class PicView(BaseIdView): +class PicView(BaseIdView, EvoScrollView): VIEW_TYPE = 'Pic' @classmethod @@ -48,7 +48,7 @@ def embed(cls, state: PicViewState): ) if state.monster.orb_skin_id else None, ) ), - evos_embed_field(state) + cls.evos_embed_field(state) ] return EmbedView( From 9d275017dbae31471b21b96b7e0648ad40c4659d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 23 Sep 2021 07:47:28 -0500 Subject: [PATCH 0970/1220] Create ^scroll to replace numberscroll pref (#1445) --- padinfo/menu/menu_map.py | 3 + padinfo/menu/monster_list.py | 16 +-- padinfo/menu/scroll.py | 19 ++++ padinfo/menu/series_scroll.py | 2 - padinfo/padinfo.py | 113 +++++++++++++++----- padinfo/view/monster_list/monster_list.py | 27 +++-- padinfo/view/monster_list/scroll.py | 121 ++++++++++++++++++++++ 7 files changed, 250 insertions(+), 51 deletions(-) create mode 100644 padinfo/menu/scroll.py create mode 100644 padinfo/view/monster_list/scroll.py diff --git a/padinfo/menu/menu_map.py b/padinfo/menu/menu_map.py index 037225b4..95de725b 100644 --- a/padinfo/menu/menu_map.py +++ b/padinfo/menu/menu_map.py @@ -6,11 +6,13 @@ from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes from padinfo.menu.monster_list import MonsterListMenu, MonsterListMenuPanes from padinfo.menu.na_diff import NaDiffMenu, NaDiffMenuPanes +from padinfo.menu.scroll import ScrollMenuPanes from padinfo.menu.series_scroll import SeriesScrollMenu, SeriesScrollMenuPanes from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState +from padinfo.view.monster_list.scroll import ScrollViewState from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState padinfo_menu_map = { @@ -23,6 +25,7 @@ MonsterListMenu.MENU_TYPE: (MonsterListMenu, MonsterListMenuPanes), AllMatsViewState.VIEW_STATE_TYPE: (MonsterListMenu, MonsterListMenuPanes), IdSearchViewState.VIEW_STATE_TYPE: (MonsterListMenu, MonsterListMenuPanes), + ScrollViewState.VIEW_STATE_TYPE: (MonsterListMenu, ScrollMenuPanes), StaticMonsterListViewState.VIEW_STATE_TYPE: (MonsterListMenu, MonsterListMenuPanes), SeriesScrollMenu.MENU_TYPE: (SeriesScrollMenu, SeriesScrollMenuPanes), SimpleTextMenu.MENU_TYPE: (SimpleTextMenu, SimpleTextMenuPanes), diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index b7e48722..01fc94ab 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -11,6 +11,7 @@ from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState from padinfo.view.monster_list.monster_list import MonsterListView, MonsterListViewState +from padinfo.view.monster_list.scroll import ScrollViewState from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState @@ -43,6 +44,7 @@ class MonsterListMenu: AllMatsViewState.VIEW_STATE_TYPE: AllMatsViewState, IdSearchViewState.VIEW_STATE_TYPE: IdSearchViewState, StaticMonsterListViewState.VIEW_STATE_TYPE: StaticMonsterListViewState, + ScrollViewState.VIEW_STATE_TYPE: ScrollViewState, } child_menu_type_to_emoji_response_map = { @@ -61,7 +63,7 @@ def menu(initial_control=None): async def _get_view_state(cls, ims: dict, **data) -> MonsterListViewState: dbcog = data['dbcog'] user_config = data['user_config'] - view_state_class = cls.view_state_types.get(ims['menu_type']) or MonsterListMenu.MENU_TYPE + view_state_class = cls.view_state_types[ims['menu_type']] return await view_state_class.deserialize(dbcog, user_config, ims) @staticmethod @@ -96,13 +98,13 @@ async def respond_with_monster_list(cls, message: Optional[Message], ims, **data @classmethod async def respond_with_previous_monster(cls, message: Optional[Message], ims, **data): view_state = await cls._get_view_state(ims, **data) - view_state.decrement_index() + await view_state.decrement_index(data['dbcog']) return MonsterListMenu.monster_list_control(view_state) @classmethod async def respond_with_next_monster(cls, message: Optional[Message], ims, **data): view_state = await cls._get_view_state(ims, **data) - view_state.increment_index() + await view_state.increment_index(data['dbcog']) return MonsterListMenu.monster_list_control(view_state) @staticmethod @@ -182,18 +184,18 @@ async def click_child_number(cls, ims, emoji_clicked, **data): @classmethod async def auto_scroll_child_left(cls, ims, _emoji_clicked, **data): view_state = await cls._get_view_state(ims, **data) - return await MonsterListMenu._scroll_child(view_state, view_state.decrement_index) + return await MonsterListMenu._scroll_child(view_state, view_state.decrement_index, data['dbcog']) @classmethod async def auto_scroll_child_right(cls, ims, _emoji_clicked, **data): view_state = await cls._get_view_state(ims, **data) - return await MonsterListMenu._scroll_child(view_state, view_state.increment_index) + return await MonsterListMenu._scroll_child(view_state, view_state.increment_index, data['dbcog']) @classmethod - async def _scroll_child(cls, view_state: MonsterListViewState, update_function): + async def _scroll_child(cls, view_state: MonsterListViewState, update_function, dbcog): # when the parent ims updated, it was a deepcopy of the ims / view_state we have now, # so we have to redo the update function that we did before - update_function() + await update_function(dbcog) extra_ims = view_state.get_serialized_child_extra_ims() emoji_response = cls.child_menu_type_to_emoji_response_map[view_state.child_menu_type] return emoji_response, extra_ims diff --git a/padinfo/menu/scroll.py b/padinfo/menu/scroll.py new file mode 100644 index 00000000..7a86863f --- /dev/null +++ b/padinfo/menu/scroll.py @@ -0,0 +1,19 @@ +from typing import List + +from padinfo.menu.monster_list import MonsterListMenuPanes, MonsterListEmoji + + +class ScrollMenuPanes(MonsterListMenuPanes): + @classmethod + def get_initial_reaction_list(cls, number_of_evos: int): + return [MonsterListEmoji.prev_mon, MonsterListEmoji.next_mon] + + @classmethod + def get_previous_reaction_list_num_monsters(cls, reaction_list: List): + # we don't want any extra emojis + return 0 + + @classmethod + def get_monster_index(cls, n: int): + # this is unimportant because we don't use indices + return 0 diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 877f1274..274cba25 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -3,7 +3,6 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils.emoji import char_to_emoji - from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji @@ -164,7 +163,6 @@ def monster_list_control(state: SeriesScrollViewState): @classmethod async def click_child_number(cls, ims, emoji_clicked, **data): - dbcog = data['dbcog'] emoji_response = IdMenuEmoji.refresh \ if SeriesScrollMenuPanes.respond_to_emoji_with_child(emoji_clicked) else None if emoji_response is None: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 8eaa7497..2e4ecb4c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -38,6 +38,7 @@ from padinfo.menu.menu_map import padinfo_menu_map from padinfo.menu.monster_list import MonsterListEmoji, MonsterListMenu, MonsterListMenuPanes from padinfo.menu.na_diff import NaDiffMenu, NaDiffMenuPanes +from padinfo.menu.scroll import ScrollMenuPanes from padinfo.menu.series_scroll import SeriesScrollEmoji, SeriesScrollMenu, SeriesScrollMenuPanes from padinfo.menu.simple_text import SimpleTextMenu from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes @@ -60,6 +61,7 @@ from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState from padinfo.view.monster_list.monster_list import MonsterListViewState +from padinfo.view.monster_list.scroll import ScrollViewState from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState from padinfo.view.otherinfo import OtherInfoViewState from padinfo.view.pantheon import PantheonViewState @@ -594,6 +596,32 @@ async def buttoninfo(self, ctx, *, query: str): menu = ButtonInfoMenu.menu() await menu.create(ctx, state) + @commands.command(aliases=["ids"]) + @checks.bot_has_permissions(embed_links=True) + async def idsearch(self, ctx, *, query): + await self._do_idsearch(ctx, query) + + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def nadiffs(self, ctx, *, query): + await self._do_idsearch(ctx, query, child_menu_type=NaDiffMenu.MENU_TYPE, + child_reaction_list=NaDiffMenuPanes.emoji_names()) + + async def _do_idsearch(self, ctx, query, child_menu_type=None, + child_reaction_list=None): + dbcog = await self.get_dbcog() + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id, query_settings) + + if monster_list is None: + await ctx.send("No monster matched.") + return + + await self._do_monster_list(ctx, dbcog, query, monster_list, 'ID Search Results', + IdSearchViewState, + child_menu_type=child_menu_type, + child_reaction_list=child_reaction_list) + @commands.command() @checks.bot_has_permissions(embed_links=True) async def allmats(self, ctx, *, query: str): @@ -678,6 +706,61 @@ async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterM # The user could delete the menu before we can do this pass + @commands.command() + @checks.bot_has_permissions(embed_links=True) + async def scroll(self, ctx, *, query: str): + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) + + if not monster: + await self.send_id_failure_message(ctx, query) + return + monster_list = await ScrollViewState.do_query(dbcog, monster) + title = 'Monster Book Scroll' + raw_query = query + original_author_id = ctx.message.author.id + color = await self.get_user_embed_color(ctx) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + initial_reaction_list = ScrollMenuPanes.get_initial_reaction_list(len(monster_list)) + instruction_message = 'Click a reaction to see monster details!' + + state = ScrollViewState(original_author_id, ScrollViewState.VIEW_STATE_TYPE, query, color, + monster_list, query_settings, + title, instruction_message, monster.monster_id, + child_menu_type=IdMenu.MENU_TYPE, + child_reaction_list=IdMenuPanes.emoji_names(), + reaction_list=initial_reaction_list + ) + parent_menu = MonsterListMenu.menu() + message = await ctx.send('Setting up!') + + ims = state.serialize() + user_config = await BotConfig.get_user(self.config, ctx.author.id) + data = { + 'dbcog': dbcog, + 'user_config': user_config, + } + + alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) + id_queried_props = await IdViewState.do_query(dbcog, monster) + full_reaction_list = IdMenuPanes.emoji_names() + is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + + child_state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + alt_monsters, is_jp_buffed, query_settings, id_queried_props, + use_evo_scroll=True, reaction_list=full_reaction_list) + child_menu = IdMenu.menu() + child_message = await child_menu.create(ctx, child_state) + + data['child_message_id'] = child_message.id + try: + await parent_menu.transition(message, ims, MonsterListEmoji.refresh, ctx.author, **data) + await message.edit(content=None) + except discord.errors.NotFound: + # The user could delete the menu before we can do this + pass + @commands.command(aliases=['collabscroll', 'ss']) @checks.bot_has_permissions(embed_links=True) async def seriesscroll(self, ctx, *, query): @@ -921,8 +1004,8 @@ async def padsay(self, ctx, server, *, query: str = None): async def idset(self, ctx): """`[p]id` settings configuration""" - @idset.command() - async def scroll(self, ctx, value): + @idset.command(name="scroll") + async def idset_scroll(self, ctx, value): """Switch between number scroll & evo scroll [p]idset scroll number @@ -1442,17 +1525,6 @@ async def idtraceback(self, ctx, *, query): IdTracebackView.VIEW_TYPE, props) await menu.create(ctx, state) - @commands.command(aliases=["ids"]) - @checks.bot_has_permissions(embed_links=True) - async def idsearch(self, ctx, *, query): - await self._do_idsearch(ctx, query) - - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def nadiffs(self, ctx, *, query): - await self._do_idsearch(ctx, query, child_menu_type=NaDiffMenu.MENU_TYPE, - child_reaction_list=NaDiffMenuPanes.emoji_names()) - @commands.command() @checks.bot_has_permissions(embed_links=True) async def expcurve(self, ctx, start, end: Optional[int], *, query=''): @@ -1495,20 +1567,5 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): ExperienceCurveView.VIEW_TYPE, props) await menu.create(ctx, state) - async def _do_idsearch(self, ctx, query, child_menu_type=None, - child_reaction_list=None): - dbcog = await self.get_dbcog() - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id, query_settings) - - if monster_list is None: - await ctx.send("No monster matched.") - return - - await self._do_monster_list(ctx, dbcog, query, monster_list, 'ID Search Results', - IdSearchViewState, - child_menu_type=child_menu_type, - child_reaction_list=child_reaction_list) - async def get_fm_flags(self, user: discord.User): return await self.bot.get_cog("DBCog").config.user(user).fm_flags() diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 6dc9164e..af8ec136 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -6,7 +6,6 @@ from discordmenu.embed.view import EmbedView from tsutils.emoji import char_to_emoji from tsutils.menu.footers import embed_footer_with_state - from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig @@ -106,17 +105,17 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): child_menu_type = ims.get('child_menu_type') child_reaction_list = ims.get('child_reaction_list') idle_message = ims.get('idle_message') - return MonsterListViewState(original_author_id, menu_type, query, user_config.color, - monster_list, query_settings, - title, idle_message, - current_page=current_page, - current_index=current_index, - child_menu_type=child_menu_type, - child_reaction_list=child_reaction_list, - reaction_list=reaction_list, - extra_state=ims, - child_message_id=child_message_id - ) + return cls(original_author_id, menu_type, query, user_config.color, + monster_list, query_settings, + title, idle_message, + current_page=current_page, + current_index=current_index, + child_menu_type=child_menu_type, + child_reaction_list=child_reaction_list, + reaction_list=reaction_list, + extra_state=ims, + child_message_id=child_message_id + ) @classmethod async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: @@ -150,7 +149,7 @@ def decrement_page(self): if len(self.paginated_monsters) > 1: self.current_index = None - def increment_index(self): + async def increment_index(self, _dbcog): max_index = len(self.paginated_monsters[self.current_page]) - 1 if self.current_index is None: self.current_index = 0 @@ -166,7 +165,7 @@ def increment_index(self): self.current_page = 0 self.current_index = 0 - def decrement_index(self): + async def decrement_index(self, _dbcog): max_index = len(self.paginated_monsters[self.current_page]) - 1 if self.current_index is None: self.current_index = max_index diff --git a/padinfo/view/monster_list/scroll.py b/padinfo/view/monster_list/scroll.py new file mode 100644 index 00000000..d0b1e292 --- /dev/null +++ b/padinfo/view/monster_list/scroll.py @@ -0,0 +1,121 @@ +from typing import TYPE_CHECKING, List, Optional + +from tsutils.query_settings import QuerySettings + +from padinfo.common.config import UserConfig +from padinfo.view.monster_list.monster_list import MonsterListViewState + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + from dbcog.database_context import DbContext + + +class ScrollViewState(MonsterListViewState): + VIEW_STATE_TYPE = "Scroll" + + def __init__(self, original_author_id, menu_type, query, color, + monster_list: List["MonsterModel"], query_settings: QuerySettings, + title, message, current_monster_id: int, + *, + current_page: int = 0, + current_index: int = None, + child_menu_type: str = None, + child_reaction_list: Optional[List] = None, + reaction_list=None, + extra_state=None, + child_message_id=None + ): + super().__init__(original_author_id, menu_type, query, color, monster_list, query_settings, title, message, + current_page=current_page, current_index=current_index, child_menu_type=child_menu_type, + child_reaction_list=child_reaction_list, reaction_list=reaction_list, + extra_state=extra_state, child_message_id=child_message_id) + + self._current_monster_id = current_monster_id + + @property + def current_monster_id(self) -> int: + return self._current_monster_id + + def serialize(self): + ret = super().serialize() + ret.update({ + 'current_monster_id': self._current_monster_id + }) + return ret + + @classmethod + async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): + if ims.get('unsupported_transition'): + return None + title = ims['title'] + + monster_list = await cls.query_from_ims(dbcog, ims) + current_page = ims['current_page'] + current_index = ims.get('current_index') + + raw_query = ims['raw_query'] + query = ims.get('query') or raw_query + query_settings = QuerySettings.deserialize(ims.get('query_settings')) + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + reaction_list = ims.get('reaction_list') + child_message_id = ims.get('child_message_id') + child_menu_type = ims.get('child_menu_type') + child_reaction_list = ims.get('child_reaction_list') + idle_message = ims.get('idle_message') + return ScrollViewState(original_author_id, menu_type, query, user_config.color, + monster_list, query_settings, + title, idle_message, ims['current_monster_id'], + current_page=current_page, + current_index=current_index, + child_menu_type=child_menu_type, + child_reaction_list=child_reaction_list, + reaction_list=reaction_list, + extra_state=ims, + child_message_id=child_message_id + ) + + @classmethod + async def do_query(cls, dbcog, monster) -> Optional[List["MonsterModel"]]: + prev_monster = dbcog.database.graph.numeric_prev_monster(monster) + next_monster = dbcog.database.graph.numeric_next_monster(monster) + + monster_list = [] + + if prev_monster is not None: + monster_list.append(prev_monster) + if next_monster is not None: + monster_list.append(next_monster) + + return monster_list + + async def decrement_index(self, dbcog): + db_context: "DbContext" = dbcog.database + monster = db_context.graph.get_monster(self.current_monster_id) + + prev_monster: "MonsterModel" = dbcog.database.graph.numeric_prev_monster(monster) + if prev_monster is None: + # TODO: raise an error here so that the "omg yikes" sign shows up once that's based on an error again + return + monster_list = await self.do_query(dbcog, prev_monster) + + self._current_monster_id = prev_monster.monster_id + self.paginated_monsters = [monster_list] + + async def increment_index(self, dbcog): + db_context: "DbContext" = dbcog.database + monster = db_context.graph.get_monster(self.current_monster_id) + next_monster = dbcog.database.graph.numeric_next_monster(monster) + if next_monster is None: + # TODO: raise an error here so that the "omg yikes" sign shows up once that's based on an error again + return + monster_list = await self.do_query(dbcog, next_monster) + + self._current_monster_id = next_monster.monster_id + self.paginated_monsters = [monster_list] + + @classmethod + async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: + monster = await dbcog.find_monster(ims['raw_query'], ims['original_author_id']) + monster_list = await cls.do_query(dbcog, monster) + return monster_list From 149c5bb05a80b540f2e25718df498c02fd3a6d16 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 23 Sep 2021 09:28:14 -0500 Subject: [PATCH 0971/1220] Skip jp monsters when scrolling with --na (#1448) --- padinfo/view/monster_list/scroll.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/view/monster_list/scroll.py b/padinfo/view/monster_list/scroll.py index d0b1e292..6edbf8d9 100644 --- a/padinfo/view/monster_list/scroll.py +++ b/padinfo/view/monster_list/scroll.py @@ -91,7 +91,7 @@ async def do_query(cls, dbcog, monster) -> Optional[List["MonsterModel"]]: async def decrement_index(self, dbcog): db_context: "DbContext" = dbcog.database - monster = db_context.graph.get_monster(self.current_monster_id) + monster = db_context.graph.get_monster(self.current_monster_id, server=self.query_settings.server) prev_monster: "MonsterModel" = dbcog.database.graph.numeric_prev_monster(monster) if prev_monster is None: @@ -104,7 +104,7 @@ async def decrement_index(self, dbcog): async def increment_index(self, dbcog): db_context: "DbContext" = dbcog.database - monster = db_context.graph.get_monster(self.current_monster_id) + monster = db_context.graph.get_monster(self.current_monster_id, server=self.query_settings.server) next_monster = dbcog.database.graph.numeric_next_monster(monster) if next_monster is None: # TODO: raise an error here so that the "omg yikes" sign shows up once that's based on an error again From d49f10cf89b5130a1d26b68c08d2c677cd79f2ea Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 23 Sep 2021 09:44:05 -0500 Subject: [PATCH 0972/1220] Remove evoscroll preference (#1446) --- padinfo/padinfo.py | 44 +++++++------------ padinfo/view/button_info.py | 3 -- padinfo/view/components/evo_scroll_mixin.py | 30 +++---------- padinfo/view/components/view_state_base_id.py | 5 --- padinfo/view/evos.py | 4 -- padinfo/view/id.py | 5 +-- padinfo/view/materials.py | 4 -- padinfo/view/pantheon.py | 4 -- 8 files changed, 23 insertions(+), 76 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 2e4ecb4c..9a346b0d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -276,7 +276,7 @@ async def _do_id(self, ctx, query: str): state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, id_queried_props, - use_evo_scroll=settings.checkEvoID(ctx.author.id), reaction_list=initial_reaction_list) + reaction_list=initial_reaction_list) menu = IdMenu.menu() await menu.create(ctx, state) @@ -399,8 +399,8 @@ async def evos(self, ctx, *, query: str): state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, alt_versions, gem_versions, - reaction_list=initial_reaction_list, - use_evo_scroll=settings.checkEvoID(ctx.author.id)) + reaction_list=initial_reaction_list + ) menu = IdMenu.menu(initial_control=IdMenu.evos_control) await menu.create(ctx, state) @@ -436,8 +436,8 @@ async def evomats(self, ctx, *, query: str): state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, - reaction_list=initial_reaction_list, - use_evo_scroll=settings.checkEvoID(ctx.author.id)) + reaction_list=initial_reaction_list + ) menu = IdMenu.menu(initial_control=IdMenu.mats_control) await menu.create(ctx, state) @@ -472,8 +472,8 @@ async def pantheon(self, ctx, *, query: str): state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, pantheon_list, series_name, base_monster, - reaction_list=initial_reaction_list, - use_evo_scroll=settings.checkEvoID(ctx.author.id)) + reaction_list=initial_reaction_list + ) menu = IdMenu.menu(initial_control=IdMenu.pantheon_control) await menu.create(ctx, state) @@ -502,8 +502,8 @@ async def pic(self, ctx, *, query: str): state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, - reaction_list=initial_reaction_list, - use_evo_scroll=settings.checkEvoID(ctx.author.id)) + reaction_list=initial_reaction_list + ) menu = IdMenu.menu(initial_control=IdMenu.pic_control) await menu.create(ctx, state) @@ -532,8 +532,8 @@ async def otherinfo(self, ctx, *, query: str): state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, - reaction_list=initial_reaction_list, - use_evo_scroll=settings.checkEvoID(ctx.author.id)) + reaction_list=initial_reaction_list + ) menu = IdMenu.menu(initial_control=IdMenu.otherinfo_control) await menu.create(ctx, state) @@ -749,7 +749,7 @@ async def scroll(self, ctx, *, query: str): child_state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, id_queried_props, - use_evo_scroll=True, reaction_list=full_reaction_list) + reaction_list=full_reaction_list) child_menu = IdMenu.menu() child_message = await child_menu.create(ctx, child_state) @@ -1005,23 +1005,9 @@ async def idset(self, ctx): """`[p]id` settings configuration""" @idset.command(name="scroll") - async def idset_scroll(self, ctx, value): - """Switch between number scroll & evo scroll - - [p]idset scroll number - [p]idset scroll evo""" - if value in ['evo', 'default']: - if settings.setEvoID(ctx.author.id): - await ctx.tick() - else: - await ctx.send(inline("You're already using evo mode")) - elif value in ['number']: - if settings.rmEvoID(ctx.author.id): - await ctx.tick() - else: - await ctx.send(inline("You're already using number mode")) - else: - await ctx.send("id_type must be `number` or `evo`") + async def idset_scroll(self, ctx): + await ctx.send(f"We no longer support number scroll as a preference. " + f"Please use the `{ctx.prefix}scroll` command for numberscroll!") @idset.command() async def survey(self, ctx, value): diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 76f695e3..3302bbd5 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -52,9 +52,6 @@ def __init__(self, original_author_id, menu_type, raw_query, color, display_opti self.alt_monsters = alt_monsters self.alt_monster_ids = [m.monster.monster_id for m in self.alt_monsters] - # numerical scroll makes no sense for ButtonInfo - self.use_evo_scroll = True - def serialize(self): ret = super().serialize() ret.update({ diff --git a/padinfo/view/components/evo_scroll_mixin.py b/padinfo/view/components/evo_scroll_mixin.py index d6fc28ba..8bddb6ae 100644 --- a/padinfo/view/components/evo_scroll_mixin.py +++ b/padinfo/view/components/evo_scroll_mixin.py @@ -7,7 +7,6 @@ from padinfo.common.external_links import puzzledragonx if TYPE_CHECKING: - from dbcog.database_context import DbContext from dbcog.models.monster_model import MonsterModel from dbcog.models.evolution_model import EvolutionModel @@ -18,7 +17,6 @@ class MonsterEvolution(NamedTuple): class EvoScrollViewState: - use_evo_scroll: bool alt_monster_ids: List[int] monster: "MonsterModel" alt_monsters: List["MonsterEvolution"] @@ -27,31 +25,17 @@ def decrement_monster(self, dbcog, ims: dict): # if this ever has a side effect in the actual class, it cannot remain as a mixin! - db_context: "DbContext" = dbcog.database - if self.use_evo_scroll: - index = self.alt_monster_ids.index(self.monster.monster_id) - prev_monster_id = self.alt_monster_ids[index - 1] - else: - prev_monster = db_context.graph.numeric_prev_monster(self.monster) - prev_monster_id = prev_monster.monster_id if prev_monster else None - if prev_monster_id is None: - ims['unsupported_transition'] = True + index = self.alt_monster_ids.index(self.monster.monster_id) + prev_monster_id = self.alt_monster_ids[index - 1] ims['resolved_monster_id'] = str(prev_monster_id) def increment_monster(self, dbcog, ims: dict): - db_context: "DbContext" = dbcog.database - if self.use_evo_scroll: - index = self.alt_monster_ids.index(self.monster.monster_id) - if index == len(self.alt_monster_ids) - 1: - # cycle back to the beginning of the evos list - next_monster_id = self.alt_monster_ids[0] - else: - next_monster_id = self.alt_monster_ids[index + 1] + index = self.alt_monster_ids.index(self.monster.monster_id) + if index == len(self.alt_monster_ids) - 1: + # cycle back to the beginning of the evos list + next_monster_id = self.alt_monster_ids[0] else: - next_monster = db_context.graph.numeric_next_monster(self.monster) - next_monster_id = next_monster.monster_id if next_monster else None - if next_monster_id is None: - ims['unsupported_transition'] = True + next_monster_id = self.alt_monster_ids[index + 1] ims['resolved_monster_id'] = str(next_monster_id) @classmethod diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 05af06f3..f784881b 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -15,7 +15,6 @@ class ViewStateBaseId(ViewState, EvoScrollViewState): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, - use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id=original_author_id, menu_type=menu_type, raw_query=raw_query, @@ -27,7 +26,6 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.is_jp_buffed = is_jp_buffed self.query = query self.query_settings = query_settings - self.use_evo_scroll = use_evo_scroll if self.query_settings.evosort == AltEvoSort.dfs: self.alt_monsters = self.dfs_alt_monsters @@ -42,7 +40,6 @@ def serialize(self): 'query': self.query, 'query_settings': self.query_settings.serialize(), 'resolved_monster_id': self.monster.monster_id, - 'use_evo_scroll': str(self.use_evo_scroll), 'reaction_list': self.reaction_list, }) return ret @@ -59,13 +56,11 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): query = ims.get('query') or raw_query query_settings = QuerySettings.deserialize(ims.get('query_settings')) original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 5d3dfffb..2e9466ee 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -25,11 +25,9 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], reaction_list: List[str] = None, - use_evo_scroll: bool = True, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) self.alt_versions = alt_versions @@ -56,7 +54,6 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): query = ims.get('query') or raw_query query_settings = QuerySettings.deserialize(ims.get('query_settings')) original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) @@ -65,7 +62,6 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): alt_monsters, is_jp_buffed, query_settings, alt_versions, gem_versions, reaction_list=reaction_list, - use_evo_scroll=use_evo_scroll, extra_state=ims) @staticmethod diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 120277b9..2d88b59c 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -43,11 +43,10 @@ class IdViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, id_queried_props: IdQueriedProps, - fallback_message: str = None, use_evo_scroll: bool = True, reaction_list: List[str] = None, + fallback_message: str = None, reaction_list: List[str] = None, is_child: bool = False, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) self.fallback_message = fallback_message @@ -83,7 +82,6 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): query_settings = QuerySettings.deserialize(ims.get('query_settings')) menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = ims.get('reaction_list') fallback_message = ims.get('message') is_child = ims.get('is_child') @@ -93,7 +91,6 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): alt_monsters, is_jp_buffed, query_settings, id_queried_props, fallback_message=fallback_message, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, is_child=is_child, extra_state=ims) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index dd4941db..ec6b0172 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -29,13 +29,11 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, gem_override: bool, - use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=reaction_list, - use_evo_scroll=use_evo_scroll, extra_state=extra_state) self.reaction_list = reaction_list self.link = link @@ -71,14 +69,12 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): query_settings = QuerySettings.deserialize(ims.get('query_settings')) menu_type = ims['menu_type'] original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index ca3cf239..63171dd9 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -29,12 +29,10 @@ class PantheonViewState(ViewStateBaseId): def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", alt_monsters, is_jp_buffed, query_settings, pantheon_list: List[MonsterEvolution], series_name: str, base_monster, - use_evo_scroll: bool = True, reaction_list: List[str] = None, extra_state=None): super().__init__(original_author_id, menu_type, raw_query, query, color, monster, alt_monsters, is_jp_buffed, query_settings, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=extra_state) self.series_name = series_name @@ -63,7 +61,6 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): query = ims.get('query') or raw_query query_settings = QuerySettings.deserialize(ims.get('query_settings')) original_author_id = ims['original_author_id'] - use_evo_scroll = ims.get('use_evo_scroll') != 'False' menu_type = ims['menu_type'] reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) @@ -71,7 +68,6 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, alt_monsters, is_jp_buffed, query_settings, pantheon_list, series_name, base_monster, - use_evo_scroll=use_evo_scroll, reaction_list=reaction_list, extra_state=ims) From 11d710c3bae8127c36c5fb6eec8e1205b564c759 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 23 Sep 2021 08:29:00 -0700 Subject: [PATCH 0973/1220] Only make roma_subname if not in NA (#1450) --- dbcog/models/monster_model.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index d5624994..331528f6 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -1,17 +1,14 @@ +import re +from collections import defaultdict + +import romkan from tsutils.enums import Server from tsutils.formatting import contains_ja, rmdiacritics -from .base_model import BaseModel -from .enum_types import Attribute -from .enum_types import MonsterType -from .enum_types import AwakeningRestrictedLatent -from .enum_types import enum_or_none from .active_skill_model import ActiveSkillModel +from .base_model import BaseModel +from .enum_types import Attribute, AwakeningRestrictedLatent, MonsterType, enum_or_none from .leader_skill_model import LeaderSkillModel -import re -from collections import defaultdict -import romkan - from .monster.monster_difference import MonsterDifference from .monster_stats import monster_stats @@ -24,6 +21,10 @@ def __init__(self, **m): self.monster_no_kr = m['monster_no_kr'] self.base_evo_id = m['base_evo_id'] + self.on_jp = m['on_jp'] + self.on_na = m['on_na'] + self.on_kr = m['on_kr'] + self.awakenings = sorted(m['awakenings'], key=lambda a: a.order_idx) self.superawakening_count = sum(int(a.is_super) for a in self.awakenings) self.leader_skill: LeaderSkillModel = m['leader_skill'] @@ -37,7 +38,7 @@ def __init__(self, **m): self.name_ko = m['name_ko'] self.name_en = self.unoverridden_name_en = m['name_en'] self.roma_subname = None - if self.name_en == self.name_ja: + if self.name_en == self.name_ja and not self.on_na: self.roma_subname = self.make_roma_subname(self.name_ja) else: # Remove annoying stuff from NA names, like Jörmungandr @@ -47,7 +48,7 @@ def __init__(self, **m): # If the NA and JP names are the same, chances are the card isn't released in both reigions, and # if we have an override, chances are it's JP only because we wouldn't need one for an NA only card. # We aren't using the on_na flag here because that flag sucks and has false positives all the time - if self.name_en == self.name_ja: + if self.name_en == self.name_ja: self.name_en_override = m['name_en_override'] if not m['name_is_translation']: self.name_en_override = m['name_en_override'] @@ -69,9 +70,6 @@ def __init__(self, **m): self.sell_gold = m['sell_gold'] self.sell_mp = m['sell_mp'] self.reg_date = m['reg_date'] - self.on_jp = m['on_jp'] - self.on_na = m['on_na'] - self.on_kr = m['on_kr'] self.attr1 = enum_or_none(Attribute, m['attribute_1_id'], Attribute.Nil) self.attr2 = enum_or_none(Attribute, m['attribute_2_id'], Attribute.Nil) self.is_equip = any([x.awoken_skill_id == 49 for x in self.awakenings]) From b48083bced877788c83f0bce16520c417f4620fd Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 23 Sep 2021 10:29:10 -0500 Subject: [PATCH 0974/1220] Show left/right arrows for emoji in numberscroll (#1451) --- padinfo/menu/monster_list.py | 19 ++++++++++---- padinfo/view/monster_list/monster_list.py | 30 +++++++++++++---------- padinfo/view/monster_list/scroll.py | 12 ++++++++- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index 01fc94ab..b74ee53c 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Type from discord import Message from discordmenu.embed.menu import EmbedControl, EmbedMenu @@ -11,7 +11,7 @@ from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState from padinfo.view.monster_list.monster_list import MonsterListView, MonsterListViewState -from padinfo.view.monster_list.scroll import ScrollViewState +from padinfo.view.monster_list.scroll import ScrollViewState, ScrollView from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState @@ -47,6 +47,10 @@ class MonsterListMenu: ScrollViewState.VIEW_STATE_TYPE: ScrollViewState, } + view_types = { + ScrollViewState.VIEW_STATE_TYPE: ScrollView + } + child_menu_type_to_emoji_response_map = { IdMenu.MENU_TYPE: IdMenuEmoji.refresh, NaDiffMenu.MENU_TYPE: NaDiffEmoji.home @@ -66,6 +70,10 @@ async def _get_view_state(cls, ims: dict, **data) -> MonsterListViewState: view_state_class = cls.view_state_types[ims['menu_type']] return await view_state_class.deserialize(dbcog, user_config, ims) + @classmethod + def _get_view(cls, state: MonsterListViewState) -> Type[MonsterListView]: + return cls.view_types.get(state.VIEW_STATE_TYPE) or MonsterListView + @staticmethod async def respond_with_refresh(message: Optional[Message], ims, **data): # this is only called once on message load @@ -160,13 +168,14 @@ async def respond_with_9(message: Optional[Message], ims, **data): async def respond_with_10(message: Optional[Message], ims, **data): return await MonsterListMenu.respond_with_n(message, ims, 10, **data) - @staticmethod - def monster_list_control(state: MonsterListViewState): + @classmethod + def monster_list_control(cls, state: MonsterListViewState): if state is None: return None reaction_list = state.reaction_list + view_type = cls._get_view(state) return EmbedControl( - [MonsterListView.embed(state)], + [view_type.embed(state)], reaction_list ) diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index af8ec136..07ee61dc 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -50,7 +50,9 @@ def __init__(self, original_author_id, menu_type, query, color, self.query_settings = query_settings @property - def current_monster_id(self) -> int: + def current_monster_id(self) -> Optional[int]: + if self.current_index is None: + return None return self.monster_list[self.current_index].monster_id @property @@ -186,23 +188,25 @@ def set_index(self, new_index): self.current_index = new_index -def _monster_list(monsters): - if not len(monsters): - return [] - return [ - MonsterHeader.short_with_emoji(mon, link=True, prefix=char_to_emoji(str(i))) - for i, mon in enumerate(monsters) - ] - - class MonsterListView: VIEW_TYPE = 'MonsterList' - @staticmethod - def embed(state: MonsterListViewState): + @classmethod + def monster_list(cls, monsters: List["MonsterModel"], current_monster_id: int): + if not len(monsters): + return [] + return [MonsterHeader.short_with_emoji( + mon, link=True, prefix=cls.get_emoji(i, current_monster_id)) for i, mon in enumerate(monsters)] + + @classmethod + def get_emoji(cls, i: int, _current_monster_id: int): + return char_to_emoji(str(i)) + + @classmethod + def embed(cls, state: MonsterListViewState): fields = [ EmbedField(state.title, - Box(*_monster_list(state.monster_list))), + Box(*cls.monster_list(state.monster_list, state.current_monster_id))), EmbedField(BoldText('Page'), Box('{} of {}'.format(state.current_page + 1, state.page_count)), inline=True diff --git a/padinfo/view/monster_list/scroll.py b/padinfo/view/monster_list/scroll.py index 6edbf8d9..e974a332 100644 --- a/padinfo/view/monster_list/scroll.py +++ b/padinfo/view/monster_list/scroll.py @@ -3,7 +3,7 @@ from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig -from padinfo.view.monster_list.monster_list import MonsterListViewState +from padinfo.view.monster_list.monster_list import MonsterListViewState, MonsterListView if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -119,3 +119,13 @@ async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: monster = await dbcog.find_monster(ims['raw_query'], ims['original_author_id']) monster_list = await cls.do_query(dbcog, monster) return monster_list + + +class ScrollView(MonsterListView): + emojis = ['\N{BLACK LEFT-POINTING TRIANGLE}', '\N{BLACK RIGHT-POINTING TRIANGLE}'] + + @classmethod + def get_emoji(cls, i: int, current_monster_id: int): + if current_monster_id == 1: + return cls.emojis[1] + return cls.emojis[i] From cac338c006b69758cf17d9bc17cae2168c6673ce Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 24 Sep 2021 02:45:59 -0700 Subject: [PATCH 0975/1220] Use transformations table in graph (#1453) --- dbcog/monster_graph.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 286c8866..3651e035 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -59,7 +59,8 @@ COALESCE(series.name_ko, 'Unsorted') AS s_name_ko, series.series_type AS s_series_type, exchanges.target_monster_id AS evo_gem_id, - drops.drop_id + drops.drop_id, + transformations{0}.to_monster_id AS transform_to_id FROM monsters{0} LEFT OUTER JOIN leader_skills{0} ON monsters{0}.leader_skill_id = leader_skills{0}.leader_skill_id @@ -70,6 +71,7 @@ LEFT OUTER JOIN exchanges ON target_monsters.monster_id = exchanges.target_monster_id LEFT OUTER JOIN drops ON monsters{0}.monster_id = drops.monster_id LEFT OUTER JOIN monster_name_overrides ON monsters{0}.monster_id = monster_name_overrides.monster_id + LEFT OUTER JOIN transformations{0} ON monsters{0}.monster_id = transformations{0}.from_monster_id GROUP BY monsters{0}.monster_id""" @@ -269,10 +271,10 @@ def build_graph(self, server: Server) -> MultiDiGraph: graph.add_node(m.monster_id, model=m_model) self.max_monster_id = max(self.max_monster_id, m.monster_id) - if m.linked_monster_id: - if self.debug_monster_ids is None or m.linked_monster_id in self.debug_monster_ids: - graph.add_edge(m.monster_id, m.linked_monster_id, type='transformation') - graph.add_edge(m.linked_monster_id, m.monster_id, type='back_transformation') + if m.transform_to_id: + if self.debug_monster_ids is None or m.transform_to_id in self.debug_monster_ids: + graph.add_edge(m.monster_id, m.transform_to_id, type='transformation') + graph.add_edge(m.transform_to_id, m.monster_id, type='back_transformation') if m.evo_gem_id: if self.debug_monster_ids is None or m.evo_gem_id in self.debug_monster_ids: @@ -316,7 +318,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: for vendor_id in re.findall(r'\d+', ex.required_monster_ids): if self.debug_monster_ids is not None: if ex.target_monster_id not in self.debug_monster_ids \ - or int(vendor_id) not in self.debug_monster_ids: + or int(vendor_id) not in self.debug_monster_ids: continue exchanges[(int(vendor_id), ex.target_monster_id)].add(model) for (sell_id, buy_id), models in exchanges.items(): @@ -364,7 +366,7 @@ def _get_edge_model_set(self, monster: MonsterModel, etype, model_type: Type[M] return {model for model in possible_results} def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER, do_logging: bool = False) \ - -> Optional[MonsterModel]: + -> Optional[MonsterModel]: if monster_id not in self.graph_dict[server].nodes: return None if 'model' not in self.graph_dict[server].nodes[monster_id]: @@ -668,7 +670,7 @@ def monster_is_currently_exchangable(self, monster: MonsterModel, server: Option now = datetime.now() for model in models: if model.start_timestamp < now < model.end_timestamp \ - and (server is None or server == model.server): + and (server is None or server == model.server): return True return False From be6e7567416249136e076762a9be547466a71691 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 24 Sep 2021 02:59:25 -0700 Subject: [PATCH 0976/1220] Add get_aliased_attribute (#1452) --- dbcog/dbcog.py | 25 ++++++++++++++++++++++--- dbcog/token_mappings.py | 6 ++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 66b3343c..f2930a8e 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -10,9 +10,11 @@ import logging import os import shutil +from functools import reduce + import time from io import BytesIO -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union import discord from redbot.core import Config, checks, commands, data_manager @@ -30,7 +32,7 @@ from .models.enum_types import DEFAULT_SERVER, SERVERS from .models.monster_model import MonsterModel from .models.monster_stats import MonsterStatModifierInput, monster_stats -from .monster_index import MONSTER_CLASS_ATTRIBUTES, MonsterIndex +from .monster_index import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES, MonsterIndex logger = logging.getLogger('red.padbot-cogs.dbcog') @@ -178,7 +180,7 @@ async def check_index(self): for channel in channels: await channel.send(box(page)) - def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> MonsterModel: + def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[MonsterModel]: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" return self.database.graph.get_monster(monster_id, server=server) @@ -315,3 +317,20 @@ async def find_monster(self, query: str, author_id: int = 0) -> Optional[Monster async def find_monsters(self, query: str, author_id: int = 0) -> List[MonsterModel]: return await FindMonster(self, await self.get_fm_flags(author_id)).find_monsters(query) + + @staticmethod + def get_aliased_attribute(monster: MonsterModel, alias: str) -> Union[Dict[str, Any], Any]: + if alias not in MONSTER_ATTR_ALIAS_TO_ATTR_MAP: + raise ValueError(f"Invalid alias {alias}") + + ret = {} + keys = [ks for ks in zip(*MONSTER_ATTR_ALIAS_TO_ATTR_MAP[alias]) + if len(ks) == 1 or not reduce(lambda x, y: x == y, ks)].pop() + for key, attrs in zip(keys, MONSTER_ATTR_ALIAS_TO_ATTR_MAP[alias]): + value: Any = monster + for attr in attrs: + value = getattr(value, attr) + ret[key] = value + if len(ret) == 1: + return set(ret.values()).pop() + return ret diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index f1e598ef..c0a19d70 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -325,6 +325,7 @@ class MiscModifiers(Enum): NUMERIC_MONSTER_ATTRIBUTE_NAMES = {*sum(NUMERIC_MONSTER_ATTRIBUTE_ALIASES.values(), ())} STRING_MONSTER_ATTRIBUTE_ALIASES = { + (('name_en',), ('name_ja',)): ('monstername',), (('leader_skill', 'name_en'),): ('lsname',), (('leader_skill', 'desc_en'),): ('lsdesc',), (('active_skill', 'name_en'),): ('asname',), @@ -341,3 +342,8 @@ class MiscModifiers(Enum): *sum(STRING_MONSTER_ATTRIBUTE_ALIASES, ()), *sum(BOOL_MONSTER_ATTRIBUTE_NAMES, ()), } +MONSTER_ATTR_ALIAS_TO_ATTR_MAP = {v: k for k, vs in { + **NUMERIC_MONSTER_ATTRIBUTE_ALIASES, + **STRING_MONSTER_ATTRIBUTE_ALIASES, + **BOOL_MONSTER_ATTRIBUTE_ALIASES, +}.items() for v in vs} From d0890c83689a1c0432b5d4bcbbde543f77042e88 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 24 Sep 2021 09:26:02 -0500 Subject: [PATCH 0977/1220] Add ^attr command (#1454) --- dbcog/dbcog.py | 13 +++++++++++++ dbcog/token_mappings.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index f2930a8e..98d3c2b3 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -334,3 +334,16 @@ def get_aliased_attribute(monster: MonsterModel, alias: str) -> Union[Dict[str, if len(ret) == 1: return set(ret.values()).pop() return ret + + @commands.command() + async def attr(self, ctx, attr, *, query): + await self.wait_until_ready() + data = self.get_aliased_attribute(await self.find_monster(query, ctx.author.id), attr) + if not isinstance(data, dict): + await ctx.send(data) + return + for k, v in data.items(): + if k.endswith('_en'): + await ctx.send(v) + return + await ctx.send('\n'.join([f"**{k}**: {v}" for k, v in data.items()])) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index c0a19d70..cae89c47 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -326,6 +326,8 @@ class MiscModifiers(Enum): STRING_MONSTER_ATTRIBUTE_ALIASES = { (('name_en',), ('name_ja',)): ('monstername',), + (('leader_skill', 'name_en'), ('active_skill', 'name_en'),): ('skillname',), + (('leader_skill', 'desc_en'), ('active_skill', 'desc_en'),): ('skilltext',), (('leader_skill', 'name_en'),): ('lsname',), (('leader_skill', 'desc_en'),): ('lsdesc',), (('active_skill', 'name_en'),): ('asname',), From c37243ba69c3d498bb83dfe149810acb540558ab Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 24 Sep 2021 10:01:12 -0500 Subject: [PATCH 0978/1220] Add a couple monster attr aliases (#1456) --- dbcog/dbcog.py | 8 +++----- dbcog/token_mappings.py | 10 +++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 98d3c2b3..da50b4d2 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -340,10 +340,8 @@ async def attr(self, ctx, attr, *, query): await self.wait_until_ready() data = self.get_aliased_attribute(await self.find_monster(query, ctx.author.id), attr) if not isinstance(data, dict): - await ctx.send(data) - return + return await ctx.send(data) for k, v in data.items(): if k.endswith('_en'): - await ctx.send(v) - return - await ctx.send('\n'.join([f"**{k}**: {v}" for k, v in data.items()])) + return await ctx.send(v) + return await ctx.send('\n'.join([f"**{k}**: {v}" for k, v in data.items()])) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index cae89c47..3b6d79a1 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -306,12 +306,12 @@ class MiscModifiers(Enum): (('leader_skill', 'leader_skill_id'),): ('lsid',), (('active_skill', 'active_skill_id'),): ('asid',), (('series', 'series_id'),): ('sid', 'seriesid'), - (('rarity',),): ('rarity',), + (('rarity',),): ('rarity', 'rare'), (('buy_mp',),): ('buymp',), (('sell_mp',),): ('sellmp',), - (('sell_gold',),): ('gold',), - (('cost',),): ('cost',), - (('exp',),): ('exp',), + (('sell_gold',),): ('gold', 'coins'), + (('cost',),): ('cost', 'teamcost'), + (('exp',),): ('exp', 'exptomax', 'xptomax'), (('fodder_exp',),): ('fodderexp',), (('level',),): ('maxlvl', 'maxlevel'), (('latent_slots',),): ('latentslots',), @@ -325,7 +325,7 @@ class MiscModifiers(Enum): NUMERIC_MONSTER_ATTRIBUTE_NAMES = {*sum(NUMERIC_MONSTER_ATTRIBUTE_ALIASES.values(), ())} STRING_MONSTER_ATTRIBUTE_ALIASES = { - (('name_en',), ('name_ja',)): ('monstername',), + (('name_en',), ('name_ja',)): ('monstername', 'cardname'), (('leader_skill', 'name_en'), ('active_skill', 'name_en'),): ('skillname',), (('leader_skill', 'desc_en'), ('active_skill', 'desc_en'),): ('skilltext',), (('leader_skill', 'name_en'),): ('lsname',), From 137bb97e4579b3f17c864f13e58ae29986b7f916 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 24 Sep 2021 10:12:27 -0500 Subject: [PATCH 0979/1220] Add some basic error handling (#1457) --- dbcog/dbcog.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index da50b4d2..630c9d63 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -338,7 +338,15 @@ def get_aliased_attribute(monster: MonsterModel, alias: str) -> Union[Dict[str, @commands.command() async def attr(self, ctx, attr, *, query): await self.wait_until_ready() - data = self.get_aliased_attribute(await self.find_monster(query, ctx.author.id), attr) + monster = await self.find_monster(query, ctx.author.id) + if monster is None: + return await ctx.send("Sorry, we could not locate this monster") + try: + data = self.get_aliased_attribute(monster, attr) + except ValueError: + return await ctx.send("Sorry, this attribute is not recognized") + except AttributeError: + return await ctx.send("Sorry, {} doesn't have this attribute".format(monster.name_en)) if not isinstance(data, dict): return await ctx.send(data) for k, v in data.items(): From 2bb2a886bf2ab7a419b96b873a175b1f5dffb499 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 28 Sep 2021 23:44:54 -0700 Subject: [PATCH 0980/1220] Fix PadEvents (#1459) --- dbcog/models/dungeon_model.py | 2 + dbcog/models/scheduled_event_model.py | 8 +- padevents/autoevent_mixin.py | 527 ++++++++++++ padevents/enums.py | 20 + padevents/events.py | 201 +++++ padevents/padevents.py | 1089 +++---------------------- 6 files changed, 881 insertions(+), 966 deletions(-) create mode 100644 padevents/autoevent_mixin.py create mode 100644 padevents/enums.py create mode 100644 padevents/events.py diff --git a/dbcog/models/dungeon_model.py b/dbcog/models/dungeon_model.py index 3fac8b7b..b0f9b7cd 100644 --- a/dbcog/models/dungeon_model.py +++ b/dbcog/models/dungeon_model.py @@ -48,6 +48,8 @@ def _make_clean_name_en(name): "Star Treasure Thieves' Den": 'STTD', 'Ruins of the Star Vault': 'Star Vault', '-★6 or lower Enhanced': '', + 'Revenge of the ': '', + 'A Gathering of ': '', } for find, replace in clean_name_map.items(): name = name.replace(find, replace) diff --git a/dbcog/models/scheduled_event_model.py b/dbcog/models/scheduled_event_model.py index b87e233f..bd004577 100644 --- a/dbcog/models/scheduled_event_model.py +++ b/dbcog/models/scheduled_event_model.py @@ -20,19 +20,19 @@ def __init__(self, **kwargs): self.sub_dungeon_id = kwargs.get("sub_dungeon_id") @property - def open_datetime(self): + def open_datetime(self) -> datetime: return datetime.utcfromtimestamp(self.start_timestamp).replace(tzinfo=pytz.UTC) @open_datetime.setter - def open_datetime(self, value): + def open_datetime(self, value: datetime): self.start_timestamp = int(value.timestamp()) @property - def close_datetime(self): + def close_datetime(self) -> datetime: return datetime.utcfromtimestamp(self.end_timestamp).replace(tzinfo=pytz.UTC) @close_datetime.setter - def close_datetime(self, value): + def close_datetime(self, value: datetime): self.end_timestamp = int(value.timestamp()) def to_dict(self): diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py new file mode 100644 index 00000000..fa694f9f --- /dev/null +++ b/padevents/autoevent_mixin.py @@ -0,0 +1,527 @@ +import logging +import re +from typing import List, Optional, Set, Union + +import discord +import time +from redbot.core import Config, checks, commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import box, humanize_timedelta, inline +from tsutils.cogs.donations import is_donor +from tsutils.emoji import NO_EMOJI, YES_EMOJI +from tsutils.enums import Server, StarterGroup +from tsutils.errors import ClientInlineTextException +from tsutils.formatting import rmdiacritics +from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message + +from padevents.events import Event + +logger = logging.getLogger('red.padbot-cogs.padevents') + +GROUPS = ['red', 'blue', 'green'] + + +def user_is_donor(ctx, only_patron=False): + if ctx.author.id in ctx.bot.owner_ids: + return True + donationcog = ctx.bot.get_cog("Donations") + if not donationcog: + return False + return donationcog.is_donor(ctx, only_patron) + + +class AutoEvent: + bot: Red + config: Config + events: List[Event] + started_events: Set[Event] + + async def do_autoevents(self): + events = filter(lambda e: e.key not in self.started_events, self.events) + for event in events: + for gid, data in (await self.config.all_guilds()).items(): + guild = self.bot.get_guild(gid) + if guild is None: + continue + for key, aep in data.get('pingroles', {}).items(): + if event.start_from_now_sec() > aep['offset'] * 60 \ + or not aep['enabled'] \ + or event.server != aep['server'] \ + or aep['searchstr'] is None \ + or str((key, event.key, gid)) in await self.config.sent(): + continue + elif aep['regex']: + matches = re.search(aep['searchstr'], event.clean_dungeon_name) + else: + matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() + + if not matches: + continue + + async with self.config.sent() as sent: + sent[str((key, event.key, gid))] = time.time() + + index = GROUPS.index(event.group) + channel = guild.get_channel(aep['channels'][index]) + if channel is None: + continue + role = guild.get_role(aep['roles'][index]) + ment = role.mention if role else "" + offsetstr = "now" + if aep['offset']: + offsetstr = f"" + try: + timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) + await channel.send(f"{event.name_and_modifier} starts {offsetstr}!" + f" It will be active for {timestr}. {ment}", + allowed_mentions=discord.AllowedMentions(roles=True)) + except Exception: + logger.exception("Failed to send AEP in channel {}".format(channel.id)) + + for uid, data in (await self.config.all_users()).items(): + user = self.bot.get_user(uid) + if user is None: + continue + for aed in data.get('dmevents', []): + if event.start_from_now_sec() > aed['offset'] * 60 \ + or (event.group not in (aed['group'], None)) \ + or event.server != aed['server'] \ + or aed['searchstr'] is None \ + or str((aed['key'], event.key, uid)) in await self.config.sent(): + continue + if aed.get('include3p') is None: + # case of legacy configs + aed['include3p'] = True + if not aed['include3p'] and event.clean_dungeon_name.startswith("Multiplayer"): + continue + async with self.config.sent() as sent: + sent[str((aed['key'], event.key, uid))] = time.time() + if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): + offsetstr = "now" + if aed['offset']: + offsetstr = f"" + timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) + try: + await user.send(f"{event.clean_dungeon_name} starts {offsetstr}!" + f" It will be active for {timestr}.") + except Exception: + logger.exception("Failed to send AED to user {}".format(user.id)) + + @commands.group(aliases=['aep']) + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def autoeventping(self, ctx): + """Auto Event Pings""" + + @autoeventping.command(name="add", ignore_extra=False) + async def aep_add(self, ctx, key, server: Server = None, searchstr=None, + red_role: Optional[discord.Role] = None, + blue_role: Optional[discord.Role] = None, + green_role: Optional[discord.Role] = None, + channel: discord.TextChannel = None): + """Add a new autoeventping. + Use `[p]aep set` with your chosen key to finish or update config. + `channel` defaults to the current channel. + + Usage: + `[p]aep add diamondra` + `[p]aep add pluspoints NA "star treasure" @red @blue @green #channel` + `[p]aep add wallace KR "wallace" @eventping #channel` + """ + + if " " in key: + await ctx.send("Multi-word keys are not allowed.") + return + + if server: + server = server.value + + if blue_role is None: + blue_role = green_role = red_role + + default = { + 'roles': [None, None, None], + 'channels': [(channel or ctx.channel).id] * 3, + 'server': server or 'NA', + 'searchstr': searchstr and rmdiacritics(searchstr), + 'regex': False, + 'enabled': bool(red_role or channel), + 'offset': 0, + } + + if red_role is not None: + default['roles'] = [red_role.id, blue_role.id, green_role and green_role.id] + + async with self.config.guild(ctx.guild).pingroles() as pingroles: + pingroles[key] = default + + await ctx.send("New AEP created:") + await self.aep_show(ctx, key) + + @autoeventping.command(name="remove", aliases=['rm', 'delete']) + async def aep_remove(self, ctx, key): + """Remove an autoeventping""" + async with self.config.guild(ctx.guild).pingroles() as pingroles: + if key not in pingroles: + await ctx.send("That key does not exist.") + return + del pingroles[key] + await ctx.tick() + + @autoeventping.command(name="show") + async def aep_show(self, ctx, key): + """Show specifics of an autoeventping""" + pingroles = await self.config.guild(ctx.guild).pingroles() + if key not in pingroles: + await ctx.send("That key does not exist.") + return + roles = [ctx.guild.get_role(role).mention + if ctx.guild.get_role(role) is not None + else "No Role" + for role + in pingroles[key]['roles'] + ] + chans = [ctx.bot.get_channel(c).mention + if ctx.bot.get_channel(c) is not None + else "No Channel" + for c + in pingroles[key]['channels'] + ] + pr = (f"Key: `{key}`\n" + f"\tStatus: {YES_EMOJI + ' enabled' if pingroles[key]['enabled'] else NO_EMOJI + ' disabled'}\n" + f"\tSearch string: `{pingroles[key]['searchstr']}` {'(regex search)' * pingroles[key]['regex']}\n" + f"\tServer: {pingroles[key]['server']}\n" + f"\tRed: {roles[0]} (In {chans[0]})\n" + f"\tBlue: {roles[1]} (In {chans[1]})\n" + f"\tGreen: {roles[2]} (In {chans[2]})\n" + f"\tOffset: `{pingroles[key]['offset']} minutes`") + await ctx.send(pr) + + @autoeventping.command(name="list") + async def aep_list(self, ctx): + """List all autoeventpings""" + pingroles = await self.config.guild(ctx.guild).pingroles() + if len(pingroles) == 0: + await ctx.send('You have no auto event pings configured currently!') + return + await ctx.send(box('\n'.join(pingroles))) + + @autoeventping.group(name="set") + async def aep_set(self, ctx): + """Sets specific parts of an autoeventping""" + + async def aepchange(self, ctx, key, k, f): + async with self.config.guild(ctx.guild).pingroles() as pingroles: + if key not in pingroles: + await ctx.send("That key does not exist.") + return + pingroles[key][k] = f(pingroles[key][k]) + + async def aepset(self, ctx, key, k, v): + await self.aepchange(ctx, key, k, lambda x: v) + + async def aepget(self, ctx, key): + pingroles = await self.config.guild(ctx.guild).pingroles() + if key not in pingroles: + raise ClientInlineTextException(f"Key `{key}` does not exist. Available keys are:" + f" {', '.join(map(inline, pingroles))}") + return pingroles[key] + + async def aep_remove_channel(self, ctx, key, group, f): + pingroles = await self.config.guild(ctx.guild).pingroles() + if key not in pingroles: + raise ClientInlineTextException(f"Key `{key}` does not exist. Available keys are:" + f" {', '.join(map(inline, pingroles))}") + + if not await get_user_confirmation(ctx, "Are you sure you want to remove the {} channel for AEP `{}`?" + .format(group, key)): + await send_cancellation_message(ctx, + "No action was taken. You can set a channel with `{}aep set {}channel {} #channel_name`" + .format(ctx.prefix, group, key)) + return + else: + await self.aepchange(ctx, key, 'channels', f) + await send_confirmation_message(ctx, "Okay, I removed the {} channel for the AEP `{}`" + .format(group, key)) + return + + @aep_set.command(name="channel") + async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): + """Sets channel to ping for all groups""" + await self.aepset(ctx, key, 'channels', [channel.id] * 3) + await ctx.tick() + + @aep_set.command(name="redchannel") + async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel = None): + """Sets channel to ping when event is red""" + if channel is None: + await self.aep_remove_channel(ctx, key, "red", lambda x: [None, x[1], x[2]]) + else: + await self.aepchange(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) + await ctx.tick() + + @aep_set.command(name="bluechannel") + async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel = None): + """Sets channel to ping when event is blue""" + if channel is None: + await self.aep_remove_channel(ctx, key, "blue", lambda x: [x[0], None, x[2]]) + else: + await self.aepchange(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) + await ctx.tick() + + @aep_set.command(name="greenchannel") + async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel = None): + """Sets channel to ping when event is green""" + if channel is None: + await self.aep_remove_channel(ctx, key, "green", lambda x: [x[0], x[1], None]) + else: + await self.aepchange(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) + await ctx.tick() + + @aep_set.command(name="roles") + async def aep_s_roles(self, ctx, key, red: discord.Role, blue: discord.Role, green: discord.Role): + """Sets roles to ping""" + await self.aepset(ctx, key, 'roles', [red.id, blue.id, green.id]) + await ctx.tick() + + @aep_set.command(name="redrole") + async def aep_s_redrole(self, ctx, key, role: discord.Role): + """Sets role to ping when event is red""" + await self.aepchange(ctx, key, 'roles', lambda x: [role.id, x[1], x[2]]) + await ctx.tick() + + @aep_set.command(name="bluerole") + async def aep_s_bluerole(self, ctx, key, role: discord.Role): + """Sets role to ping when event is blue""" + await self.aepchange(ctx, key, 'roles', lambda x: [x[0], role.id, x[2]]) + await ctx.tick() + + @aep_set.command(name="greenrole") + async def aep_s_greenrole(self, ctx, key, role: discord.Role): + """Sets role to ping when event is green""" + await self.aepchange(ctx, key, 'roles', lambda x: [x[0], x[1], role.id]) + await ctx.tick() + + @aep_set.command(name="server") + async def aep_s_server(self, ctx, key, server: Server): + """Sets which server to listen to events in""" + server = server.value + await self.aepset(ctx, key, 'server', server) + await ctx.tick() + + @aep_set.command(name="searchstr") + async def aep_s_searchstr(self, ctx, key, *, searchstr): + """Sets what string is tested against event name""" + searchstr = rmdiacritics(searchstr).strip('"') + if (await self.aepget(ctx, key))['regex']: + try: + re.compile(searchstr) + except re.error: + await ctx.send("Invalid regex searchstr. (`{}`)".format(searchstr)) + return + await self.aepset(ctx, key, 'searchstr', searchstr) + await ctx.tick() + + @aep_set.command(name="regex") + async def aep_s_regex(self, ctx, key, regex: bool): + """Sets whether searchstr is calculated via regex""" + if regex: + try: + re.compile((await self.aepget(ctx, key))['searchstr']) + except re.error: + await ctx.send("Invalid regex searchstr. (`{}`)".format((await self.aepget(ctx, key))['searchstr'])) + return + await self.aepset(ctx, key, 'regex', regex) + await ctx.tick() + + @aep_set.command(name="enabled", aliases=['enable']) + async def aep_s_enabled(self, ctx, key, enabled: bool = True): + """Sets whether or not ping is enabled""" + await self.aepset(ctx, key, 'enabled', enabled) + await ctx.tick() + + @aep_set.command(name="disabled", aliases=['disable']) + async def aep_s_disabled(self, ctx, key, disabled: bool = True): + """Sets whether or not ping is disabled""" + await self.aepset(ctx, key, 'enabled', not disabled) + await ctx.tick() + + @aep_set.command(name="offset") + async def aep_s_offset(self, ctx, key, offset: int): + """Sets how many minutes before event should ping happen""" + if offset < 0: + await ctx.send("Offset cannot be negative.") + return + await self.aepset(ctx, key, 'offset', offset) + await ctx.tick() + + @commands.group(aliases=['aed']) + async def autoeventdm(self, ctx): + """Auto Event DMs""" + + @autoeventdm.command(name="add") + async def aed_add(self, ctx, server: Server, searchstr, group: Optional[StarterGroup] = None, time_offset: int = 0): + """Add a new autoeventdm""" + server = server.value + + if group is None: + group = 'red' + else: + group = GROUPS[group.value] + + if time_offset and not user_is_donor(ctx): + await ctx.send("You must be a donor to set a time offset!") + return + if time_offset < 0: + await ctx.send("Offset cannot be negative") + return + + default = { + 'key': time.time(), + 'server': server, + 'group': group, + 'searchstr': searchstr, + 'include3p': True, + 'offset': time_offset, + } + + index = await self._do_aed_add(ctx, default) + await ctx.send("New AED created:") + await self.aed_show(ctx, index) + + @autoeventdm.command(name="remove", aliases=['rm', 'delete']) + async def aed_remove(self, ctx, index): + """Remove an autoeventdm""" + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return + async with self.config.user(ctx.author).dmevents() as dmevents: + if not await get_user_confirmation(ctx, "Are you sure you want to delete autoeventdm {}. `{}`?" + .format(corrected_index + 1, dmevents[corrected_index]['searchstr'])): + return + dmevents.pop(corrected_index) + await ctx.tick() + + @autoeventdm.command(name="show") + async def aed_show(self, ctx, index): + """Show specifics of an autoeventdm""" + dmevents = await self.config.user(ctx.author).dmevents() + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return + if dmevents[corrected_index].get('include3p') is None: + # case of legacy configs + dmevents[corrected_index]['include3p'] = True + ret = (f"Lookup number: `{corrected_index + 1}`\n" + f"\tSearch string: `{dmevents[corrected_index]['searchstr']}`\n" + f"\t3P: {'included' if dmevents[corrected_index]['include3p'] else 'excluded'}\n" + f"\tServer: {dmevents[corrected_index]['server']}\n" + f"\tGroup: {dmevents[corrected_index]['group'].title()} \n" + f"\tOffset (Donor Only): `{dmevents[corrected_index]['offset']} minutes`") + await ctx.send(ret) + + @autoeventdm.command(name="list") + async def aed_list(self, ctx): + """List current autoeventdms""" + dmevents = await self.config.user(ctx.author).dmevents() + msg = "\n".join("{}. {}".format(c, aed['searchstr']) for c, aed in enumerate(dmevents, 1)) + if msg: + await ctx.send(box(msg)) + else: + await ctx.send("You have no autoeventdms set up.") + + @autoeventdm.command(name="purge") + async def aed_purge(self, ctx): + """Remove an autoeventdm""" + if not await self.config.user(ctx.author).dmevents(): + await ctx.send("You don't have any autoeventdms.") + return + if not await get_user_confirmation(ctx, "Are you sure you want to purge your autoeventdms?"): + return + await self.config.user(ctx.author).dmevents.set([]) + await ctx.tick() + + @autoeventdm.group(name="edit", aliases=["set"]) + async def aed_e(self, ctx): + """Edit a property of the autoeventdm""" + + async def _do_aed_add(self, ctx, item): + """Add autoeventdm and return its index""" + async with self.config.user(ctx.author).dmevents() as dmevents: + dmevents.append(item) + return len(dmevents) + + @is_donor() + @aed_e.command(name="offset") + async def aed_e_offset(self, ctx, index, offset: int): + """(DONOR ONLY) Set time offset to an AED to allow you to prepare for a dungeon""" + if offset < 0: + await send_cancellation_message(ctx, "Offset cannot be negative") + return + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return + async with self.config.user(ctx.author).dmevents() as dmevents: + if not await get_user_confirmation(ctx, "Modify the offset for {}. `{}` to {} minutes?" + .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], offset)): + return + dmevents[corrected_index]['offset'] = offset + await ctx.tick() + + @aed_e.command(name="searchstr") + async def aed_e_searchstr(self, ctx, index, *, searchstr): + """Set search string of an autoeventdm""" + searchstr = searchstr.strip('"') + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return + async with self.config.user(ctx.author).dmevents() as dmevents: + if not await get_user_confirmation(ctx, "Modify the search string for {}. `{}` to `{}`?" + .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], searchstr)): + return + dmevents[corrected_index]['searchstr'] = searchstr + await ctx.tick() + + @aed_e.command(name="toggle3p") + async def aed_e_toggle3p(self, ctx, index): + """Include/exclude 3-player dungeons in an autoeventdm""" + corrected_index = await self._get_and_validate_aed_index(ctx, index) + if corrected_index is None: + return + async with self.config.user(ctx.author).dmevents() as dmevents: + event = dmevents[corrected_index]['searchstr'] + if dmevents[corrected_index].get('include3p') is None: + # case of legacy configs + dmevents[corrected_index]['include3p'] = True + if dmevents[corrected_index]['include3p']: + dmevents[corrected_index]['include3p'] = False + await send_confirmation_message(ctx, "I will **exclude** 3P dungeons for {}. `{}`" + .format(corrected_index + 1, event)) + return + dmevents[corrected_index]['include3p'] = True + await send_confirmation_message(ctx, "I will **include** 3P dungeons for {}. `{}`" + .format(corrected_index + 1, event)) + return + + async def _get_and_validate_aed_index(self, ctx, user_index: Union[str, int]): + """Returns the corrected index for the autoeventdm of interest, based on Python list indexing""" + async with self.config.user(ctx.author).dmevents() as dmevents: + if isinstance(user_index, int) or user_index.isdigit(): + user_index = int(user_index) + if not 0 < user_index <= len(dmevents): + await send_cancellation_message(ctx, "That isn't a valid numerical index.") + return None + else: + str_index = None + for i in range(1, len(dmevents) + 1, 1): + if dmevents[i - 1]['searchstr'] == user_index: + str_index = i + break + user_index = str_index + if user_index is None: + await send_cancellation_message(ctx, f"That string did not match any existing patterns." + f" Printing all of your Auto Event DMs (you can also" + f" see this with `{ctx.prefix}autoeventdm list`):") + await self.aed_list(ctx) + return None + return user_index - 1 diff --git a/padevents/enums.py b/padevents/enums.py new file mode 100644 index 00000000..d271f7da --- /dev/null +++ b/padevents/enums.py @@ -0,0 +1,20 @@ +from enum import Enum, auto + + +class EventLength(Enum): + limited = auto() + daily = auto() + weekly = auto() + special = auto() + + +class DungeonType(Enum): + Unknown = -1 + Normal = 0 + Special = 1 + Technical = 2 + SoloSpecial = 3 + Tournament = 4 + ThreePlayer = 7 + Story = 9 + EightPlayer = 10 diff --git a/padevents/events.py b/padevents/events.py new file mode 100644 index 00000000..a01eab13 --- /dev/null +++ b/padevents/events.py @@ -0,0 +1,201 @@ +import datetime +from datetime import timedelta +from typing import Callable, List, TYPE_CHECKING + +import itertools +import pytz +from tsutils.formatting import normalize_server_name +from tsutils.time import JP_TIMEZONE, NA_TIMEZONE, KR_TIMEZONE + +from padevents.enums import DungeonType, EventLength + +if TYPE_CHECKING: + from dbcog.models.scheduled_event_model import ScheduledEventModel + +SUPPORTED_SERVERS = ["JP", "NA", "KR"] +SERVER_TIMEZONES = { + "JP": JP_TIMEZONE, + "NA": NA_TIMEZONE, + "KR": KR_TIMEZONE, +} + + +class Event: + def __init__(self, scheduled_event: "ScheduledEventModel"): + self.key = scheduled_event.event_id + self.server = SUPPORTED_SERVERS[scheduled_event.server_id] + self.open_datetime = scheduled_event.open_datetime + self.close_datetime = scheduled_event.close_datetime + self.group = scheduled_event.group_name + self.dungeon = scheduled_event.dungeon + self.dungeon_name = self.dungeon.name_en if self.dungeon else 'unknown_dungeon' + self.event_name = '' + + self.clean_dungeon_name = self.dungeon.clean_name_en if self.dungeon else 'unknown_dungeon' + self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') + + self.name_and_modifier = self.clean_dungeon_name + if self.clean_event_name != '': + self.name_and_modifier += ', ' + self.clean_event_name + + self.dungeon_type = DungeonType(self.dungeon.dungeon_type) if self.dungeon else DungeonType.Unknown + + def start_from_now_sec(self): + now = datetime.datetime.now(pytz.utc) + return (self.open_datetime - now).total_seconds() + + def end_from_now_sec(self): + now = datetime.datetime.now(pytz.utc) + return (self.close_datetime - now).total_seconds() + + def is_started(self): + """True if past the open time for the event.""" + return self.start_from_now_sec() <= 0 + + def is_finished(self): + """True if past the close time for the event.""" + return self.end_from_now_sec() <= 0 + + def is_active(self): + """True if between open and close time for the event.""" + return self.is_started() and not self.is_finished() + + def is_pending(self): + """True if event has not started.""" + return not self.is_started() + + def is_available(self): + """True if event has not finished.""" + return not self.is_finished() + + @property + def event_length(self) -> EventLength: + # This is a little off. I don't know how exact these things are. + length = self.close_datetime - self.open_datetime + if length > timedelta(days=8): + return EventLength.special + if length > timedelta(days=2): + return EventLength.weekly + if length > timedelta(hours=20): + return EventLength.daily + return EventLength.limited + + def tostr(self): + return self.open_datetime.strftime("%Y-%m-%d %H:%M") + "," + self.close_datetime.strftime("%Y-%m-%d %H:%M") \ + + "," + self.group + "," + self.dungeon_name + + def start_pst(self): + tz = pytz.timezone('US/Pacific') + return self.open_datetime.astimezone(tz) + + def start_est(self): + tz = pytz.timezone('US/Eastern') + return self.open_datetime.astimezone(tz) + + def start_from_now(self): + return f'{self.start_from_now_sec() // 3600:2}h' \ + f' {(self.start_from_now_sec() % 3600) // 60:2}m' + + def end_from_now(self): + return f'{self.end_from_now_sec() // 3600:2}h' \ + f' {(self.end_from_now_sec() % 3600) // 60:2}m' + + def end_from_now_full_min(self): + days, sec = divmod(self.end_from_now_sec(), 86400) + hours, sec = divmod(sec, 3600) + minutes, sec = divmod(sec, 60) + + if days > 0: + return '{:2}d {:2}h'.format(int(days), int(hours)) + elif hours > 0: + return '{:2}h {:2}m'.format(int(hours), int(minutes)) + else: + return '{:2}m'.format(int(minutes)) + + def to_guerrilla_str(self): + return self.start_pst().strftime("%H:%M") + + def to_date_str(self): + return self.server + "," + self.group + "," + self.start_pst().strftime("%Y-%m-%d %H:%M") + "," + \ + self.start_est().strftime("%Y-%m-%d %H:%M") + "," + self.start_from_now() + + def group_short_name(self): + return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') + + def group_long_name(self): + return self.group.upper() if self.group is not None else "UNGROUPED" + + def to_partial_event(self, pe): + group = self.group_short_name() + if self.is_started(): + return group + " " + self.end_from_now() + " " + self.name_and_modifier + else: + return group + " " + self.start_pst().strftime("%H:%M") + " " + self.start_est().strftime("%H:%M") \ + + " " + self.start_from_now() + " " + self.name_and_modifier + + +class EventList: + def __init__(self, event_list: List[Event]): + self.event_list = event_list + + def with_func(self, func: Callable[[Event], bool], exclude=False): + if exclude: + return EventList(list(itertools.filterfalse(func, self.event_list))) + else: + return EventList(list(filter(func, self.event_list))) + + def with_server(self, server): + return self.with_func(lambda e: e.server == normalize_server_name(server)) + + def with_type(self, event_type): + return self.with_func(lambda e: e.event_type == event_type) + + def in_type(self, event_types): + return self.with_func(lambda e: e.event_type in event_types) + + def with_length(self, event_length): + return self.with_func(lambda e: e.event_length == event_length) + + def in_length(self, event_lengths): + return self.with_func(lambda e: e.event_length in event_lengths) + + def with_dungeon_type(self, dungeon_type, exclude=False): + return self.with_func(lambda e: e.dungeon_type == dungeon_type, exclude) + + def in_dungeon_type(self, dungeon_types, exclude=False): + return self.with_func(lambda e: e.dungeon_type in dungeon_types, exclude) + + def is_grouped(self, exclude=False): + return self.with_func(lambda e: e.group is not None, exclude) + + def with_name_contains(self, name, exclude=False): + return self.with_func(lambda e: name.lower() in e.dungeon_name.lower(), exclude) + + def started_only(self): + return self.with_func(lambda e: e.is_started()) + + def pending_only(self): + return self.with_func(lambda e: e.is_pending()) + + def active_only(self): + return self.with_func(lambda e: e.is_active()) + + def available_only(self): + return self.with_func(lambda e: e.is_available()) + + def today_only(self, server: str): + server_timezone = SERVER_TIMEZONES[normalize_server_name(server)] + today = datetime.datetime.now(server_timezone).day + return self.with_func(lambda e: e.open_datetime.astimezone(server_timezone).day == today) + + def items_by_open_time(self, reverse=False): + return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) + + def items_by_close_time(self, reverse=False): + return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) + + def __iter__(self): + return iter(self.event_list) + + def __bool__(self): + return bool(self.event_list) diff --git a/padevents/padevents.py b/padevents/padevents.py index 36a12acb..c771491c 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -1,43 +1,26 @@ import asyncio import datetime import logging -import re +import time from collections import defaultdict from contextlib import suppress from datetime import timedelta -from enum import Enum from io import BytesIO -from typing import List, NoReturn, Optional, TYPE_CHECKING, Union +from typing import Any, NoReturn, Optional import discord -import itertools import prettytable import pytz -import time from redbot.core import Config, checks, commands -from redbot.core.utils.chat_formatting import box, humanize_timedelta, inline, pagify -from tsutils.cog_settings import CogSettings -from tsutils.cogs.donations import is_donor -from tsutils.emoji import NO_EMOJI, YES_EMOJI +from redbot.core.utils.chat_formatting import box, pagify from tsutils.enums import Server, StarterGroup -from tsutils.errors import ClientInlineTextException -from tsutils.formatting import normalize_server_name, rmdiacritics +from tsutils.formatting import normalize_server_name from tsutils.helper_classes import DummyObject -from tsutils.helper_functions import repeating_timer -from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message - -if TYPE_CHECKING: - from dbcog.models.scheduled_event_model import ScheduledEventModel - - -def user_is_donor(ctx, only_patron=False): - if ctx.author.id in ctx.bot.owner_ids: - return True - donationcog = ctx.bot.get_cog("Donations") - if not donationcog: - return False - return donationcog.is_donor(ctx, only_patron) +from tsutils.helper_functions import conditional_iterator, repeating_timer +from padevents.autoevent_mixin import AutoEvent +from padevents.events import Event, EventList, SERVER_TIMEZONES +from padevents.enums import DungeonType, EventLength logger = logging.getLogger('red.padbot-cogs.padevents') @@ -45,17 +28,17 @@ def user_is_donor(ctx, only_patron=False): GROUPS = ['red', 'blue', 'green'] -class PadEvents(commands.Cog): +class PadEvents(commands.Cog, AutoEvent): """Pad Event Tracker""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.settings = PadEventSettings("padevents") self.config = Config.get_conf(self, identifier=940373775) - self.config.register_global(sent={}) + self.config.register_global(sent={}, last_daychange=None) self.config.register_guild(pingroles={}) + self.config.register_channel(guerrilla_servers=[], daily_servers=[]) self.config.register_user(dmevents=[]) # Load event data @@ -66,6 +49,7 @@ def __init__(self, bot, *args, **kwargs): self._event_loop = bot.loop.create_task(self.reload_padevents()) self._refresh_loop = bot.loop.create_task(self.do_loop()) + self._daily_event_loop = bot.loop.create_task(self.show_daily_info()) async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -85,6 +69,7 @@ def cog_unload(self): self.started_events = set() self._event_loop.cancel() self._refresh_loop.cancel() + self._daily_event_loop.cancel() async def reload_padevents(self) -> NoReturn: await self.bot.wait_until_ready() @@ -109,8 +94,26 @@ async def do_loop(self) -> NoReturn: except Exception: logger.exception("Error in loop:") + async def show_daily_info(self) -> NoReturn: + async def is_day_change(): + curserver = self.get_most_recent_day_change() + oldserver = self.config.last_daychange + if curserver != await oldserver(): + await oldserver.set(curserver) + return curserver + + await self.bot.wait_until_ready() + with suppress(asyncio.CancelledError): + async for server in conditional_iterator(is_day_change, poll_interval=10): + try: + await self.do_daily_post(server) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Error in loop:") + async def refresh_data(self): - dbcog = self.bot.get_cog('DBCog') + dbcog: Any = self.bot.get_cog('DBCog') await dbcog.wait_until_ready() scheduled_events = dbcog.database.get_all_events() @@ -128,113 +131,36 @@ async def refresh_data(self): if value < time.time() + 60 * 60: del seen[key] - async def do_autoevents(self): - events = filter(lambda e: e.key not in self.started_events, self.events) - for event in events: - for gid, data in (await self.config.all_guilds()).items(): - guild = self.bot.get_guild(gid) - if guild is None: - continue - for key, aep in data.get('pingroles', {}).items(): - if event.start_from_now_sec() > aep['offset'] * 60 \ - or not aep['enabled'] \ - or event.server != aep['server'] \ - or str((key, event.key, gid)) in await self.config.sent(): - continue - elif aep['regex']: - matches = re.search(aep['searchstr'], event.clean_dungeon_name) - else: - matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() - - if not matches: - continue - - async with self.config.sent() as sent: - sent[str((key, event.key, gid))] = time.time() - - index = GROUPS.index(event.group) - channel = guild.get_channel(aep['channels'][index]) - if channel is None: - continue - role = guild.get_role(aep['roles'][index]) - ment = role.mention if role else "" - offsetstr = "now" - if aep['offset']: - offsetstr = f"" - try: - timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) - await channel.send(f"{event.name_and_modifier} starts {offsetstr}!" - f" It will be active for {timestr}. {ment}", - allowed_mentions=discord.AllowedMentions(roles=True)) - except Exception: - logger.exception("Failed to send AEP in channel {}".format(channel.id)) - - for uid, data in (await self.config.all_users()).items(): - user = self.bot.get_user(uid) - if user is None: - continue - for aed in data.get('dmevents', []): - if event.start_from_now_sec() > aed['offset'] * 60 \ - or (event.group not in (aed['group'], None)) \ - or event.server != aed['server'] \ - or str((aed['key'], event.key, uid)) in await self.config.sent(): - continue - if aed.get('include3p') is None: - # case of legacy configs - aed['include3p'] = True - if not aed['include3p'] and event.clean_dungeon_name.startswith("Multiplayer"): - continue - async with self.config.sent() as sent: - sent[str((aed['key'], event.key, uid))] = time.time() - if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): - offsetstr = "now" - if aed['offset']: - offsetstr = f"" - timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) - try: - await user.send(f"{event.clean_dungeon_name} starts {offsetstr}!" - f" It will be active for {timestr}.") - except Exception: - logger.exception("Failed to send AED to user {}".format(user.id)) - async def do_eventloop(self): events = filter(lambda e: e.is_started() and e.key not in self.started_events, self.events) daily_refresh_servers = set() for event in events: self.started_events.add(event.key) - if event.event_type in [EventType.Guerrilla, EventType.GuerrillaNew, EventType.SpecialWeek, - EventType.Week]: - for gr in list(self.settings.list_guerrilla_reg()): - if event.server == gr['server']: - try: - channel = self.bot.get_channel(int(gr['channel_id'])) - if channel is None: - continue - - role_name = '{}_group_{}'.format(event.server, event.group_long_name()) - role = channel.guild.get_role(role_name) - if role and role.mentionable: - message = "{}`: {} is starting`".format(role.mention, event.name_and_modifier) - else: - message = box(f"Server {event.server}, group {event.group_long_name()}:" - f" {event.name_and_modifier}") - - await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) - except Exception as ex: - # self.settings.remove_guerrilla_reg(gr['channel_id'], gr['server']) - logger.exception("caught exception while sending guerrilla msg:") - - else: - if event.dungeon_type not in [DungeonType.Normal]: - msg = self.make_active_text(event.server) - for daily_registration in list(self.settings.list_daily_reg()): - try: - if event.server == daily_registration['server']: - await self.page_output(self.bot.get_channel(daily_registration['channel_id']), - msg, channel_id=daily_registration['channel_id']) - logger.info("daily_reg server") - except Exception as ex: - logger.exception("caught exception while sending daily msg:") + if event.event_length != EventLength.limited: + continue + for cid, data in (await self.config.all_channels()).items(): + if (channel := self.bot.get_channel(cid)) is None \ + or event.server not in data['guerrilla_servers']: + continue + role_name = f'{event.server}_group_{event.group_long_name()}' + role = channel.guild.get_role(role_name) + if role and role.mentionable: + message = f"{role.mention} {event.name_and_modifier} is starting" + else: + message = box(f"Server {event.server}, group {event.group_long_name()}:" + f" {event.name_and_modifier}") + with suppress(discord.Forbidden): + await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) + + async def do_daily_post(self, server): + msg = self.make_active_text(server) + for cid, data in (await self.config.all_channels()).items(): + if (channel := self.bot.get_channel(cid)) is None \ + or server not in data['daily_servers']: + continue + for page in pagify(msg, delims=['\n\n']): + with suppress(discord.Forbidden): + await channel.send(box(page)) @commands.group(aliases=['pde']) @checks.mod_or_permissions(manage_guild=True) @@ -246,7 +172,7 @@ async def padevents(self, ctx): async def testevent(self, ctx, server: Server, seconds: int = 0): server = server.value - dbcog = self.bot.get_cog('DBCog') + dbcog: Any = self.bot.get_cog('DBCog') await dbcog.wait_until_ready() # TODO: Don't use this awful importing hack dg_module = __import__('.'.join(dbcog.__module__.split('.')[:-1]) + ".models.scheduled_event_model") @@ -256,14 +182,14 @@ async def testevent(self, ctx, server: Server, seconds: int = 0): te = dg_module.models.scheduled_event_model.ScheduledEventModel( event_id=self.fake_uid, server_id=SUPPORTED_SERVERS.index(server), - event_type_id=EventType.Guerrilla.value, + event_type_id=-1, start_timestamp=timestamp, end_timestamp=timestamp + 60, group_name='red', dungeon_model=DummyObject( name_en='fake_dungeon_name', clean_name_en='fake_dungeon_name', - dungeon_type=DungeonType.Unknown7, + dungeon_type=DungeonType.ThreePlayer, dungeon_id=1, ) ) @@ -273,511 +199,50 @@ async def testevent(self, ctx, server: Server, seconds: int = 0): @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addchannel(self, ctx, server: Server): + async def addchannel(self, ctx, channel: Optional[discord.TextChannel], server: Server): server = server.value - channel_id = ctx.channel.id - if self.settings.check_guerrilla_reg(channel_id, server): - await ctx.send("Channel already active.") - return - - self.settings.add_guerrilla_reg(channel_id, server) + async with self.config.channel(channel or ctx.channel).guerrilla_servers() as guerillas: + if server in guerillas: + return await ctx.send("Channel already active.") + guerillas.append(server) await ctx.send("Channel now active.") @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmchannel(self, ctx, server: Server): + async def rmchannel(self, ctx, channel: Optional[discord.TextChannel], server: Server): server = server.value - channel_id = ctx.channel.id - if not self.settings.check_guerrilla_reg(channel_id, server): - await ctx.send("Channel is not active.") - return - - self.settings.remove_guerrilla_reg(channel_id, server) - await ctx.send("Channel deactivated.") + async with self.config.channel(channel or ctx.channel).guerrilla_servers() as guerillas: + if server not in guerillas: + return await ctx.send("Channel already inactive.") + guerillas.remove(server) + await ctx.send("Channel now inactive.") @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addchanneldaily(self, ctx, server: Server): + async def addchanneldaily(self, ctx, channel: Optional[discord.TextChannel], server: Server): server = server.value - channel_id = ctx.channel.id - if self.settings.check_daily_reg(channel_id, server): - await ctx.send("Channel already active.") - return - - self.settings.add_daily_reg(channel_id, server) + async with self.config.channel(channel or ctx.channel).daily_servers() as dailies: + if server in dailies: + return await ctx.send("Channel already active.") + dailies.append(server) await ctx.send("Channel now active.") @padevents.command() @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmchanneldaily(self, ctx, server: Server): + async def rmchanneldaily(self, ctx, channel: Optional[discord.TextChannel], server: Server): server = server.value - channel_id = ctx.channel.id - if not self.settings.check_daily_reg(channel_id, server): - await ctx.send("Channel is not active.") - return - - self.settings.remove_daily_reg(channel_id, server) - await ctx.send("Channel deactivated.") - - @commands.group(aliases=['aep']) - @commands.guild_only() - @checks.mod_or_permissions(manage_guild=True) - async def autoeventping(self, ctx): - """Auto Event Pings""" - - @autoeventping.command(name="add", ignore_extra=False) - async def aep_add(self, ctx, key, server: Server = None, searchstr=None, - red_role: Optional[discord.Role] = None, - blue_role: Optional[discord.Role] = None, - green_role: Optional[discord.Role] = None, - channel: discord.TextChannel = None): - """Add a new autoeventping. - Use `[p]aep set` with your chosen key to finish or update config. - `channel` defaults to the current channel. - - Usage: - `[p]aep add diamondra` - `[p]aep add pluspoints NA "star treasure" @red @blue @green #channel` - `[p]aep add wallace KR "wallace" @eventping #channel` - """ - - if " " in key: - await ctx.send("Multi-word keys are not allowed.") - return - - if server: - server = server.value - - if blue_role is None: - blue_role = green_role = red_role - - default = { - 'roles': [None, None, None], - 'channels': [(channel or ctx.channel).id] * 3, - 'server': server or 'NA', - 'searchstr': searchstr and rmdiacritics(searchstr), - 'regex': False, - 'enabled': bool(red_role or channel), - 'offset': 0, - } - - if red_role is not None: - default['roles'] = [red_role.id, blue_role.id, green_role and green_role.id] - - async with self.config.guild(ctx.guild).pingroles() as pingroles: - pingroles[key] = default - - await ctx.send("New AEP created:") - await self.aep_show(ctx, key) - - @autoeventping.command(name="remove", aliases=['rm', 'delete']) - async def aep_remove(self, ctx, key): - """Remove an autoeventping""" - async with self.config.guild(ctx.guild).pingroles() as pingroles: - if key not in pingroles: - await ctx.send("That key does not exist.") - return - del pingroles[key] - await ctx.tick() - - @autoeventping.command(name="show") - async def aep_show(self, ctx, key): - """Show specifics of an autoeventping""" - pingroles = await self.config.guild(ctx.guild).pingroles() - if key not in pingroles: - await ctx.send("That key does not exist.") - return - roles = [ctx.guild.get_role(role).mention - if ctx.guild.get_role(role) is not None - else "No Role" - for role - in pingroles[key]['roles'] - ] - chans = [ctx.bot.get_channel(c).mention - if ctx.bot.get_channel(c) is not None - else "No Channel" - for c - in pingroles[key]['channels'] - ] - pr = (f"Key: `{key}`\n" - f"\tStatus: {YES_EMOJI + ' enabled' if pingroles[key]['enabled'] else NO_EMOJI + ' disabled'}\n" - f"\tSearch string: `{pingroles[key]['searchstr']}` {'(regex search)' * pingroles[key]['regex']}\n" - f"\tServer: {pingroles[key]['server']}\n" - f"\tRed: {roles[0]} (In {chans[0]})\n" - f"\tBlue: {roles[1]} (In {chans[1]})\n" - f"\tGreen: {roles[2]} (In {chans[2]})\n" - f"\tOffset: `{pingroles[key]['offset']} minutes`") - await ctx.send(pr) - - @autoeventping.command(name="list") - async def aep_list(self, ctx): - """List all autoeventpings""" - pingroles = await self.config.guild(ctx.guild).pingroles() - if len(pingroles) == 0: - await ctx.send('You have no auto event pings configured currently!') - return - await ctx.send(box('\n'.join(pingroles))) - - @autoeventping.group(name="set") - async def aep_set(self, ctx): - """Sets specific parts of an autoeventping""" - - async def aepchange(self, ctx, key, k, f): - async with self.config.guild(ctx.guild).pingroles() as pingroles: - if key not in pingroles: - await ctx.send("That key does not exist.") - return - pingroles[key][k] = f(pingroles[key][k]) - - async def aepset(self, ctx, key, k, v): - await self.aepchange(ctx, key, k, lambda x: v) - - async def aepget(self, ctx, key): - pingroles = await self.config.guild(ctx.guild).pingroles() - if key not in pingroles: - raise ClientInlineTextException(f"Key `{key}` does not exist. Available keys are:" - f" {', '.join(map(inline, pingroles))}") - return pingroles[key] - - async def aep_remove_channel(self, ctx, key, group, f): - pingroles = await self.config.guild(ctx.guild).pingroles() - if key not in pingroles: - raise ClientInlineTextException(f"Key `{key}` does not exist. Available keys are:" - f" {', '.join(map(inline, pingroles))}") - - if not await get_user_confirmation(ctx, "Are you sure you want to remove the {} channel for AEP `{}`?" - .format(group, key)): - await send_cancellation_message(ctx, - "No action was taken. You can set a channel with `{}aep set {}channel {} #channel_name`" - .format(ctx.prefix, group, key)) - return - else: - await self.aepchange(ctx, key, 'channels', f) - await send_confirmation_message(ctx, "Okay, I removed the {} channel for the AEP `{}`" - .format(group, key)) - return - - @aep_set.command(name="channel") - async def aep_s_channel(self, ctx, key, channel: discord.TextChannel): - """Sets channel to ping for all groups""" - await self.aepset(ctx, key, 'channels', [channel.id] * 3) - await ctx.tick() - - @aep_set.command(name="redchannel") - async def aep_s_redchannel(self, ctx, key, channel: discord.TextChannel = None): - """Sets channel to ping when event is red""" - if channel is None: - await self.aep_remove_channel(ctx, key, "red", lambda x: [None, x[1], x[2]]) - else: - await self.aepchange(ctx, key, 'channels', lambda x: [channel.id, x[1], x[2]]) - await ctx.tick() - - @aep_set.command(name="bluechannel") - async def aep_s_bluechannel(self, ctx, key, channel: discord.TextChannel = None): - """Sets channel to ping when event is blue""" - if channel is None: - await self.aep_remove_channel(ctx, key, "blue", lambda x: [x[0], None, x[2]]) - else: - await self.aepchange(ctx, key, 'channels', lambda x: [x[0], channel.id, x[2]]) - await ctx.tick() - - @aep_set.command(name="greenchannel") - async def aep_s_greenchannel(self, ctx, key, channel: discord.TextChannel = None): - """Sets channel to ping when event is green""" - if channel is None: - await self.aep_remove_channel(ctx, key, "green", lambda x: [x[0], x[1], None]) - else: - await self.aepchange(ctx, key, 'channels', lambda x: [x[0], x[1], channel.id]) - await ctx.tick() - - @aep_set.command(name="roles") - async def aep_s_roles(self, ctx, key, red: discord.Role, blue: discord.Role, green: discord.Role): - """Sets roles to ping""" - await self.aepset(ctx, key, 'roles', [red.id, blue.id, green.id]) - await ctx.tick() - - @aep_set.command(name="redrole") - async def aep_s_redrole(self, ctx, key, role: discord.Role): - """Sets role to ping when event is red""" - await self.aepchange(ctx, key, 'roles', lambda x: [role.id, x[1], x[2]]) - await ctx.tick() - - @aep_set.command(name="bluerole") - async def aep_s_bluerole(self, ctx, key, role: discord.Role): - """Sets role to ping when event is blue""" - await self.aepchange(ctx, key, 'roles', lambda x: [x[0], role.id, x[2]]) - await ctx.tick() - - @aep_set.command(name="greenrole") - async def aep_s_greenrole(self, ctx, key, role: discord.Role): - """Sets role to ping when event is green""" - await self.aepchange(ctx, key, 'roles', lambda x: [x[0], x[1], role.id]) - await ctx.tick() - - @aep_set.command(name="server") - async def aep_s_server(self, ctx, key, server: Server): - """Sets which server to listen to events in""" - server = server.value - await self.aepset(ctx, key, 'server', server) - await ctx.tick() - - @aep_set.command(name="searchstr") - async def aep_s_searchstr(self, ctx, key, *, searchstr): - """Sets what string is tested against event name""" - searchstr = rmdiacritics(searchstr).strip('"') - if (await self.aepget(ctx, key))['regex']: - try: - re.compile(searchstr) - except re.error: - await ctx.send("Invalid regex searchstr. (`{}`)".format(searchstr)) - return - await self.aepset(ctx, key, 'searchstr', searchstr) - await ctx.tick() - - @aep_set.command(name="regex") - async def aep_s_regex(self, ctx, key, regex: bool): - """Sets whether searchstr is calculated via regex""" - if regex: - try: - re.compile((await self.aepget(ctx, key))['searchstr']) - except re.error: - await ctx.send("Invalid regex searchstr. (`{}`)".format((await self.aepget(ctx, key))['searchstr'])) - return - await self.aepset(ctx, key, 'regex', regex) - await ctx.tick() - - @aep_set.command(name="enabled", aliases=['enable']) - async def aep_s_enabled(self, ctx, key, enabled: bool = True): - """Sets whether or not ping is enabled""" - await self.aepset(ctx, key, 'enabled', enabled) - await ctx.tick() - - @aep_set.command(name="disabled", aliases=['disable']) - async def aep_s_disabled(self, ctx, key, disabled: bool = True): - """Sets whether or not ping is disabled""" - await self.aepset(ctx, key, 'enabled', not disabled) - await ctx.tick() - - @aep_set.command(name="offset") - async def aep_s_offset(self, ctx, key, offset: int): - """Sets how many minutes before event should ping happen""" - if offset < 0: - await ctx.send("Offset cannot be negative.") - return - await self.aepset(ctx, key, 'offset', offset) - await ctx.tick() - - @commands.group(aliases=['aed']) - async def autoeventdm(self, ctx): - """Auto Event DMs""" - - @autoeventdm.command(name="add") - async def aed_add(self, ctx, server: Server, searchstr, group: Optional[StarterGroup] = None, time_offset: int = 0): - """Add a new autoeventdm""" - server = server.value - - if group is None: - group = 'red' - else: - group = GROUPS[group.value] - - if time_offset and not user_is_donor(ctx): - await ctx.send("You must be a donor to set a time offset!") - return - if time_offset < 0: - await ctx.send("Offset cannot be negative") - return - - default = { - 'key': time.time(), - 'server': server, - 'group': group, - 'searchstr': searchstr, - 'include3p': True, - 'offset': time_offset, - } - - index = await self._do_aed_add(ctx, default) - await ctx.send("New AED created:") - await self.aed_show(ctx, index) - - @autoeventdm.command(name="remove", aliases=['rm', 'delete']) - async def aed_remove(self, ctx, index): - """Remove an autoeventdm""" - corrected_index = await self._get_and_validate_aed_index(ctx, index) - if corrected_index is None: - return - async with self.config.user(ctx.author).dmevents() as dmevents: - if not await get_user_confirmation(ctx, "Are you sure you want to delete autoeventdm {}. `{}`?" - .format(corrected_index + 1, dmevents[corrected_index]['searchstr'])): - return - dmevents.pop(corrected_index) - await ctx.tick() - - @autoeventdm.command(name="show") - async def aed_show(self, ctx, index): - """Show specifics of an autoeventdm""" - dmevents = await self.config.user(ctx.author).dmevents() - corrected_index = await self._get_and_validate_aed_index(ctx, index) - if corrected_index is None: - return - if dmevents[corrected_index].get('include3p') is None: - # case of legacy configs - dmevents[corrected_index]['include3p'] = True - ret = (f"Lookup number: `{corrected_index + 1}`\n" - f"\tSearch string: `{dmevents[corrected_index]['searchstr']}`\n" - f"\t3P: {'included' if dmevents[corrected_index]['include3p'] else 'excluded'}\n" - f"\tServer: {dmevents[corrected_index]['server']}\n" - f"\tGroup: {dmevents[corrected_index]['group'].title()} \n" - f"\tOffset (Donor Only): `{dmevents[corrected_index]['offset']} minutes`") - await ctx.send(ret) - - @autoeventdm.command(name="list") - async def aed_list(self, ctx): - """List current autoeventdms""" - dmevents = await self.config.user(ctx.author).dmevents() - msg = "\n".join("{}. {}".format(c, aed['searchstr']) for c, aed in enumerate(dmevents, 1)) - if msg: - await ctx.send(box(msg)) - else: - await ctx.send("You have no autoeventdms set up.") - - @autoeventdm.command(name="purge") - async def aed_purge(self, ctx): - """Remove an autoeventdm""" - if not await self.config.user(ctx.author).dmevents(): - await ctx.send("You don't have any autoeventdms.") - return - if not await get_user_confirmation(ctx, "Are you sure you want to purge your autoeventdms?"): - return - await self.config.user(ctx.author).dmevents.set([]) - await ctx.tick() - - @autoeventdm.group(name="edit", aliases=["set"]) - async def aed_e(self, ctx): - """Edit a property of the autoeventdm""" - - async def _do_aed_add(self, ctx, item): - """Add autoeventdm and return its index""" - async with self.config.user(ctx.author).dmevents() as dmevents: - dmevents.append(item) - return len(dmevents) - - @is_donor() - @aed_e.command(name="offset") - async def aed_e_offset(self, ctx, index, offset: int): - """(DONOR ONLY) Set time offset to an AED to allow you to prepare for a dungeon""" - if offset < 0: - await send_cancellation_message(ctx, "Offset cannot be negative") - return - corrected_index = await self._get_and_validate_aed_index(ctx, index) - if corrected_index is None: - return - async with self.config.user(ctx.author).dmevents() as dmevents: - if not await get_user_confirmation(ctx, "Modify the offset for {}. `{}` to {} minutes?" - .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], offset)): - return - dmevents[corrected_index]['offset'] = offset - await ctx.tick() - - @aed_e.command(name="searchstr") - async def aed_e_searchstr(self, ctx, index, *, searchstr): - """Set search string of an autoeventdm""" - searchstr = searchstr.strip('"') - corrected_index = await self._get_and_validate_aed_index(ctx, index) - if corrected_index is None: - return - async with self.config.user(ctx.author).dmevents() as dmevents: - if not await get_user_confirmation(ctx, "Modify the search string for {}. `{}` to `{}`?" - .format(corrected_index + 1, dmevents[corrected_index]['searchstr'], searchstr)): - return - dmevents[corrected_index]['searchstr'] = searchstr - await ctx.tick() - - @aed_e.command(name="toggle3p") - async def aed_e_toggle3p(self, ctx, index): - """Include/exclude 3-player dungeons in an autoeventdm""" - corrected_index = await self._get_and_validate_aed_index(ctx, index) - if corrected_index is None: - return - async with self.config.user(ctx.author).dmevents() as dmevents: - event = dmevents[corrected_index]['searchstr'] - if dmevents[corrected_index].get('include3p') is None: - # case of legacy configs - dmevents[corrected_index]['include3p'] = True - if dmevents[corrected_index]['include3p']: - dmevents[corrected_index]['include3p'] = False - await send_confirmation_message(ctx, "I will **exclude** 3P dungeons for {}. `{}`" - .format(corrected_index + 1, event)) - return - dmevents[corrected_index]['include3p'] = True - await send_confirmation_message(ctx, "I will **include** 3P dungeons for {}. `{}`" - .format(corrected_index + 1, event)) - return - - async def _get_and_validate_aed_index(self, ctx, user_index: Union[str, int]): - """Returns the corrected index for the autoeventdm of interest, based on Python list indexing""" - async with self.config.user(ctx.author).dmevents() as dmevents: - if isinstance(user_index, int) or user_index.isdigit(): - user_index = int(user_index) - if not 0 < user_index <= len(dmevents): - await send_cancellation_message(ctx, "That isn't a valid numerical index.") - return None - else: - str_index = None - for i in range(1, len(dmevents) + 1, 1): - if dmevents[i - 1]['searchstr'] == user_index: - str_index = i - break - user_index = str_index - if user_index is None: - await send_cancellation_message(ctx, "That string did not match any existing patterns. " - "Printing all of your Auto Event DMs (you can also see this with `{}aed list`):" - .format(ctx.prefix)) - await self.aed_list(ctx) - return None - return user_index - 1 - - @padevents.command() - @checks.is_owner() - async def listallchannels(self, ctx): - msg = 'Following daily channels are registered:\n' - msg += self.make_channel_list(self.settings.list_daily_reg()) - msg += "\n" - msg += 'Following guerilla channels are registered:\n' - msg += self.make_channel_list(self.settings.list_guerrilla_reg()) - await self.page_output(ctx, msg) - - @padevents.command() - @checks.mod_or_permissions(manage_guild=True) - async def listchannels(self, ctx): - msg = 'Following daily channels are registered:\n' - msg += self.make_channel_list(self.settings.list_daily_reg(), lambda c: c in ctx.guild.channels) - msg += "\n" - msg += 'Following guerilla channels are registered:\n' - msg += self.make_channel_list(self.settings.list_guerrilla_reg(), lambda c: c in ctx.guild.channels) - await self.page_output(ctx, msg) - - def make_channel_list(self, reg_list, filt=None): - if filt is None: - def filt(x): - return x - msg = "" - for cr in reg_list: - reg_channel_id = cr['channel_id'] - channel = self.bot.get_channel(int(reg_channel_id)) - if filt(channel): - channel_name = channel.name if channel else 'Unknown(' + reg_channel_id + ')' - server_name = channel.guild.name if channel else 'Unknown server' - msg += " " + cr['server'] + " : " + server_name + '(' + channel_name + ')\n' - return msg + async with self.config.channel(channel or ctx.channel).daily_servers() as dailies: + if server not in dailies: + return await ctx.send("Channel already inactive.") + dailies.remove(server) + await ctx.send("Channel now inactive.") @padevents.command() @checks.mod_or_permissions(manage_guild=True) @@ -785,103 +250,42 @@ async def active(self, ctx, server: Server): server = server.value msg = self.make_active_text(server) - await self.page_output(ctx, msg) + for page in pagify(msg, delims=['\n\n']): + await ctx.send(box(page)) def make_active_text(self, server): + server = normalize_server_name(server) + server_events = EventList(self.events).with_server(server) active_events = server_events.active_only() - pending_events = server_events.pending_only() - available_events = server_events.available_only() + events_today = server_events.today_only(server) - msg = "Listing all events for " + server + active_special = active_events.in_dungeon_type([DungeonType.Special]) - """ - special_events = active_events.with_type( - EventType.Special).items_by_close_time() - if len(special_events) > 0: - msg += "\n\n" + self.make_active_output('Special Events', special_events) - - all_etc_events = active_events.with_type(EventType.Etc) - - etc_events = all_etc_events.with_dungeon_type( - DungeonType.Etc).exclude_unwanted_events().items_by_close_time() - if len(etc_events) > 0: - msg += "\n\n" + self.make_active_output('Etc Events', etc_events) - - # Old-style guerrillas - active_guerrilla_events = active_events.with_type(EventType.Guerrilla).items() - if len(active_guerrilla_events) > 0: - msg += "\n\n" + \ - self.make_active_guerrilla_output('Active Guerrillas', active_guerrilla_events) - - guerrilla_events = pending_events.with_type(EventType.Guerrilla).items() - if len(guerrilla_events) > 0: - msg += "\n\n" + self.make_full_guerrilla_output('Guerrilla Events', guerrilla_events) - - # New-style guerrillas - active_guerrilla_events = active_events.with_type(EventType.SpecialWeek).items() - if len(active_guerrilla_events) > 0: - msg += "\n\n" + \ - self.make_active_guerrilla_output('Active Guerrillas', active_guerrilla_events) - - guerrilla_events = pending_events.with_type(EventType.SpecialWeek).items() - if len(guerrilla_events) > 0: - msg += "\n\n" + \ - self.make_full_guerrilla_output( - 'Guerrilla Events', guerrilla_events, starter_guerilla=True) - """ - active_cdo_events = active_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped(True).items() - if len(active_cdo_events) > 0: - msg += "\n\n" + \ - self.make_active_output( - 'Active Events', active_cdo_events) - """ - cdo_events = pending_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped(False).items() - if len(cdo_events) > 0: - msg += "\n\n" + \ - self.make_active_output( - 'Reward Events', cdo_events) - """ + msg = server + " Events - " + datetime.datetime.now(SERVER_TIMEZONES[server]).strftime('%A, %B %e') - active_grouped_cdo_events = active_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped().items() - if len(active_grouped_cdo_events) > 0: - msg += "\n\n" + \ - self.make_active_guerrilla_output( - 'Active Guerrillas', active_grouped_cdo_events) - grouped_cdo_events = pending_events.with_dungeon_type(DungeonType.CoinDailyOther).is_grouped().items() - if len(grouped_cdo_events) > 0: - msg += "\n\n" + \ - self.make_full_guerrilla_output( - 'Guerrillas', grouped_cdo_events, starter_guerilla=True) - - active_etc_events = active_events.with_dungeon_type(DungeonType.Etc).items() - if len(active_etc_events) > 0: - msg += "\n\n" + \ - self.make_active_output( - 'Active Other Events', active_etc_events) - """ - etc_events = pending_events.with_dungeon_type(DungeonType.Etc).items() - if len(etc_events) > 0: - msg += "\n\n" + \ - self.make_active_output( - 'Other Events', etc_events) - """ - # clean up long headers - msg = msg.replace('-------------------------------------', '-----------------------') + ongoing_events = active_events.in_length([EventLength.weekly, EventLength.special]) + if ongoing_events: + msg += "\n\n" + self.make_active_output('Ongoing Events', ongoing_events) + + active_dailies_events = active_events.with_length(EventLength.daily) + if active_dailies_events: + msg += "\n\n" + self.make_daily_output('Daily Dungeons', active_dailies_events) + + limited_events = events_today.with_length(EventLength.limited) + if limited_events: + msg += "\n\n" + self.make_full_guerrilla_output('Limited Events', limited_events) return msg - async def page_output(self, ctx, msg, channel_id=None, format_type=box): - msg = msg.strip() - msg = pagify(msg, ["\n"], shorten_by=20) - for page in msg: - try: - if channel_id is None: - await ctx.send(format_type(page)) - else: - await self.bot.get_channel(int(channel_id)).send(format_type(page)) - except Exception as e: - logger.exception("page output failed " + str(e), "tried to output: " + page) + def make_daily_output(self, table_name, event_list): + tbl = prettytable.PrettyTable([table_name]) + tbl.hrules = prettytable.HEADER + tbl.vrules = prettytable.NONE + tbl.align[table_name] = "l" + for e in event_list: + tbl.add_row([e.name_and_modifier]) + return tbl.get_string() def make_active_output(self, table_name, event_list): tbl = prettytable.PrettyTable(["Time", table_name]) @@ -903,7 +307,7 @@ def make_active_guerrilla_output(self, table_name, event_list): tbl.add_row([e.name_and_modifier, e.group, e.end_from_now_full_min().strip()]) return tbl.get_string() - def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=False): + def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=True): events_by_name = defaultdict(list) for event in event_list: events_by_name[event.name_and_modifier].append(event) @@ -929,12 +333,15 @@ def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=Fa did_work = True event = grp_list.pop(0) row.append(event.to_guerrilla_str()) - if did_work: + + if did_work and len(set(row)) == 2: # One for Header and all three times are same + rows.append([row[0], row[1], '=', '=']) + elif did_work: rows.append(row) else: done = True - col1 = "Pending" + col1 = "Limited" tbl = prettytable.PrettyTable([col1] + grps) tbl.align[col1] = "l" tbl.hrules = prettytable.HEADER @@ -943,7 +350,7 @@ def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=Fa for r in rows: tbl.add_row(r) - header = "Times are PT below\n\n" + header = "Times are PT below\n" return header + tbl.get_string() + "\n" @commands.command(aliases=['events']) @@ -969,7 +376,7 @@ async def do_partial(self, ctx, server: Server, group: StarterGroup = None): events = EventList(self.events) events = events.with_server(server) - events = events.in_dungeon_type([DungeonType.Etc, DungeonType.CoinDailyOther]) + events = events.in_dungeon_type([DungeonType.SoloSpecial, DungeonType.Special]) events = events.is_grouped() active_events = events.active_only().items_by_open_time(reverse=True) @@ -982,9 +389,6 @@ async def do_partial(self, ctx, server: Server, group: StarterGroup = None): group_to_active_event = {e.group: e for e in active_events} group_to_pending_event = {e.group: e for e in pending_events} - # active_events = list(group_to_active_event.values()) - # pending_events = list(group_to_pending_event.values()) - active_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) pending_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) @@ -1007,6 +411,17 @@ async def do_partial(self, ctx, server: Server, group: StarterGroup = None): for page in pagify(output): await ctx.send(box(page)) + def get_most_recent_day_change(self): + now = datetime.datetime.utcnow().time() + if now < datetime.time(8): + return "JP" + elif now < datetime.time(15): + return "NA" + elif now < datetime.time(16): + return "KR" + else: + return "JP" + def make_channel_reg(channel_id, server): server = normalize_server_name(server) @@ -1014,253 +429,3 @@ def make_channel_reg(channel_id, server): "channel_id": channel_id, "server": server } - - -class PadEventSettings(CogSettings): - def make_default_settings(self): - config = { - 'guerrilla_regs': [], - 'daily_regs': [], - } - return config - - def list_guerrilla_reg(self): - return self.bot_settings['guerrilla_regs'] - - def add_guerrilla_reg(self, channel_id, server): - self.list_guerrilla_reg().append(make_channel_reg(channel_id, server)) - self.save_settings() - - def check_guerrilla_reg(self, channel_id, server): - return make_channel_reg(channel_id, server) in self.list_guerrilla_reg() - - def remove_guerrilla_reg(self, channel_id, server): - if self.check_guerrilla_reg(channel_id, server): - self.list_guerrilla_reg().remove(make_channel_reg(channel_id, server)) - self.save_settings() - - def list_daily_reg(self): - return self.bot_settings['daily_regs'] - - def add_daily_reg(self, channel_id, server): - self.list_daily_reg().append(make_channel_reg(channel_id, server)) - self.save_settings() - - def check_daily_reg(self, channel_id, server): - return make_channel_reg(channel_id, server) in self.list_daily_reg() - - def remove_daily_reg(self, channel_id, server): - if self.check_daily_reg(channel_id, server): - self.list_daily_reg().remove(make_channel_reg(channel_id, server)) - self.save_settings() - - -class Event: - def __init__(self, scheduled_event: "ScheduledEventModel"): - self.key = scheduled_event.event_id - self.server = SUPPORTED_SERVERS[scheduled_event.server_id] - self.open_datetime = scheduled_event.open_datetime - self.close_datetime = scheduled_event.close_datetime - self.group = scheduled_event.group_name - self.dungeon = scheduled_event.dungeon - self.dungeon_name = self.dungeon.name_en if self.dungeon else 'unknown_dungeon' - self.event_name = '' # scheduled_event.event.name if scheduled_event.event else '' - - self.clean_dungeon_name = self.dungeon.clean_name_en if self.dungeon else 'unknown_dungeon' - self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') - - self.name_and_modifier = self.clean_dungeon_name - if self.clean_event_name != '': - self.name_and_modifier += ', ' + self.clean_event_name - - self.event_type_id = scheduled_event.event_type_id - self.event_type = None - - self.dungeon_type = DungeonType(self.dungeon.dungeon_type) if self.dungeon else DungeonType.Unknown - - def start_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.open_datetime - now).total_seconds() - - def end_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.close_datetime - now).total_seconds() - - def is_started(self): - """True if past the open time for the event.""" - return self.start_from_now_sec() <= 0 - - def is_finished(self): - """True if past the close time for the event.""" - return self.end_from_now_sec() <= 0 - - def is_active(self): - """True if between open and close time for the event.""" - return self.is_started() and not self.is_finished() - - def is_pending(self): - """True if event has not started.""" - return not self.is_started() - - def is_available(self): - """True if event has not finished.""" - return not self.is_finished() - - def tostr(self): - return fmt_time(self.open_datetime) + "," + fmt_time( - self.close_datetime) + "," + self.group + "," + self.dungeon_name + "," + self.event_type - - def start_pst(self): - tz = pytz.timezone('US/Pacific') - return self.open_datetime.astimezone(tz) - - def start_est(self): - tz = pytz.timezone('US/Eastern') - return self.open_datetime.astimezone(tz) - - def start_from_now(self): - return fmt_hrs_mins(self.start_from_now_sec()) - - def end_from_now(self): - return fmt_hrs_mins(self.end_from_now_sec()) - - def end_from_now_full_min(self): - return fmt_days_hrs_mins_short(self.end_from_now_sec()) - - def to_guerrilla_str(self): - return fmt_time_short(self.start_pst()) - - def to_date_str(self): - return self.server + "," + self.group + "," + fmt_time(self.start_pst()) + "," + fmt_time( - self.start_est()) + "," + self.start_from_now() - - def group_short_name(self): - return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') - - def group_long_name(self): - return self.group.upper() if self.group is not None else "UNGROUPED" - - def to_partial_event(self, pe): - group = self.group_short_name() - if self.is_started(): - return group + " " + self.end_from_now() + " " + self.name_and_modifier - else: - return group + " " + fmt_time_short(self.start_pst()) + " " + fmt_time_short( - self.start_est()) + " " + self.start_from_now() + " " + self.name_and_modifier - - -class EventList: - def __init__(self, event_list): - self.event_list: List[Event] = event_list - - def with_func(self, func, exclude=False): - if exclude: - return EventList(list(itertools.filterfalse(func, self.event_list))) - else: - return EventList(list(filter(func, self.event_list))) - - def with_server(self, server): - return self.with_func(lambda e: e.server == normalize_server_name(server)) - - def with_type(self, event_type): - return self.with_func(lambda e: e.event_type == event_type) - - def in_type(self, event_types): - return self.with_func(lambda e: e.event_type in event_types) - - def with_dungeon_type(self, dungeon_type, exclude=False): - return self.with_func(lambda e: e.dungeon_type == dungeon_type, exclude) - - def in_dungeon_type(self, dungeon_types, exclude=False): - return self.with_func(lambda e: e.dungeon_type in dungeon_types, exclude) - - def is_grouped(self, exclude=False): - return self.with_func(lambda e: e.group is not None, exclude) - - def with_name_contains(self, name, exclude=False): - return self.with_func(lambda e: name.lower() in e.dungeon_name.lower(), exclude) - - def exclude_unwanted_events(self): - return self.with_func(is_event_wanted) - - def items(self): - return self.event_list - - def started_only(self): - return self.with_func(lambda e: e.is_started()) - - def pending_only(self): - return self.with_func(lambda e: e.is_pending()) - - def active_only(self): - return self.with_func(lambda e: e.is_active()) - - def available_only(self): - return self.with_func(lambda e: e.is_available()) - - def items_by_open_time(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) - - def items_by_close_time(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) - - -# TIME_FMT = """%a %b %d %H:%M:%S %Y""" - - -class EventType(Enum): - Unknown = -1 - Week = 0 - Special = 1 - SpecialWeek = 2 - Guerrilla = 3 - GuerrillaNew = 4 - Etc = -100 - - -class DungeonType(Enum): - Unknown = -1 - Normal = 0 - CoinDailyOther = 1 - Technical = 2 - Etc = 3 - Unknown4 = 4 - Unknown7 = 7 - Unknown9 = 9 - Unknown10 = 10 - - -def fmt_time(dt): - return dt.strftime("%Y-%m-%d %H:%M") - - -def fmt_time_short(dt): - return dt.strftime("%H:%M") - - -def fmt_hrs_mins(seconds): - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - return '{:2}h {:2}m'.format(int(hours), int(minutes)) - - -def fmt_days_hrs_mins_short(sec): - days, sec = divmod(sec, 86400) - hours, sec = divmod(sec, 3600) - minutes, sec = divmod(sec, 60) - - if days > 0: - return '{:2}d {:2}h'.format(int(days), int(hours)) - elif hours > 0: - return '{:2}h {:2}m'.format(int(hours), int(minutes)) - else: - return '{:2}m'.format(int(minutes)) - - -def is_event_wanted(event): - name = event.name_and_modifier.lower() - if 'castle of satan' in name: - # eliminate things like : TAMADRA Invades in [Castle of Satan][Castle of Satan in the Abyss] - return False - - return True From 91e6f39b56d8fe8b61890d725cc339271cb81f67 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 29 Sep 2021 14:36:14 -0700 Subject: [PATCH 0981/1220] Use rollover instead of day change (#1460) --- crowddata/mixins/vem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 6e9b0d4c..a63d5628 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -232,5 +232,5 @@ async def add_pull(self, ctx, values: Any, at_time: float): @staticmethod def midnight() -> float: - return datetime.now(timezone(timedelta(hours=-8))).replace(hour=0, minute=0, second=0, microsecond=0) \ + return datetime.now(timezone(timedelta(hours=-8))).replace(hour=4, minute=0, second=0, microsecond=0) \ .astimezone(timezone.utc).timestamp() From f65977e61da41cc95dc1bfccd56e69c8de036981 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 30 Sep 2021 00:15:49 -0700 Subject: [PATCH 0982/1220] Use new TSUtils functions for AdPEM (#1462) --- crowddata/mixins/vem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index a63d5628..288db603 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -1,11 +1,12 @@ -import time -from datetime import datetime, timedelta, timezone +from datetime import timezone from typing import Any, TYPE_CHECKING +import time from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import Config, commands from tsutils.cog_mixins import CogMixin from tsutils.emoji import NO_EMOJI, char_to_emoji +from tsutils.time import NA_TIMEZONE, ROLLOVER, get_last_time from tsutils.user_interaction import get_user_confirmation, get_user_reaction if TYPE_CHECKING: @@ -232,5 +233,4 @@ async def add_pull(self, ctx, values: Any, at_time: float): @staticmethod def midnight() -> float: - return datetime.now(timezone(timedelta(hours=-8))).replace(hour=4, minute=0, second=0, microsecond=0) \ - .astimezone(timezone.utc).timestamp() + return get_last_time(ROLLOVER, NA_TIMEZONE).astimezone(timezone.utc).timestamp() From 9e48783ad7ad38daaba2df1be0d48a02f3d49b7b Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 30 Sep 2021 10:17:14 -0700 Subject: [PATCH 0983/1220] Use DAY_CHANGE instead of ROLLOVER (#1464) --- crowddata/mixins/vem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 288db603..2a0c0a72 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -6,7 +6,7 @@ from redbot.core import Config, commands from tsutils.cog_mixins import CogMixin from tsutils.emoji import NO_EMOJI, char_to_emoji -from tsutils.time import NA_TIMEZONE, ROLLOVER, get_last_time +from tsutils.time import DAY_CHANGE, NA_TIMEZONE, get_last_time from tsutils.user_interaction import get_user_confirmation, get_user_reaction if TYPE_CHECKING: @@ -233,4 +233,4 @@ async def add_pull(self, ctx, values: Any, at_time: float): @staticmethod def midnight() -> float: - return get_last_time(ROLLOVER, NA_TIMEZONE).astimezone(timezone.utc).timestamp() + return get_last_time(DAY_CHANGE, NA_TIMEZONE).astimezone(timezone.utc).timestamp() From 0f744d7bb47d577a3f2b397171de63904440ebb0 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 30 Sep 2021 10:52:47 -0700 Subject: [PATCH 0984/1220] Fix Emoji Cache issues (#1465) --- padinfo/view/components/base_id_main_view.py | 17 +++++++++-------- padinfo/view/id.py | 4 ++-- padinfo/view/transforminfo.py | 17 +++++++++-------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index a0e9f434..45799673 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -1,8 +1,8 @@ from abc import abstractmethod -from typing import TYPE_CHECKING, List +from typing import List, TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.text import Text, BoldText +from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView from tsutils.enums import LsMultiplier @@ -25,10 +25,9 @@ def _killer_latent_emoji(latent_name: str): class BaseIdMainView(BaseIdView): - transform_emojis = [get_emoji('downr'), get_emoji('downo'), get_emoji('downy'), get_emoji('downg'), - get_emoji('downb'), get_emoji('downp')] - up_emoji = get_emoji('upgr') - down_emoji = transform_emojis[0] + transform_emoji_names = ['downr', 'downo', 'downy', 'downg', 'downb', 'downp'] + up_emoji_name = 'upgr' + down_emoji_name = transform_emoji_names[0] @staticmethod def normal_awakenings_row(m: "MonsterModel"): @@ -75,8 +74,10 @@ def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["Monst cooldown_text = '({}cd)'.format(str(skill.turn_max)) if skill.turn_min != skill.turn_max: cooldown_text = '{} -> {}'.format(skill.turn_min, skill.turn_max) - skill_texts.append('{}{}'.format(cls.transform_emojis[i % len(cls.transform_emojis)], cooldown_text)) - skill_texts.append('{} ({} cd)'.format(cls.up_emoji, m.active_skill.turn_max)) + skill_texts.append( + '{}{}'.format(get_emoji(cls.transform_emoji_names[i % len(cls.transform_emoji_names)]), + cooldown_text)) + skill_texts.append('{} ({} cd)'.format(get_emoji(cls.up_emoji_name), m.active_skill.turn_max)) active_cd = ' '.join(skill_texts) return Box( BoldText('Active Skill'), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 2d88b59c..d1540543 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.text import BoldText, LabeledText, Text from discordmenu.embed.view import EmbedView -from tsutils.enums import CardPlusModifier, Server, CardModeModifier, CardLevelModifier +from tsutils.enums import CardLevelModifier, CardModeModifier, CardPlusModifier, Server from tsutils.menu.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings @@ -171,7 +171,7 @@ def all_awakenings_row(cls, m: "MonsterModel", transform_base): return Box( Box( - cls.up_emoji if m != transform_base else '', + get_emoji(cls.up_emoji_name) if m != transform_base else '', cls.normal_awakenings_row(m), delimiter=' ' ), diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 03854bd1..07d2ba8e 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -1,14 +1,15 @@ from typing import TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField -from discordmenu.embed.text import Text, BoldText, LabeledText +from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail +from discordmenu.embed.text import BoldText, LabeledText, Text from discordmenu.embed.view import EmbedView from tsutils.enums import LsMultiplier from tsutils.menu.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings from padinfo.common.config import UserConfig +from padinfo.common.emoji_map import get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.monster.header import MonsterHeader @@ -101,7 +102,7 @@ class TransformInfoView(BaseIdMainView): def base_info(cls, m: "MonsterModel"): return Box( Box( - cls.down_emoji, + get_emoji(cls.down_emoji_name), cls.normal_awakenings_row(m) if len(m.awakenings) != 0 else Box(Text('No Awakenings')), delimiter=' ' @@ -136,7 +137,7 @@ def transform_active_header(cls, m: "MonsterModel"): active_skill = m.active_skill active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' return Box( - cls.up_emoji, + get_emoji(cls.up_emoji_name), BoldText('Transform Active Skill {}'.format(active_cd)), delimiter=' ' ) @@ -144,7 +145,7 @@ def transform_active_header(cls, m: "MonsterModel"): @classmethod def base_active_header(cls, m: "MonsterModel"): return Box( - cls.down_emoji, + get_emoji(cls.down_emoji_name), BoldText('Base'), cls.active_skill_header(m, []), delimiter=' ' @@ -153,10 +154,10 @@ def base_active_header(cls, m: "MonsterModel"): @classmethod def leader_header(cls, m: "MonsterModel", is_base: bool, lsmultiplier: LsMultiplier, base_mon: "MonsterModel"): if is_base: - emoji = cls.down_emoji + emoji = get_emoji(cls.down_emoji_name) label = 'Base' else: - emoji = cls.up_emoji + emoji = get_emoji(cls.up_emoji_name) label = 'Transform' return Box( @@ -176,7 +177,7 @@ def embed(cls, state: TransformInfoViewState): '/'.join(['{}'.format(t.name) for t in transformed_mon.types]), Box( Box( - cls.up_emoji, + get_emoji(cls.up_emoji_name), cls.normal_awakenings_row(transformed_mon) if len(transformed_mon.awakenings) != 0 else Box(Text('No Awakenings')), delimiter=' ' From 9c3f621873cb379087b36cf45e4caad95c252e53 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Oct 2021 00:15:25 -0700 Subject: [PATCH 0985/1220] Add transform alias tokens (#1466) --- dbcog/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 3b6d79a1..eef59484 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -78,7 +78,7 @@ class EvoTypes(Enum): EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo', 'suvo'), EvoTypes.BASETRANS: ('transformbase', 'transbase'), - EvoTypes.TRANS: ('transform', 'trans', 'transformed'), + EvoTypes.TRANS: ('transform', 'trans', 'transformed', 'xf', 'xform', 'tf'), EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), EvoTypes.MEGA: ('mega', 'mawoken', 'mawo', 'ma', 'megaawoken'), EvoTypes.REVO: ('revo', 'reincarnated', 'rv'), From bec7dcfc95c6b2b2c47a2c04698d7612e4981da6 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Oct 2021 04:52:13 -0700 Subject: [PATCH 0986/1220] Add regdate attr (#1467) --- dbcog/token_mappings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index eef59484..3497347f 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -333,6 +333,7 @@ class MiscModifiers(Enum): (('active_skill', 'name_en'),): ('asname',), (('active_skill', 'desc_en'),): ('asdesc',), (('series', 'name_en'),): ('seriesname',), + (('history_us',),): ('regdate',), } STRING_MONSTER_ATTRIBUTE_NAMES = {*sum(STRING_MONSTER_ATTRIBUTE_ALIASES.values(), ())} From ad126b51a997dfd29cd9a8bf314b1b157e04f745 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Oct 2021 05:16:30 -0700 Subject: [PATCH 0987/1220] Add `dateadded` as an alias (#1468) --- dbcog/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 3497347f..eaab9665 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -333,7 +333,7 @@ class MiscModifiers(Enum): (('active_skill', 'name_en'),): ('asname',), (('active_skill', 'desc_en'),): ('asdesc',), (('series', 'name_en'),): ('seriesname',), - (('history_us',),): ('regdate',), + (('history_us',),): ('regdate', 'dateadded'), } STRING_MONSTER_ATTRIBUTE_NAMES = {*sum(STRING_MONSTER_ATTRIBUTE_ALIASES.values(), ())} From 5a595f685a1ac5e9dd7b0cf0a3848fe44a328ce6 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 7 Oct 2021 13:12:26 -0700 Subject: [PATCH 0988/1220] Fix idtest remove (#1476) --- dbcog/idtest_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/idtest_mixin.py b/dbcog/idtest_mixin.py index eb75159a..0fd87fdb 100644 --- a/dbcog/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -211,7 +211,7 @@ async def idt_remove(self, ctx, *, item): await ctx.react_quietly("\N{CROSS MARK}") return res = suite[case]['result'] - ref = suite.index(case) + ref = sorted(suite).index(case) del suite[case] await ctx.send(f"Removed test case `{case}: {res}` with ref {ref}") From c8f88577265dc0cb429b47fef69ab336541b949d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 9 Oct 2021 02:07:41 -0500 Subject: [PATCH 0989/1220] Update daily info header (#1477) --- padevents/padevents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index c771491c..97af3703 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -262,7 +262,7 @@ def make_active_text(self, server): active_special = active_events.in_dungeon_type([DungeonType.Special]) - msg = server + " Events - " + datetime.datetime.now(SERVER_TIMEZONES[server]).strftime('%A, %B %e') + msg = server + " Events - " + datetime.datetime.now(SERVER_TIMEZONES[server]).strftime('%A, %B%e') ongoing_events = active_events.in_length([EventLength.weekly, EventLength.special]) if ongoing_events: @@ -350,7 +350,7 @@ def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=Tr for r in rows: tbl.add_row(r) - header = "Times are PT below\n" + header = "Times are shown in Pacific Time\n= means same for all groups\n" return header + tbl.get_string() + "\n" @commands.command(aliases=['events']) From f22968db4b39320ef0223b9ff3eaae927668696d Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Oct 2021 22:36:19 -0700 Subject: [PATCH 0990/1220] Add daily AEP post (#1479) --- padevents/autoevent_mixin.py | 64 +++++++++++++++++++----------------- padevents/padevents.py | 34 +++++++++++++++++-- 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index fa694f9f..a2c8e1d2 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -1,6 +1,6 @@ import logging import re -from typing import List, Optional, Set, Union +from typing import Any, Callable, Dict, List, Optional, Set, TypeVar, Union import discord import time @@ -17,6 +17,7 @@ from padevents.events import Event logger = logging.getLogger('red.padbot-cogs.padevents') +T = TypeVar("T") GROUPS = ['red', 'blue', 'green'] @@ -45,17 +46,9 @@ async def do_autoevents(self): continue for key, aep in data.get('pingroles', {}).items(): if event.start_from_now_sec() > aep['offset'] * 60 \ - or not aep['enabled'] \ - or event.server != aep['server'] \ - or aep['searchstr'] is None \ or str((key, event.key, gid)) in await self.config.sent(): continue - elif aep['regex']: - matches = re.search(aep['searchstr'], event.clean_dungeon_name) - else: - matches = aep['searchstr'].lower() in event.clean_dungeon_name.lower() - - if not matches: + if not self.event_matches_autoevent(event, aep): continue async with self.config.sent() as sent: @@ -84,28 +77,33 @@ async def do_autoevents(self): continue for aed in data.get('dmevents', []): if event.start_from_now_sec() > aed['offset'] * 60 \ - or (event.group not in (aed['group'], None)) \ - or event.server != aed['server'] \ - or aed['searchstr'] is None \ or str((aed['key'], event.key, uid)) in await self.config.sent(): continue - if aed.get('include3p') is None: - # case of legacy configs - aed['include3p'] = True - if not aed['include3p'] and event.clean_dungeon_name.startswith("Multiplayer"): + if not self.event_matches_autoevent(event, aed): continue async with self.config.sent() as sent: sent[str((aed['key'], event.key, uid))] = time.time() - if aed['searchstr'].lower() in event.clean_dungeon_name.lower(): - offsetstr = "now" - if aed['offset']: - offsetstr = f"" - timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) - try: - await user.send(f"{event.clean_dungeon_name} starts {offsetstr}!" - f" It will be active for {timestr}.") - except Exception: - logger.exception("Failed to send AED to user {}".format(user.id)) + offsetstr = "now" + if aed['offset']: + offsetstr = f"" + timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) + try: + await user.send(f"{event.clean_dungeon_name} starts {offsetstr}!" + f" It will be active for {timestr}.") + except Exception: + logger.exception("Failed to send AED to user {}".format(user.id)) + + def event_matches_autoevent(self, event: Event, autoevent: Dict[str, Any]): + if not autoevent.get('enabled', True) \ + or event.server != autoevent['server'] \ + or autoevent['searchstr'] is None: + return False + if not autoevent.get('include3p', True) and event.clean_dungeon_name.startswith("Multiplayer"): + return False + elif autoevent.get('regex'): + return re.search(autoevent['searchstr'], event.clean_dungeon_name) + else: + return autoevent['searchstr'].lower() in event.clean_dungeon_name.lower() @commands.group(aliases=['aep']) @commands.guild_only() @@ -210,17 +208,17 @@ async def aep_list(self, ctx): async def aep_set(self, ctx): """Sets specific parts of an autoeventping""" - async def aepchange(self, ctx, key, k, f): + async def aepchange(self, ctx, key: str, k: str, f: Callable[[T], T]): async with self.config.guild(ctx.guild).pingroles() as pingroles: if key not in pingroles: await ctx.send("That key does not exist.") return pingroles[key][k] = f(pingroles[key][k]) - async def aepset(self, ctx, key, k, v): + async def aepset(self, ctx, key: str, k: str, v: Any): await self.aepchange(ctx, key, k, lambda x: v) - async def aepget(self, ctx, key): + async def aepget(self, ctx, key: str): pingroles = await self.config.guild(ctx.guild).pingroles() if key not in pingroles: raise ClientInlineTextException(f"Key `{key}` does not exist. Available keys are:" @@ -355,6 +353,12 @@ async def aep_s_offset(self, ctx, key, offset: int): await self.aepset(ctx, key, 'offset', offset) await ctx.tick() + @autoeventping.command(name="channelmessage") + async def aep_channelmessage(self, ctx, channel: Optional[discord.TextChannel], enable: bool): + """Whether to show daily AEP summaries""" + await self.config.channel(channel or ctx.channel).do_aep_post.set(enable) + await ctx.tick() + @commands.group(aliases=['aed']) async def autoeventdm(self, ctx): """Auto Event DMs""" diff --git a/padevents/padevents.py b/padevents/padevents.py index 97af3703..a50e8b81 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -38,7 +38,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=940373775) self.config.register_global(sent={}, last_daychange=None) self.config.register_guild(pingroles={}) - self.config.register_channel(guerrilla_servers=[], daily_servers=[]) + self.config.register_channel(guerrilla_servers=[], daily_servers=[], do_aep_post=True) self.config.register_user(dmevents=[]) # Load event data @@ -107,6 +107,7 @@ async def is_day_change(): async for server in conditional_iterator(is_day_change, poll_interval=10): try: await self.do_daily_post(server) + await self.do_autoevent_summary(server) except asyncio.CancelledError: raise except Exception: @@ -162,6 +163,29 @@ async def do_daily_post(self, server): with suppress(discord.Forbidden): await channel.send(box(page)) + async def do_autoevent_summary(self, server): + events = EventList(self.events).with_server(server).today_only('NA') + for gid, data in (await self.config.all_guilds()).items(): + if (guild := self.bot.get_guild(gid)) is None: + continue + channels = defaultdict(list) + for key, aep in data.get('pingroles', {}).items(): + for channel in aep['channels']: + if channel is not None: + channels[channel].append(aep) + for cid, aeps in channels.items(): + if (channel := self.bot.get_channel(cid)) is None: + continue + if not await self.config.channel(channel).do_aep_post(): + continue + aepevents = events.with_func(lambda e: any(self.event_matches_autoevent(e, ae) for ae in aeps)) + if not aepevents: + continue + msg = self.make_full_guerrilla_output('AEP Event', aepevents) + for page in pagify(msg, delims=['\n\n']): + with suppress(discord.Forbidden): + await channel.send(box(page)) + @commands.group(aliases=['pde']) @checks.mod_or_permissions(manage_guild=True) async def padevents(self, ctx): @@ -318,7 +342,11 @@ def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=Tr events = sorted(events, key=lambda e: e.open_datetime) events_by_group = defaultdict(list) for event in events: - events_by_group[event.group.upper()].append(event) + if event.group is not None: + events_by_group[event.group.upper()].append(event) + else: + for group in grps: + events_by_group[group].append(event) done = False while not done: @@ -341,7 +369,7 @@ def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=Tr else: done = True - col1 = "Limited" + col1 = table_name tbl = prettytable.PrettyTable([col1] + grps) tbl.align[col1] = "l" tbl.hrules = prettytable.HEADER From 8cbdec286e897dd99949ec8677b988b6406203ef Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 11 Oct 2021 08:27:44 -0700 Subject: [PATCH 0991/1220] [AdPEM] Notify on verification ignore (#1481) --- crowddata/mixins/vem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 2a0c0a72..6a4d12e4 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -86,8 +86,11 @@ def get_vem_evo(mon: "MonsterModel") -> "MonsterModel": check = '\n\t'.join(pdicog.monster_header.fmt_id_header(m, use_emoji=True).to_markdown() for m in correct_evos) - if not await get_user_confirmation(ctx, f"Are these monsters correct?\n\t{check}", - timeout=30, force_delete=False, show_feedback=True): + confirmation = await get_user_confirmation(ctx, f"Are these monsters correct?\n\t{check}", + timeout=30, force_delete=False, show_feedback=True) + if not confirmation: + if confirmation is None: + await ctx.send(ctx.author.mention + " Submission timed out. Please resubmit and verify.") return await self.add_pull(ctx, [m.monster_id for m in correct_evos], time.time() - offset) From 7df51839cf674331315bd63cbc8cde03bdd2d463 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Mon, 11 Oct 2021 23:00:41 -0400 Subject: [PATCH 0992/1220] Make padto work with aliases (#1483) --- padglobal/padglobal.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index b128f3fd..5de05bf7 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -471,6 +471,10 @@ async def padto(self, ctx, to_user: discord.Member, *, term: str): """ corrected_term = self._lookup_command(term) result = self.c_commands.get(corrected_term, None) + # go a level deeper if this is an alias + if result in self.c_commands: + result = self.c_commands[result] + await self._do_send_term(ctx, to_user, term, corrected_term, result) async def _do_send_term(self, ctx, to_user: discord.Member, term, corrected_term, result): From 9173c33a3c12c187c10fe6f760fc2bd40509f4cc Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Oct 2021 00:20:56 -0700 Subject: [PATCH 0993/1220] Fix PadEvents multiple pings (#1487) --- padevents/padevents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padevents/padevents.py b/padevents/padevents.py index a50e8b81..214fcc51 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -129,7 +129,7 @@ async def refresh_data(self): self.started_events = {ev.key for ev in new_events if ev.is_started()} async with self.config.sent() as seen: for key, value in [*seen.items()]: - if value < time.time() + 60 * 60: + if value < time.time() - 60 * 60: del seen[key] async def do_eventloop(self): From 978c4a2d35dce220ac8a76c6f63e3f20c3ee9e01 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 14 Oct 2021 14:07:19 -0700 Subject: [PATCH 0994/1220] Instantiate MonsterIndex objects synchronously (#1488) --- dbcog/dbcog.py | 13 +++++--- dbcog/idtest_mixin.py | 4 +-- dbcog/monster_index.py | 71 ++++++++++++++++++++++++------------------ 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 630c9d63..c7ff463f 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -50,7 +50,10 @@ def __init__(self, bot, *args, **kwargs): self._is_ready = asyncio.Event() self.database = None - self.indexes = {} # type: Dict[Server, MonsterIndex] + self.indexes = { + Server.COMBINED: MonsterIndex(Server.COMBINED), + Server.NA: MonsterIndex(Server.NA), + } self.fir_lock = asyncio.Lock() self.fir3_lock = asyncio.Lock() @@ -142,8 +145,8 @@ async def forceindexreload3(self, ctx): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" for server in SERVERS: - index = await MonsterIndex(self.database.graph, server) # noqa - self.indexes[server] = index + self.indexes[server] = MonsterIndex(server) + await self.indexes[server].setup(self.database.graph) self.mon_finder = FindMonster(self, self.fm_flags_default) asyncio.create_task(self.check_index()) @@ -208,13 +211,13 @@ async def reload_data_task(self): self._is_ready.set() except Exception as ex: short_wait = True - logger.exception("dbcog data download/refresh failed: %s", ex) + logger.exception("DBCog data download/refresh failed: %s", ex) try: wait_time = 60 if short_wait else 60 * 60 * 4 await asyncio.sleep(wait_time) except Exception as ex: - logger.exception("dbcog data wait loop failed: %s", ex) + logger.exception("DBCog data wait loop failed: %s", ex) raise async def download_and_refresh_nicknames(self): diff --git a/dbcog/idtest_mixin.py b/dbcog/idtest_mixin.py index 0fd87fdb..3e9439ce 100644 --- a/dbcog/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -1,6 +1,6 @@ import re from datetime import datetime -from typing import Callable, Coroutine, List, Mapping, Optional +from typing import Callable, Coroutine, Dict, List, Optional from redbot.core import Config, checks, commands from redbot.core.bot import Red @@ -17,7 +17,7 @@ class IdTest: bot: Red config: Config - indexes: Mapping[Server, MonsterIndex] + indexes: Dict[Server, MonsterIndex] find_monster: Callable[[int], Coroutine[None, None, MonsterModel]] wait_until_ready: Callable[[], Coroutine[None, None, None]] diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index ef3109db..f34b4210 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -3,13 +3,12 @@ import io import logging import re -from typing import Dict, List +from typing import Dict, List, Optional import aiohttp from redbot.core.utils import AsyncIter from tsutils.enums import Server from tsutils.formatting import contains_ja -from tsutils.helper_classes import aobject from .errors import InvalidGraphState from .models.monster_model import MonsterModel @@ -29,34 +28,51 @@ logger = logging.getLogger('red.pad-cogs.dbcog.monster_index') -class MonsterIndex(aobject): - async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): - self.graph = graph +class MonsterIndex: + def __init__(self, server: Server = DEFAULT_SERVER): + self.is_ready = asyncio.Event() self.server = server - monsters = graph.get_all_monsters(server) - self.issues = [] + self.graph: Optional[MonsterGraph] = None + self.monster_id_to_nickname = defaultdict(set) self.monster_id_to_nametokens = defaultdict(set) self.monster_id_to_forcedfluff = defaultdict(set) self.monster_id_to_treename = defaultdict(set) + self.series_id_to_pantheon_nickname = defaultdict(set) + self.mwtoken_creators = defaultdict(set) + self.replacement_tokens = defaultdict(set) + self.remove_mods = defaultdict(set) + self.manual_prefixes = defaultdict(set) + self.manual_nick = defaultdict(set) + self.manual_tree = defaultdict(set) + self.name_tokens = defaultdict(set) + self.fluff_tokens = defaultdict(set) + self.modifiers = defaultdict(set) + self.treename_overrides = set() + self.multi_word_tokens = {} + self._known_mods = {} + self.manual = {} + self.all_name_tokens = {} + self.all_modifiers = set() + self.suffixes = set() + self.mwt_to_len = defaultdict(lambda: 1) + + async def setup(self, graph: MonsterGraph): + self.graph = graph + monsters = graph.get_all_monsters(self.server) + self.series_id_to_pantheon_nickname = \ defaultdict(set, {m.series_id: {m.series.name_en.lower().replace(" ", "")} for m in monsters if m.series.name_en.lower() not in PROBLEMATIC_SERIES_TOKENS}) - self.mwtoken_creators = defaultdict(set) - self.multi_word_tokens = {tuple(m.series.name_en.lower().split()) for m in monsters if " " in m.series.name_en.strip()}.union(MULTI_WORD_TOKENS) - self.replacement_tokens = defaultdict(set) - self.remove_mods = defaultdict(set) - self.treename_overrides = set() - treenames_data, nickname_data, pantheon_data, nt_alias_data, treemod_data, mod_data = await asyncio.gather( sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, ('base_id', 'new_treename', 'normal_prio', 'overrides')), @@ -77,20 +93,21 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): mid = int(data['base_id']) name = data['new_treename'].strip().lower() try: - monster = graph.get_monster(mid, server=server) + monster = graph.get_monster(mid, server=self.server) except InvalidGraphState: continue if not monster: continue if data['overrides']: - for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): + for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=self.server)): self.treename_overrides.add(evomid) if data['normal_prio']: - for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): + for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=self.server)): self.monster_id_to_nametokens[evomid].update(self._name_to_tokens(name)) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")].add(graph.get_monster(mid, server=server)) + self.mwtoken_creators[name.lower().replace(" ", "")]\ + .add(graph.get_monster(mid, server=self.server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_treename[mid].add(name.lower().replace(" ", "")) @@ -99,7 +116,7 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): mid = int(data['monster_id']) name = data['name_en'].strip().lower() try: - monster = graph.get_monster(mid, server=server) + monster = graph.get_monster(mid, server=self.server) except InvalidGraphState: continue if not monster: @@ -115,7 +132,8 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.treename_overrides.add(mid) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")].add(graph.get_monster(mid, server=server)) + self.mwtoken_creators[name.lower().replace(" ", "")]\ + .add(graph.get_monster(mid, server=self.server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_nickname[mid].add(name.lower().replace(" ", "")) @@ -130,12 +148,11 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): for data in nt_alias_data: self.replacement_tokens[frozenset(re.split(r'[,\s]+', data['tokens']))].add(data['alias']) - self.manual_prefixes = defaultdict(set) for data in mod_data: if data['monster_id'].isdigit(): mid = int(data['monster_id']) try: - monster = graph.get_monster(mid, server=server) + monster = graph.get_monster(mid, server=self.server) except InvalidGraphState: continue if not monster: @@ -151,7 +168,7 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.manual_prefixes[mid].update(get_modifier_aliases(mod)) for data in treemod_data: - if data['base_id'].isdigit() and graph.get_monster(int(data['base_id']), server=server): + if data['base_id'].isdigit() and graph.get_monster(int(data['base_id']), server=self.server): mid = int(data['base_id']) for mod in data['modifiers'].split(","): mod = mod.strip().lower() @@ -159,18 +176,12 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.multi_word_tokens.add(tuple(mod.split(" "))) mod = mod.replace(" ", "") aliases = get_modifier_aliases(mod) - for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=server)): + for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=self.server)): self.manual_prefixes[evomid].update(aliases) self._known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() for x in xs}.union(KNOWN_MODIFIERS) - self.manual_nick = defaultdict(set) - self.manual_tree = defaultdict(set) - self.name_tokens = defaultdict(set) - self.fluff_tokens = defaultdict(set) - self.modifiers = defaultdict(set) - await self._build_monster_index(monsters) self.manual = combine_tokens_dicts(self.manual_nick, self.manual_tree) @@ -179,7 +190,7 @@ async def __ainit__(self, graph: MonsterGraph, server: Server = DEFAULT_SERVER): self.suffixes = LEGAL_END_TOKENS self.mwt_to_len = defaultdict(lambda: 1, {"".join(mw): len(mw) for mw in self.multi_word_tokens}) - __init__ = __ainit__ + self.is_ready.set() async def _build_monster_index(self, monsters): async for m in AsyncIter(monsters): From 24111ca4d41d24dd428a45a995e72ad4e43add23 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Sat, 16 Oct 2021 14:24:38 -0400 Subject: [PATCH 0995/1220] Change TURN to CD (#1489) --- dungeoncog/view/dungeon.py | 49 +++++++------------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index d801f654..eb537bde 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -126,7 +126,6 @@ def indent(level): def embed_helper(level, names, values, line): """ Adds a line in such a way to maximize discord embed limits. - We first try to add it to the most recent name part of the name/value pair. If that fails we try to add it to the corresponding value. If that fails, we create another name/value pair. @@ -147,6 +146,13 @@ def embed_helper(level, names, values, line): values.append("") DungeonView.embed_helper(level, names, values, line) + @staticmethod + def make_embed_description(dungeon_monster, desc=""): + return "HP:{} ATK:{} DEF:{} CD:{}{}".format(f'{dungeon_monster.hp:,}', + f'{dungeon_monster.atk:,}', + f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc) + @staticmethod def embed(state: DungeonViewState): fields = [] @@ -206,18 +212,12 @@ def make_embed(dungeon_monster: DungeonMonster, verbose: bool = False, spawn: Li title="{} Spawn:{}/{} Floor:{}/{} Page:".format(dungeon_monster.name, spawn[0], spawn[1], floor[0], floor[1]), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', - f'{dungeon_monster.atk:,}', - f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc) + description=DungeonView.make_embed_description(dungeon_monster, desc) ) else: embed = discord.Embed( title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', - f'{dungeon_monster.atk:,}', - f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc) + description=DungeonView.make_embed_description(dungeon_monster, desc) ) embeds.append(embed) @@ -272,34 +272,3 @@ def make_embed(dungeon_monster: DungeonMonster, verbose: bool = False, spawn: Li embeds[0].title += '2' embeds[1].title += '2' return embeds - - @staticmethod - async def make_preempt_embed(dungeon_monster, spawn: "list[int]" = None, floor: "list[int]" = None, - technical: int = None): - """ - Currently unused: when called it creates an embed that only contains embed information. - """ - skills = await dungeon_monster.collect_skills() - desc = "" - for s in skills: - if "Passive" in s.type or "Preemptive" in s.type: - desc += "\n{}".format(s.give_string(verbose=True)) - if technical == 0: - desc = "" - if spawn is not None: - embed = discord.Embed( - title="Enemy:{} at Level: {} Spawn:{}/{} Floor:{}/{}".format(dungeon_monster.name, - dungeon_monster.level, spawn[0], spawn[1], - floor[0], floor[1]), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', - f'{dungeon_monster.atk:,}', - f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc)) - else: - embed = discord.Embed( - title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), - description="HP:{} ATK:{} DEF:{} TURN:{}{}".format(f'{dungeon_monster.hp:,}', - f'{dungeon_monster.atk:,}', - f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc)) - return [embed, discord.Embed(title="test", desc="test")] From b0eba1704dbea1c0cffa30cee11c3eeb2720eddf Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 17 Oct 2021 04:53:28 -0700 Subject: [PATCH 0996/1220] Use NEW_DAY in AdPEM (#1490) --- crowddata/mixins/vem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 6a4d12e4..5f28fe07 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -6,7 +6,7 @@ from redbot.core import Config, commands from tsutils.cog_mixins import CogMixin from tsutils.emoji import NO_EMOJI, char_to_emoji -from tsutils.time import DAY_CHANGE, NA_TIMEZONE, get_last_time +from tsutils.time import NA_TIMEZONE, NEW_DAY, get_last_time from tsutils.user_interaction import get_user_confirmation, get_user_reaction if TYPE_CHECKING: @@ -236,4 +236,4 @@ async def add_pull(self, ctx, values: Any, at_time: float): @staticmethod def midnight() -> float: - return get_last_time(DAY_CHANGE, NA_TIMEZONE).astimezone(timezone.utc).timestamp() + return get_last_time(NEW_DAY, NA_TIMEZONE).astimezone(timezone.utc).timestamp() From e9d754977601a38f891455d145e911c857f7f9a1 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 17 Oct 2021 12:42:39 -0700 Subject: [PATCH 0997/1220] Add hasgem prefix (#1491) --- dbcog/monster_index.py | 4 ++++ dbcog/token_mappings.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index f34b4210..2d84909b 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -514,6 +514,10 @@ def is_story(m, do_transform=True): if monster.monster_id > 10000: modifiers.add("idna") + # Has Gem + if self.graph.evo_gem_monster(monster) is not None: + modifiers.update(MISC_MAP[MiscModifiers.HAS_GEM]) + modifiers.difference_update(self.remove_mods[monster.monster_id]) return modifiers diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index eaab9665..06db2a77 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -199,6 +199,7 @@ class MiscModifiers(Enum): CURRENT_EXCHANGE_KR = 'Currently exchangable in KR' PERMANENT_EXCHANGE = 'Permanently exchangable' TEMP_EXCHANGE = 'Temporarily exchangable at some point in time' + HAS_GEM = 'Has an evo gem' MISC_MAP = { @@ -228,6 +229,7 @@ class MiscModifiers(Enum): MiscModifiers.CURRENT_EXCHANGE_KR: ('nowshopkr', 'shopnowkr'), MiscModifiers.PERMANENT_EXCHANGE: ('permshop', 'shopperm'), MiscModifiers.TEMP_EXCHANGE: ('tempshop', 'shoptemp'), + MiscModifiers.HAS_GEM: ('hasgem',), } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { From 56b10ed60f4dd30a0702f653d93e1a6bdebbd8b6 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 18 Oct 2021 14:06:35 -0700 Subject: [PATCH 0998/1220] Add some extra end user data statements (#1492) --- crud/__init__.py | 2 +- crud/crud.py | 13 +++--- crud/info.json | 2 +- dbcog/__init__.py | 6 ++- dbcog/dbcog.py | 16 ++++--- dbcog/info.json | 6 +-- dungeoncog/__init__.py | 8 +++- monidlistener/monidlistener.py | 4 +- padevents/__init__.py | 6 ++- padevents/info.json | 2 +- padevents/padevents.py | 14 +++--- padglobal/padglobal.py | 85 +++++++++++++++++----------------- padinfo/__init__.py | 6 ++- padinfo/info.json | 2 +- padinfo/padinfo.py | 14 +++++- 15 files changed, 111 insertions(+), 75 deletions(-) diff --git a/crud/__init__.py b/crud/__init__.py index e7f08ed1..6dcb4639 100644 --- a/crud/__init__.py +++ b/crud/__init__.py @@ -1,6 +1,6 @@ from .crud import Crud -__red_end_user_data_statement__ = "No personal data is stored." +__red_end_user_data_statement__ = "User email addresses are stored persistently." def setup(bot): diff --git a/crud/crud.py b/crud/crud.py index 02e5062c..7f9f42fc 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -67,15 +67,16 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) + email = self.config.user_from_id(user_id) + if email: + data = f"You have a stored email ({email})" + else: + data = "No data is stored for user with ID {}.\n".format(user_id) return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return + """Delete a user's personal data.""" + await self.config.user_from_id(user_id).clear() async def get_cursor(self): async with aiofiles.open(await self.config.config_file(), 'r') as db_config: diff --git a/crud/info.json b/crud/info.json index 11f525b4..6a6457ae 100644 --- a/crud/info.json +++ b/crud/info.json @@ -3,7 +3,7 @@ "Aradia Megido#2552" ], "description": "Crud for the Pipeline Database.", - "end_user_data_statement": "No personal data is stored.", + "end_user_data_statement": "User email addresses are stored persistently", "install_msg": "Use [p]crud to get started.", "short": "Pipeline CRUD", "min_bot_version": "3.4.0", diff --git a/dbcog/__init__.py b/dbcog/__init__.py index 6206b544..a788f06c 100644 --- a/dbcog/__init__.py +++ b/dbcog/__init__.py @@ -1,6 +1,10 @@ +import json +from pathlib import Path + from .dbcog import DBCog -__red_end_user_data_statement__ = "No personal data is stored." +with open(Path(__file__).parent / "info.json") as file: + __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] def setup(bot): diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index c7ff463f..a4e0ab47 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -105,15 +105,19 @@ async def wait_until_ready_verbose(self, ctx) -> None: async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) + udata = await self.config.user_from_id(user_id).ans() + + data = "Your stored query flags are: " + "\n".join(udata['fm_flags'].keys()) + data += f"\nUse '{(await self.bot.get_valid_prefixes())[0]}idset list' to see what they're set to." + + if not udata: + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return + """Delete a user's personal data.""" + await self.config.user_from_id(user_id).clear() @commands.command(aliases=['fir']) @auth_check('contentadmin') diff --git a/dbcog/info.json b/dbcog/info.json index 5b498c3d..bcbb230a 100644 --- a/dbcog/info.json +++ b/dbcog/info.json @@ -2,10 +2,10 @@ "author": [ "The Tsubaki Bot Team" ], - "description": "No commands, but loads all the monster data stored in Tsubaki.", - "end_user_data_statement": "No personal data is stored.", + "description": "Very few commands, but loads all the monster data stored in Tsubaki.", + "end_user_data_statement": "ID Query Settings are stored in this cog.", "install_msg": "Use [p]dbcog setdatafile if it's applicable to you.", - "short": "Loads monster data (LOAD THIS FIRST)", + "short": "Loads monster data", "min_bot_version": "3.4.0", "max_bot_version": "0.0.0", "min_python_version": [ diff --git a/dungeoncog/__init__.py b/dungeoncog/__init__.py index e65b9960..1184bac8 100644 --- a/dungeoncog/__init__.py +++ b/dungeoncog/__init__.py @@ -1,10 +1,14 @@ -import asyncio +import json +from pathlib import Path from .dungeoncog import DungeonCog +with open(Path(__file__).parent / "info.json") as file: + __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] + def setup(bot): n = DungeonCog(bot) bot.add_cog(n) bot.loop.create_task(n.register_menu()) - asyncio.create_task(n.load_emojis()) + bot.loop.create_task(n.load_emojis()) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 9f8a0c4d..5ddaa08c 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -7,8 +7,10 @@ class MonIdListener(commands.Cog): """Monster Name from ID""" - def __init__(self, bot): + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) self.bot = bot + self.config = Config.get_conf(self, identifier=10100779) self.config.register_channel(enabled=False) diff --git a/padevents/__init__.py b/padevents/__init__.py index fae15e3d..915c1c04 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -1,6 +1,10 @@ +import json +from pathlib import Path + from .padevents import PadEvents -__red_end_user_data_statement__ = "No personal data is stored." +with open(Path(__file__).parent / "info.json") as file: + __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] def setup(bot): diff --git a/padevents/info.json b/padevents/info.json index dd8565b2..723769c4 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -3,7 +3,7 @@ "The Tsubaki Bot Team" ], "description": "Stay up to date on PAD dungeons.", - "end_user_data_statement": "No personal data is stored.", + "end_user_data_statement": "Auto Event DMs are stored with your user ID.", "install_msg": "Use [p]padevents to get started.", "short": "PAD Event tracker", "min_bot_version": "3.4.0", diff --git a/padevents/padevents.py b/padevents/padevents.py index 214fcc51..44516a42 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -53,15 +53,17 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) + aeds = await self.config.user_from_id(user_id).dmevents() + if aeds: + data = f"You have {len(aeds)} AEDs stored. Use" \ + f" {(await self.bot.get_valid_prefixes())[0]}aed list to see what they are.\n" + else: + data = f"No data is stored for user with ID {user_id}." return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return + """Delete a user's personal data.""" + await self.config.user_from_id(user_id).clear() def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 5de05bf7..b4838e41 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -11,10 +11,9 @@ import discord import prettytable import pytz - from discord import Color -from redbot.core import checks, data_manager, commands, errors -from redbot.core.utils.chat_formatting import box, inline, pagify, humanize_timedelta, bold +from redbot.core import checks, commands, data_manager, errors +from redbot.core.utils.chat_formatting import bold, box, humanize_timedelta, inline, pagify from tsutils.cog_settings import CogSettings from tsutils.cogs.globaladmin import auth_check from tsutils.emoji import fix_emojis_for_server, replace_emoji_names_with_code @@ -23,17 +22,13 @@ from tsutils.tsubaki import CLOUDFRONT_URL from tsutils.user_interaction import get_user_confirmation, get_user_reaction -auth_check, get_user_reaction - from padglobal.menu.closable_embed import ClosableEmbedMenu from padglobal.menu.menu_map import padglobal_menu_map from padglobal.view.closable_embed import ClosableEmbedViewState -from padglobal.view.which import WhichView, WhichViewProps, UNKNOWN_EDIT_TIMESTAMP +from padglobal.view.which import UNKNOWN_EDIT_TIMESTAMP, WhichView, WhichViewProps logger = logging.getLogger('red.padbot-cogs.padglobal') -global PADGLOBAL_COG - def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padglobal')), file_name) @@ -76,7 +71,7 @@ def mod_help(self, ctx, help_type): async def check_enabled(ctx): """If the server is disabled, raise a warning and return False""" - if PADGLOBAL_COG.settings.checkDisabled(ctx.message): + if ctx.bot.get_cog("PadGlobal").settings.checkDisabled(ctx.message): msg = await ctx.send(inline(DISABLED_MSG)) await asyncio.sleep(3) await msg.delete() @@ -91,8 +86,6 @@ class PadGlobal(commands.Cog): def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) - global PADGLOBAL_COG - PADGLOBAL_COG = self self.bot = bot GADMIN_COG = self.bot.get_cog("GlobalAdmin") @@ -244,9 +237,9 @@ async def _add(self, ctx, command, text, confirm=True): return if self.c_commands[text] in self.c_commands: source = self.c_commands[text] - if await get_user_confirmation(ctx, '{} is already an alias for {}, and you can\'t alias to an alias.' - .format(inline(text), inline(source)) - + ' Would you like to alias to {} instead?'.format(inline(source))): + if await get_user_confirmation(ctx, f'{inline(text)} is already an alias for {inline(source)},' + f' and you can\'t alias to an alias. Would you like to' + f' alias to {inline(source)} instead?'): # change target text = source else: @@ -260,7 +253,7 @@ async def _add(self, ctx, command, text, confirm=True): alias = True if confirm: conf = await get_user_confirmation(ctx, "Are you sure you want to edit the {}command {}?" - .format("alias to " if alias else "", ted)) + "".format("alias to " if alias else "", ted)) if not conf: return else: @@ -281,8 +274,8 @@ async def delete(self, ctx, command: str): aliases = await self._find_aliases(command) if aliases: if not await get_user_confirmation(ctx, - 'Are you sure? `{}` has {} alias(es): `{}` which will also be deleted.' - .format(command, bold(str(len(aliases))), '`, `'.join(aliases))): + 'Are you sure? `{}` has {} alias(es): `{}` which will also be deleted.' + ''.format(command, bold(str(len(aliases))), '`, `'.join(aliases))): await ctx.send('Cancelling delete of `{}`.'.format(command)) return @@ -564,7 +557,8 @@ async def _pglossary_add(self, ctx, term, definition, need_confirm=True): op = 'edited' if term in self.settings.glossary() else 'added' if op == 'edited' and need_confirm: - if not await get_user_confirmation(ctx, "Are you sure you want to edit the glossary info for {}?".format(term)): + if not await get_user_confirmation(ctx, + "Are you sure you want to edit the glossary info for {}?".format(term)): return self.settings.addGlossary(term, definition) await ctx.send("PAD glossary term successfully {}.".format(bold(op))) @@ -577,7 +571,8 @@ async def pglossary_remove(self, ctx, *, term): await ctx.send("Glossary item doesn't exist.") return if not await get_user_confirmation(ctx, - "Are you sure you want to globally remove the glossary data for {}?".format(term)): + "Are you sure you want to globally remove the glossary data for {}?".format( + term)): return self.settings.rmGlossary(term) await ctx.tick() @@ -673,7 +668,8 @@ async def _pboss_add(self, ctx, term, definition, need_confirm=True): op = 'edited' if base.monster_id in self.settings.boss() else 'added' if op == 'edited' and need_confirm: if not await get_user_confirmation(ctx, - "Are you sure you want to edit the boss info for {}?".format(base.name_en)): + "Are you sure you want to edit the boss info for {}?".format( + base.name_en)): return definition = clean_global_mentions(definition) definition = definition.replace(u'\u200b', '') @@ -699,8 +695,8 @@ async def pboss_remove(self, ctx, *, term): await ctx.send("Boss mechanics item doesn't exist.") return if not await get_user_confirmation(ctx, - "Are you sure you want to globally remove the boss data for {}?".format( - base.name_en)): + "Are you sure you want to globally remove the boss data for {}?".format( + base.name_en)): return self.settings.rmBoss(base.monster_id) @@ -876,8 +872,9 @@ async def pwhich_remove(self, ctx, *, monster_id: int): if m != base_monster: m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) - if not await get_user_confirmation(ctx, "Are you sure you want to globally remove the which data for {}?".format( - m.name_en)): + if not await get_user_confirmation(ctx, + "Are you sure you want to globally remove the which data for {}?".format( + m.name_en)): return name = m.monster_id @@ -916,16 +913,18 @@ async def _concatenate_which(self, ctx, term: str, operation: str, addition): # ask for extra confirmation if the term was not an id if not re.fullmatch(r'\d+', term): - if not await get_user_confirmation(ctx, 'Are you sure you want to {} to the which info for {} [{}] {}?'.format( - operation, - pdicog.get_attribute_emoji_by_monster(m), - m.monster_no_na, - m.name_en)): + if not await get_user_confirmation(ctx, + 'Are you sure you want to {} to the which info for {} [{}] {}?'.format( + operation, + pdicog.get_attribute_emoji_by_monster(m), + m.monster_no_na, + m.name_en)): return if mon_id not in self.settings.which(): - if await get_user_confirmation(ctx, "No which info exists for {}. Would you like to add a new entry?".format( - m.name_en)): + if await get_user_confirmation(ctx, + "No which info exists for {}. Would you like to add a new entry?".format( + m.name_en)): self.settings.addWhich(mon_id, addition) await ctx.send("PAD which info successfully {}.".format(bold('added'))) return @@ -968,11 +967,11 @@ async def pwhich_list(self, ctx): destination = channel if len(self.settings.which()) > MAX_WHICH_LIST_BEFORE_DM_PROMPT: destination = await get_user_reaction(ctx, - 'This will send a lot of messages. Are you sure? ' - + '(Yes / DM me instead / Cancel)', - channel, - send_as_dm, - cancel) + 'This will send a lot of messages. Are you sure? ' + + '(Yes / DM me instead / Cancel)', + channel, + send_as_dm, + cancel) if destination == cancel or destination is None: return @@ -1303,7 +1302,8 @@ async def _dungeon_add(self, ctx, term: str, definition: str, need_confirm=True) op = 'edited' if term in self.settings.dungeonGuide() else 'added' if op == 'edited' and need_confirm: if not await get_user_confirmation(ctx, - "Are you sure you want to edit the dungeon guide info for {}?".format(term)): + "Are you sure you want to edit the dungeon guide info for {}?".format( + term)): return self.settings.addDungeonGuide(term, definition) await ctx.send("PAD dungeon guide successfully {}.".format(bold(op))) @@ -1316,8 +1316,8 @@ async def dungeon_remove(self, ctx, term: str): await ctx.send("DungeonGuide doesn't exist.") return if not await get_user_confirmation(ctx, - "Are you sure you want to globally remove the dungeonguide data for {}?".format( - term)): + "Are you sure you want to globally remove the dungeonguide data for {}?".format( + term)): return self.settings.rmDungeonGuide(term) await ctx.tick() @@ -1347,7 +1347,8 @@ async def _leader_add(self, ctx, monster_id: int, definition: str, need_confirm= op = 'edited' if name in self.settings.leaderGuide() else 'added' if op == 'edited' and need_confirm: if not await get_user_confirmation(ctx, - "Are you sure you want to edit the leader guide for {}?".format(m.name_en)): + "Are you sure you want to edit the leader guide for {}?".format( + m.name_en)): return self.settings.addLeaderGuide(name, definition) await ctx.send("PAD leader guide info successfully {}.".format(bold(op))) @@ -1362,8 +1363,8 @@ async def leader_remove(self, ctx, monster_id: int): m = base_monster await ctx.send("I think you meant {} for {}.".format(m.monster_no_na, m.name_en)) if not await get_user_confirmation(ctx, - "Are you sure you want to globally remove the leaderguide data for {}?".format( - m.name_en)): + "Are you sure you want to globally remove the leaderguide data for {}?".format( + m.name_en)): return name = m.monster_id diff --git a/padinfo/__init__.py b/padinfo/__init__.py index 56291a27..b71d631e 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -1,6 +1,10 @@ +import json +from pathlib import Path + from .padinfo import PadInfo -__red_end_user_data_statement__ = "No personal data is stored." +with open(Path(__file__).parent / "info.json") as file: + __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] def setup(bot): diff --git a/padinfo/info.json b/padinfo/info.json index 47ecf85c..319eb9c3 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -3,7 +3,7 @@ "The Tsubaki Bot Team" ], "description": "Contains the bulk of PAD commands, allowing users to get data for monsters.", - "end_user_data_statement": "No personal data is stored.", + "end_user_data_statement": "Previous ID queries are stored for easy reuse.", "install_msg": "Use [p]helpid for help", "short": "Serves monster data (LOAD THIS SECOND)", "min_bot_version": "3.4.0", diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9a346b0d..674fdb17 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -129,7 +129,12 @@ def __init__(self, bot, *args, **kwargs): async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) + idhist = await self.config.user_from_id(user_id).id_history() + if idhist: + data = f"You have {len(idhist)} past queries stored. Use" \ + f" {(await self.bot.get_valid_prefixes())[0]}idhistory to see what they are.\n" + else: + data = f"No data is stored for user with ID {user_id}." return {"user_data.txt": BytesIO(data.encode())} async def red_delete_data_for_user(self, *, requester, user_id): @@ -137,7 +142,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): No personal data is stored in this cog. """ - return + await self.config.user_from_id(user_id).clear() async def register_menu(self): await self.bot.wait_until_ready() @@ -1004,6 +1009,11 @@ async def padsay(self, ctx, server, *, query: str = None): async def idset(self, ctx): """`[p]id` settings configuration""" + @idset.command(name="list") + async def idset_list(self, ctx): + """`[p]id` settings list""" + await ctx.send("This command is still in progress!") + @idset.command(name="scroll") async def idset_scroll(self, ctx): await ctx.send(f"We no longer support number scroll as a preference. " From 5b673c46547940c560a3eb04d1f2268c198373c0 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Fri, 22 Oct 2021 19:49:28 -0400 Subject: [PATCH 0999/1220] Added awakening restricted latents to ^pdchu help message (#1493) --- padbuildimg/padbuildimg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 150b744a..57392159 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -34,6 +34,7 @@ Stats (+ for 2 slot): hp, atk, rcv, all(all stat), hp+, atk+, rcv+ Resists (+ for 2 slot): rres, bres, gres, lres, dres, rres+, bres+, gres+, lres+, dres+ AA4: ls(leader swap), jsf(jammer skyfall), psf(poison skyfall), attr(attribute absorb res), vdp(void damage penatration) + Awakening restricted: unm(unmatchable clear), spn(spinner clear), abs(void damage absorb) Others: sdr, ah(autoheal) Repeat: *# defines number of times to repeat this particular card From 385033a037fad7c714d5153d31a5c1845fb5e81f Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Wed, 27 Oct 2021 19:32:44 -0400 Subject: [PATCH 1000/1220] Edit ^events to include Discord countdown (#1494) --- padevents/events.py | 13 ++++++------- padevents/padevents.py | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/padevents/events.py b/padevents/events.py index a01eab13..3a86cd26 100644 --- a/padevents/events.py +++ b/padevents/events.py @@ -93,12 +93,10 @@ def start_est(self): return self.open_datetime.astimezone(tz) def start_from_now(self): - return f'{self.start_from_now_sec() // 3600:2}h' \ - f' {(self.start_from_now_sec() % 3600) // 60:2}m' + return "" def end_from_now(self): - return f'{self.end_from_now_sec() // 3600:2}h' \ - f' {(self.end_from_now_sec() % 3600) // 60:2}m' + return "" def end_from_now_full_min(self): days, sec = divmod(self.end_from_now_sec(), 86400) @@ -128,10 +126,11 @@ def group_long_name(self): def to_partial_event(self, pe): group = self.group_short_name() if self.is_started(): - return group + " " + self.end_from_now() + " " + self.name_and_modifier + return "`" + group + " " + self.name_and_modifier + " " * ( + max(24 - len(self.name_and_modifier), 0)) + "-`" + self.end_from_now() else: - return group + " " + self.start_pst().strftime("%H:%M") + " " + self.start_est().strftime("%H:%M") \ - + " " + self.start_from_now() + " " + self.name_and_modifier + return "`" + group + " " + self.name_and_modifier + " " * ( + max(24 - len(self.name_and_modifier), 0)) + "-`" + self.start_from_now() class EventList: diff --git a/padevents/padevents.py b/padevents/padevents.py index 44516a42..2f3d739d 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -426,20 +426,20 @@ async def do_partial(self, ctx, server: Server, group: StarterGroup = None): await ctx.send("No events available for " + server) return - output = "Events for {}".format(server) + output = "**Events for {}**".format(server) if len(active_events) > 0: - output += "\n\n" + " Remaining Dungeon" + output += "\n\n" + "` Remaining Dungeon - Ending Time`" for e in active_events: output += "\n" + e.to_partial_event(self) if len(pending_events) > 0: - output += "\n\n" + " PT ET ETA Dungeon" + output += "\n\n" + "` Dungeon - ETA`" for e in pending_events: output += "\n" + e.to_partial_event(self) for page in pagify(output): - await ctx.send(box(page)) + await ctx.send(page) def get_most_recent_day_change(self): now = datetime.datetime.utcnow().time() From be60a2f4a5c440a54d5eaa0792e2de9ebf419105 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 2 Nov 2021 14:22:53 -0700 Subject: [PATCH 1001/1220] Don't multi-ping for AEP and AED (#1496) --- padevents/autoevent_mixin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index a2c8e1d2..26e8dc02 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -100,6 +100,8 @@ def event_matches_autoevent(self, event: Event, autoevent: Dict[str, Any]): return False if not autoevent.get('include3p', True) and event.clean_dungeon_name.startswith("Multiplayer"): return False + if autoevent.get('group', 'red') != event.group.lower(): + return False elif autoevent.get('regex'): return re.search(autoevent['searchstr'], event.clean_dungeon_name) else: From 511bfb2940a93737038f54c7a0f1fdf5735440c9 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 3 Nov 2021 02:56:30 -0700 Subject: [PATCH 1002/1220] Make adpem data queryable (#1495) --- crowddata/mixins/vem.py | 78 +++++++++++++++++++++++++++++++++- dbcog/models/exchange_model.py | 4 +- dbcog/monster_graph.py | 8 +++- dbcog/monster_index.py | 2 + dbcog/token_mappings.py | 2 + 5 files changed, 90 insertions(+), 4 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index 5f28fe07..e0d52d7c 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -1,14 +1,23 @@ +from collections import Counter from datetime import timezone -from typing import Any, TYPE_CHECKING +from typing import Any, List, TYPE_CHECKING import time + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail +from discordmenu.embed.text import LabeledText +from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import Config, commands +from redbot.core.bot import Red from tsutils.cog_mixins import CogMixin from tsutils.emoji import NO_EMOJI, char_to_emoji from tsutils.time import NA_TIMEZONE, NEW_DAY, get_last_time from tsutils.user_interaction import get_user_confirmation, get_user_reaction +from padinfo.view.components.monster.image import MonsterImage + if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -22,6 +31,7 @@ async def check(ctx): class VEM(CogMixin): config: Config + bot: Red def setup_self(self): self.config.register_global(pulls=[]) @@ -215,6 +225,48 @@ async def y_toggleskipconfirm(self, ctx): """Toggle if you want to confirm on skip/forgot""" await self.toggleskipconfirm(ctx) + @adpem.command() + async def showstats(self, ctx, *, query): + dbcog: Any = ctx.bot.get_cog("DBCog") + pdicog: Any = ctx.bot.get_cog("PadInfo") + if dbcog is None or pdicog is None: + return await ctx.send("Required cogs not loaded. Please alert a bot owner.") + await dbcog.wait_until_ready() + + query = '" "'.join(query.split()) + monsters = await dbcog.find_monsters(f'"inadpem" "{query}"') + if not monsters: + return await ctx.send("No monsters matched that query.") + + pulls = await self.config.pulls() + data = [d for d in pulls if type(d[1]) != str] + total = [dbcog.get_monster(p) for uid, ps, ts in data for p in ps] + adj = [dbcog.get_monster(p) for uid, ps, ts in data if self.has_good_data(uid, pulls) for p in ps] + you = [dbcog.get_monster(p) for uid, ps, ts in data if uid == ctx.author.id for p in ps] + valid = [m for m in total if m in monsters] + + fields = [] + for mon, c in Counter(valid).most_common(6 if len(valid) == 6 else 5): + fields.append(EmbedField(mon.name_en, Box( + LabeledText("Net", self.get_count(total, mon)), + LabeledText("Adj", self.get_count(adj, mon)), + LabeledText("You", self.get_count(you, mon)) + ), inline=True)) + if len(set(valid)) > 6: + fields.append(EmbedField("... & More", Box(f"+ {len(valid) - 5} more monsters"), inline=True)) + await ctx.send(embed=EmbedView( + EmbedMain( + title=f"AdPEM Data for query: {query}", + description=Box( + LabeledText("Net", self.get_count(total, *valid)), + LabeledText("Adj", self.get_count(adj, *valid)), + LabeledText("You", self.get_count(you, *valid)) + ) + ), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(Counter(valid).most_common(1)[0][0])), + embed_fields=fields + ).to_embed()) + async def assert_ready(self, ctx, midnight: float) -> bool: pulls = await self.config.pulls() numtoday = len([1 for uid, v, ts in pulls @@ -234,6 +286,30 @@ async def add_pull(self, ctx, values: Any, at_time: float): async with self.config.pulls() as pulls: pulls.append((ctx.author.id, values, at_time)) + def has_good_data(self, uid: int, pulls): + dbcog: Any = self.bot.get_cog("DBCog") + + udata = [d for d in pulls if d[0] == uid] + if len([1 for uid, ps, t in udata if ps == "Forgot"]) / len(udata) > .3: + return False + if len(udata) <= 2: + return False + + # upulls = [dbcog.get_monster(p) for u, ps, t in udata if isinstance(ps, list) for p in ps] + # if len([m for m in upulls if dbcog.database.graph.monster_is_rem_evo(m)]) / len(upulls) > .5: + # return False + + return True + @staticmethod def midnight() -> float: return get_last_time(NEW_DAY, NA_TIMEZONE).astimezone(timezone.utc).timestamp() + + @staticmethod + def get_count(arr: List[int], *elements: int) -> str: + if not arr: + return "N/A" + count = sum(arr.count(e) for e in elements) + if count == 0: + return "0" + return f"{round(100*count/len(arr), 3)}%" diff --git a/dbcog/models/exchange_model.py b/dbcog/models/exchange_model.py index 9f3d1121..038c3926 100644 --- a/dbcog/models/exchange_model.py +++ b/dbcog/models/exchange_model.py @@ -24,6 +24,6 @@ def __init__(self, **kwargs): def to_dict(self): return { - 'from': self.target_monster_id, - 'to': self.required_monster_ids, + 'from': self.required_monster_ids, + 'to': self.target_monster_id, } diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 3651e035..f6fc759d 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -336,7 +336,13 @@ def _cache_graphs(self) -> None: if self.debug_monster_ids is not None: self.graph_dict[server].nodes[mid]['model'].base_evo_id = alt_ids[0] else: - self.issues.append(f"{mid} has no model in the {server.name} graph.") + alert = False + for edges in self.graph_dict[server][mid].values(): + for edge in edges.values(): + if not edge['type'].startswith('exchange'): + alert |= True + if alert: + self.issues.append(f"{mid} has no model in the {server.name} graph.") def _get_edges(self, monster: MonsterModel, etype) -> Set[int]: return {mid for mid, atlas in self.graph_dict[monster.server_priority][monster.monster_id].items() diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 2d84909b..57ccce5d 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -466,6 +466,8 @@ def is_story(m, do_transform=True): modifiers.update(MISC_MAP[MiscModifiers.PEM]) if self.graph.monster_is_vem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.VEM]) + if monster.in_vem: + modifiers.update(MISC_MAP[MiscModifiers.INVEM]) if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 06db2a77..deab3725 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -180,6 +180,7 @@ class MiscModifiers(Enum): REM = 'In REM' PEM = 'In PEM' VEM = 'In VEM' + INVEM = 'This evo is in VEM' MP = 'MP' INJP = 'In JP Server' ONLYJP = 'Only in JP Server' @@ -210,6 +211,7 @@ class MiscModifiers(Enum): MiscModifiers.REM: ('rem',), MiscModifiers.PEM: ('pem',), MiscModifiers.VEM: ('vem', 'adpem'), + MiscModifiers.INVEM: ('invem', 'inadpem'), MiscModifiers.MP: ('mp',), MiscModifiers.INJP: ('injp',), MiscModifiers.INNA: ('inna',), From aa1df0a30a85b48f4fb978921da823d5e748b768 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 7 Nov 2021 11:06:09 -0700 Subject: [PATCH 1003/1220] [AdPEM] Remove duplicates in valid list (#1498) --- crowddata/mixins/vem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem.py index e0d52d7c..18bf16cb 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem.py @@ -258,9 +258,9 @@ async def showstats(self, ctx, *, query): EmbedMain( title=f"AdPEM Data for query: {query}", description=Box( - LabeledText("Net", self.get_count(total, *valid)), - LabeledText("Adj", self.get_count(adj, *valid)), - LabeledText("You", self.get_count(you, *valid)) + LabeledText("Net", self.get_count(total, *set(valid))), + LabeledText("Adj", self.get_count(adj, *set(valid))), + LabeledText("You", self.get_count(you, *set(valid))) ) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(Counter(valid).most_common(1)[0][0])), @@ -309,7 +309,7 @@ def midnight() -> float: def get_count(arr: List[int], *elements: int) -> str: if not arr: return "N/A" - count = sum(arr.count(e) for e in elements) + count = sum(1 for m in arr if m in elements) if count == 0: return "0" return f"{round(100*count/len(arr), 3)}%" From 1c7bd5beec46b2c33b4e8b80ee878d48f3dea39a Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 9 Nov 2021 14:32:43 -0700 Subject: [PATCH 1004/1220] Use aiomysql in CRUD (#1499) --- crud/crud.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 7f9f42fc..a1c00161 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -6,9 +6,9 @@ from io import BytesIO import aiofiles +import aiomysql import discord import pygit2 -import pymysql from redbot.core import Config, checks, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils.cogs.globaladmin import auth_check @@ -80,27 +80,27 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def get_cursor(self): async with aiofiles.open(await self.config.config_file(), 'r') as db_config: - return self.connect(json.loads(await db_config.read())).cursor() + return (await self.connect(json.loads(await db_config.read()))).cursor() def connect(self, db_config): - return pymysql.connect(host=db_config['host'], + return aiomysql.connect(host=db_config['host'], user=db_config['user'], password=db_config['password'], db=db_config['db'], charset=db_config['charset'], - cursorclass=pymysql.cursors.DictCursor, + cursorclass=aiomysql.cursors.DictCursor, autocommit=True) async def execute_write(self, ctx, sql, replacements): - with await self.get_cursor() as cursor: - affected = cursor.execute(sql, replacements) + async with await self.get_cursor() as cursor: + affected = await cursor.execute(sql, replacements) await ctx.send(inline(cursor._executed)) await ctx.send("{} row(s) affected.".format(affected)) async def execute_read(self, ctx, sql, replacements): - with await self.get_cursor() as cursor: - cursor.execute(sql, replacements) - results = list(cursor.fetchall()) + async with await self.get_cursor() as cursor: + await cursor.execute(sql, replacements) + results = list(await cursor.fetchall()) msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False, sort_keys=True) await ctx.send(inline(cursor._executed)) for page in pagify(msg): @@ -163,9 +163,9 @@ async def editmonsname(self, ctx, monster_id: int, *, name): if name.lower() in ["none", "null", ""]: name = None - with await self.get_cursor() as cursor: + async with await self.get_cursor() as cursor: # Get the most recent english translation override - cursor.execute("""SELECT + await cursor.execute("""SELECT monsters.name_en AS monster_name, monster_name_overrides.name_en AS old_override FROM monster_name_overrides @@ -173,7 +173,7 @@ async def editmonsname(self, ctx, monster_id: int, *, name): ON monsters.monster_id = monster_name_overrides.monster_id WHERE monsters.monster_id = %s AND (is_translation = 1 OR is_translation IS NULL) ORDER BY monster_name_overrides.tstamp DESC LIMIT 1""", (monster_id,)) - old_val = cursor.fetchall() + old_val = await cursor.fetchall() if not old_val: await ctx.send(f"There is no monster with id {monster_id}") @@ -191,8 +191,8 @@ async def editmonsname(self, ctx, monster_id: int, *, name): @crud.command() async def editmonsseries(self, ctx, monster_id: int, series_id: int): """Change a monster's main series_id""" - with await self.get_cursor() as cursor: - cursor.execute("""SELECT + async with await self.get_cursor() as cursor: + await cursor.execute("""SELECT COALESCE(mno.name_en, monsters.name_en) AS monster_name, series.name_en AS series_name FROM monster_series @@ -201,15 +201,15 @@ async def editmonsseries(self, ctx, monster_id: int, series_id: int): LEFT JOIN monster_name_overrides mno ON mno.monster_id = monsters.monster_id AND is_translation = 1 WHERE monsters.monster_id = %s AND priority = 1""", (monster_id,)) - rows = cursor.fetchall() + rows = await cursor.fetchall() if not rows: await ctx.send("There is no monster with id: {}".format(monster_id)) return monster_name = rows[0]['monster_name'] old_series = rows[0]['series_name'] - cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) - rows = cursor.fetchall() + await cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) + rows = await cursor.fetchall() if not rows: await ctx.send(f"There is no series with id {series_id}") return @@ -268,9 +268,9 @@ async def series_add(self, ctx, *elements): return EXTRAS = {} - with await self.get_cursor() as cursor: - cursor.execute("SELECT MAX(series_id) AS max_id FROM series") - max_val = cursor.fetchall()[0]['max_id'] + async with await self.get_cursor() as cursor: + await cursor.execute("SELECT MAX(series_id) AS max_id FROM series") + max_val = (await cursor.fetchall())[0]['max_id'] EXTRAS['series_id'] = max_val + 1 EXTRAS['tstamp'] = int(datetime.now().timestamp()) elements = {**SERIES_KEYS, **EXTRAS, **elements} From 6a1d0b09143726c096b9202a93670bf3793a0252 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Nov 2021 16:34:00 -0700 Subject: [PATCH 1005/1220] Propagate orbskin modifier through evos (#1503) --- dbcog/monster_graph.py | 3 +++ dbcog/monster_index.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index f6fc759d..4980619e 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -645,6 +645,9 @@ def monster_is_rem_evo(self, monster: MonsterModel) -> bool: def monster_is_vem_evo(self, monster: MonsterModel) -> bool: return any(alt.in_vem for alt in self.get_alt_monsters(monster)) + def monster_is_orb_skin_evo(self, monster: MonsterModel) -> bool: + return any(alt.orb_skin_id for alt in self.get_alt_monsters(monster)) + def monster_is_exchange(self, monster: MonsterModel) -> bool: return bool(self._get_edges(monster, 'exchange_from')) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 57ccce5d..80f57649 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -497,7 +497,7 @@ def is_story(m, do_transform=True): modifiers.update(MISC_MAP[MiscModifiers.TRADEABLE]) # Art - if monster.orb_skin_id: + if self.graph.monster_is_orb_skin_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.ORBSKIN]) if monster.has_animation: modifiers.update(MISC_MAP[MiscModifiers.ANIMATED]) From 84189993fd8a4f98263df163a277a8b9f5f244d9 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 14 Nov 2021 23:54:23 -0700 Subject: [PATCH 1006/1220] Allow comments in ^id (#1504) --- dbcog/find_monster.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index ed6812bf..527c0eec 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -303,6 +303,7 @@ async def find_monster_debug(self, query: str) -> \ findmonster or findmonsters instead.""" await self.dbcog.wait_until_ready() + query = query.split('//')[0] # Remove comments query = rmdiacritics(query).lower().replace(",", "") query = re.sub(r'(\s|^)\'(\S+)\'(\s|$)', r'\1"\2"\3', query) # Replace ' with " around tokens query = re.sub(r':=?r?([\'"])[^\1]+\1', lambda m: m.group(0).replace(' ', '\0'), query) # Keep modded spaces From c53036dd5553e63b474c8c321ea7a8930cca4e31 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 21 Nov 2021 19:48:08 -0700 Subject: [PATCH 1007/1220] Fix timezone in ^whichinvade (#1505) --- padglobal/padglobal.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index b4838e41..5dd3fb61 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -19,6 +19,7 @@ from tsutils.emoji import fix_emojis_for_server, replace_emoji_names_with_code from tsutils.formatting import clean_global_mentions, strip_right_multiline from tsutils.json_utils import safe_read_json +from tsutils.time import NA_TIMEZONE from tsutils.tsubaki import CLOUDFRONT_URL from tsutils.user_interaction import get_user_confirmation, get_user_reaction @@ -1378,14 +1379,13 @@ async def leader_remove(self, ctx, monster_id: int): @commands.command(aliases=['currentinvade']) async def whichinvade(self, ctx): """Display which yinyangdra is currently invading for Mystics & Spectres event""" - pst = pytz.timezone("America/Los_Angeles") - curtime = datetime.datetime.now(pst) - if datetime.time(7) < curtime.time() < datetime.time(19): + curtime = datetime.datetime.now(NA_TIMEZONE) + if datetime.time(6) < curtime.time() < datetime.time(18): await ctx.send(self.c_commands['redinvadecurrent']) - totime = curtime.replace(hour=19, minute=0, second=0, microsecond=0) + totime = curtime.replace(hour=18, minute=0, second=0, microsecond=0) else: await ctx.send(self.c_commands['blueinvadecurrent']) - totime = curtime.replace(hour=7, minute=0, second=0, microsecond=0) + totime = curtime.replace(hour=6, minute=0, second=0, microsecond=0) if totime < curtime: totime += datetime.timedelta(1) await ctx.send(inline("Invade switches in: " + humanize_timedelta(timedelta=totime - curtime))) From 271c7884a89bc45c1eaf89abad1b85cc9fa7fa7c Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 27 Nov 2021 02:46:11 -0700 Subject: [PATCH 1008/1220] [CRUD] Update info.json (#1500) --- crud/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crud/info.json b/crud/info.json index 6a6457ae..e64d0a3f 100644 --- a/crud/info.json +++ b/crud/info.json @@ -20,7 +20,7 @@ }, "requirements": [ "tsutils>=5.0.0", - "pymysql", + "aiomysql", "aiofiles", "cryptography", "pygit2" From 29ed1c885559bbaf1b53af550d1088533ca7aa2c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 7 Dec 2021 13:57:15 -0600 Subject: [PATCH 1009/1220] Move ClosableEmbedMenu & SimpleTextMenu to tsutils (#1506) --- crowddata/crowddata.py | 17 ++++- crowddata/menu_map.py | 5 ++ crowddata/mixins/vem/__init__.py | 0 crowddata/mixins/vem/menu/closable_embed.py | 9 +++ crowddata/mixins/{ => vem}/vem.py | 51 +++++--------- crowddata/mixins/vem/view/__init__.py | 0 crowddata/mixins/vem/view/show_stats.py | 66 +++++++++++++++++++ dungeoncog/menu/dungeon.py | 2 +- dungeoncog/menu/simple.py | 2 +- dungeoncog/view/dungeon.py | 2 +- dungeoncog/view/simple.py | 2 +- padevents/padevents.py | 2 +- padglobal/menu/closable_embed.py | 2 +- padglobal/view/which.py | 2 +- padinfo/common/config.py | 15 ----- padinfo/common/discord.py | 12 ---- padinfo/menu/awakening_list.py | 2 +- padinfo/menu/button_info.py | 2 +- padinfo/menu/closable_embed.py | 37 ++--------- padinfo/menu/components/evo_scroll_mixin.py | 2 +- padinfo/menu/id.py | 4 +- padinfo/menu/leader_skill.py | 2 +- padinfo/menu/leader_skill_single.py | 2 +- padinfo/menu/menu_map.py | 6 +- padinfo/menu/monster_list.py | 2 +- padinfo/menu/na_diff.py | 6 +- padinfo/menu/series_scroll.py | 2 +- padinfo/menu/simple_text.py | 60 ----------------- padinfo/menu/transforminfo.py | 2 +- padinfo/padinfo.py | 8 +-- padinfo/view/awakening_help.py | 2 +- padinfo/view/awakening_list.py | 6 +- padinfo/view/button_info.py | 6 +- padinfo/view/closable_embed.py | 10 --- padinfo/view/components/view_state_base.py | 16 ----- padinfo/view/components/view_state_base_id.py | 2 +- padinfo/view/evos.py | 7 +- padinfo/view/experience_curve.py | 2 +- padinfo/view/id.py | 4 +- padinfo/view/id_traceback.py | 2 +- padinfo/view/leader_skill.py | 9 ++- padinfo/view/leader_skill_single.py | 13 ++-- padinfo/view/materials.py | 4 +- padinfo/view/monster_list/monster_list.py | 6 +- padinfo/view/monster_list/scroll.py | 2 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 4 +- padinfo/view/pic.py | 2 +- padinfo/view/series_scroll.py | 7 +- padinfo/view/simple_text.py | 48 -------------- padinfo/view/transforminfo.py | 6 +- 51 files changed, 191 insertions(+), 295 deletions(-) create mode 100644 crowddata/menu_map.py create mode 100644 crowddata/mixins/vem/__init__.py create mode 100644 crowddata/mixins/vem/menu/closable_embed.py rename crowddata/mixins/{ => vem}/vem.py (89%) create mode 100644 crowddata/mixins/vem/view/__init__.py create mode 100644 crowddata/mixins/vem/view/show_stats.py delete mode 100644 padinfo/common/config.py delete mode 100644 padinfo/common/discord.py delete mode 100644 padinfo/menu/simple_text.py delete mode 100644 padinfo/view/closable_embed.py delete mode 100644 padinfo/view/components/view_state_base.py delete mode 100644 padinfo/view/simple_text.py diff --git a/crowddata/crowddata.py b/crowddata/crowddata.py index 2730ad04..7f9b810e 100644 --- a/crowddata/crowddata.py +++ b/crowddata/crowddata.py @@ -4,7 +4,8 @@ from redbot.core import Config from redbot.core.bot import Red -from crowddata.mixins.vem import VEM +from crowddata.menu_map import crowddata_menu_map +from crowddata.mixins.vem.vem import VEM logger = logging.getLogger('red.misc-cogs.crowddata') @@ -12,6 +13,8 @@ class CrowdData(VEM): """Stores user preferences for users.""" + menu_map = crowddata_menu_map + def __init__(self, bot: Red, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -32,3 +35,15 @@ async def red_get_data_for_user(self, *, user_id): async def red_delete_data_for_user(self, *, requester, user_id): """Delete a user's personal data.""" await self.delete_mixin_user_data(requester, user_id) + + async def register_menu(self): + await self.bot.wait_until_ready() + menulistener = self.bot.get_cog("MenuListener") + if menulistener is None: + logger.warning("MenuListener is not loaded.") + return + await menulistener.register(self) + + async def get_menu_default_data(self, ims): + data = {} + return data diff --git a/crowddata/menu_map.py b/crowddata/menu_map.py new file mode 100644 index 00000000..643a66a4 --- /dev/null +++ b/crowddata/menu_map.py @@ -0,0 +1,5 @@ +from crowddata.mixins.vem.menu.closable_embed import ClosableEmbedMenu + +crowddata_menu_map = { + ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu +} diff --git a/crowddata/mixins/vem/__init__.py b/crowddata/mixins/vem/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/crowddata/mixins/vem/menu/closable_embed.py b/crowddata/mixins/vem/menu/closable_embed.py new file mode 100644 index 00000000..6cfb9b62 --- /dev/null +++ b/crowddata/mixins/vem/menu/closable_embed.py @@ -0,0 +1,9 @@ +from tsutils.menu.closable_embed_base import ClosableEmbedMenuBase + +from crowddata.mixins.vem.view.show_stats import ShowStatsView + + +class ClosableEmbedMenu(ClosableEmbedMenuBase): + view_types = { + ShowStatsView.VIEW_TYPE: ShowStatsView + } diff --git a/crowddata/mixins/vem.py b/crowddata/mixins/vem/vem.py similarity index 89% rename from crowddata/mixins/vem.py rename to crowddata/mixins/vem/vem.py index 18bf16cb..fe3a87cc 100644 --- a/crowddata/mixins/vem.py +++ b/crowddata/mixins/vem/vem.py @@ -4,6 +4,7 @@ import time +from discord import Color from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.text import LabeledText @@ -13,9 +14,12 @@ from redbot.core.bot import Red from tsutils.cog_mixins import CogMixin from tsutils.emoji import NO_EMOJI, char_to_emoji +from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.time import NA_TIMEZONE, NEW_DAY, get_last_time from tsutils.user_interaction import get_user_confirmation, get_user_reaction +from crowddata.mixins.vem.menu.closable_embed import ClosableEmbedMenu +from crowddata.mixins.vem.view.show_stats import ShowStatsViewProps, ShowStatsView from padinfo.view.components.monster.image import MonsterImage if TYPE_CHECKING: @@ -228,13 +232,15 @@ async def y_toggleskipconfirm(self, ctx): @adpem.command() async def showstats(self, ctx, *, query): dbcog: Any = ctx.bot.get_cog("DBCog") - pdicog: Any = ctx.bot.get_cog("PadInfo") - if dbcog is None or pdicog is None: + + original_author_id = ctx.message.author.id + + if dbcog is None: return await ctx.send("Required cogs not loaded. Please alert a bot owner.") await dbcog.wait_until_ready() query = '" "'.join(query.split()) - monsters = await dbcog.find_monsters(f'"inadpem" "{query}"') + monsters = await dbcog.find_monsters(f'inadpem {query}') if not monsters: return await ctx.send("No monsters matched that query.") @@ -245,27 +251,15 @@ async def showstats(self, ctx, *, query): you = [dbcog.get_monster(p) for uid, ps, ts in data if uid == ctx.author.id for p in ps] valid = [m for m in total if m in monsters] - fields = [] - for mon, c in Counter(valid).most_common(6 if len(valid) == 6 else 5): - fields.append(EmbedField(mon.name_en, Box( - LabeledText("Net", self.get_count(total, mon)), - LabeledText("Adj", self.get_count(adj, mon)), - LabeledText("You", self.get_count(you, mon)) - ), inline=True)) - if len(set(valid)) > 6: - fields.append(EmbedField("... & More", Box(f"+ {len(valid) - 5} more monsters"), inline=True)) - await ctx.send(embed=EmbedView( - EmbedMain( - title=f"AdPEM Data for query: {query}", - description=Box( - LabeledText("Net", self.get_count(total, *set(valid))), - LabeledText("Adj", self.get_count(adj, *set(valid))), - LabeledText("You", self.get_count(you, *set(valid))) - ) - ), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(Counter(valid).most_common(1)[0][0])), - embed_fields=fields - ).to_embed()) + if not valid: + return await ctx.send("No monsters matching that query have been pulled.") + + menu = ClosableEmbedMenu.menu() + props = ShowStatsViewProps(total, adj, you, valid) + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, + Color.default(), ShowStatsView.VIEW_TYPE, props) + + await menu.create(ctx, state) async def assert_ready(self, ctx, midnight: float) -> bool: pulls = await self.config.pulls() @@ -304,12 +298,3 @@ def has_good_data(self, uid: int, pulls): @staticmethod def midnight() -> float: return get_last_time(NEW_DAY, NA_TIMEZONE).astimezone(timezone.utc).timestamp() - - @staticmethod - def get_count(arr: List[int], *elements: int) -> str: - if not arr: - return "N/A" - count = sum(1 for m in arr if m in elements) - if count == 0: - return "0" - return f"{round(100*count/len(arr), 3)}%" diff --git a/crowddata/mixins/vem/view/__init__.py b/crowddata/mixins/vem/view/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/crowddata/mixins/vem/view/show_stats.py b/crowddata/mixins/vem/view/show_stats.py new file mode 100644 index 00000000..6c8d72e6 --- /dev/null +++ b/crowddata/mixins/vem/view/show_stats.py @@ -0,0 +1,66 @@ +from collections import Counter +from typing import TYPE_CHECKING, List + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedThumbnail, EmbedField +from discordmenu.embed.text import LabeledText +from discordmenu.embed.view import EmbedView +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.closable_embed import ClosableEmbedViewState + +from padinfo.view.components.monster.image import MonsterImage + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + +ORDINAL_WORDS = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth'] + + +class ShowStatsViewProps: + def __init__(self, total: List["MonsterModel"], adj: List["MonsterModel"], + you: List["MonsterModel"], valid: List["MonsterModel"]): + self.valid = valid + self.total = total + self.adj = adj + self.you = you + + +class ShowStatsView: + VIEW_TYPE = 'ShowStats' + + @staticmethod + def get_count(arr: List["MonsterModel"], *elements: "MonsterModel") -> str: + if not arr: + return "N/A" + count = sum(1 for m in arr if m in elements) + if count == 0: + return "0" + return f"{round(100*count/len(arr), 3)}%" + + @staticmethod + def embed(state: ClosableEmbedViewState, props: ShowStatsViewProps): + + fields = [] + for mon, c in Counter(props.valid).most_common(6 if len(props.valid) == 6 else 5): + fields.append(EmbedField(mon.name_en, Box( + LabeledText("Net", ShowStatsView.get_count(props.total, mon)), + LabeledText("Adj", ShowStatsView.get_count(props.adj, mon)), + LabeledText("You", ShowStatsView.get_count(props.you, mon)) + ), inline=True)) + if len(set(props.valid)) > 6: + fields.append(EmbedField("... & More", Box(f"+ {len(props.valid) - 5} more monsters"), inline=True)) + + return EmbedView( + EmbedMain( + title=f"AdPEM Data for query: {state.raw_query}", + description=Box( + LabeledText("Net", ShowStatsView.get_count(props.total, *set(props.valid))), + LabeledText("Adj", ShowStatsView.get_count(props.adj, *set(props.valid))), + LabeledText("You", ShowStatsView.get_count(props.you, *set(props.valid))) + ) + ), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(Counter(props.valid).most_common(1)[0][0])), + embed_fields=fields, + embed_footer=embed_footer_with_state(state) + ) + diff --git a/dungeoncog/menu/dungeon.py b/dungeoncog/menu/dungeon.py index 3e899518..0cbf18e8 100644 --- a/dungeoncog/menu/dungeon.py +++ b/dungeoncog/menu/dungeon.py @@ -3,7 +3,7 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from tsutils.menu.panes import MenuPanes, emoji_buttons +from tsutils.menu.components.panes import MenuPanes, emoji_buttons from dungeoncog.view.dungeon import DungeonViewState, DungeonView diff --git a/dungeoncog/menu/simple.py b/dungeoncog/menu/simple.py index ba714224..cc5bb6eb 100644 --- a/dungeoncog/menu/simple.py +++ b/dungeoncog/menu/simple.py @@ -3,7 +3,7 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from tsutils.menu.panes import MenuPanes, emoji_buttons +from tsutils.menu.components.panes import MenuPanes, emoji_buttons from dungeoncog.view.simple import SimpleViewState, SimpleView diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index eb537bde..0f73f70a 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -7,7 +7,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView from discordmenu.embed.view_state import ViewState -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skills_pb2 import MonsterBehavior diff --git a/dungeoncog/view/simple.py b/dungeoncog/view/simple.py index 026c1315..9b1c3679 100644 --- a/dungeoncog/view/simple.py +++ b/dungeoncog/view/simple.py @@ -2,7 +2,7 @@ from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.view import EmbedView from discordmenu.embed.view_state import ViewState -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state class SimpleViewState(ViewState): diff --git a/padevents/padevents.py b/padevents/padevents.py index 2f3d739d..aaf233a2 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -19,8 +19,8 @@ from tsutils.helper_functions import conditional_iterator, repeating_timer from padevents.autoevent_mixin import AutoEvent -from padevents.events import Event, EventList, SERVER_TIMEZONES from padevents.enums import DungeonType, EventLength +from padevents.events import Event, EventList, SERVER_TIMEZONES logger = logging.getLogger('red.padbot-cogs.padevents') diff --git a/padglobal/menu/closable_embed.py b/padglobal/menu/closable_embed.py index e36cb991..34b602b7 100644 --- a/padglobal/menu/closable_embed.py +++ b/padglobal/menu/closable_embed.py @@ -1,7 +1,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu +from tsutils.menu.components.panes import MenuPanes -from tsutils.menu.panes import MenuPanes from padglobal.view.closable_embed import ClosableEmbedViewState from padglobal.view.which import WhichView diff --git a/padglobal/view/which.py b/padglobal/view/which.py index bba0967f..56e30be7 100644 --- a/padglobal/view/which.py +++ b/padglobal/view/which.py @@ -3,7 +3,7 @@ from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state STAR = '* ' BULLET_EMOJIS = ['db', 'dp', 'dg'] diff --git a/padinfo/common/config.py b/padinfo/common/config.py deleted file mode 100644 index 6db5416b..00000000 --- a/padinfo/common/config.py +++ /dev/null @@ -1,15 +0,0 @@ -from padinfo.common.discord import user_color_to_discord_color - - -class UserConfig: - def __init__(self, color): - self.color = color - - -class BotConfig: - @staticmethod - async def get_user(config, user_id): - user_config = config.user_from_id(user_id) - user_color = await user_config.color() - color = user_color_to_discord_color(user_color) - return UserConfig(color) diff --git a/padinfo/common/discord.py b/padinfo/common/discord.py deleted file mode 100644 index c26d5459..00000000 --- a/padinfo/common/discord.py +++ /dev/null @@ -1,12 +0,0 @@ -import random - -from discord import Color - - -def user_color_to_discord_color(color): - if color is None: - return Color.default() - elif color == "random": - return Color(random.randint(0x000000, 0xffffff)) - else: - return Color(color) diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index ffa6663c..9786db48 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -4,7 +4,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu from tsutils.enums import ChildMenuType -from tsutils.menu.panes import emoji_buttons, MenuPanes +from tsutils.menu.components.panes import emoji_buttons, MenuPanes from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListView, AwakeningListSortTypes diff --git a/padinfo/menu/button_info.py b/padinfo/menu/button_info.py index 32301a57..c085e291 100644 --- a/padinfo/menu/button_info.py +++ b/padinfo/menu/button_info.py @@ -4,7 +4,7 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu from tsutils.emoji import char_to_emoji -from tsutils.menu.panes import emoji_buttons, MenuPanes +from tsutils.menu.components.panes import emoji_buttons, MenuPanes from padinfo.menu.components.evo_scroll_mixin import EvoScrollMenu from padinfo.view.button_info import ButtonInfoOptions, ButtonInfoToggles, ButtonInfoViewState, ButtonInfoView diff --git a/padinfo/menu/closable_embed.py b/padinfo/menu/closable_embed.py index a745d289..aebc818d 100644 --- a/padinfo/menu/closable_embed.py +++ b/padinfo/menu/closable_embed.py @@ -1,36 +1,13 @@ -from discordmenu.embed.control import EmbedControl -from discordmenu.embed.menu import EmbedMenu +from tsutils.menu.closable_embed_base import ClosableEmbedMenuBase -from tsutils.menu.panes import MenuPanes from padinfo.view.awakening_help import AwakeningHelpView -from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.experience_curve import ExperienceCurveView from padinfo.view.id_traceback import IdTracebackView -view_types = { - AwakeningHelpView.VIEW_TYPE: AwakeningHelpView, - IdTracebackView.VIEW_TYPE: IdTracebackView, - ExperienceCurveView.VIEW_TYPE: ExperienceCurveView, -} - -class ClosableEmbedMenu: - MENU_TYPE = 'ClosableEmbedMenu' - message = None - - @staticmethod - def menu(): - embed = EmbedMenu({}, ClosableEmbedMenu.message_control) - return embed - - @staticmethod - def message_control(state: ClosableEmbedViewState): - view = view_types[state.view_type] - return EmbedControl( - [view.embed(state, state.props)], - [] - ) - - -class ClosableEmbedMenuPanes(MenuPanes): - pass +class ClosableEmbedMenu(ClosableEmbedMenuBase): + view_types = { + AwakeningHelpView.VIEW_TYPE: AwakeningHelpView, + IdTracebackView.VIEW_TYPE: IdTracebackView, + ExperienceCurveView.VIEW_TYPE: ExperienceCurveView, + } diff --git a/padinfo/menu/components/evo_scroll_mixin.py b/padinfo/menu/components/evo_scroll_mixin.py index ede5c370..8e6f06c0 100644 --- a/padinfo/menu/components/evo_scroll_mixin.py +++ b/padinfo/menu/components/evo_scroll_mixin.py @@ -2,7 +2,7 @@ from typing import Optional from discord import Message -from tsutils.menu.panes import MenuPanes +from tsutils.menu.components.panes import MenuPanes from padinfo.view.components.evo_scroll_mixin import EvoScrollViewState diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index 3c9e1797..d5ade24e 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -4,10 +4,10 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils.emoji import char_to_emoji from tsutils.enums import ChildMenuType -from tsutils.menu.panes import MenuPanes +from tsutils.menu.components.panes import MenuPanes +from tsutils.menu.simple_text import SimpleTextMenu from padinfo.menu.components.evo_scroll_mixin import EvoScrollMenu -from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.components.view_state_base_id import ViewStateBaseId from padinfo.view.evos import EvosView, EvosViewState from padinfo.view.id import IdView, IdViewState diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 250bf46b..7d7b6531 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -4,7 +4,7 @@ from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils.emoji import char_to_emoji -from tsutils.menu.panes import MenuPanes, emoji_buttons +from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill import LeaderSkillView, LeaderSkillViewState diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index 09defeb8..fff3a651 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -2,7 +2,7 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl -from tsutils.menu.panes import MenuPanes, emoji_buttons +from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.leader_skill_single import LeaderSkillSingleView, LeaderSkillSingleViewState diff --git a/padinfo/menu/menu_map.py b/padinfo/menu/menu_map.py index 95de725b..b97953b1 100644 --- a/padinfo/menu/menu_map.py +++ b/padinfo/menu/menu_map.py @@ -1,6 +1,9 @@ +from tsutils.menu.closable_embed_base import ClosableEmbedMenuPanes +from tsutils.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes + from padinfo.menu.awakening_list import AwakeningListMenu, AwakeningListMenuPanes from padinfo.menu.button_info import ButtonInfoMenu, ButtonInfoMenuPanes -from padinfo.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes +from padinfo.menu.closable_embed import ClosableEmbedMenu from padinfo.menu.id import IdMenu, IdMenuPanes from padinfo.menu.leader_skill import LeaderSkillMenu, LeaderSkillMenuPanes from padinfo.menu.leader_skill_single import LeaderSkillSingleMenu, LeaderSkillSingleMenuPanes @@ -8,7 +11,6 @@ from padinfo.menu.na_diff import NaDiffMenu, NaDiffMenuPanes from padinfo.menu.scroll import ScrollMenuPanes from padinfo.menu.series_scroll import SeriesScrollMenu, SeriesScrollMenuPanes -from padinfo.menu.simple_text import SimpleTextMenu, SimpleTextMenuPanes from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index b74ee53c..b09ea329 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -3,7 +3,7 @@ from discord import Message from discordmenu.embed.menu import EmbedControl, EmbedMenu from tsutils.emoji import char_to_emoji -from tsutils.menu.panes import MenuPanes, emoji_buttons +from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.menu.id import IdMenu, IdMenuEmoji from padinfo.menu.na_diff import NaDiffEmoji, NaDiffMenu diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index b7321a25..1285257c 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -4,11 +4,11 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu from tsutils.enums import Server, ChildMenuType -from tsutils.menu.panes import MenuPanes +from tsutils.menu.components.panes import MenuPanes +from tsutils.menu.simple_text import SimpleTextMenu +from tsutils.menu.view.simple_text import SimpleTextViewState, SimpleTextView -from padinfo.menu.simple_text import SimpleTextMenu from padinfo.view.id import IdViewState, IdView -from padinfo.view.simple_text import SimpleTextViewState, SimpleTextView class NaDiffEmoji: diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 274cba25..84663144 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -3,7 +3,7 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils.emoji import char_to_emoji -from tsutils.menu.panes import MenuPanes, emoji_buttons +from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji from padinfo.view.id import IdView diff --git a/padinfo/menu/simple_text.py b/padinfo/menu/simple_text.py deleted file mode 100644 index 6da5d41d..00000000 --- a/padinfo/menu/simple_text.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import Optional - -from discord import Message -from discordmenu.embed.control import EmbedControl -from discordmenu.embed.menu import EmbedMenu - -from tsutils.menu.panes import MenuPanes, emoji_buttons -from padinfo.view.simple_text import SimpleTextView, SimpleTextViewState - - -class SimpleTextNames: - home = 'home' - - -class SimpleTextMenu: - MENU_TYPE = 'SimpleTextMenu' - message = None - - @staticmethod - def menu(): - embed = EmbedMenu(SimpleTextMenuPanes.transitions(), SimpleTextMenu.message_control, - delete_func=SimpleTextMenu.respond_with_delete) - return embed - - @staticmethod - async def respond_with_message(message: Optional[Message], ims, **data): - dbcog = data.get('dbcog') - user_config = data.get('user_config') - view_state = await SimpleTextViewState.deserialize(dbcog, user_config, ims) - control = SimpleTextMenu.message_control(view_state) - return control - - @staticmethod - async def respond_with_delete(message: Optional[Message], ims, **data): - return await message.delete() - - @staticmethod - def message_control(state: SimpleTextViewState): - if state is None: - return None - reaction_list = state.reaction_list - return EmbedControl( - [SimpleTextView.embed(state)], - reaction_list - ) - - -class SimpleTextEmoji: - home = emoji_buttons['home'] - - -class SimpleTextMenuPanes(MenuPanes): - INITIAL_EMOJI = SimpleTextEmoji.home - DATA = { - SimpleTextEmoji.home: (SimpleTextMenu.respond_with_message, SimpleTextNames.home), - - } - HIDDEN_EMOJIS = [ - SimpleTextNames.home, - ] diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 2a98c009..72953f5b 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -3,8 +3,8 @@ from discord import Message from discordmenu.embed.menu import EmbedMenu, EmbedControl from tsutils.emoji import char_to_emoji +from tsutils.menu.components.panes import MenuPanes, emoji_buttons -from tsutils.menu.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState from padinfo.view.transforminfo import TransformInfoView, TransformInfoViewState diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 674fdb17..e02a2f30 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -19,10 +19,13 @@ from tsutils.enums import AltEvoSort, CardLevelModifier, CardModeModifier, CardPlusModifier, EvoGrouping, LsMultiplier, \ Server from tsutils.json_utils import safe_read_json +from tsutils.menu.components.config import BotConfig +from tsutils.menu.simple_text import SimpleTextMenu +from tsutils.menu.view.closable_embed import ClosableEmbedViewState +from tsutils.menu.view.simple_text import SimpleTextViewState from tsutils.query_settings import QuerySettings from tsutils.user_interaction import get_user_confirmation, send_cancellation_message -from padinfo.common.config import BotConfig from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ get_attribute_emoji_by_monster, get_awakening_emoji, get_type_emoji from padinfo.core.button_info import button_info @@ -40,13 +43,11 @@ from padinfo.menu.na_diff import NaDiffMenu, NaDiffMenuPanes from padinfo.menu.scroll import ScrollMenuPanes from padinfo.menu.series_scroll import SeriesScrollEmoji, SeriesScrollMenu, SeriesScrollMenuPanes -from padinfo.menu.simple_text import SimpleTextMenu from padinfo.menu.transforminfo import TransformInfoMenu, TransformInfoMenuPanes from padinfo.reaction_list import get_id_menu_initial_reaction_list from padinfo.view.awakening_help import AwakeningHelpView, AwakeningHelpViewProps from padinfo.view.awakening_list import AwakeningListSortTypes, AwakeningListViewState from padinfo.view.button_info import ButtonInfoToggles, ButtonInfoViewState -from padinfo.view.closable_embed import ClosableEmbedViewState from padinfo.view.common import invalid_monster_text from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.evos import EvosViewState @@ -67,7 +68,6 @@ from padinfo.view.pantheon import PantheonViewState from padinfo.view.pic import PicViewState from padinfo.view.series_scroll import SeriesScrollViewState -from padinfo.view.simple_text import SimpleTextViewState from padinfo.view.transforminfo import TransformInfoViewState if TYPE_CHECKING: diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 83674333..47dd8afe 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedAuthor from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state from padinfo.common.emoji_map import get_awakening_emoji from padinfo.common.external_links import puzzledragonx diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index f43cad4b..f0162cd7 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -3,11 +3,11 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase -from padinfo.common.config import UserConfig from padinfo.view.common import get_awoken_skill_description -from padinfo.view.components.view_state_base import ViewStateBase class AwakeningListSortTypes: diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 3302bbd5..a84c56f0 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -4,17 +4,17 @@ from discordmenu.embed.components import EmbedAuthor, EmbedField, EmbedMain from discordmenu.embed.text import Text, BlockText from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL, ButtonInfoStatSet, \ ButtonInfoResult from padinfo.view.components.evo_scroll_mixin import EvoScrollView, EvoScrollViewState, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/closable_embed.py b/padinfo/view/closable_embed.py deleted file mode 100644 index 1c2f4ab7..00000000 --- a/padinfo/view/closable_embed.py +++ /dev/null @@ -1,10 +0,0 @@ -from padinfo.view.components.view_state_base import ViewStateBase - - -class ClosableEmbedViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, - color, view_type, props): - super().__init__(original_author_id, menu_type, raw_query) - self.color = color - self.view_type = view_type - self.props = props diff --git a/padinfo/view/components/view_state_base.py b/padinfo/view/components/view_state_base.py deleted file mode 100644 index f6971883..00000000 --- a/padinfo/view/components/view_state_base.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import List - -from discordmenu.embed.view_state import ViewState - - -class ViewStateBase(ViewState): - def __init__(self, original_author_id, menu_type, raw_query, extra_state=None, reaction_list: List = None): - self.reaction_list = reaction_list - super().__init__(original_author_id=original_author_id, menu_type=menu_type, raw_query=raw_query, - extra_state=extra_state) - - def serialize(self): - ret = super().serialize() - if self.reaction_list is not None: - ret['reaction_list'] = self.reaction_list - return ret diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index f784881b..6b8b0980 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -2,9 +2,9 @@ from discordmenu.embed.view_state import ViewState from tsutils.enums import AltEvoSort +from tsutils.menu.components.config import UserConfig from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollViewState, MonsterEvolution diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 2e9466ee..008236ed 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -3,18 +3,17 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state - +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims +from padinfo.view.components.evo_scroll_mixin import MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId -from padinfo.view.components.evo_scroll_mixin import MonsterEvolution if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index 589dd184..79511e72 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -7,7 +7,7 @@ from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core.utils.chat_formatting import humanize_number -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader diff --git a/padinfo/view/id.py b/padinfo/view/id.py index d1540543..cde0464e 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -5,10 +5,10 @@ from discordmenu.embed.text import BoldText, LabeledText, Text from discordmenu.embed.view import EmbedView from tsutils.enums import CardLevelModifier, CardModeModifier, CardPlusModifier, Server -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.view.common import get_monster_from_ims, invalid_monster_text diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index 15f9aa57..1f41d18c 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.text import LabeledText from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state from padinfo.common.emoji_map import get_attribute_emoji_by_monster from padinfo.view.components.monster.header import MonsterHeader diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 61237c21..deecf6b5 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -4,15 +4,14 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state - +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig -from padinfo.core.leader_skills import ls_multiplier_text from padinfo.core.leader_skills import leaderskill_query +from padinfo.core.leader_skills import ls_multiplier_text from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: pass diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index d93452ab..b09b58bd 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -4,15 +4,14 @@ from discordmenu.embed.components import EmbedMain from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state - +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.core.leader_skills import ls_single_multiplier_text -from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.view_state_base import ViewStateBase from padinfo.view.common import get_monster_from_ims +from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: pass @@ -40,7 +39,9 @@ async def deserialize(dbcog, user_config: UserConfig, ims: dict): query_settings = QuerySettings.deserialize(ims['query_settings']) mon = await get_monster_from_ims(dbcog, ims) - return LeaderSkillSingleViewState(original_author_id, menu_type, raw_query, query_settings, user_config.color, mon) + return LeaderSkillSingleViewState(original_author_id, menu_type, + raw_query, query_settings, user_config.color, + mon) class LeaderSkillSingleView: diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index ec6b0172..a80c849c 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -4,10 +4,10 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.common.external_links import ilmina_skill from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 07ee61dc..5498af60 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -5,12 +5,12 @@ from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils.emoji import char_to_emoji -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/monster_list/scroll.py b/padinfo/view/monster_list/scroll.py index e974a332..62c8d015 100644 --- a/padinfo/view/monster_list/scroll.py +++ b/padinfo/view/monster_list/scroll.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, List, Optional +from tsutils.menu.components.config import UserConfig from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.view.monster_list.monster_list import MonsterListViewState, MonsterListView if TYPE_CHECKING: diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 6aa867da..64bdb4cf 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -6,7 +6,7 @@ from discordmenu.embed.text import LabeledText, Text from discordmenu.embed.view import EmbedView from redbot.core.utils.chat_formatting import box -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 63171dd9..bbf7a34a 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -3,10 +3,10 @@ from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ get_rarity_emoji from padinfo.common.external_links import puzzledragonx diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 12557ab4..8f17135c 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -2,7 +2,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage from discordmenu.embed.text import LinkedText, Text from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.footers import embed_footer_with_state from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 8d6a99de..af1b4df2 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -5,13 +5,12 @@ from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils.emoji import char_to_emoji -from tsutils.menu.footers import embed_footer_with_state - +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/simple_text.py b/padinfo/view/simple_text.py deleted file mode 100644 index 686dc923..00000000 --- a/padinfo/view/simple_text.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import List - -from discordmenu.embed.components import EmbedMain -from discordmenu.embed.view import EmbedView -from tsutils.menu.footers import embed_footer_with_state - -from padinfo.common.config import UserConfig -from padinfo.view.components.view_state_base import ViewStateBase - - -class SimpleTextViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, - color, message, - reaction_list: List[str] = None, - extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, - reaction_list=reaction_list, extra_state=extra_state) - self.message = message - self.color = color - - def serialize(self): - ret = super().serialize() - ret.update({ - 'message': self.message, - }) - return ret - - @classmethod - async def deserialize(cls, _dbcog, user_config: UserConfig, ims: dict): - original_author_id = ims['original_author_id'] - menu_type = ims['menu_type'] - raw_query = ims.get('raw_query') - return cls(original_author_id, menu_type, raw_query, user_config.color, ims.get('message'), - reaction_list=ims.get('reaction_list'), extra_state=ims) - - -class SimpleTextView: - VIEW_TYPE = 'SimpleText' - - @staticmethod - def embed(state: SimpleTextViewState): - return EmbedView( - EmbedMain( - color=state.color, - description=state.message - ), - embed_footer=embed_footer_with_state(state), - ) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 07d2ba8e..c55aa80c 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -5,16 +5,16 @@ from discordmenu.embed.text import BoldText, LabeledText, Text from discordmenu.embed.view import EmbedView from tsutils.enums import LsMultiplier -from tsutils.menu.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from padinfo.common.config import UserConfig from padinfo.common.emoji_map import get_emoji from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.monster.image import MonsterImage -from padinfo.view.components.view_state_base import ViewStateBase if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel From 06e701fc6536b253137713c2688265e11b8ebb9a Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Wed, 8 Dec 2021 20:24:18 -0500 Subject: [PATCH 1010/1220] Add untradable message to monidlistener (#1511) --- monidlistener/monidlistener.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 5ddaa08c..2a0af782 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -13,7 +13,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=10100779) self.config.register_channel(enabled=False) - + async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" data = "No data is stored for user with ID {}.\n".format(user_id) @@ -30,7 +30,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def on_message(self, message): channel = message.channel content = message.content - content = re.sub(r'297','',content) + content = re.sub(r'297', '', content) if await self.config.channel(channel).enabled(): if message.guild is None: # dms return @@ -50,12 +50,12 @@ async def on_message(self, message): matches = re.findall(r'\b\d{3,4}\b', content) ret = "" for i in matches: - if(i=="100"): # skip when people say "is 100 mp or over" ~~and ryan's posts~~ + if (i == "100"): # skip when people say "is 100 mp or over" ~~and ryan's posts~~ continue m = await dbcog.find_monster(i, message.author.id) if not m: # monster not found continue - ret += "[{}] {}\n".format(i, m.name_en) + ret += "[{}] {}{}\n".format(i, m.name_en, " (untradable)" if m.sell_mp >= 100 else "") if ret != "": await channel.send(ret) From dd0c05d50f891aa42d93aa2639f855d9f3354bda Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:15:25 -0500 Subject: [PATCH 1011/1220] Add eHP to leader skill display (#1512) --- padinfo/core/leader_skills.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index ede2155a..c14705b3 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -57,7 +57,9 @@ def _format_number(val): def _format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): - resist = ' Resist {}%'.format(_format_number(100 * resist)) if resist else '' + resist_val = ' Resist {}%'.format(_format_number(100 * resist)) if resist else '' + + ehp = ' [{}x eHP]'.format(_format_number(hp / (1 - resist))) if resist else '' combos = '+{}c'.format(combo) if combo else '' true_damage = '{}'.format(_humanize_number(fua, 2)) if fua else '' @@ -66,7 +68,7 @@ def _format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): joined = ' '.join((a for a in [combos, true_damage, any_fua] if a)) extras = f"[{joined}]" if joined else '' - return f"[{_format_number(hp)}/{_format_number(atk)}/{_format_number(rcv)}{resist}] {extras}" + return f"[{_format_number(hp)}/{_format_number(atk)}/{_format_number(rcv)}{resist_val}]{ehp} {extras}" async def leaderskill_query(dbcog, raw_query, author_id): From c1d66bfdaa66d764c23e0476452faf5def1cb56f Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 17 Dec 2021 15:41:31 -0700 Subject: [PATCH 1012/1220] Add new pipeline commands (#1513) --- padguidedb/padguidedb.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 695ff402..2c5fa588 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -31,7 +31,7 @@ def __init__(self, bot, *args, **kwargs): self.settings = PadGuideDbSettings("padguidedb") self.queue_size = 0 - self.full_etl_lock = asyncio.Lock() + self.pipeline_lock = asyncio.Lock() self.dungeon_lock = asyncio.Lock() self.extract_images_lock = asyncio.Lock() self.dungeon_load_lock = asyncio.Lock() @@ -223,28 +223,38 @@ async def pipeline(self, context): @pipeline.command() @is_padguidedb_admin() - async def fulletl(self, ctx): + async def fulletl(self, ctx, server='COMBINED'): """Runs a job which downloads pad data, and updates the padguide database.""" - if self.full_etl_lock.locked(): - await ctx.send(inline('Full ETL already running')) + await self.runetl(ctx, '--server='+server) + + @pipeline.command() + @is_padguidedb_admin() + async def savechanges(self, ctx): + """Runs a job which saves changes made by CRUD.""" + await self.runetl(ctx, '--skipdownload', '--processors=Monsters') + + async def runetl(self, ctx, *args): + if self.pipeline_lock.locked(): + await ctx.send(inline('Manual pipeline already running')) return - async with self.full_etl_lock: - await ctx.send(inline('Running full ETL pipeline: this could take a while')) + async with self.pipeline_lock: + await ctx.send(inline('Running pipeline: this could take a while')) process = await asyncio.create_subprocess_exec( 'bash', self.settings.fullETLFile(), + *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() - if b"Pipeline completed successfully!" not in stderr: - logger.error("Full ETL Error:\n" + stderr.decode()) - await ctx.send(inline('Full ETL failed')) - return - await ctx.send(inline('Full ETL finished')) + if b"Pipeline completed successfully!" not in stderr: + logger.error("Pipeline Error:\n" + stderr.decode()) + await ctx.send(inline('Pipeline failed')) + else: + await ctx.send(inline('Pipeline finished')) @pipeline.command() @is_padguidedb_admin() From 68a0fc3519aa9ac27b51dddd22e83a35c72355b2 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 23 Dec 2021 07:36:15 -0800 Subject: [PATCH 1013/1220] Set pulls to 5 (#1515) --- crowddata/mixins/vem/vem.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crowddata/mixins/vem/vem.py b/crowddata/mixins/vem/vem.py index fe3a87cc..a291f4b9 100644 --- a/crowddata/mixins/vem/vem.py +++ b/crowddata/mixins/vem/vem.py @@ -72,11 +72,11 @@ async def report_at_time(self, ctx, pulls, offset): if not await self.config.user(ctx.author).opted_in(): return await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") - if len(pulls := pulls.split(',')) != 4: - return await ctx.send(f"Please supply all 4 pulls with commas in between them. You can use names or" + if len(pulls := pulls.split(',')) != 5: + return await ctx.send(f"Please supply all 5 pulls with commas in between them. You can use names or" f" numbers.\n\t" - f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013`\n\t" - f"Valid input: `{ctx.prefix}adpem report Enoch, Facet, D Globe, B Super King`") + f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013, 618`\n\t" + f"Valid input: `{ctx.prefix}adpem report Enoch, Facet, D Globe, B Super King, Facet`") dbcog: Any = ctx.bot.get_cog("DBCog") pdicog: Any = ctx.bot.get_cog("PadInfo") From 954be28d6b2dcd67cf51381a96a85670f7eaa9fd Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 25 Dec 2021 23:03:16 -0800 Subject: [PATCH 1014/1220] Shout if using VEM and not opted in (#1516) --- crowddata/mixins/vem/vem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crowddata/mixins/vem/vem.py b/crowddata/mixins/vem/vem.py index a291f4b9..2b6e1fa3 100644 --- a/crowddata/mixins/vem/vem.py +++ b/crowddata/mixins/vem/vem.py @@ -28,7 +28,11 @@ def opted_in(is_opted): async def check(ctx): - return is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in() + if is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in(): + return True + if is_opted and len(ctx.invoked_parents) != 1: + await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") + return False return commands.check(check) @@ -69,9 +73,6 @@ async def reportyesterday(self, ctx, *, pulls): await self.report_at_time(ctx, pulls, 24 * 60 * 60) async def report_at_time(self, ctx, pulls, offset): - if not await self.config.user(ctx.author).opted_in(): - return await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") - if len(pulls := pulls.split(',')) != 5: return await ctx.send(f"Please supply all 5 pulls with commas in between them. You can use names or" f" numbers.\n\t" From ba52ff78590b741f018e5a17ce4ffffaad5c470c Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 29 Dec 2021 09:35:51 -0800 Subject: [PATCH 1015/1220] Rename VEM to AdPEM (#1517) --- crowddata/crowddata.py | 4 +-- crowddata/menu_map.py | 2 +- crowddata/mixins/{vem => adpem}/__init__.py | 0 .../mixins/{vem/vem.py => adpem/adpem.py} | 32 ++++++------------- .../{vem => adpem}/menu/closable_embed.py | 2 +- .../mixins/{vem => adpem}/view/__init__.py | 0 .../mixins/{vem => adpem}/view/show_stats.py | 0 dbcog/dbcog.py | 7 ++-- dbcog/monster_graph.py | 8 ++--- dbcog/monster_index.py | 4 +-- dbcog/token_mappings.py | 8 ++--- 11 files changed, 28 insertions(+), 39 deletions(-) rename crowddata/mixins/{vem => adpem}/__init__.py (100%) rename crowddata/mixins/{vem/vem.py => adpem/adpem.py} (91%) rename crowddata/mixins/{vem => adpem}/menu/closable_embed.py (74%) rename crowddata/mixins/{vem => adpem}/view/__init__.py (100%) rename crowddata/mixins/{vem => adpem}/view/show_stats.py (100%) diff --git a/crowddata/crowddata.py b/crowddata/crowddata.py index 7f9b810e..7453f7a0 100644 --- a/crowddata/crowddata.py +++ b/crowddata/crowddata.py @@ -5,12 +5,12 @@ from redbot.core.bot import Red from crowddata.menu_map import crowddata_menu_map -from crowddata.mixins.vem.vem import VEM +from crowddata.mixins.adpem.adpem import AdPEMStats logger = logging.getLogger('red.misc-cogs.crowddata') -class CrowdData(VEM): +class CrowdData(AdPEMStats): """Stores user preferences for users.""" menu_map = crowddata_menu_map diff --git a/crowddata/menu_map.py b/crowddata/menu_map.py index 643a66a4..7e77dfab 100644 --- a/crowddata/menu_map.py +++ b/crowddata/menu_map.py @@ -1,4 +1,4 @@ -from crowddata.mixins.vem.menu.closable_embed import ClosableEmbedMenu +from crowddata.mixins.adpem.menu.closable_embed import ClosableEmbedMenu crowddata_menu_map = { ClosableEmbedMenu.MENU_TYPE: ClosableEmbedMenu diff --git a/crowddata/mixins/vem/__init__.py b/crowddata/mixins/adpem/__init__.py similarity index 100% rename from crowddata/mixins/vem/__init__.py rename to crowddata/mixins/adpem/__init__.py diff --git a/crowddata/mixins/vem/vem.py b/crowddata/mixins/adpem/adpem.py similarity index 91% rename from crowddata/mixins/vem/vem.py rename to crowddata/mixins/adpem/adpem.py index 2b6e1fa3..5e4f00bd 100644 --- a/crowddata/mixins/vem/vem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -1,14 +1,8 @@ -from collections import Counter from datetime import timezone -from typing import Any, List, TYPE_CHECKING +from typing import Any import time - from discord import Color -from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail -from discordmenu.embed.text import LabeledText -from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import Config, commands from redbot.core.bot import Red @@ -18,12 +12,8 @@ from tsutils.time import NA_TIMEZONE, NEW_DAY, get_last_time from tsutils.user_interaction import get_user_confirmation, get_user_reaction -from crowddata.mixins.vem.menu.closable_embed import ClosableEmbedMenu -from crowddata.mixins.vem.view.show_stats import ShowStatsViewProps, ShowStatsView -from padinfo.view.components.monster.image import MonsterImage - -if TYPE_CHECKING: - from dbcog.models.monster_model import MonsterModel +from crowddata.mixins.adpem.menu.closable_embed import ClosableEmbedMenu +from crowddata.mixins.adpem.view.show_stats import ShowStatsView, ShowStatsViewProps def opted_in(is_opted): @@ -37,7 +27,7 @@ async def check(ctx): return commands.check(check) -class VEM(CogMixin): +class AdPEMStats(CogMixin): config: Config bot: Red @@ -88,27 +78,23 @@ async def report_at_time(self, ctx, pulls, offset): if not await self.assert_ready(ctx, self.midnight() - offset): return - monsters = [await dbcog.find_monster('vem ' + pull.strip()) + monsters = [await dbcog.find_monster('inadpem ' + pull.strip()) for pull in pulls] + if not all(monsters): unknown = '\n\t'.join(s for s, m in zip(pulls, monsters) if m is None) return await ctx.send(f"Not all monsters were valid. The following could not be processed:\n\t{unknown}") - def get_vem_evo(mon: "MonsterModel") -> "MonsterModel": - return {m for m in dbcog.database.graph.get_alt_monsters(mon) if m.in_vem}.pop() - - correct_evos = [get_vem_evo(m) for m in monsters] - check = '\n\t'.join(pdicog.monster_header.fmt_id_header(m, use_emoji=True).to_markdown() - for m in correct_evos) + for m in monsters) confirmation = await get_user_confirmation(ctx, f"Are these monsters correct?\n\t{check}", - timeout=30, force_delete=False, show_feedback=True) + timeout=30, force_delete=False, show_feedback=True) if not confirmation: if confirmation is None: await ctx.send(ctx.author.mention + " Submission timed out. Please resubmit and verify.") return - await self.add_pull(ctx, [m.monster_id for m in correct_evos], time.time() - offset) + await self.add_pull(ctx, [m.monster_id for m in monsters], time.time() - offset) @report.command() async def skip(self, ctx): diff --git a/crowddata/mixins/vem/menu/closable_embed.py b/crowddata/mixins/adpem/menu/closable_embed.py similarity index 74% rename from crowddata/mixins/vem/menu/closable_embed.py rename to crowddata/mixins/adpem/menu/closable_embed.py index 6cfb9b62..cb654734 100644 --- a/crowddata/mixins/vem/menu/closable_embed.py +++ b/crowddata/mixins/adpem/menu/closable_embed.py @@ -1,6 +1,6 @@ from tsutils.menu.closable_embed_base import ClosableEmbedMenuBase -from crowddata.mixins.vem.view.show_stats import ShowStatsView +from crowddata.mixins.adpem.view.show_stats import ShowStatsView class ClosableEmbedMenu(ClosableEmbedMenuBase): diff --git a/crowddata/mixins/vem/view/__init__.py b/crowddata/mixins/adpem/view/__init__.py similarity index 100% rename from crowddata/mixins/vem/view/__init__.py rename to crowddata/mixins/adpem/view/__init__.py diff --git a/crowddata/mixins/vem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py similarity index 100% rename from crowddata/mixins/vem/view/show_stats.py rename to crowddata/mixins/adpem/view/show_stats.py diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index a4e0ab47..3e09d902 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -14,7 +14,7 @@ import time from io import BytesIO -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union import discord from redbot.core import Config, checks, commands, data_manager @@ -27,7 +27,7 @@ from . import token_mappings from .database_loader import load_database -from .find_monster import FindMonster +from .find_monster import FindMonster, MatchMap from .idtest_mixin import IdTest from .models.enum_types import DEFAULT_SERVER, SERVERS from .models.monster_model import MonsterModel @@ -325,6 +325,9 @@ async def find_monster(self, query: str, author_id: int = 0) -> Optional[Monster async def find_monsters(self, query: str, author_id: int = 0) -> List[MonsterModel]: return await FindMonster(self, await self.get_fm_flags(author_id)).find_monsters(query) + async def _find_monster_debug(self, query: str) -> Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel], int]: + return await FindMonster(self, self.fm_flags_default).find_monster_debug(query) + @staticmethod def get_aliased_attribute(monster: MonsterModel, alias: str) -> Union[Dict[str, Any], Any]: if alias not in MONSTER_ATTR_ALIAS_TO_ATTR_MAP: diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 4980619e..7846963d 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -160,10 +160,10 @@ def build_graph(self, server: Server) -> MultiDiGraph: awakening_model = AwakeningModel(awoken_skill_model=awoken_skill_model, **a) mtoawo[a.monster_id].append(awakening_model) - mtoegg = defaultdict(lambda: {'pem': False, 'rem': False, 'vem': False}) + mtoegg = defaultdict(lambda: {'pem': False, 'rem': False, 'adpem': False}) for e in ems: data = json.loads(e.contents) - e_type = 'pem' if e.type == "PEM" else 'vem' if e.type == "VEM" else 'rem' + e_type = 'pem' if e.type == "PEM" else 'adpem' if e.type == "VEM" else 'rem' for m in data: idx = int(m[1:-1]) # Remove parentheses mtoegg[idx][e_type] = True @@ -228,7 +228,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: is_farmable=m.drop_id is not None, in_pem=mtoegg[m.monster_id]['pem'], in_rem=mtoegg[m.monster_id]['rem'], - in_vem=mtoegg[m.monster_id]['vem'], + in_vem=mtoegg[m.monster_id]['adpem'], buy_mp=m.buy_mp, sell_mp=m.sell_mp, sell_gold=m.sell_gold, @@ -716,7 +716,7 @@ def monster_acquisition(self, monster: MonsterModel) -> str: elif self.monster_is_pem_evo(monster): return 'PEM Card' elif self.monster_is_vem_evo(monster): - return 'VEM Card' + return 'AdPem Card' def numeric_next_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: next_monster = None diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 80f57649..84ca6962 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -465,9 +465,9 @@ def is_story(m, do_transform=True): if self.graph.monster_is_pem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.PEM]) if self.graph.monster_is_vem_evo(monster): - modifiers.update(MISC_MAP[MiscModifiers.VEM]) + modifiers.update(MISC_MAP[MiscModifiers.ADPEM]) if monster.in_vem: - modifiers.update(MISC_MAP[MiscModifiers.INVEM]) + modifiers.update(MISC_MAP[MiscModifiers.INADPEM]) if self.graph.monster_is_rem_evo(monster): modifiers.update(MISC_MAP[MiscModifiers.REM]) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index deab3725..b1c7b316 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -179,8 +179,8 @@ class MiscModifiers(Enum): TRADEABLE = 'Tradeable' REM = 'In REM' PEM = 'In PEM' - VEM = 'In VEM' - INVEM = 'This evo is in VEM' + ADPEM = 'In AdPEM' + INADPEM = 'This evo is in AdPEM' MP = 'MP' INJP = 'In JP Server' ONLYJP = 'Only in JP Server' @@ -210,8 +210,8 @@ class MiscModifiers(Enum): MiscModifiers.TRADEABLE: ('tradeable', 'tradable'), MiscModifiers.REM: ('rem',), MiscModifiers.PEM: ('pem',), - MiscModifiers.VEM: ('vem', 'adpem'), - MiscModifiers.INVEM: ('invem', 'inadpem'), + MiscModifiers.ADPEM: ('adpem', 'adpem'), + MiscModifiers.INADPEM: ('invem', 'inadpem'), MiscModifiers.MP: ('mp',), MiscModifiers.INJP: ('injp',), MiscModifiers.INNA: ('inna',), From 29dcd78efd529449cbef0a50f393bdf2da5867e2 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 1 Jan 2022 05:25:20 -0600 Subject: [PATCH 1016/1220] [adpem] support either 4 or 5 pulls (#1521) --- crowddata/mixins/adpem/adpem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index 5e4f00bd..e37eb354 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -63,8 +63,9 @@ async def reportyesterday(self, ctx, *, pulls): await self.report_at_time(ctx, pulls, 24 * 60 * 60) async def report_at_time(self, ctx, pulls, offset): - if len(pulls := pulls.split(',')) != 5: - return await ctx.send(f"Please supply all 5 pulls with commas in between them. You can use names or" + pulls_split = pulls.split(',') + if len(pulls_split) != 5 and len(pulls_split) != 4: + return await ctx.send(f"Please supply all of your pulls with commas in between them. You can use names or" f" numbers.\n\t" f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013, 618`\n\t" f"Valid input: `{ctx.prefix}adpem report Enoch, Facet, D Globe, B Super King, Facet`") @@ -79,7 +80,7 @@ async def report_at_time(self, ctx, pulls, offset): return monsters = [await dbcog.find_monster('inadpem ' + pull.strip()) - for pull in pulls] + for pull in pulls_split] if not all(monsters): unknown = '\n\t'.join(s for s, m in zip(pulls, monsters) if m is None) From e689023275e7af5b0c82b564a3b3801d8a186973 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 1 Jan 2022 08:22:29 -0600 Subject: [PATCH 1017/1220] Force adpem length to be 4 now (#1523) --- crowddata/mixins/adpem/adpem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index e37eb354..3c737625 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -64,7 +64,7 @@ async def reportyesterday(self, ctx, *, pulls): async def report_at_time(self, ctx, pulls, offset): pulls_split = pulls.split(',') - if len(pulls_split) != 5 and len(pulls_split) != 4: + if len(pulls_split) != 4: return await ctx.send(f"Please supply all of your pulls with commas in between them. You can use names or" f" numbers.\n\t" f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013, 618`\n\t" From 570b5615d655f9fff4a5e7fcadd0996d43c62e11 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 2 Jan 2022 13:14:17 -0800 Subject: [PATCH 1018/1220] Add type hints to the database & refactor (#1524) --- dbcog/database_context.py | 37 +++++++++++----------- dbcog/database_manager.py | 64 +++++++++++++++----------------------- dbcog/monster_graph.py | 10 +++--- padbuildimg/padbuildimg.py | 2 +- 4 files changed, 49 insertions(+), 64 deletions(-) diff --git a/dbcog/database_context.py b/dbcog/database_context.py index 39a5e963..c6f9a1de 100644 --- a/dbcog/database_context.py +++ b/dbcog/database_context.py @@ -1,4 +1,4 @@ -from typing import Generator, List, Optional +from typing import Callable, Iterable, List, Optional from tsutils.enums import Server @@ -7,6 +7,7 @@ from .models.awoken_skill_model import AwokenSkillModel from .models.dungeon_model import DungeonModel from .models.enum_types import DEFAULT_SERVER +from .models.monster_model import MonsterModel from .models.scheduled_event_model import ScheduledEventModel from .models.series_model import SeriesModel from .monster_graph import MonsterGraph @@ -32,37 +33,35 @@ def __init__(self, database: DBCogDatabase, graph: MonsterGraph, dungeon: Dungeo self.awoken_skill_map = {awsk.awoken_skill_id: awsk for awsk in self.get_all_awoken_skills()} - def get_awoken_skill_ids(self): - SELECT_AWOKEN_SKILL_IDS = 'SELECT awoken_skill_id FROM awoken_skills' - return [r.awoken_skill_id for r in - self.database.query_many( - SELECT_AWOKEN_SKILL_IDS, (), as_generator=True)] - - def get_monsters_where(self, f, *, server: Server): + def get_monsters_where(self, f: Callable[[MonsterModel], bool], *, server: Server) -> List[MonsterModel]: return [m for m in self.get_all_monsters(server) if f(m)] - def get_monsters_by_series(self, series_id: int, *, server: Server): + def get_monsters_by_series(self, series_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.series_id == series_id, server=server) - def get_monsters_by_active(self, active_skill_id: int, *, server: Server): + def get_monsters_by_active(self, active_skill_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id, server=server) - def get_all_monster_ids_query(self, server: Server): + def get_monsters_by_leader(self, leader_skill_id: int, *, server: Server) -> List[MonsterModel]: + return self.get_monsters_where(lambda m: m.leader_skill_id == leader_skill_id, server=server) + + def get_all_monster_ids_query(self, server: Server) -> Iterable[int]: # We don't need to query if we're in debug mode. We already know exactly which monsters we're working with if self.debug_monster_ids is not None: return self.debug_monster_ids table = 'monsters_na' if server == Server.NA else 'monsters' query = self.database.query_many( - self.database.select_builder(tables={table: ('monster_id',)}), (), + self.database.select_builder(tables={table: ('monster_id',)}), + (), as_generator=True) return map(lambda m: m.monster_id, query) - def get_all_monsters(self, server: Server = DEFAULT_SERVER): + def get_all_monsters(self, server: Server = DEFAULT_SERVER) -> Iterable[MonsterModel]: return (self.graph.get_monster(mid, server=server) for mid in self.get_all_monster_ids_query(server)) - def get_all_events(self) -> Generator[ScheduledEventModel, None, None]: - result = self.database.query_many(SCHEDULED_EVENT_QUERY, ()) + def get_all_events(self) -> Iterable[ScheduledEventModel]: + result = self.database.query_many(SCHEDULED_EVENT_QUERY) for se in result: se['dungeon_model'] = DungeonModel(name_ja=se['d_name_ja'], name_en=se['d_name_en'], @@ -71,15 +70,15 @@ def get_all_events(self) -> Generator[ScheduledEventModel, None, None]: yield ScheduledEventModel(**se) def get_all_awoken_skills(self) -> List[AwokenSkillModel]: - result = self.database.query_many("SELECT * FROM awoken_skills", ()) + result = self.database.query_many("SELECT * FROM awoken_skills") return [AwokenSkillModel(**r) for r in result] def get_all_series(self) -> List[SeriesModel]: - result = self.database.query_many("SELECT * FROM series", ()) + result = self.database.query_many("SELECT * FROM series") return [SeriesModel(**r) for r in result] - def has_database(self): + def has_database(self) -> bool: return self.database.has_database() - def close(self): + def close(self) -> None: self.database.close() diff --git a/dbcog/database_manager.py b/dbcog/database_manager.py index 4d521463..0eb5cd00 100644 --- a/dbcog/database_manager.py +++ b/dbcog/database_manager.py @@ -1,17 +1,20 @@ -import sqlite3 as lite - import logging +import sqlite3 as lite +from typing import Dict, Generator, List, Optional, Tuple, TypeVar, Union logger = logging.getLogger('red.padbot-cogs.dbcog.database_manager') +T = TypeVar('T') -class DBCogTableNotFound(Exception): - def __init__(self, table_name): - self.message = '{} not found'.format(table_name) + +class DictWithAttrAccess(Dict[str, T]): + def __init__(self, item: Dict[str, T]): + super(DictWithAttrAccess, self).__init__(item) + self.__dict__ = self -class DBCogDatabase(object): - def __init__(self, data_file): +class DBCogDatabase: + def __init__(self, data_file: str): self._con = lite.connect(data_file, detect_types=lite.PARSE_DECLTYPES) self._con.row_factory = lite.Row @@ -19,16 +22,17 @@ def __del__(self): self.close() logger.info("Garbage Collecting Old Database") - def has_database(self): + def has_database(self) -> bool: return self._con is not None - def close(self): + def close(self) -> None: if self._con: self._con.close() self._con = None @staticmethod - def select_builder(tables, key=None, where=None, order=None, distinct=False): + def select_builder(tables, key: Optional[str] = None, where: Optional[str] = None, + order: Optional[str] = None, distinct: bool = False) -> str: if distinct: SELECT_FROM = 'SELECT DISTINCT {fields} FROM {first_table}' else: @@ -60,7 +64,10 @@ def select_builder(tables, key=None, where=None, order=None, distinct=False): query.append(ORDER.format(order=order)) return ' '.join(query) - def query_one(self, query, param): + def query_one(self, query: str, param: Tuple = None) -> Optional[DictWithAttrAccess]: + if param is None: + param = () + cursor = self._con.cursor() cursor.execute(query, param) res = cursor.fetchone() @@ -68,42 +75,21 @@ def query_one(self, query, param): return DictWithAttrAccess(res) return None - @staticmethod - def as_generator(cursor): - res = cursor.fetchone() - while res is not None: - yield DictWithAttrAccess(res) - res = cursor.fetchone() + def query_many(self, query: str, param: Tuple = None, idx_key: Optional[str] = None, as_generator: bool = False) \ + -> Union[Generator[DictWithAttrAccess, None, None], + List[DictWithAttrAccess], + DictWithAttrAccess[DictWithAttrAccess]]: + if param is None: + param = () - def query_many(self, query, param, idx_key=None, as_generator=False): cursor = self._con.cursor() cursor.execute(query, param) if cursor.rowcount == 0: return [] if as_generator: - return (DictWithAttrAccess(res) - for res in cursor.fetchall()) + return (DictWithAttrAccess(res) for res in cursor.fetchall()) else: if idx_key is None: return [DictWithAttrAccess(res) for res in cursor.fetchall()] else: return DictWithAttrAccess({res[idx_key]: DictWithAttrAccess(res) for res in cursor.fetchall()}) - - def get_table_fields(self, table_name: str): - # SQL inject vulnerable :v - table_info = self.query_many('PRAGMA table_info(' + table_name + ')', (), dict) - pk = None - fields = [] - for c in table_info: - if c['pk'] == 1: - pk = c['name'] - fields.append(c['name']) - if len(fields) == 0: - raise DBCogTableNotFound(table_name) - return fields, pk - - -class DictWithAttrAccess(dict): - def __init__(self, item): - super(DictWithAttrAccess, self).__init__(item) - self.__dict__ = self diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 7846963d..02891cef 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -148,11 +148,11 @@ def build_graph(self, server: Server) -> MultiDiGraph: table_suffix = "_" + server.value.lower() where = SERVER_ID_WHERE_CONDITION.format(["JP", "NA", "KR"].index(server.value)) - ms = self.database.query_many(MONSTER_QUERY.format(table_suffix), ()) - es = self.database.query_many(EVOS_QUERY.format(table_suffix), ()) - aws = self.database.query_many(AWAKENINGS_QUERY.format(table_suffix), ()) - ems = self.database.query_many(EGG_QUERY.format(table_suffix) + where, ()) - exs = self.database.query_many(EXCHANGE_QUERY.format(table_suffix) + where, ()) + ms = self.database.query_many(MONSTER_QUERY.format(table_suffix)) + es = self.database.query_many(EVOS_QUERY.format(table_suffix)) + aws = self.database.query_many(AWAKENINGS_QUERY.format(table_suffix)) + ems = self.database.query_many(EGG_QUERY.format(table_suffix) + where) + exs = self.database.query_many(EXCHANGE_QUERY.format(table_suffix) + where) mtoawo = defaultdict(list) for a in aws: diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 57392159..4aa3600a 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -852,7 +852,7 @@ async def refreshassets(self, ctx): Refresh assets folder """ await ctx.send('Downloading assets to {}'.format(self.settings.buildImgParams().ASSETS_DIR)) - awk_ids = self.bot.get_cog('DBCog').database.get_awoken_skill_ids() + awk_ids = self.bot.get_cog('DBCog').database.self.awoken_skill_map.keys() await self.settings.downloadAllAssets(awk_ids) await ctx.tick() From 56e660f556ecc1f722062b7afaa2f2dfdfa3230a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:31:27 -0600 Subject: [PATCH 1019/1220] Add awoken skill emojis to AS text (#1528) --- padinfo/view/components/base_id_main_view.py | 16 +++++++++++++++- padinfo/view/id.py | 12 ++++++++---- padinfo/view/transforminfo.py | 12 +++++++----- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 45799673..631be79e 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import List, TYPE_CHECKING +from typing import List, TYPE_CHECKING, Dict from discordmenu.embed.base import Box from discordmenu.embed.text import BoldText, Text @@ -13,7 +13,9 @@ if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel + from dbcog.models.active_skill_model import ActiveSkillModel from dbcog.models.awakening_model import AwakeningModel + from dbcog.models.awoken_skill_model import AwokenSkillModel def _get_awakening_text(awakening: "AwakeningModel"): @@ -85,6 +87,18 @@ def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["Monst delimiter=' ' ) + @classmethod + def active_skill_text(cls, active_skill: "ActiveSkillModel", + awoken_skill_map: Dict[int, "AwokenSkillModel"]): + if active_skill is None: + return 'None' + desc = active_skill.desc + for idx, awo_skill in awoken_skill_map.items(): + phrase = awo_skill.name_en + ' awoken skill on the team' + if phrase in desc: + return desc.replace(phrase, f"{get_awakening_emoji(idx)} {phrase}") + return desc + @staticmethod def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): return Box( diff --git a/padinfo/view/id.py b/padinfo/view/id.py index cde0464e..b377e7df 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,4 +1,4 @@ -from typing import List, Optional, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING, Dict from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail @@ -21,18 +21,20 @@ if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel from dbcog.models.awakening_model import AwakeningModel + from dbcog.models.awoken_skill_model import AwokenSkillModel from dbcog.database_context import DbContext class IdQueriedProps: def __init__(self, acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_evolutions, - previous_transforms): + previous_transforms, awoken_skill_map: Dict[int, "AwokenSkillModel"]): self.previous_evolutions = previous_evolutions self.true_evo_type_raw = true_evo_type_raw self.transform_base = transform_base self.previous_transforms = previous_transforms self.base_rarity = base_rarity self.acquire_raw = acquire_raw + self.awoken_skill_map = awoken_skill_map class IdViewState(ViewStateBaseId): @@ -57,6 +59,7 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst self.previous_transforms = id_queried_props.previous_transforms self.transform_base: "MonsterModel" = id_queried_props.transform_base self.true_evo_type_raw = id_queried_props.true_evo_type_raw + self.awoken_skill_map = id_queried_props.awoken_skill_map def serialize(self): ret = super().serialize() @@ -119,8 +122,9 @@ async def _get_monster_misc_info(cls, db_context: "DbContext", monster) -> IdQue base_rarity = db_context.graph.get_base_monster(monster).rarity previous_evolutions = db_context.graph.get_all_prev_evolutions(monster, include_self=True) previous_transforms = db_context.graph.get_all_prev_transforms(monster, include_self=False) + awoken_skill_map = db_context.awoken_skill_map return IdQueriedProps(acquire_raw, base_rarity, transform_base, true_evo_type_raw, previous_evolutions, - previous_transforms) + previous_transforms, awoken_skill_map) def set_na_diff_invalid_message(self, ims: dict) -> bool: message = self.get_na_diff_invalid_message() @@ -275,7 +279,7 @@ def embed(cls, state: IdViewState): ), EmbedField( cls.active_skill_header(m, state.previous_transforms).to_markdown(), - Text(m.active_skill.desc if m.active_skill else 'None') + Text(cls.active_skill_text(m.active_skill, state.awoken_skill_map)) ), EmbedField( cls.leader_skill_header(m, state.query_settings.lsmultiplier, state.transform_base).to_markdown(), diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index c55aa80c..c8db8dd0 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -22,9 +22,10 @@ class TransformInfoQueriedProps: - def __init__(self, acquire_raw, previous_transforms): + def __init__(self, acquire_raw, previous_transforms, awoken_skill_map): self.acquire_raw = acquire_raw self.previous_transforms = previous_transforms + self.awoken_skill_map = awoken_skill_map class TransformInfoViewState(ViewStateBase): @@ -37,6 +38,7 @@ def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, tr self.transformed_mon = transformed_mon self.acquire_raw = tfinfo_queried_props.acquire_raw self.previous_transforms = tfinfo_queried_props.previous_transforms + self.awoken_skill_map = tfinfo_queried_props.awoken_skill_map self.monster_ids = monster_ids self.is_jp_buffed = is_jp_buffed self.query_settings: QuerySettings = query_settings @@ -80,7 +82,8 @@ async def do_query(dbcog, transformed_mon) -> TransformInfoQueriedProps: db_context: "DbContext" acquire_raw = db_context.graph.monster_acquisition(transformed_mon) previous_transforms = db_context.graph.get_all_prev_transforms(transformed_mon) - return TransformInfoQueriedProps(acquire_raw, previous_transforms) + awoken_skill_map = db_context.awoken_skill_map + return TransformInfoQueriedProps(acquire_raw, previous_transforms, awoken_skill_map) def _get_tf_stat_diff_text(stat, tf_stat): @@ -199,10 +202,9 @@ def embed(cls, state: TransformInfoViewState): EmbedField( transformat(TransformInfoView.transform_active_header(transformed_mon).to_markdown()), Box( - Text(transformed_mon.active_skill.desc if transformed_mon.active_skill - else 'None'), + Text(cls.active_skill_text(transformed_mon.active_skill, state.awoken_skill_map)), TransformInfoView.base_active_header(base_mon).to_markdown(), - Text(base_mon.active_skill.desc if base_mon.active_skill else 'None') + Text(cls.active_skill_text(base_mon.active_skill, state.awoken_skill_map)) ) ), EmbedField( From fcd73a23a5d2e622184e96886612c812aacc243d Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 2 Jan 2022 16:04:11 -0800 Subject: [PATCH 1020/1220] Add blob boost (#1530) --- dbcog/models/enum_types.py | 1 + dbcog/token_mappings.py | 1 + padinfo/common/emoji_map.py | 1 + 3 files changed, 3 insertions(+) diff --git a/dbcog/models/enum_types.py b/dbcog/models/enum_types.py index 3bd46a2b..fe7bf21d 100644 --- a/dbcog/models/enum_types.py +++ b/dbcog/models/enum_types.py @@ -140,3 +140,4 @@ class AwokenSkills(Enum): ATTR3BOOST = 79 ATTR4BOOST = 80 ATTR5BOOST = 81 + BLOBBOOST = 82 diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index b1c7b316..5c38da66 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -169,6 +169,7 @@ class EvoTypes(Enum): AwokenSkills.ATTR3BOOST: ('attr3', '3attr'), AwokenSkills.ATTR4BOOST: ('attr4', '4attr'), AwokenSkills.ATTR5BOOST: ('attr5', '5attr'), + AwokenSkills.BLOBBOOST: ('blobboost', 'blob'), } diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py index becd85ac..b354127d 100644 --- a/padinfo/common/emoji_map.py +++ b/padinfo/common/emoji_map.py @@ -89,6 +89,7 @@ 79: 'boost_attr3', 80: 'boost_attr4', 81: 'boost_attr5', + 82: 'blob_boost', } AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { From 389d1235f4e0cc0d2e1b9b862685d2b10a4f4c5f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 2 Jan 2022 20:50:20 -0600 Subject: [PATCH 1021/1220] Support awo emojis in AS boosting multiple awo (#1531) --- padinfo/view/components/base_id_main_view.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 631be79e..d3f52321 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -1,3 +1,4 @@ +import re from abc import abstractmethod from typing import List, TYPE_CHECKING, Dict @@ -94,9 +95,10 @@ def active_skill_text(cls, active_skill: "ActiveSkillModel", return 'None' desc = active_skill.desc for idx, awo_skill in awoken_skill_map.items(): - phrase = awo_skill.name_en + ' awoken skill on the team' - if phrase in desc: - return desc.replace(phrase, f"{get_awakening_emoji(idx)} {phrase}") + # locate awoken skill names within the same clause as "on the team" + phrase_find = re.escape(awo_skill.name_en) + r'([^;]*?) awoken skill on the team' + phrase_repl = awo_skill.name_en + r'\1 awoken skill on the team' + desc = re.sub(phrase_find, f"{get_awakening_emoji(idx)} {phrase_repl}", desc) return desc @staticmethod From c7829aa95ca1202e175722a49053b2c04549cfdc Mon Sep 17 00:00:00 2001 From: muffin-rice <44299050+muffin-rice@users.noreply.github.com> Date: Mon, 3 Jan 2022 21:18:34 -0800 Subject: [PATCH 1022/1220] Fix adpem optin discrepancy issue (#1532) --- crowddata/mixins/adpem/adpem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index 3c737625..ab22d49b 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -21,7 +21,7 @@ async def check(ctx): if is_opted == await ctx.bot.get_cog("CrowdData").config.user(ctx.author).opted_in(): return True if is_opted and len(ctx.invoked_parents) != 1: - await ctx.send(f"You need to opt in first via `{ctx.prefix}{' '.join(ctx.invoked_parents)} optin`") + await ctx.send(f"You need to opt in first via `{ctx.prefix}{ctx.invoked_parents[0]} optin`") return False return commands.check(check) From 1e658ab2f03230aec79b651ba5f4fd6b0d43042e Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 8 Jan 2022 21:50:51 -0800 Subject: [PATCH 1023/1220] Add active subskills and parts (#1539) --- dbcog/database_manager.py | 21 ++--- dbcog/models/active_skill_model.py | 81 ++++++++++++++++++- dbcog/monster_graph.py | 124 ++++++++++++++++++++++++----- 3 files changed, 192 insertions(+), 34 deletions(-) diff --git a/dbcog/database_manager.py b/dbcog/database_manager.py index 0eb5cd00..9b308390 100644 --- a/dbcog/database_manager.py +++ b/dbcog/database_manager.py @@ -1,6 +1,6 @@ import logging import sqlite3 as lite -from typing import Dict, Generator, List, Optional, Tuple, TypeVar, Union +from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar, Union logger = logging.getLogger('red.padbot-cogs.dbcog.database_manager') @@ -8,7 +8,9 @@ class DictWithAttrAccess(Dict[str, T]): - def __init__(self, item: Dict[str, T]): + def __init__(self, item: Optional[Dict[str, T]] = None, **items): + if items: + item = items super(DictWithAttrAccess, self).__init__(item) self.__dict__ = self @@ -75,10 +77,11 @@ def query_one(self, query: str, param: Tuple = None) -> Optional[DictWithAttrAcc return DictWithAttrAccess(res) return None - def query_many(self, query: str, param: Tuple = None, idx_key: Optional[str] = None, as_generator: bool = False) \ - -> Union[Generator[DictWithAttrAccess, None, None], - List[DictWithAttrAccess], - DictWithAttrAccess[DictWithAttrAccess]]: + def query_many(self, query: str, param: Tuple = None, idx_key: Optional[str] = None, + as_generator: bool = False, as_type: Type[T] = DictWithAttrAccess) \ + -> Union[Generator[T, None, None], + List[T], + Dict[Any, T]]: if param is None: param = () @@ -87,9 +90,9 @@ def query_many(self, query: str, param: Tuple = None, idx_key: Optional[str] = N if cursor.rowcount == 0: return [] if as_generator: - return (DictWithAttrAccess(res) for res in cursor.fetchall()) + return (as_type(**res) for res in cursor.fetchall()) else: if idx_key is None: - return [DictWithAttrAccess(res) for res in cursor.fetchall()] + return [as_type(**res) for res in cursor.fetchall()] else: - return DictWithAttrAccess({res[idx_key]: DictWithAttrAccess(res) for res in cursor.fetchall()}) + return {res[idx_key]: as_type(**res) for res in cursor.fetchall()} diff --git a/dbcog/models/active_skill_model.py b/dbcog/models/active_skill_model.py index 4643ae21..e4ddf26f 100644 --- a/dbcog/models/active_skill_model.py +++ b/dbcog/models/active_skill_model.py @@ -1,20 +1,89 @@ +from typing import Any, Collection, Dict, List, Optional, Sequence, Union + from .base_model import BaseModel -class ActiveSkillModel(BaseModel): +class ActivePartModel(BaseModel): def __init__(self, **kwargs): - self.active_skill_id = kwargs['active_skill_id'] + self.active_part_id = kwargs['active_part_id'] + self.active_skill_type_id = kwargs['active_skill_type_id'] + + self.desc_ja = kwargs['desc_ja'] + self.desc_en = kwargs['desc_en'] + self.desc_ko = kwargs['desc_ko'] + self.desc_templated_ja = kwargs['desc_templated_ja'] + self.desc_templated_en = kwargs['desc_templated_en'] + self.desc_templated_ko = kwargs['desc_templated_ko'] + + @property + def desc(self): + return self.desc_en or self.desc_ja + + def to_dict(self): + return { + 'active_part_id': self.active_part_id, + 'desc_ja': self.desc_ja, + 'desc_en': self.desc_en, + } + + +class ActiveSubskillModel(BaseModel): + def __init__(self, *, active_parts: Sequence[Union[ActivePartModel, Dict[str, Any]]], **kwargs): + if active_parts and isinstance(active_parts[0], dict): + active_parts = [ActivePartModel(**ap) for ap in active_parts] + + self.active_subskill_id = kwargs['active_subskill_id'] + self.name_ja = kwargs['name_ja'] self.name_en = kwargs['name_en'] self.name_ko = kwargs['name_ko'] + self.desc_ja = kwargs['desc_ja'] + self.desc_en = kwargs['desc_en'] + self.desc_ko = kwargs['desc_ko'] + self.desc_templated_ja = kwargs['desc_templated_ja'] + self.desc_templated_en = kwargs['desc_templated_en'] + self.desc_templated_ko = kwargs['desc_templated_ko'] + + self.active_parts = active_parts + + @property + def desc(self): + return self.desc_en or self.desc_ja + + def to_dict(self): + return { + 'active_subskill_id': self.active_subskill_id, + 'desc_ja': self.desc_ja, + 'desc_en': self.desc_en, + } + + +class ActiveSkillModel(BaseModel): + def __init__(self, *, active_subskills: Sequence[Union[ActiveSubskillModel, Dict[str, Any]]], **kwargs): + if active_subskills and isinstance(active_subskills[0], dict): + active_subskills = [ActiveSubskillModel(**ass) for ass in active_subskills] + + self.active_skill_id = kwargs['active_skill_id'] + self.compound_skill_type_id = kwargs['compound_skill_type_id'] + self.name_ja = kwargs['name_ja'] + self.name_en = kwargs['name_en'] + self.name_ko = kwargs['name_ko'] self.desc_ja = kwargs['desc_ja'] self.desc_en = kwargs['desc_en'] self.desc_ko = kwargs['desc_ko'] + self.desc_templated_ja = kwargs['desc_templated_ja'] + self.desc_templated_en = kwargs['desc_templated_en'] + self.desc_templated_ko = kwargs['desc_templated_ko'] + self.desc_official_ja = kwargs['desc_official_ja'] + self.desc_official_en = kwargs['desc_official_en'] + self.desc_official_ko = kwargs['desc_official_ko'] self.turn_max = kwargs['turn_max'] self.turn_min = kwargs['turn_min'] + self.active_subskills = active_subskills + @property def desc(self): return self.desc_en or self.desc_ja @@ -25,7 +94,7 @@ def name(self): def to_dict(self): return { - 'monster_id': self.active_skill_id, + 'active_skill_id': self.active_skill_id, 'name_ja': self.name_ja, 'name_en': self.name_en, } @@ -36,4 +105,10 @@ def __eq__(self, other): and self.desc_en == other.desc_en \ and self.turn_max == other.turn_max \ and self.turn_min == other.turn_min + elif isinstance(other, ActiveSubskillModel): + return self.active_skill_id == other.active_subskill_id \ + and self.desc_en == other.desc_en + elif isinstance(other, ActivePartModel): + return self.active_skill_id == other.active_part_id \ + and self.desc_en == other.desc_en return False diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 02891cef..7b515170 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -9,7 +9,7 @@ from tsutils.enums import Server from .database_manager import DBCogDatabase -from .models.active_skill_model import ActiveSkillModel +from .models.active_skill_model import ActivePartModel, ActiveSkillModel, ActiveSubskillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel from .models.base_model import BaseModel @@ -38,21 +38,12 @@ leader_skills{0}.max_hp, leader_skills{0}.max_atk, leader_skills{0}.max_rcv, - leader_skills{0}.max_rcv, leader_skills{0}.max_shield, leader_skills{0}.max_combos, leader_skills{0}.bonus_damage, leader_skills{0}.mult_bonus_damage, leader_skills{0}.extra_time, leader_skills{0}.tags, - active_skills{0}.name_ja AS as_name_ja, - active_skills{0}.name_en AS as_name_en, - active_skills{0}.name_ko AS as_name_ko, - active_skills{0}.desc_ja AS as_desc_ja, - active_skills{0}.desc_en AS as_desc_en, - active_skills{0}.desc_ko AS as_desc_ko, - active_skills{0}.turn_max, - active_skills{0}.turn_min, COALESCE(series.series_id, 0) AS s_series_id, COALESCE(series.name_ja, 'Unsorted') AS s_name_ja, COALESCE(series.name_en, 'Unsorted') AS s_name_en, @@ -75,6 +66,53 @@ GROUP BY monsters{0}.monster_id""" +ACTIVE_QUERY = """SELECT + act.active_skill_id, + act.compound_skill_type_id, + act.name_ja AS act_name_ja, + act.name_en AS act_name_en, + act.name_ko AS act_name_ko, + act.desc_ja AS act_desc_ja, + act.desc_en AS act_desc_en, + act.desc_ko AS act_desc_ko, + act.desc_templated_ja AS act_desc_templated_ja, + act.desc_templated_en AS act_desc_templated_en, + act.desc_templated_ko AS act_desc_templated_ko, + act.desc_official_ja AS act_desc_official_ja, + act.desc_official_ko AS act_desc_official_ko, + act.desc_official_en AS act_desc_official_en, + act.turn_max, + act.turn_min, + ass.active_subskill_id, + ass.name_ja AS ass_name_ja, + ass.name_en AS ass_name_en, + ass.name_ko AS ass_name_ko, + ass.desc_ja AS ass_desc_ja, + ass.desc_en AS ass_desc_en, + ass.desc_ko AS ass_desc_ko, + ass.desc_templated_ja AS ass_desc_templated_ja, + ass.desc_templated_en AS ass_desc_templated_en, + ass.desc_templated_ko AS ass_desc_templated_ko, + ap.active_part_id, + ap.active_skill_type_id, + ap.desc_ja AS ap_desc_ja, + ap.desc_en AS ap_desc_en, + ap.desc_ko AS ap_desc_ko, + ap.desc_templated_ja AS ap_desc_templated_ja, + ap.desc_templated_en AS ap_desc_templated_en, + ap.desc_templated_ko AS ap_desc_templated_ko, + ass_ap.order_idx AS subskill_idx +FROM + active_skills{0} AS act + JOIN active_skills_subskills{0} AS act_ass ON act.active_skill_id = act_ass.active_skill_id + JOIN active_subskills{0} AS ass ON act_ass.active_subskill_id = ass.active_subskill_id + JOIN active_subskills_parts{0} AS ass_ap ON ass.active_subskill_id = ass_ap.active_subskill_id + JOIN active_parts{0} AS ap ON ass_ap.active_part_id = ap.active_part_id +ORDER BY + ass_ap.order_idx, + act_ass.order_idx +""" + # make sure we're only looking at the most recent row for any evolution # since the database might have old data in it still # group by `to_id` and not `evolution_id` because PAD monsters can only have 1 recipe, and @@ -154,6 +192,59 @@ def build_graph(self, server: Server) -> MultiDiGraph: ems = self.database.query_many(EGG_QUERY.format(table_suffix) + where) exs = self.database.query_many(EXCHANGE_QUERY.format(table_suffix) + where) + as_query = self.database.query_many(ACTIVE_QUERY.format(table_suffix)) + acts = {} + for row in as_query: + if row.active_skill_id not in acts: + acts[row.active_skill_id] = { + 'active_skill_id': row.active_skill_id, + 'compound_skill_type_id': row.compound_skill_type_id, + 'name_ja': row.act_name_ja, + 'name_en': row.act_name_en, + 'name_ko': row.act_name_ko, + 'desc_ja': row.act_desc_ja, + 'desc_en': row.act_desc_en, + 'desc_ko': row.act_desc_ko, + 'desc_templated_ja': row.act_desc_templated_ja, + 'desc_templated_en': row.act_desc_templated_en, + 'desc_templated_ko': row.act_desc_templated_ko, + 'desc_official_ja': row.act_desc_official_ja, + 'desc_official_ko': row.act_desc_official_ko, + 'desc_official_en': row.act_desc_official_en, + 'turn_max': row.turn_max, + 'turn_min': row.turn_min, + + 'active_subskills': [] + } + skill = acts[row.active_skill_id] + if len(skill['active_subskills']) <= row.subskill_idx: + skill['active_subskills'].append({ + 'active_subskill_id': row.active_subskill_id, + 'name_ja': row.ass_name_ja, + 'name_en': row.ass_name_en, + 'name_ko': row.ass_name_ko, + 'desc_ja': row.ass_desc_ja, + 'desc_en': row.ass_desc_en, + 'desc_ko': row.ass_desc_ko, + 'desc_templated_ja': row.ass_desc_templated_ja, + 'desc_templated_en': row.ass_desc_templated_en, + 'desc_templated_ko': row.ass_desc_templated_ko, + + 'active_parts': [] + }) + subskill = skill['active_subskills'][row.subskill_idx] + subskill['active_parts'].append({ + 'active_part_id': row.active_part_id, + 'active_skill_type_id': row.active_skill_type_id, + 'desc_ja': row.ap_desc_ja, + 'desc_en': row.ap_desc_en, + 'desc_ko': row.ap_desc_ko, + 'desc_templated_ja': row.ap_desc_templated_ja, + 'desc_templated_en': row.ap_desc_templated_en, + 'desc_templated_ko': row.ap_desc_templated_ko, + }) + acts = {asid: ActiveSkillModel(**act) for asid, act in acts.items()} + mtoawo = defaultdict(list) for a in aws: awoken_skill_model = AwokenSkillModel(**a) @@ -189,17 +280,6 @@ def build_graph(self, server: Server) -> MultiDiGraph: tags=m.tags, ) if m.leader_skill_id != 0 else None - as_model = ActiveSkillModel(active_skill_id=m.active_skill_id, - name_ja=m.as_name_ja, - name_en=m.as_name_en, - name_ko=m.as_name_ko, - desc_ja=m.as_desc_ja, - desc_en=m.as_desc_en, - desc_ko=m.as_desc_ko, - turn_max=m.turn_max, - turn_min=m.turn_min - ) if m.active_skill_id != 0 else None - s_model = SeriesModel(series_id=m.s_series_id, name_ja=m.s_name_ja, name_en=m.s_name_en, @@ -215,7 +295,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: name_is_translation=m.is_translation, awakenings=mtoawo[m.monster_id], leader_skill=ls_model, - active_skill=as_model, + active_skill=acts.get(m.active_skill_id), series=s_model, series_id=m.s_series_id, attribute_1_id=m.attribute_1_id, From 33e968f7c0f249d1e0af3d3c21f7a148e6f9ca96 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 Jan 2022 09:26:55 -0800 Subject: [PATCH 1024/1220] Fix showstats (#1541) --- crowddata/mixins/adpem/adpem.py | 28 ++++++++++++++--------- crowddata/mixins/adpem/view/show_stats.py | 22 +++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index ab22d49b..1df7d72c 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -1,3 +1,4 @@ +from collections import Counter from datetime import timezone from typing import Any @@ -15,6 +16,9 @@ from crowddata.mixins.adpem.menu.closable_embed import ClosableEmbedMenu from crowddata.mixins.adpem.view.show_stats import ShowStatsView, ShowStatsViewProps +# This is the timestamp of the most recent AdPEM reset +LAST_RESET = 1640246400 + def opted_in(is_opted): async def check(ctx): @@ -227,23 +231,25 @@ async def showstats(self, ctx, *, query): return await ctx.send("Required cogs not loaded. Please alert a bot owner.") await dbcog.wait_until_ready() - query = '" "'.join(query.split()) - monsters = await dbcog.find_monsters(f'inadpem {query}') - if not monsters: - return await ctx.send("No monsters matched that query.") + async with ctx.typing(): + query = '" "'.join(query.split()) + monsters = await dbcog.find_monsters(f'inadpem "{query}"') + if not monsters: + return await ctx.send("No monsters matched that query.") - pulls = await self.config.pulls() - data = [d for d in pulls if type(d[1]) != str] - total = [dbcog.get_monster(p) for uid, ps, ts in data for p in ps] - adj = [dbcog.get_monster(p) for uid, ps, ts in data if self.has_good_data(uid, pulls) for p in ps] - you = [dbcog.get_monster(p) for uid, ps, ts in data if uid == ctx.author.id for p in ps] - valid = [m for m in total if m in monsters] + pulls = await self.config.pulls() + data = [(uid, ps, ts) for uid, ps, ts in pulls if not isinstance(ps, str) and ts > LAST_RESET] + total = [dbcog.get_monster(p) for uid, ps, ts in data for p in ps] + adj = [dbcog.get_monster(p) for uid, ps, ts in data if self.has_good_data(uid, pulls) for p in ps] + you = [dbcog.get_monster(p) for uid, ps, ts in data if uid == ctx.author.id for p in ps] + valid = {m for m in total if m in monsters} + most_common = Counter(m for m in total if m in monsters).most_common(1)[0][0] if not valid: return await ctx.send("No monsters matching that query have been pulled.") menu = ClosableEmbedMenu.menu() - props = ShowStatsViewProps(total, adj, you, valid) + props = ShowStatsViewProps(total, adj, you, valid, most_common) state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, Color.default(), ShowStatsView.VIEW_TYPE, props) diff --git a/crowddata/mixins/adpem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py index 6c8d72e6..68621fd1 100644 --- a/crowddata/mixins/adpem/view/show_stats.py +++ b/crowddata/mixins/adpem/view/show_stats.py @@ -1,5 +1,5 @@ from collections import Counter -from typing import TYPE_CHECKING, List +from typing import Collection, TYPE_CHECKING, List from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedThumbnail, EmbedField @@ -17,19 +17,21 @@ class ShowStatsViewProps: - def __init__(self, total: List["MonsterModel"], adj: List["MonsterModel"], - you: List["MonsterModel"], valid: List["MonsterModel"]): + def __init__(self, total: Collection["MonsterModel"], adj: Collection["MonsterModel"], + you: Collection["MonsterModel"], valid: Collection["MonsterModel"], + most_common: "MonsterModel"): self.valid = valid self.total = total self.adj = adj self.you = you + self.most_commmon = most_common class ShowStatsView: VIEW_TYPE = 'ShowStats' @staticmethod - def get_count(arr: List["MonsterModel"], *elements: "MonsterModel") -> str: + def get_count(arr: Collection["MonsterModel"], *elements: "MonsterModel") -> str: if not arr: return "N/A" count = sum(1 for m in arr if m in elements) @@ -39,7 +41,6 @@ def get_count(arr: List["MonsterModel"], *elements: "MonsterModel") -> str: @staticmethod def embed(state: ClosableEmbedViewState, props: ShowStatsViewProps): - fields = [] for mon, c in Counter(props.valid).most_common(6 if len(props.valid) == 6 else 5): fields.append(EmbedField(mon.name_en, Box( @@ -47,20 +48,19 @@ def embed(state: ClosableEmbedViewState, props: ShowStatsViewProps): LabeledText("Adj", ShowStatsView.get_count(props.adj, mon)), LabeledText("You", ShowStatsView.get_count(props.you, mon)) ), inline=True)) - if len(set(props.valid)) > 6: + if len(props.valid) > 6: fields.append(EmbedField("... & More", Box(f"+ {len(props.valid) - 5} more monsters"), inline=True)) return EmbedView( EmbedMain( title=f"AdPEM Data for query: {state.raw_query}", description=Box( - LabeledText("Net", ShowStatsView.get_count(props.total, *set(props.valid))), - LabeledText("Adj", ShowStatsView.get_count(props.adj, *set(props.valid))), - LabeledText("You", ShowStatsView.get_count(props.you, *set(props.valid))) + LabeledText("Net", ShowStatsView.get_count(props.total, *props.valid)), + LabeledText("Adj", ShowStatsView.get_count(props.adj, *props.valid)), + LabeledText("You", ShowStatsView.get_count(props.you, *props.valid)) ) ), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(Counter(props.valid).most_common(1)[0][0])), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(props.most_commmon)), embed_fields=fields, embed_footer=embed_footer_with_state(state) ) - From d2e307bfaa13dc02842108c5020a63b3a040687e Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 9 Jan 2022 09:52:33 -0800 Subject: [PATCH 1025/1220] Add compound awoken skill handling (#1540) --- dbcog/models/active_skill_model.py | 20 +++++++- dbcog/monster_graph.py | 2 +- padinfo/view/components/base_id_main_view.py | 48 +++++++++++++++----- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/dbcog/models/active_skill_model.py b/dbcog/models/active_skill_model.py index e4ddf26f..037729f6 100644 --- a/dbcog/models/active_skill_model.py +++ b/dbcog/models/active_skill_model.py @@ -19,6 +19,10 @@ def __init__(self, **kwargs): def desc(self): return self.desc_en or self.desc_ja + @property + def desc_templated(self): + return self.desc_templated_en or self.desc_templated_ja + def to_dict(self): return { 'active_part_id': self.active_part_id, @@ -46,10 +50,18 @@ def __init__(self, *, active_parts: Sequence[Union[ActivePartModel, Dict[str, An self.active_parts = active_parts + @property + def name(self): + return self.name_en or self.name_ja + @property def desc(self): return self.desc_en or self.desc_ja + @property + def desc_templated(self): + return self.desc_templated_en or self.desc_templated_ja + def to_dict(self): return { 'active_subskill_id': self.active_subskill_id, @@ -84,13 +96,17 @@ def __init__(self, *, active_subskills: Sequence[Union[ActiveSubskillModel, Dict self.active_subskills = active_subskills + @property + def name(self): + return self.name_en or self.name_ja + @property def desc(self): return self.desc_en or self.desc_ja @property - def name(self): - return self.name_en or self.name_ja + def desc_templated(self): + return self.desc_templated_en or self.desc_templated_ja def to_dict(self): return { diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 7b515170..90c62c1e 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -101,7 +101,7 @@ ap.desc_templated_ja AS ap_desc_templated_ja, ap.desc_templated_en AS ap_desc_templated_en, ap.desc_templated_ko AS ap_desc_templated_ko, - ass_ap.order_idx AS subskill_idx + act_ass.order_idx AS subskill_idx FROM active_skills{0} AS act JOIN active_skills_subskills{0} AS act_ass ON act.active_skill_id = act_ass.active_skill_id diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index d3f52321..4742d340 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -1,10 +1,12 @@ -import re -from abc import abstractmethod -from typing import List, TYPE_CHECKING, Dict +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, TYPE_CHECKING +import jinja2 from discordmenu.embed.base import Box from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView +from discordmenu.emoji.emoji_cache import emoji_cache +from tsutils.emoji import char_to_emoji from tsutils.enums import LsMultiplier from padinfo.common.emoji_map import get_awakening_emoji, get_emoji @@ -27,7 +29,27 @@ def _killer_latent_emoji(latent_name: str): return get_emoji('latent_killer_{}'.format(latent_name.lower())) -class BaseIdMainView(BaseIdView): +def _get_compound_active_text(active: Optional["ActiveSkillModel"]) -> Optional[str]: + if active is None: + return None + return { + 0: None, + 1: f"[\N{GAME DIE} Random]", + 2: f"[\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16} Transforms]", + 3: f"[\N{BLACK UNIVERSAL RECYCLING SYMBOL}\N{VARIATION SELECTOR-16} Cycles]" + }.get(active.compound_skill_type_id) + + +def _make_prefix(idx, compound_skill_type_id): + return { + 0: '', + 1: emoji_cache.get_emoji('bd') + ' ', + 2: char_to_emoji(str(idx)) + ' ', + 3: char_to_emoji(str(idx)) + ' ' + }.get(compound_skill_type_id) + + +class BaseIdMainView(BaseIdView, ABC): transform_emoji_names = ['downr', 'downo', 'downy', 'downg', 'downb', 'downp'] up_emoji_name = 'upgr' down_emoji_name = transform_emoji_names[0] @@ -84,22 +106,24 @@ def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["Monst active_cd = ' '.join(skill_texts) return Box( BoldText('Active Skill'), + BoldText(_get_compound_active_text(active_skill)), BoldText(active_cd), delimiter=' ' ) @classmethod - def active_skill_text(cls, active_skill: "ActiveSkillModel", + def active_skill_text(cls, active_skill: Optional["ActiveSkillModel"], awoken_skill_map: Dict[int, "AwokenSkillModel"]): + jinja2_replacements = { + 'awoskills': {f"id{awid}": f"{get_awakening_emoji(awid)} {awo.name_en}" + for awid, awo in awoken_skill_map.items()} + } + if active_skill is None: return 'None' - desc = active_skill.desc - for idx, awo_skill in awoken_skill_map.items(): - # locate awoken skill names within the same clause as "on the team" - phrase_find = re.escape(awo_skill.name_en) + r'([^;]*?) awoken skill on the team' - phrase_repl = awo_skill.name_en + r'\1 awoken skill on the team' - desc = re.sub(phrase_find, f"{get_awakening_emoji(idx)} {phrase_repl}", desc) - return desc + return "\n".join(_make_prefix(c, active_skill.compound_skill_type_id) + + jinja2.Template(subskill.desc_templated).render(**jinja2_replacements) + for c, subskill in enumerate(active_skill.active_subskills, 1)) @staticmethod def leader_skill_header(m: "MonsterModel", lsmultiplier: LsMultiplier, transform_base: "MonsterModel"): From 28ce4480294b7ff4a801a6bdfa6dd269fd95f4d2 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 9 Jan 2022 12:34:30 -0600 Subject: [PATCH 1026/1220] Fix active skills without types (#1550) --- padinfo/view/components/base_id_main_view.py | 34 ++++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 4742d340..73f942fc 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -29,17 +29,6 @@ def _killer_latent_emoji(latent_name: str): return get_emoji('latent_killer_{}'.format(latent_name.lower())) -def _get_compound_active_text(active: Optional["ActiveSkillModel"]) -> Optional[str]: - if active is None: - return None - return { - 0: None, - 1: f"[\N{GAME DIE} Random]", - 2: f"[\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16} Transforms]", - 3: f"[\N{BLACK UNIVERSAL RECYCLING SYMBOL}\N{VARIATION SELECTOR-16} Cycles]" - }.get(active.compound_skill_type_id) - - def _make_prefix(idx, compound_skill_type_id): return { 0: '', @@ -54,6 +43,13 @@ class BaseIdMainView(BaseIdView, ABC): up_emoji_name = 'upgr' down_emoji_name = transform_emoji_names[0] + active_skill_type_texts = { + 0: None, + 1: f"[\N{GAME DIE} Random]", + 2: f"[\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16} Transforms]", + 3: f"[\N{BLACK UNIVERSAL RECYCLING SYMBOL}\N{VARIATION SELECTOR-16} Cycles]" + } + @staticmethod def normal_awakenings_row(m: "MonsterModel"): normal_awakenings = len(m.awakenings) - m.superawakening_count @@ -87,9 +83,11 @@ def killers_row(m: "MonsterModel", transform_base): def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["MonsterModel"]): active_skill = m.active_skill + if active_skill is None: + return BoldText('Active Skill') + if len(previous_transforms) == 0: - active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) \ - if active_skill else 'None' + active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) else: skill_texts = [] previous_transforms.reverse() @@ -104,13 +102,21 @@ def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["Monst cooldown_text)) skill_texts.append('{} ({} cd)'.format(get_emoji(cls.up_emoji_name), m.active_skill.turn_max)) active_cd = ' '.join(skill_texts) + return Box( BoldText('Active Skill'), - BoldText(_get_compound_active_text(active_skill)), + cls.get_compound_active_text(active_skill), BoldText(active_cd), delimiter=' ' ) + @classmethod + def get_compound_active_text(cls, active: Optional["ActiveSkillModel"]) -> Optional[Box]: + text = cls.active_skill_type_texts.get(active.compound_skill_type_id) + if text is None: + return None + return BoldText(text) + @classmethod def active_skill_text(cls, active_skill: Optional["ActiveSkillModel"], awoken_skill_map: Dict[int, "AwokenSkillModel"]): From 1f90ca20c1b7d269805897d761771afb5ecc1371 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 10 Jan 2022 09:20:24 -0800 Subject: [PATCH 1027/1220] Fix AdPEM issues (#1555) --- crowddata/mixins/adpem/adpem.py | 10 +++++----- crowddata/mixins/adpem/view/show_stats.py | 16 +++++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index 1df7d72c..021095fd 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -1,5 +1,5 @@ from collections import Counter -from datetime import timezone +from datetime import datetime, timezone from typing import Any import time @@ -17,7 +17,7 @@ from crowddata.mixins.adpem.view.show_stats import ShowStatsView, ShowStatsViewProps # This is the timestamp of the most recent AdPEM reset -LAST_RESET = 1640246400 +LAST_RESET = datetime(year=2021, month=12, day=24).timestamp() def opted_in(is_opted): @@ -87,7 +87,7 @@ async def report_at_time(self, ctx, pulls, offset): for pull in pulls_split] if not all(monsters): - unknown = '\n\t'.join(s for s, m in zip(pulls, monsters) if m is None) + unknown = '\n\t'.join(s for s, m in zip(pulls_split, monsters) if m is None) return await ctx.send(f"Not all monsters were valid. The following could not be processed:\n\t{unknown}") check = '\n\t'.join(pdicog.monster_header.fmt_id_header(m, use_emoji=True).to_markdown() @@ -243,14 +243,14 @@ async def showstats(self, ctx, *, query): adj = [dbcog.get_monster(p) for uid, ps, ts in data if self.has_good_data(uid, pulls) for p in ps] you = [dbcog.get_monster(p) for uid, ps, ts in data if uid == ctx.author.id for p in ps] valid = {m for m in total if m in monsters} - most_common = Counter(m for m in total if m in monsters).most_common(1)[0][0] if not valid: return await ctx.send("No monsters matching that query have been pulled.") + most_common = Counter(m for m in total if m in monsters).most_common(1)[0][0] menu = ClosableEmbedMenu.menu() props = ShowStatsViewProps(total, adj, you, valid, most_common) - state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query.replace('"', ''), Color.default(), ShowStatsView.VIEW_TYPE, props) await menu.create(ctx, state) diff --git a/crowddata/mixins/adpem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py index 68621fd1..f0d2cb5b 100644 --- a/crowddata/mixins/adpem/view/show_stats.py +++ b/crowddata/mixins/adpem/view/show_stats.py @@ -30,6 +30,8 @@ def __init__(self, total: Collection["MonsterModel"], adj: Collection["MonsterMo class ShowStatsView: VIEW_TYPE = 'ShowStats' + MAX_EXPANDED_RESULTS = 6 + @staticmethod def get_count(arr: Collection["MonsterModel"], *elements: "MonsterModel") -> str: if not arr: @@ -39,17 +41,21 @@ def get_count(arr: Collection["MonsterModel"], *elements: "MonsterModel") -> str return "0" return f"{round(100*count/len(arr), 3)}%" - @staticmethod - def embed(state: ClosableEmbedViewState, props: ShowStatsViewProps): + @classmethod + def embed(cls, state: ClosableEmbedViewState, props: ShowStatsViewProps): fields = [] - for mon, c in Counter(props.valid).most_common(6 if len(props.valid) == 6 else 5): + for mon, c in Counter(props.valid).most_common( + cls.MAX_EXPANDED_RESULTS if len(props.valid) == cls.MAX_EXPANDED_RESULTS + else cls.MAX_EXPANDED_RESULTS-1): fields.append(EmbedField(mon.name_en, Box( LabeledText("Net", ShowStatsView.get_count(props.total, mon)), LabeledText("Adj", ShowStatsView.get_count(props.adj, mon)), LabeledText("You", ShowStatsView.get_count(props.you, mon)) ), inline=True)) - if len(props.valid) > 6: - fields.append(EmbedField("... & More", Box(f"+ {len(props.valid) - 5} more monsters"), inline=True)) + if len(props.valid) > cls.MAX_EXPANDED_RESULTS: + fields.append(EmbedField("... & More", + Box(f"+ {len(props.valid)-cls.MAX_EXPANDED_RESULTS-1} more monsters"), + inline=True)) return EmbedView( EmbedMain( From b7c68496efd2b53d37f29bbc42d95a1464eebf8b Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 10 Jan 2022 09:21:52 -0800 Subject: [PATCH 1028/1220] Add ^adpem config maxrolls (#1542) --- crowddata/mixins/adpem/adpem.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index 021095fd..33202eda 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -36,7 +36,7 @@ class AdPEMStats(CogMixin): bot: Red def setup_self(self): - self.config.register_global(pulls=[]) + self.config.register_global(pulls=[], valid_lengths=[4]) self.config.register_user(opted_in=False, accounts=1, skipconf=False) async def red_get_data_for_user(self, *, user_id): @@ -68,7 +68,7 @@ async def reportyesterday(self, ctx, *, pulls): async def report_at_time(self, ctx, pulls, offset): pulls_split = pulls.split(',') - if len(pulls_split) != 4: + if len(pulls_split) not in await self.config.valid_lengths(): return await ctx.send(f"Please supply all of your pulls with commas in between them. You can use names or" f" numbers.\n\t" f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013, 618`\n\t" @@ -221,6 +221,21 @@ async def y_toggleskipconfirm(self, ctx): """Toggle if you want to confirm on skip/forgot""" await self.toggleskipconfirm(ctx) + @adpem.group(name="config") + @commands.is_owner() + async def v_config(self, ctx): + """Admin config""" + pass + + @v_config.command() + async def maxrolls(self, ctx, *lens: int): + """Change the valid number of pulls. + + If inputting multiple, separate with spaces + """ + await self.config.valid_lengths.set(lens) + await ctx.tick() + @adpem.command() async def showstats(self, ctx, *, query): dbcog: Any = ctx.bot.get_cog("DBCog") From f5fcf8b8dda2b274ae667cd9e7eb18171942f7fb Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 10 Jan 2022 12:27:12 -0600 Subject: [PATCH 1029/1220] Move ActiveSkillModel @properties to init (#1556) --- dbcog/models/active_skill_model.py | 39 +++++++----------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/dbcog/models/active_skill_model.py b/dbcog/models/active_skill_model.py index 037729f6..7311e335 100644 --- a/dbcog/models/active_skill_model.py +++ b/dbcog/models/active_skill_model.py @@ -1,4 +1,4 @@ -from typing import Any, Collection, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, Sequence, Union from .base_model import BaseModel @@ -15,13 +15,8 @@ def __init__(self, **kwargs): self.desc_templated_en = kwargs['desc_templated_en'] self.desc_templated_ko = kwargs['desc_templated_ko'] - @property - def desc(self): - return self.desc_en or self.desc_ja - - @property - def desc_templated(self): - return self.desc_templated_en or self.desc_templated_ja + self.desc = self.desc_en or self.desc_ja + self.desc_templated = self.desc_templated_en or self.desc_templated_ja def to_dict(self): return { @@ -50,17 +45,9 @@ def __init__(self, *, active_parts: Sequence[Union[ActivePartModel, Dict[str, An self.active_parts = active_parts - @property - def name(self): - return self.name_en or self.name_ja - - @property - def desc(self): - return self.desc_en or self.desc_ja - - @property - def desc_templated(self): - return self.desc_templated_en or self.desc_templated_ja + self.name = self.name_en or self.name_ja + self.desc = self.desc_en or self.desc_ja + self.desc_templated = self.desc_templated_en or self.desc_templated_ja def to_dict(self): return { @@ -96,17 +83,9 @@ def __init__(self, *, active_subskills: Sequence[Union[ActiveSubskillModel, Dict self.active_subskills = active_subskills - @property - def name(self): - return self.name_en or self.name_ja - - @property - def desc(self): - return self.desc_en or self.desc_ja - - @property - def desc_templated(self): - return self.desc_templated_en or self.desc_templated_ja + self.name = self.name_en or self.name_ja + self.desc = self.desc_en or self.desc_ja + self.desc_templated = self.desc_templated_en or self.desc_templated_ja def to_dict(self): return { From 4d27b6cecfdced0fd3a9ad61ab59c75aed163e07 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 10 Jan 2022 13:01:45 -0600 Subject: [PATCH 1030/1220] Use number_emoji_small (#1557) --- padinfo/view/components/base_id_main_view.py | 6 +++--- requirements.txt | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 73f942fc..b4135209 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -6,7 +6,7 @@ from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache -from tsutils.emoji import char_to_emoji +from tsutils.tsubaki import number_emoji_small from tsutils.enums import LsMultiplier from padinfo.common.emoji_map import get_awakening_emoji, get_emoji @@ -33,8 +33,8 @@ def _make_prefix(idx, compound_skill_type_id): return { 0: '', 1: emoji_cache.get_emoji('bd') + ' ', - 2: char_to_emoji(str(idx)) + ' ', - 3: char_to_emoji(str(idx)) + ' ' + 2: number_emoji_small(idx) + ' ', + 3: number_emoji_small(idx) + ' ' }.get(compound_skill_type_id) diff --git a/requirements.txt b/requirements.txt index 48852b9c..c48882c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ aioodbc discord discord-menu git+git://github.com/TsubakiBotPad/python-romkan +jinja2 networkx opencv-python Pillow From d2e0755492c08cfe1ced79b7421c2c69e4d08aff Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 10 Jan 2022 13:28:09 -0600 Subject: [PATCH 1031/1220] Use right hook arrow instead of right arrow (#1558) --- padinfo/view/components/base_id_main_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index b4135209..d0b22e13 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -46,7 +46,7 @@ class BaseIdMainView(BaseIdView, ABC): active_skill_type_texts = { 0: None, 1: f"[\N{GAME DIE} Random]", - 2: f"[\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16} Transforms]", + 2: f"[\N{RIGHTWARDS ARROW WITH HOOK}\N{VARIATION SELECTOR-16} Transforms]", 3: f"[\N{BLACK UNIVERSAL RECYCLING SYMBOL}\N{VARIATION SELECTOR-16} Cycles]" } From 0b8678887ac9a83a8a3c10d6bf0c281530c8fa04 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 11 Jan 2022 00:42:49 -0800 Subject: [PATCH 1032/1220] Add cooldowns to subskills (#1559) --- dbcog/models/active_skill_model.py | 9 +++---- dbcog/models/monster_model.py | 4 ++-- dbcog/monster_graph.py | 10 ++++---- padbuildimg/padbuildimg.py | 2 +- padinfo/view/components/base_id_main_view.py | 25 ++++++++++---------- padinfo/view/transforminfo.py | 2 +- 6 files changed, 27 insertions(+), 25 deletions(-) diff --git a/dbcog/models/active_skill_model.py b/dbcog/models/active_skill_model.py index 7311e335..742abc67 100644 --- a/dbcog/models/active_skill_model.py +++ b/dbcog/models/active_skill_model.py @@ -42,6 +42,7 @@ def __init__(self, *, active_parts: Sequence[Union[ActivePartModel, Dict[str, An self.desc_templated_ja = kwargs['desc_templated_ja'] self.desc_templated_en = kwargs['desc_templated_en'] self.desc_templated_ko = kwargs['desc_templated_ko'] + self.cooldown = kwargs['cooldown'] self.active_parts = active_parts @@ -78,8 +79,8 @@ def __init__(self, *, active_subskills: Sequence[Union[ActiveSubskillModel, Dict self.desc_official_en = kwargs['desc_official_en'] self.desc_official_ko = kwargs['desc_official_ko'] - self.turn_max = kwargs['turn_max'] - self.turn_min = kwargs['turn_min'] + self.cooldown_turns_max = kwargs['cooldown_turns_max'] + self.cooldown_turns_min = kwargs['cooldown_turns_min'] self.active_subskills = active_subskills @@ -98,8 +99,8 @@ def __eq__(self, other): if isinstance(other, ActiveSkillModel): return self.active_skill_id == other.active_skill_id \ and self.desc_en == other.desc_en \ - and self.turn_max == other.turn_max \ - and self.turn_min == other.turn_min + and self.cooldown_turns_max == other.cooldown_turns_max \ + and self.cooldown_turns_min == other.cooldown_turns_min elif isinstance(other, ActiveSubskillModel): return self.active_skill_id == other.active_subskill_id \ and self.desc_en == other.desc_en diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index 331528f6..36b426e7 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -214,8 +214,8 @@ def __init__(self, m: MonsterModel): self.active_name = active_skill.name.lower() if active_skill and active_skill.name else '' self.active_desc = active_skill.desc.lower() if active_skill and active_skill.desc else '' self.active = '{} {}'.format(self.active_name, self.active_desc) - self.active_min = active_skill.turn_min if active_skill else None - self.active_max = active_skill.turn_max if active_skill else None + self.active_min = active_skill.cooldown_turns_min if active_skill else None + self.active_max = active_skill.cooldown_turns_max if active_skill else None self.color = [m.attr1.name.lower()] self.hascolor = [c.name.lower() for c in [m.attr1, m.attr2] if c] diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 90c62c1e..89dcbeb5 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -81,8 +81,8 @@ act.desc_official_ja AS act_desc_official_ja, act.desc_official_ko AS act_desc_official_ko, act.desc_official_en AS act_desc_official_en, - act.turn_max, - act.turn_min, + act.cooldown_turns_max, + act.cooldown_turns_min, ass.active_subskill_id, ass.name_ja AS ass_name_ja, ass.name_en AS ass_name_en, @@ -93,6 +93,7 @@ ass.desc_templated_ja AS ass_desc_templated_ja, ass.desc_templated_en AS ass_desc_templated_en, ass.desc_templated_ko AS ass_desc_templated_ko, + ass.cooldown AS ass_cooldown, ap.active_part_id, ap.active_skill_type_id, ap.desc_ja AS ap_desc_ja, @@ -211,8 +212,8 @@ def build_graph(self, server: Server) -> MultiDiGraph: 'desc_official_ja': row.act_desc_official_ja, 'desc_official_ko': row.act_desc_official_ko, 'desc_official_en': row.act_desc_official_en, - 'turn_max': row.turn_max, - 'turn_min': row.turn_min, + 'cooldown_turns_max': row.cooldown_turns_max, + 'cooldown_turns_min': row.cooldown_turns_min, 'active_subskills': [] } @@ -229,6 +230,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: 'desc_templated_ja': row.ass_desc_templated_ja, 'desc_templated_en': row.ass_desc_templated_en, 'desc_templated_ko': row.ass_desc_templated_ko, + 'cooldown': row.ass_cooldown, 'active_parts': [] }) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 4aa3600a..6e2c8668 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -557,7 +557,7 @@ async def process_card(self, card_str, is_assist=False): 120 if card.limit_mult is not None and card.limit_mult > 1 else card.level ) if card.active_skill: - result_card['MAX_SLV'] = card.active_skill.turn_max - card.active_skill.turn_min + 1 + result_card['MAX_SLV'] = card.active_skill.cooldown_turns_max - card.active_skill.cooldown_turns_min + 1 else: result_card['MAX_SLV'] = 0 result_card['MAX_AWAKE'] = len(card.awakenings) - card.superawakening_count diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index d0b22e13..3a2124af 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -29,13 +29,12 @@ def _killer_latent_emoji(latent_name: str): return get_emoji('latent_killer_{}'.format(latent_name.lower())) -def _make_prefix(idx, compound_skill_type_id): - return { - 0: '', - 1: emoji_cache.get_emoji('bd') + ' ', - 2: number_emoji_small(idx) + ' ', - 3: number_emoji_small(idx) + ' ' - }.get(compound_skill_type_id) +def _make_prefix(idx, compound_skill_type_id, subskill): + if compound_skill_type_id == 1: + return emoji_cache.get_emoji('bd') + ' ' + elif compound_skill_type_id in (2, 3): + return number_emoji_small(idx) + (f' [{subskill.cooldown}cd] ' if idx != 1 else ' ') + return '' class BaseIdMainView(BaseIdView, ABC): @@ -87,20 +86,20 @@ def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["Monst return BoldText('Active Skill') if len(previous_transforms) == 0: - active_cd = "({} -> {})".format(active_skill.turn_max, active_skill.turn_min) + active_cd = "({} -> {})".format(active_skill.cooldown_turns_max, active_skill.cooldown_turns_min) else: skill_texts = [] previous_transforms.reverse() for i, mon in enumerate(previous_transforms): skill = mon.active_skill # we can assume skill is not None because the monster transforms - cooldown_text = '({}cd)'.format(str(skill.turn_max)) - if skill.turn_min != skill.turn_max: - cooldown_text = '{} -> {}'.format(skill.turn_min, skill.turn_max) + cooldown_text = '({}cd)'.format(str(skill.cooldown_turns_max)) + if skill.cooldown_turns_min != skill.cooldown_turns_max: + cooldown_text = '{} -> {}'.format(skill.cooldown_turns_min, skill.cooldown_turns_max) skill_texts.append( '{}{}'.format(get_emoji(cls.transform_emoji_names[i % len(cls.transform_emoji_names)]), cooldown_text)) - skill_texts.append('{} ({} cd)'.format(get_emoji(cls.up_emoji_name), m.active_skill.turn_max)) + skill_texts.append('{} ({} cd)'.format(get_emoji(cls.up_emoji_name), m.active_skill.cooldown_turns_max)) active_cd = ' '.join(skill_texts) return Box( @@ -127,7 +126,7 @@ def active_skill_text(cls, active_skill: Optional["ActiveSkillModel"], if active_skill is None: return 'None' - return "\n".join(_make_prefix(c, active_skill.compound_skill_type_id) + return "\n".join(_make_prefix(c, active_skill.compound_skill_type_id, subskill) + jinja2.Template(subskill.desc_templated).render(**jinja2_replacements) for c, subskill in enumerate(active_skill.active_subskills, 1)) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index c8db8dd0..ec3e23d2 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -138,7 +138,7 @@ def stats(cls, base_mon: "MonsterModel", transformed_mon: "MonsterModel"): @classmethod def transform_active_header(cls, m: "MonsterModel"): active_skill = m.active_skill - active_cd = '({} cd)'.format(active_skill.turn_min) if active_skill else 'None' + active_cd = '({} cd)'.format(active_skill.cooldown_turns_min) if active_skill else 'None' return Box( get_emoji(cls.up_emoji_name), BoldText('Transform Active Skill {}'.format(active_cd)), From 6bad8c06f6798a4fbfafa678471fe34eae2f8374 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 11 Jan 2022 02:46:23 -0600 Subject: [PATCH 1033/1220] Change transform emoji again (#1560) --- padinfo/view/components/base_id_main_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 3a2124af..14014832 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -45,7 +45,7 @@ class BaseIdMainView(BaseIdView, ABC): active_skill_type_texts = { 0: None, 1: f"[\N{GAME DIE} Random]", - 2: f"[\N{RIGHTWARDS ARROW WITH HOOK}\N{VARIATION SELECTOR-16} Transforms]", + 2: f"[\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE} Transforms]", 3: f"[\N{BLACK UNIVERSAL RECYCLING SYMBOL}\N{VARIATION SELECTOR-16} Cycles]" } From c04e453b35caef727f43e1cccfeb3030a1f45f40 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 11 Jan 2022 05:07:01 -0600 Subject: [PATCH 1034/1220] Bold subskill cd (#1561) --- padinfo/view/components/base_id_main_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 14014832..5b727e04 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -33,7 +33,7 @@ def _make_prefix(idx, compound_skill_type_id, subskill): if compound_skill_type_id == 1: return emoji_cache.get_emoji('bd') + ' ' elif compound_skill_type_id in (2, 3): - return number_emoji_small(idx) + (f' [{subskill.cooldown}cd] ' if idx != 1 else ' ') + return number_emoji_small(idx) + (f' **[{subskill.cooldown}cd]** ' if idx != 1 else ' ') return '' From 173a24a647f0e6d1aa002c3f97077c6f5084b207 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 11 Jan 2022 19:13:58 -0800 Subject: [PATCH 1035/1220] Allow group to be None (#1563) --- padevents/autoevent_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index 26e8dc02..51103a38 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -100,7 +100,7 @@ def event_matches_autoevent(self, event: Event, autoevent: Dict[str, Any]): return False if not autoevent.get('include3p', True) and event.clean_dungeon_name.startswith("Multiplayer"): return False - if autoevent.get('group', 'red') != event.group.lower(): + if autoevent.get('group') != event.group: return False elif autoevent.get('regex'): return re.search(autoevent['searchstr'], event.clean_dungeon_name) From ce5f6ce001f36b96915bfa377202422a5453a232 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Fri, 14 Jan 2022 03:06:44 -0500 Subject: [PATCH 1036/1220] Add padglobal eventswap command (#1562) --- padglobal/padglobal.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 5dd3fb61..581c93cc 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -328,6 +328,28 @@ async def append(self, ctx, command: str, *, addition): await ctx.send("Successfully appended to {}PAD command `{}`.".format("source " if alias else "", source_cmd if alias else corrected_cmd)) + @padglobal.command() + async def eventswap(self, ctx): + if 'currentevent' not in self.c_commands or 'nextevent' not in self.c_commands: + await ctx.send(f"Please populate both {inline('currentevent')} and {inline('nextevent')} first." + f" Cancelling.") + return + + current_text = self.c_commands['currentevent'] + next_text = self.c_commands['nextevent'] + + if not await get_user_confirmation(ctx, "Are you sure you want to update for the new event?"): + await ctx.send("Making no change to current and next event.") + return + + self.c_commands['lastevent'] = current_text + self.c_commands['currentevent'] = next_text + self.c_commands['nextevent'] = "N/A" + + json.dump(self.c_commands, open(self.file_path, 'w+')) + await ctx.send(f"Okay, I've updated {inline('lastevent')} and {inline('currentevent')} accordingly," + f" and cleared {inline('nextevent')}.") + async def _find_aliases(self, command: str): aliases = [] for cmd in self.c_commands: From aed841f620fd5782342c7bc048082f1c3e6d0d62 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 19 Jan 2022 11:27:37 -0700 Subject: [PATCH 1037/1220] Default group to red (#1566) --- padevents/autoevent_mixin.py | 2 +- padevents/padevents.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index 51103a38..8744baa8 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -54,7 +54,7 @@ async def do_autoevents(self): async with self.config.sent() as sent: sent[str((key, event.key, gid))] = time.time() - index = GROUPS.index(event.group) + index = GROUPS.index(event.group or 'red') channel = guild.get_channel(aep['channels'][index]) if channel is None: continue diff --git a/padevents/padevents.py b/padevents/padevents.py index aaf233a2..25367cf1 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -419,8 +419,8 @@ async def do_partial(self, ctx, server: Server, group: StarterGroup = None): group_to_active_event = {e.group: e for e in active_events} group_to_pending_event = {e.group: e for e in pending_events} - active_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) - pending_events.sort(key=lambda e: (GROUPS.index(e.group), e.open_datetime)) + active_events.sort(key=lambda e: (GROUPS.index(e.group or 'red'), e.open_datetime)) + pending_events.sort(key=lambda e: (GROUPS.index(e.group or 'red'), e.open_datetime)) if len(active_events) == 0 and len(pending_events) == 0: await ctx.send("No events available for " + server) From e12bc36512552c11ede205aa0d0b40dc9dda5a0c Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 20 Jan 2022 14:42:36 -0600 Subject: [PATCH 1038/1220] Change 'defense' text to shield emoji (#1567) --- padinfo/core/leader_skills.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index c14705b3..e42a4b24 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,5 +1,7 @@ import re +from padinfo.common.emoji_map import get_emoji + def _humanize_number(number, sigfigs=2): n = float("{0:.{1}g}".format(number, sigfigs)) @@ -57,7 +59,7 @@ def _format_number(val): def _format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): - resist_val = ' Resist {}%'.format(_format_number(100 * resist)) if resist else '' + resist_val = ' {}{}%'.format(get_emoji('defense'), _format_number(100 * resist)) if resist else '' ehp = ' [{}x eHP]'.format(_format_number(hp / (1 - resist))) if resist else '' From d7bd16dc1f5c8439ac34a52b62186304f729b594 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 23 Jan 2022 10:57:53 -0700 Subject: [PATCH 1039/1220] Fix autoevent matching function (#1570) --- padevents/autoevent_mixin.py | 4 ++-- padevents/padevents.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index 8744baa8..27234c75 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -100,9 +100,9 @@ def event_matches_autoevent(self, event: Event, autoevent: Dict[str, Any]): return False if not autoevent.get('include3p', True) and event.clean_dungeon_name.startswith("Multiplayer"): return False - if autoevent.get('group') != event.group: + if autoevent.get('group', 'red') != (event.group or 'red'): return False - elif autoevent.get('regex'): + if autoevent.get('regex'): return re.search(autoevent['searchstr'], event.clean_dungeon_name) else: return autoevent['searchstr'].lower() in event.clean_dungeon_name.lower() diff --git a/padevents/padevents.py b/padevents/padevents.py index 25367cf1..28e205fc 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -195,8 +195,10 @@ async def padevents(self, ctx): @padevents.command() @checks.is_owner() - async def testevent(self, ctx, server: Server, seconds: int = 0): + async def testevent(self, ctx, server: Server, seconds: int = 0, group='red'): server = server.value + if group.lower() not in ('red', 'blue', 'green'): + group = None dbcog: Any = self.bot.get_cog('DBCog') await dbcog.wait_until_ready() @@ -211,7 +213,7 @@ async def testevent(self, ctx, server: Server, seconds: int = 0): event_type_id=-1, start_timestamp=timestamp, end_timestamp=timestamp + 60, - group_name='red', + group_name=group and group.lower(), dungeon_model=DummyObject( name_en='fake_dungeon_name', clean_name_en='fake_dungeon_name', From d77e6cb5fb886924b1bc5fab836bb38a7bc1c329 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 23 Jan 2022 10:58:25 -0700 Subject: [PATCH 1040/1220] Fix ^pde active (#1569) --- padevents/autoevent_mixin.py | 2 +- padevents/events.py | 149 ++++++++++------------------------- padevents/padevents.py | 89 ++++++++++----------- 3 files changed, 86 insertions(+), 154 deletions(-) diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index 27234c75..909aa4be 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -65,7 +65,7 @@ async def do_autoevents(self): offsetstr = f"" try: timestr = humanize_timedelta(timedelta=event.close_datetime - event.open_datetime) - await channel.send(f"{event.name_and_modifier} starts {offsetstr}!" + await channel.send(f"{event.clean_dungeon_name} starts {offsetstr}!" f" It will be active for {timestr}. {ment}", allowed_mentions=discord.AllowedMentions(roles=True)) except Exception: diff --git a/padevents/events.py b/padevents/events.py index 3a86cd26..611b309e 100644 --- a/padevents/events.py +++ b/padevents/events.py @@ -1,6 +1,6 @@ import datetime from datetime import timedelta -from typing import Callable, List, TYPE_CHECKING +from typing import Callable, Collection, List, TYPE_CHECKING import itertools import pytz @@ -29,45 +29,11 @@ def __init__(self, scheduled_event: "ScheduledEventModel"): self.group = scheduled_event.group_name self.dungeon = scheduled_event.dungeon self.dungeon_name = self.dungeon.name_en if self.dungeon else 'unknown_dungeon' - self.event_name = '' self.clean_dungeon_name = self.dungeon.clean_name_en if self.dungeon else 'unknown_dungeon' - self.clean_event_name = self.event_name.replace('!', '').replace(' ', '') - - self.name_and_modifier = self.clean_dungeon_name - if self.clean_event_name != '': - self.name_and_modifier += ', ' + self.clean_event_name self.dungeon_type = DungeonType(self.dungeon.dungeon_type) if self.dungeon else DungeonType.Unknown - def start_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.open_datetime - now).total_seconds() - - def end_from_now_sec(self): - now = datetime.datetime.now(pytz.utc) - return (self.close_datetime - now).total_seconds() - - def is_started(self): - """True if past the open time for the event.""" - return self.start_from_now_sec() <= 0 - - def is_finished(self): - """True if past the close time for the event.""" - return self.end_from_now_sec() <= 0 - - def is_active(self): - """True if between open and close time for the event.""" - return self.is_started() and not self.is_finished() - - def is_pending(self): - """True if event has not started.""" - return not self.is_started() - - def is_available(self): - """True if event has not finished.""" - return not self.is_finished() - @property def event_length(self) -> EventLength: # This is a little off. I don't know how exact these things are. @@ -80,25 +46,29 @@ def event_length(self) -> EventLength: return EventLength.daily return EventLength.limited - def tostr(self): - return self.open_datetime.strftime("%Y-%m-%d %H:%M") + "," + self.close_datetime.strftime("%Y-%m-%d %H:%M") \ - + "," + self.group + "," + self.dungeon_name + def start_from_now_sec(self) -> float: + now = datetime.datetime.now(pytz.utc) + return (self.open_datetime - now).total_seconds() + + def end_from_now_sec(self) -> float: + now = datetime.datetime.now(pytz.utc) + return (self.close_datetime - now).total_seconds() - def start_pst(self): - tz = pytz.timezone('US/Pacific') - return self.open_datetime.astimezone(tz) + def is_started(self): + """True if past the open time for the event.""" + return self.start_from_now_sec() <= 0 - def start_est(self): - tz = pytz.timezone('US/Eastern') - return self.open_datetime.astimezone(tz) + def is_finished(self): + """True if past the close time for the event.""" + return self.end_from_now_sec() <= 0 - def start_from_now(self): - return "" + def start_from_now_discord(self) -> str: + return f"" - def end_from_now(self): - return "" + def end_from_now_discord(self) -> str: + return f"" - def end_from_now_full_min(self): + def end_from_now_full_min(self) -> str: days, sec = divmod(self.end_from_now_sec(), 86400) hours, sec = divmod(sec, 3600) minutes, sec = divmod(sec, 60) @@ -110,88 +80,55 @@ def end_from_now_full_min(self): else: return '{:2}m'.format(int(minutes)) - def to_guerrilla_str(self): - return self.start_pst().strftime("%H:%M") - - def to_date_str(self): - return self.server + "," + self.group + "," + self.start_pst().strftime("%Y-%m-%d %H:%M") + "," + \ - self.start_est().strftime("%Y-%m-%d %H:%M") + "," + self.start_from_now() - - def group_short_name(self): - return self.group.upper().replace('RED', 'R').replace('BLUE', 'B').replace('GREEN', 'G') - def group_long_name(self): return self.group.upper() if self.group is not None else "UNGROUPED" def to_partial_event(self, pe): - group = self.group_short_name() + group = self.group_long_name()[0] if self.is_started(): - return "`" + group + " " + self.name_and_modifier + " " * ( - max(24 - len(self.name_and_modifier), 0)) + "-`" + self.end_from_now() + return "`" + group + " " + self.clean_dungeon_name + " " * ( + max(24 - len(self.clean_dungeon_name), 0)) + "-`" + self.end_from_now_discord() else: - return "`" + group + " " + self.name_and_modifier + " " * ( - max(24 - len(self.name_and_modifier), 0)) + "-`" + self.start_from_now() + return "`" + group + " " + self.clean_dungeon_name + " " * ( + max(24 - len(self.clean_dungeon_name), 0)) + "-`" + self.start_from_now_discord() + + def __repr__(self): + return f"Event<{self.clean_dungeon_name} ({self.group} {self.server})>" class EventList: - def __init__(self, event_list: List[Event]): + def __init__(self, event_list: Collection[Event]): self.event_list = event_list - def with_func(self, func: Callable[[Event], bool], exclude=False): - if exclude: - return EventList(list(itertools.filterfalse(func, self.event_list))) - else: - return EventList(list(filter(func, self.event_list))) - - def with_server(self, server): - return self.with_func(lambda e: e.server == normalize_server_name(server)) + def with_func(self, func: Callable[[Event], bool]) -> "EventList": + return EventList(list(filter(func, self.event_list))) - def with_type(self, event_type): - return self.with_func(lambda e: e.event_type == event_type) + def with_server(self, *servers): + servers = {normalize_server_name(s) for s in servers} + return self.with_func(lambda e: e.server in servers) - def in_type(self, event_types): + def with_type(self, *event_types): return self.with_func(lambda e: e.event_type in event_types) - def with_length(self, event_length): - return self.with_func(lambda e: e.event_length == event_length) - - def in_length(self, event_lengths): + def with_length(self, *event_lengths): return self.with_func(lambda e: e.event_length in event_lengths) - def with_dungeon_type(self, dungeon_type, exclude=False): - return self.with_func(lambda e: e.dungeon_type == dungeon_type, exclude) - - def in_dungeon_type(self, dungeon_types, exclude=False): - return self.with_func(lambda e: e.dungeon_type in dungeon_types, exclude) + def with_dungeon_type(self, *dungeon_types): + return self.with_func(lambda e: e.dungeon_type in dungeon_types) - def is_grouped(self, exclude=False): - return self.with_func(lambda e: e.group is not None, exclude) - - def with_name_contains(self, name, exclude=False): - return self.with_func(lambda e: name.lower() in e.dungeon_name.lower(), exclude) - - def started_only(self): - return self.with_func(lambda e: e.is_started()) + def is_grouped(self): + return self.with_func(lambda e: e.group is not None) def pending_only(self): - return self.with_func(lambda e: e.is_pending()) + return self.with_func(lambda e: not e.is_started()) def active_only(self): - return self.with_func(lambda e: e.is_active()) - - def available_only(self): - return self.with_func(lambda e: e.is_available()) + return self.with_func(lambda e: e.is_started() and not e.is_finished()) def today_only(self, server: str): server_timezone = SERVER_TIMEZONES[normalize_server_name(server)] - today = datetime.datetime.now(server_timezone).day - return self.with_func(lambda e: e.open_datetime.astimezone(server_timezone).day == today) - - def items_by_open_time(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.open_datetime, e.dungeon_name)), reverse=reverse)) - - def items_by_close_time(self, reverse=False): - return list(sorted(self.event_list, key=(lambda e: (e.close_datetime, e.dungeon_name)), reverse=reverse)) + today = datetime.datetime.now(server_timezone).date() + return self.with_func(lambda e: e.open_datetime.astimezone(server_timezone).date() == today) def __iter__(self): return iter(self.event_list) diff --git a/padevents/padevents.py b/padevents/padevents.py index 28e205fc..7b60a300 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -148,10 +148,10 @@ async def do_eventloop(self): role_name = f'{event.server}_group_{event.group_long_name()}' role = channel.guild.get_role(role_name) if role and role.mentionable: - message = f"{role.mention} {event.name_and_modifier} is starting" + message = f"{role.mention} {event.clean_dungeon_name} is starting" else: message = box(f"Server {event.server}, group {event.group_long_name()}:" - f" {event.name_and_modifier}") + f" {event.clean_dungeon_name}") with suppress(discord.Forbidden): await channel.send(message, allowed_mentions=discord.AllowedMentions(roles=True)) @@ -288,11 +288,11 @@ def make_active_text(self, server): active_events = server_events.active_only() events_today = server_events.today_only(server) - active_special = active_events.in_dungeon_type([DungeonType.Special]) + active_special = active_events.with_dungeon_type(DungeonType.Special) - msg = server + " Events - " + datetime.datetime.now(SERVER_TIMEZONES[server]).strftime('%A, %B%e') + msg = server + " Events - " + datetime.datetime.now(SERVER_TIMEZONES[server]).strftime('%A, %B %-e') - ongoing_events = active_events.in_length([EventLength.weekly, EventLength.special]) + ongoing_events = active_events.with_length(EventLength.weekly, EventLength.special) if ongoing_events: msg += "\n\n" + self.make_active_output('Ongoing Events', ongoing_events) @@ -312,7 +312,7 @@ def make_daily_output(self, table_name, event_list): tbl.vrules = prettytable.NONE tbl.align[table_name] = "l" for e in event_list: - tbl.add_row([e.name_and_modifier]) + tbl.add_row([e.clean_dungeon_name]) return tbl.get_string() def make_active_output(self, table_name, event_list): @@ -322,68 +322,63 @@ def make_active_output(self, table_name, event_list): tbl.align[table_name] = "l" tbl.align["Time"] = "r" for e in event_list: - tbl.add_row([e.end_from_now_full_min().strip(), e.name_and_modifier]) + tbl.add_row([e.end_from_now_full_min().strip(), e.clean_dungeon_name]) return tbl.get_string() - def make_active_guerrilla_output(self, table_name, event_list): + def make_active_guerrilla_output(self, table_name: str, event_list: EventList) -> str: tbl = prettytable.PrettyTable([table_name, "Group", "Time"]) tbl.hrules = prettytable.HEADER tbl.vrules = prettytable.NONE tbl.align[table_name] = "l" tbl.align["Time"] = "r" for e in event_list: - tbl.add_row([e.name_and_modifier, e.group, e.end_from_now_full_min().strip()]) + tbl.add_row([e.clean_dungeon_name, e.group, e.end_from_now_full_min().strip()]) return tbl.get_string() - def make_full_guerrilla_output(self, table_name, event_list, starter_guerilla=True): - events_by_name = defaultdict(list) + def make_full_guerrilla_output(self, table_name, event_list): + events_by_name = defaultdict(set) for event in event_list: - events_by_name[event.name_and_modifier].append(event) + events_by_name[event.clean_dungeon_name].add(event) - rows = list() - grps = ["RED", "BLUE", "GREEN"] if starter_guerilla else ["A", "B", "C", "D", "E"] + rows = [] for name, events in events_by_name.items(): events = sorted(events, key=lambda e: e.open_datetime) - events_by_group = defaultdict(list) + + events_by_group = {group: [] for group in GROUPS} for event in events: if event.group is not None: - events_by_group[event.group.upper()].append(event) + events_by_group[event.group].append(event) else: - for group in grps: + for group in GROUPS: events_by_group[group].append(event) - done = False - while not done: - did_work = False - row = list() - row.append(name) - for g in grps: - grp_list = events_by_group[g] - if len(grp_list) == 0: - row.append("") + while True: + row = [] + for group in GROUPS: + if len(events_by_group[group]) == 0: + row.append('') else: - did_work = True - event = grp_list.pop(0) - row.append(event.to_guerrilla_str()) - - if did_work and len(set(row)) == 2: # One for Header and all three times are same - rows.append([row[0], row[1], '=', '=']) - elif did_work: - rows.append(row) - else: - done = True + # Get the timestamp of the earliest event in this group in PST + start = events_by_group[group].pop(0).open_datetime.astimezone(pytz.timezone('US/Pacific')) + row.append(start.strftime("%H:%M")) - col1 = table_name - tbl = prettytable.PrettyTable([col1] + grps) - tbl.align[col1] = "l" - tbl.hrules = prettytable.HEADER - tbl.vrules = prettytable.ALL + if not any(row): + break - for r in rows: - tbl.add_row(r) + if row[0] == row[1] == row[2]: + rows.append([name, row[0], '=', '=']) + else: + rows.append([name] + row) header = "Times are shown in Pacific Time\n= means same for all groups\n" - return header + tbl.get_string() + "\n" + table = prettytable.PrettyTable([table_name, 'Red', 'Blue', 'Green']) + table.align[table_name] = "l" + table.hrules = prettytable.HEADER + table.vrules = prettytable.ALL + for r in rows: + table.add_row(r) + + return header + table.get_string() + "\n" @commands.command(aliases=['events']) async def eventsna(self, ctx, group: StarterGroup = None): @@ -408,11 +403,11 @@ async def do_partial(self, ctx, server: Server, group: StarterGroup = None): events = EventList(self.events) events = events.with_server(server) - events = events.in_dungeon_type([DungeonType.SoloSpecial, DungeonType.Special]) + events = events.with_dungeon_type(DungeonType.SoloSpecial, DungeonType.Special) events = events.is_grouped() - active_events = events.active_only().items_by_open_time(reverse=True) - pending_events = events.pending_only().items_by_open_time(reverse=True) + active_events = sorted(events.active_only(), key=lambda e: (e.open_datetime, e.dungeon_name), reverse=True) + pending_events = sorted(events.pending_only(), key=lambda e: (e.open_datetime, e.dungeon_name), reverse=True) if group is not None: active_events = [e for e in active_events if e.group == group.lower()] From bc7d5f4f65d96c6d84ad3375d3b37434f2dfd8a3 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 24 Jan 2022 01:37:06 -0600 Subject: [PATCH 1041/1220] Allow less than max pulls (#1571) --- crowddata/mixins/adpem/adpem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index 33202eda..0da467d0 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -68,7 +68,8 @@ async def reportyesterday(self, ctx, *, pulls): async def report_at_time(self, ctx, pulls, offset): pulls_split = pulls.split(',') - if len(pulls_split) not in await self.config.valid_lengths(): + # TODO: support sending pulls throughout the day, and just have a single max # of pulls + if len(pulls_split) > max(await self.config.valid_lengths()): return await ctx.send(f"Please supply all of your pulls with commas in between them. You can use names or" f" numbers.\n\t" f"Valid input: `{ctx.prefix}adpem report 618, 3719, 3600, 3013, 618`\n\t" From ce41e327857bc07251b1d35fc6cccab8261e927e Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 26 Jan 2022 21:50:06 -0600 Subject: [PATCH 1042/1220] shield_amount emoji (#1574) --- padinfo/core/leader_skills.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index e42a4b24..fb0c97eb 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -59,7 +59,7 @@ def _format_number(val): def _format_ls_text(hp, atk, rcv, resist=0, combo=0, fua=0, mfua=0, te=0): - resist_val = ' {}{}%'.format(get_emoji('defense'), _format_number(100 * resist)) if resist else '' + resist_val = ' {}{}%'.format(get_emoji('shield_amount'), _format_number(100 * resist)) if resist else '' ehp = ' [{}x eHP]'.format(_format_number(hp / (1 - resist))) if resist else '' From 66d1facf1f92e02331aab3c00c60151b6b59ffb7 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 28 Jan 2022 03:09:30 -0700 Subject: [PATCH 1043/1220] Add depth to the index and priority tuple (#1577) --- dbcog/find_monster.py | 2 +- dbcog/monster_graph.py | 14 ++++++++++++++ dbcog/monster_index.py | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index 527c0eec..f2c23e9c 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -236,7 +236,7 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = -self.dbcog.database.graph.get_base_id(monster), monster.on_na if self.flags['na_prio'] else True, not monster.is_equip, - monster.rarity, + self.dbcog.database.graph.get_monster_depth(monster), monster.monster_no_na) def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 89dcbeb5..5008b7d6 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -706,6 +706,20 @@ def get_next_transform(self, monster: MonsterModel) -> Optional[MonsterModel]: nt = self.get_next_transform_id(monster) return nt and self.get_monster(nt, server=monster.server_priority) + def get_monster_depth(self, monster: MonsterModel) -> float: + if monster.monster_id == 5802: + # DMG sucks! + return 0 + + depth = 0.0 + while None is not (prev := self.get_prev_transform(monster)) \ + and prev.monster_id < monster.monster_id: + monster = prev + depth += .01 + while None is not (monster := self.get_prev_evolution(monster)): + depth += 1 + return depth + def evo_mats(self, monster: MonsterModel) -> List[MonsterModel]: evo = self.get_evolution(monster) if evo is None: diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 84ca6962..9efe0660 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -411,6 +411,8 @@ async def get_modifiers(self, monster: MonsterModel): lambda m: (m.name_ja.startswith('ドット') or m.name_en.startswith('pixel') or self.graph.true_evo_type(m).value == "Pixel"), else_mods=EVO_MAP[EvoTypes.NONPIXEL]) + # Depth + modifiers.add(f"{int(self.graph.get_monster_depth(monster))}depth") # Awakenings for aw in monster.awakenings: From c698c9ab02625cd15c7fae82dc9dc5e4595462cd Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 28 Jan 2022 22:57:01 -0700 Subject: [PATCH 1044/1220] Add adjusted rarity calculation (#1579) --- dbcog/find_monster.py | 2 +- dbcog/monster_graph.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index f2c23e9c..fcc55d3d 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -236,7 +236,7 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = -self.dbcog.database.graph.get_base_id(monster), monster.on_na if self.flags['na_prio'] else True, not monster.is_equip, - self.dbcog.database.graph.get_monster_depth(monster), + self.dbcog.database.graph.get_adjusted_rarity(monster), monster.monster_no_na) def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 5008b7d6..4e2b835b 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -164,7 +164,7 @@ SERVER_ID_WHERE_CONDITION = " AND server_id = {}" -class MonsterGraph(object): +class MonsterGraph: def __init__(self, database: DBCogDatabase, debug_monster_ids: Optional[List[int]] = None): self.issues = [] self.debug_monster_ids = debug_monster_ids @@ -720,6 +720,11 @@ def get_monster_depth(self, monster: MonsterModel) -> float: depth += 1 return depth + def get_adjusted_rarity(self, monster: MonsterModel) -> float: + # After the revo/srevo changes, rarity doesn't work, and Mega GFEs means that depth doesn't work + # The max of base rarity + the depth or the current rarity. This fixes revo rarities being nerfed + return max(self.get_base_monster(monster).rarity + self.get_monster_depth(monster), monster.rarity) + def evo_mats(self, monster: MonsterModel) -> List[MonsterModel]: evo = self.get_evolution(monster) if evo is None: From 9d50d31315b2a956e91f286e345e0e6fe3eba251 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 28 Jan 2022 23:04:25 -0700 Subject: [PATCH 1045/1220] Identify evo gems by name, not shop (#1580) --- dbcog/monster_graph.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 4e2b835b..f7341c69 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -848,7 +848,8 @@ def get_monster_from_evo_gem(self, monster: MonsterModel) -> Optional[MonsterMod return self.get_monster(self._get_edge_or_none(monster, "evo_gem_of"), server=monster.server_priority) def monster_is_evo_gem(self, monster: MonsterModel) -> bool: - return bool(self.get_monster_from_evo_gem(monster)) + # Evo gems can no longer be calculated via exchange as of Pixel Volcano Dragon + return monster.name_en.endswith("'s Gem") or monster.name_ja.endswith("の希石") def material_of_ids(self, monster: MonsterModel) -> List[int]: return sorted(self._get_edges(monster, 'material_of')) From 8d625e69e9f66c2c7a8ddcf51829b64ae87343f4 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 1 Feb 2022 00:27:50 -0600 Subject: [PATCH 1046/1220] Move MonsterImage to tsutils (#1584) --- crowddata/mixins/adpem/view/show_stats.py | 4 +- padinfo/view/awakening_help.py | 4 +- padinfo/view/button_info.py | 4 +- padinfo/view/components/monster/image.py | 46 ----------------------- padinfo/view/evos.py | 4 +- padinfo/view/experience_curve.py | 4 +- padinfo/view/id.py | 4 +- padinfo/view/links.py | 4 +- padinfo/view/lookup.py | 4 +- padinfo/view/materials.py | 4 +- padinfo/view/pantheon.py | 4 +- padinfo/view/pic.py | 12 +++--- padinfo/view/transforminfo.py | 4 +- 13 files changed, 28 insertions(+), 74 deletions(-) delete mode 100644 padinfo/view/components/monster/image.py diff --git a/crowddata/mixins/adpem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py index f0d2cb5b..c4342bd7 100644 --- a/crowddata/mixins/adpem/view/show_stats.py +++ b/crowddata/mixins/adpem/view/show_stats.py @@ -8,7 +8,7 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.closable_embed import ClosableEmbedViewState -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -66,7 +66,7 @@ def embed(cls, state: ClosableEmbedViewState, props: ShowStatsViewProps): LabeledText("You", ShowStatsView.get_count(props.you, *props.valid)) ) ), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(props.most_commmon)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(props.most_commmon.monster_id)), embed_fields=fields, embed_footer=embed_footer_with_state(state) ) diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 47dd8afe..3d147dc8 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -10,7 +10,7 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.common import get_awoken_skill_description from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -78,7 +78,7 @@ def embed(state, props: AwakeningHelpViewProps): embed_author=EmbedAuthor( MonsterHeader.long_v2(monster).to_markdown(), puzzledragonx(monster), - MonsterImage.icon(monster) + MonsterImage.icon(monster.monster_id) ), embed_footer=embed_footer_with_state(state), embed_fields=fields diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index a84c56f0..62bc69f9 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -14,7 +14,7 @@ ButtonInfoResult from padinfo.view.components.evo_scroll_mixin import EvoScrollView, EvoScrollViewState, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -208,7 +208,7 @@ def embed(cls, state: ButtonInfoViewState): embed_author=EmbedAuthor( MonsterHeader.long_v2(monster).to_markdown(), puzzledragonx(monster), - MonsterImage.icon(monster) + MonsterImage.icon(monster.monster_id) ), embed_footer=embed_footer_with_state(state), embed_fields=fields diff --git a/padinfo/view/components/monster/image.py b/padinfo/view/components/monster/image.py deleted file mode 100644 index abf2ea6d..00000000 --- a/padinfo/view/components/monster/image.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import TYPE_CHECKING - - -from discordmenu.embed.components import EmbedThumbnail -from tsutils.tsubaki import CLOUDFRONT_URL - -if TYPE_CHECKING: - from dbcog.models.monster_model import MonsterModel - -MEDIA_PATH = CLOUDFRONT_URL + '/media/' -ICON_TEMPLATE = MEDIA_PATH + 'icons/{0:05d}.png' -RPAD_PIC_TEMPLATE = MEDIA_PATH + 'portraits/{0:05d}.png?cachebuster=2' -VIDEO_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.mp4' -GIF_TEMPLATE = MEDIA_PATH + 'animated_portraits/{0:05d}.gif' -ORB_SKIN_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}.png' -ORB_SKIN_CB_TEMPLATE = MEDIA_PATH + 'orb_skins/jp/{0:03d}cb.png' - - -class MonsterImage: - @staticmethod - def icon(m: "MonsterModel"): - return ICON_TEMPLATE.format(m.monster_id) - - @staticmethod - def picture(m: "MonsterModel"): - return RPAD_PIC_TEMPLATE.format(m.monster_id) - - @staticmethod - def video(m: "MonsterModel"): - return VIDEO_TEMPLATE.format(m.monster_no_jp) - - @staticmethod - def gif(m: "MonsterModel"): - return GIF_TEMPLATE.format(m.monster_no_jp) - - @staticmethod - def orb_skin(m: "MonsterModel"): - return ORB_SKIN_TEMPLATE.format(m.orb_skin_id) - - @staticmethod - def orb_skin_colorblind(m: "MonsterModel"): - return ORB_SKIN_CB_TEMPLATE.format(m.orb_skin_id) - - -def monster_thumbnail(m: "MonsterModel"): - return EmbedThumbnail(MonsterImage.icon(m)) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 008236ed..ae8ea840 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -12,7 +12,7 @@ from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -107,6 +107,6 @@ def embed(cls, state: EvosViewState): state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), url=puzzledragonx(state.monster)), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index 79511e72..b280ebf3 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -11,7 +11,7 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -125,7 +125,7 @@ def embed(state, props: ExperienceCurveViewProps): + (trunc_humanize(props.monster.exp_curve) if props.monster.exp_curve else "no") + f' curve)'), ), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(props.monster)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(props.monster.monster_id)), embed_fields=[ EmbedField( title='Exact', diff --git a/padinfo/view/id.py b/padinfo/view/id.py index b377e7df..44adb56b 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -15,7 +15,7 @@ from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -295,6 +295,6 @@ def embed(cls, state: IdViewState): state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), url=puzzledragonx(m)), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 254f25a2..f14b3353 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -7,7 +7,7 @@ from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -33,4 +33,4 @@ def embed(m: "MonsterModel", color): title=MonsterHeader.long_v2(m).to_markdown(), description=LinksView.linksbox(m), url=puzzledragonx(m)), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m))) + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index f5db5128..aad80d35 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -5,7 +5,7 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -21,4 +21,4 @@ def embed(m: "MonsterModel", color): embed_author=EmbedAuthor( MonsterHeader.long_v2(m).to_markdown(), puzzledragonx(m), - MonsterImage.icon(m))) + MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index a80c849c..be160f1a 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -14,7 +14,7 @@ from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -161,7 +161,7 @@ def embed(cls, state: MaterialsViewState): state.is_jp_buffed).to_markdown(), url=puzzledragonx(state.monster) ), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=[f for f in [ mat_use_field(state.mats, "Evo materials") diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index bbf7a34a..5e02c06e 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -14,7 +14,7 @@ from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -178,5 +178,5 @@ def embed(cls, state: PantheonViewState): url=puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), embed_fields=fields, - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), ) diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 8f17135c..709ffbbe 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -8,7 +8,7 @@ from padinfo.view.base import BaseIdView from padinfo.view.components.evo_scroll_mixin import EvoScrollView from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -27,23 +27,23 @@ class PicView(BaseIdView, EvoScrollView): @classmethod def embed(cls, state: PicViewState): - url = MonsterImage.picture(state.monster) + url = MonsterImage.picture(state.monster.monster_id) animated = state.monster.has_animation fields = [EmbedField( 'Extra Links', Box( Box( Text('Animation:'), - LinkedText('(MP4)', MonsterImage.video(state.monster)), + LinkedText('(MP4)', MonsterImage.video(state.monster.monster_no_jp)), Text('|'), - LinkedText('(GIF)', MonsterImage.gif(state.monster)), + LinkedText('(GIF)', MonsterImage.gif(state.monster.monster_no_jp)), delimiter=' ' ) if animated else None, Box( Text('Orb Skin:'), - LinkedText('Regular', MonsterImage.orb_skin(state.monster)), + LinkedText('Regular', MonsterImage.orb_skin(state.monster.orb_skin_id)), Text('|'), - LinkedText('Color Blind', MonsterImage.orb_skin_colorblind(state.monster)), + LinkedText('Color Blind', MonsterImage.orb_skin_colorblind(state.monster.orb_skin_id)), delimiter=' ' ) if state.monster.orb_skin_id else None, ) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index ec3e23d2..d27b8c80 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -14,7 +14,7 @@ from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.monster.header import MonsterHeader -from padinfo.view.components.monster.image import MonsterImage +from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -227,7 +227,7 @@ def embed(cls, state: TransformInfoViewState): state.is_jp_buffed).to_markdown(), url=puzzledragonx(transformed_mon) ), - embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=fields ) From 7c9d2c9ef2cbcbca5dea88f79d80a8793afd573f Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 31 Jan 2022 23:43:54 -0700 Subject: [PATCH 1047/1220] Coalesce repetitive events (#1582) --- padevents/autoevent_mixin.py | 2 +- padevents/padevents.py | 38 ++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index 909aa4be..89196946 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -34,7 +34,7 @@ def user_is_donor(ctx, only_patron=False): class AutoEvent: bot: Red config: Config - events: List[Event] + events: Set[Event] started_events: Set[Event] async def do_autoevents(self): diff --git a/padevents/padevents.py b/padevents/padevents.py index 7b60a300..7878b8d5 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -6,7 +6,7 @@ from contextlib import suppress from datetime import timedelta from io import BytesIO -from typing import Any, NoReturn, Optional +from typing import Any, Iterable, NoReturn, Optional, Set import discord import prettytable @@ -42,7 +42,7 @@ def __init__(self, bot, *args, **kwargs): self.config.register_user(dmevents=[]) # Load event data - self.events = list() + self.events = set() self.started_events = set() self.fake_uid = -time.time() @@ -67,7 +67,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): def cog_unload(self): # Manually nulling out database because the GC for cogs seems to be pretty shitty - self.events = list() + self.events = set() self.started_events = set() self._event_loop.cancel() self._refresh_loop.cancel() @@ -120,14 +120,14 @@ async def refresh_data(self): await dbcog.wait_until_ready() scheduled_events = dbcog.database.get_all_events() - new_events = [] + new_events = set() for se in scheduled_events: try: - new_events.append(Event(se)) + new_events.add(Event(se)) except Exception as ex: logger.exception("Refresh error:") - self.events = new_events + self.events = self.coalesce_event_data(new_events) self.started_events = {ev.key for ev in new_events if ev.is_started()} async with self.config.sent() as seen: for key, value in [*seen.items()]: @@ -221,7 +221,7 @@ async def testevent(self, ctx, server: Server, seconds: int = 0, group='red'): dungeon_id=1, ) ) - self.events.append(Event(te)) + self.events.add(Event(te)) await ctx.tick() @padevents.command() @@ -449,10 +449,22 @@ def get_most_recent_day_change(self): else: return "JP" + def coalesce_event_data(self, events: Iterable[Event]) -> Set[Event]: + all_events = set() -def make_channel_reg(channel_id, server): - server = normalize_server_name(server) - return { - "channel_id": channel_id, - "server": server - } + grouped = defaultdict(lambda: {}) + for event in events: + if event.group is None: + all_events.add(event) + continue + key = (event.open_datetime, event.close_datetime, event.server, event.dungeon.dungeon_id) + grouped[key][event.group] = event + + for _, grouped_events in grouped.items(): + if len(grouped_events) != 3: + all_events.update(grouped_events.values()) + continue + grouped_events['red'].group = None + all_events.add(grouped_events['red']) + + return all_events From f4a1e2d4eeec8e6f966a94ff340365b46fd27378 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 1 Feb 2022 00:56:12 -0600 Subject: [PATCH 1048/1220] Use tsutils ClosableEmbedMenu for which (#1585) --- crowddata/mixins/adpem/view/show_stats.py | 3 +-- padglobal/menu/closable_embed.py | 33 ++++------------------- padglobal/menu/menu_map.py | 5 ++-- padglobal/padglobal.py | 2 +- padglobal/view/closable_embed.py | 12 --------- 5 files changed, 10 insertions(+), 45 deletions(-) delete mode 100644 padglobal/view/closable_embed.py diff --git a/crowddata/mixins/adpem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py index c4342bd7..fcafe509 100644 --- a/crowddata/mixins/adpem/view/show_stats.py +++ b/crowddata/mixins/adpem/view/show_stats.py @@ -1,5 +1,5 @@ from collections import Counter -from typing import Collection, TYPE_CHECKING, List +from typing import Collection, TYPE_CHECKING from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedThumbnail, EmbedField @@ -7,7 +7,6 @@ from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.closable_embed import ClosableEmbedViewState - from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: diff --git a/padglobal/menu/closable_embed.py b/padglobal/menu/closable_embed.py index 34b602b7..00a5a29e 100644 --- a/padglobal/menu/closable_embed.py +++ b/padglobal/menu/closable_embed.py @@ -1,32 +1,9 @@ -from discordmenu.embed.control import EmbedControl -from discordmenu.embed.menu import EmbedMenu -from tsutils.menu.components.panes import MenuPanes +from tsutils.menu.closable_embed_base import ClosableEmbedMenuBase -from padglobal.view.closable_embed import ClosableEmbedViewState from padglobal.view.which import WhichView -view_types = { - WhichView.VIEW_TYPE: WhichView -} - -class ClosableEmbedMenu: - MENU_TYPE = 'ClosableEmbedMenu' - message = None - - @staticmethod - def menu(): - embed = EmbedMenu({}, ClosableEmbedMenu.message_control) - return embed - - @staticmethod - def message_control(state: ClosableEmbedViewState): - view = view_types[state.view_type] - return EmbedControl( - [view.embed(state, state.props)], - [] - ) - - -class ClosableEmbedMenuPanes(MenuPanes): - pass +class ClosableEmbedMenu(ClosableEmbedMenuBase): + view_types = { + WhichView.VIEW_TYPE: WhichView + } diff --git a/padglobal/menu/menu_map.py b/padglobal/menu/menu_map.py index 1f8e2a01..96d502f4 100644 --- a/padglobal/menu/menu_map.py +++ b/padglobal/menu/menu_map.py @@ -1,6 +1,7 @@ -from padglobal.menu.closable_embed import ClosableEmbedMenu, ClosableEmbedMenuPanes +from tsutils.menu.closable_embed_base import ClosableEmbedMenuPanes +from padglobal.menu.closable_embed import ClosableEmbedMenu padglobal_menu_map = { - ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), + ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes) } diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 581c93cc..ee2dab75 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -19,13 +19,13 @@ from tsutils.emoji import fix_emojis_for_server, replace_emoji_names_with_code from tsutils.formatting import clean_global_mentions, strip_right_multiline from tsutils.json_utils import safe_read_json +from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.time import NA_TIMEZONE from tsutils.tsubaki import CLOUDFRONT_URL from tsutils.user_interaction import get_user_confirmation, get_user_reaction from padglobal.menu.closable_embed import ClosableEmbedMenu from padglobal.menu.menu_map import padglobal_menu_map -from padglobal.view.closable_embed import ClosableEmbedViewState from padglobal.view.which import UNKNOWN_EDIT_TIMESTAMP, WhichView, WhichViewProps logger = logging.getLogger('red.padbot-cogs.padglobal') diff --git a/padglobal/view/closable_embed.py b/padglobal/view/closable_embed.py deleted file mode 100644 index 26e5f8d7..00000000 --- a/padglobal/view/closable_embed.py +++ /dev/null @@ -1,12 +0,0 @@ -from discordmenu.embed.view_state import ViewState - - -class ClosableEmbedViewState(ViewState): - def __init__(self, original_author_id, menu_type, raw_query, color, view_type, props): - super().__init__(original_author_id, menu_type, raw_query) - self.color = color - self.view_type = view_type - self.props = props - - def serialize(self): - return super().serialize() From 7c6c37eb0e1c23c25128586e3dc2a78f74f25e7e Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Feb 2022 12:18:53 -0700 Subject: [PATCH 1049/1220] Disallow possessive token propagation to other posessive tokens (#1587) --- dbcog/monster_index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 9efe0660..9df11172 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -252,7 +252,8 @@ async def _build_monster_index(self, monsters): possessives = re.findall(r"(\w+)'s", m.name_en.lower()) for mevo in alt_monsters: for token2 in possessives: - if token2 in self._name_to_tokens(mevo.name_en.lower()): + if token2 in self._name_to_tokens(mevo.name_en.lower()) \ + and token2+"'s" not in self._name_to_tokens(mevo.name_en.lower()): self.add_name_token(self.name_tokens, token2, mevo) else: for mevo in alt_monsters: From dbf25133c1a593e69fe86dcea30b23864169f830 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 1 Feb 2022 16:18:35 -0700 Subject: [PATCH 1050/1220] Make name_tokens and fluff_tokens disjoint (#1589) --- dbcog/monster_index.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 9df11172..b62998ed 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -217,7 +217,7 @@ async def _build_monster_index(self, monsters): for me in alt_monsters: for t in self.monster_id_to_nametokens[me.monster_id]: if t in nametokens: - self.add_name_token(self.name_tokens, t, m) + self.add_name_token(t, m) if me.is_equip or contains_ja(me.name_en): continue if last_token != me.name_en.split(',')[-1].strip(): @@ -237,28 +237,28 @@ async def _build_monster_index(self, monsters): # Add important tokens for token in self.monster_id_to_nametokens[m.monster_id]: - self.add_name_token(self.name_tokens, token, m) + self.add_name_token(token, m) if m.monster_id in self.treename_overrides: pass elif autotoken: # Add a consistant last token as important token for token in self._name_to_tokens(m.name_en.split(',')[-1].strip()): - self.add_name_token(self.name_tokens, token, m) + self.add_name_token(token, m) else: # Add name tokens by guessing which ones are important for token in self._get_important_tokens(m.name_en, treenames) + self._name_to_tokens(m.roma_subname): - self.add_name_token(self.name_tokens, token, m) + self.add_name_token(token, m) if m.is_equip: possessives = re.findall(r"(\w+)'s", m.name_en.lower()) for mevo in alt_monsters: for token2 in possessives: if token2 in self._name_to_tokens(mevo.name_en.lower()) \ and token2+"'s" not in self._name_to_tokens(mevo.name_en.lower()): - self.add_name_token(self.name_tokens, token2, mevo) + self.add_name_token(token2, mevo) else: for mevo in alt_monsters: if token in self._name_to_tokens(mevo.name_en): - self.add_name_token(self.name_tokens, token, mevo) + self.add_name_token(token, mevo) # For equips only, add every name token from every other non-equip monster in the tree. # This has the effect of making automated name tokens behave slightly more like treenames @@ -271,24 +271,38 @@ async def _build_monster_index(self, monsters): if not mevo.is_equip: for token2 in self._get_important_tokens(mevo.name_en, treenames): if token2 not in HAZARDOUS_IN_NAME_MODS: - self.add_name_token(self.name_tokens, token2, m) + self.add_name_token(token2, m) # Fluff tokens for token in nametokens + list(self.monster_id_to_forcedfluff[m.monster_id]): if m in self.name_tokens[token.lower()]: continue - self.add_name_token(self.fluff_tokens, token, m) + self.add_fluff_token(token, m) # Monster Nickname for nick in self.monster_id_to_nickname[m.monster_id]: - self.add_name_token(self.manual_nick, nick, m) + self.add_manual_nick_token(nick, m) # Tree Nickname base_id = self.graph.get_base_id(m) for nick in self.monster_id_to_treename[base_id]: - self.add_name_token(self.manual_tree, nick, m) + self.add_manual_tree_token(nick, m) - def add_name_token(self, token_dict, token, m, depth=5): + def add_name_token(self, token, monster): + if monster in self.fluff_tokens[token.lower()]: + self.fluff_tokens[token.lower()].remove(monster) + self._add_content_token(self.name_tokens, token, monster) + + def add_fluff_token(self, token, monster): + self._add_content_token(self.fluff_tokens, token, monster) + + def add_manual_nick_token(self, token, monster): + self._add_content_token(self.manual_nick, token, monster) + + def add_manual_tree_token(self, token, monster): + self._add_content_token(self.manual_tree, token, monster) + + def _add_content_token(self, token_dict, token, m, depth=5): if depth <= 0: logger.warning(f"Depth exceeded with token {token}. Aborting.") return @@ -300,7 +314,7 @@ def add_name_token(self, token_dict, token, m, depth=5): # Replacements for ts in (k for k in self.replacement_tokens if token.lower() in k and all(m in token_dict[t] for t in k)): for t in self.replacement_tokens[ts]: - self.add_name_token(token_dict, t, m, depth - 1) + self._add_content_token(token_dict, t, m, depth - 1) @staticmethod def _name_to_tokens(oname): From beac32d4ae879ae6eb6b2e2452ca41381c1d55ef Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 2 Feb 2022 08:06:36 -0700 Subject: [PATCH 1051/1220] Refactor the Monster Index (#1590) --- dbcog/dbcog.py | 3 +- dbcog/monster_index.py | 114 ++++++++++++++++++++++------------------- padinfo/padinfo.py | 6 +-- 3 files changed, 65 insertions(+), 58 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 3e09d902..3e0d1228 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -32,7 +32,8 @@ from .models.enum_types import DEFAULT_SERVER, SERVERS from .models.monster_model import MonsterModel from .models.monster_stats import MonsterStatModifierInput, monster_stats -from .monster_index import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES, MonsterIndex +from .monster_index import MonsterIndex +from .token_mappings import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES logger = logging.getLogger('red.padbot-cogs.dbcog') diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index b62998ed..35320eac 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -3,6 +3,7 @@ import io import logging import re +from collections import defaultdict from typing import Dict, List, Optional import aiohttp @@ -11,21 +12,23 @@ from tsutils.formatting import contains_ja from .errors import InvalidGraphState +from .models.enum_types import AwokenSkills, DEFAULT_SERVER from .models.monster_model import MonsterModel from .monster_graph import MonsterGraph -from .models.enum_types import DEFAULT_SERVER -from .token_mappings import * +from .token_mappings import ALL_TOKEN_DICTS, AWOKEN_SKILL_MAP, COLOR_MAP, DUAL_COLOR_MAP, EVO_MAP, EvoTypes, \ + HAZARDOUS_IN_NAME_MODS, KNOWN_MODIFIERS, LEGAL_END_TOKENS, MISC_MAP, MULTI_WORD_TOKENS, MiscModifiers, \ + PROBLEMATIC_SERIES_TOKENS, SUB_COLOR_MAP, TYPE_MAP + +logger = logging.getLogger('red.pad-cogs.dbcog.monster_index') SHEETS_PATTERN = 'https://docs.google.com/spreadsheets/d/1EoZJ3w5xsXZ67kmarLE4vfrZSIIIAfj04HXeZVST3eY' \ '/pub?gid={}&single=true&output=csv' -NICKNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('0') -GROUP_TREENAMES_OVERRIDES_SHEET = SHEETS_PATTERN.format('2070615818') -PANTHNAME_OVERRIDES_SHEET = SHEETS_PATTERN.format('959933643') -NAME_TOKEN_ALIAS_SHEET = SHEETS_PATTERN.format('1229125459') -MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('2089525837') -TREE_MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format('1372419168') - -logger = logging.getLogger('red.pad-cogs.dbcog.monster_index') +CARDNAME_OVERRIDE_SHEET = SHEETS_PATTERN.format(0) +TREENAME_OVERRIDES_SHEET = SHEETS_PATTERN.format(2070615818) +CARD_MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format(2089525837) +TREE_MODIFIER_OVERRIDE_SHEET = SHEETS_PATTERN.format(1372419168) +CONTENT_TOKEN_ALIAS_SHEET = SHEETS_PATTERN.format(1229125459) +SERIES_OVERRIDES_SHEET = SHEETS_PATTERN.format(959933643) class MonsterIndex: @@ -34,31 +37,34 @@ def __init__(self, server: Server = DEFAULT_SERVER): self.server = server self.issues = [] - self.graph: Optional[MonsterGraph] = None - - self.monster_id_to_nickname = defaultdict(set) - self.monster_id_to_nametokens = defaultdict(set) - self.monster_id_to_forcedfluff = defaultdict(set) + self.monster_id_to_cardname = defaultdict(set) self.monster_id_to_treename = defaultdict(set) + self.treename_overrides = set() + self.monster_id_to_name = defaultdict(set) + self.monster_id_to_forcedfluff = defaultdict(set) self.series_id_to_pantheon_nickname = defaultdict(set) - self.mwtoken_creators = defaultdict(set) - self.replacement_tokens = defaultdict(set) - self.remove_mods = defaultdict(set) - self.manual_prefixes = defaultdict(set) - self.manual_nick = defaultdict(set) - self.manual_tree = defaultdict(set) + self.content_token_aliases = defaultdict(set) + self.manual_modifiers = defaultdict(set) + self.manual_removed_modifiers = defaultdict(set) + + self.all_name_tokens = {} + self.manual = {} + self.manual_cardnames = defaultdict(set) + self.manual_treenames = defaultdict(set) self.name_tokens = defaultdict(set) self.fluff_tokens = defaultdict(set) - self.modifiers = defaultdict(set) - self.treename_overrides = set() - self.multi_word_tokens = {} - self._known_mods = {} - self.manual = {} - self.all_name_tokens = {} + self.all_modifiers = set() + self._known_mods = set() + self.modifiers = defaultdict(set) self.suffixes = set() + + self.multi_word_tokens = {} + self.mwtoken_creators = defaultdict(set) self.mwt_to_len = defaultdict(lambda: 1) + self.graph: Optional[MonsterGraph] = None + async def setup(self, graph: MonsterGraph): self.graph = graph monsters = graph.get_all_monsters(self.server) @@ -74,17 +80,17 @@ async def setup(self, graph: MonsterGraph): if " " in m.series.name_en.strip()}.union(MULTI_WORD_TOKENS) treenames_data, nickname_data, pantheon_data, nt_alias_data, treemod_data, mod_data = await asyncio.gather( - sheet_to_reader(GROUP_TREENAMES_OVERRIDES_SHEET, + sheet_to_reader(TREENAME_OVERRIDES_SHEET, ('base_id', 'new_treename', 'normal_prio', 'overrides')), - sheet_to_reader(NICKNAME_OVERRIDES_SHEET, + sheet_to_reader(CARDNAME_OVERRIDE_SHEET, ('monster_id', 'name_en', 'normal_prio', 'overrides', 'fluff')), - sheet_to_reader(PANTHNAME_OVERRIDES_SHEET, + sheet_to_reader(SERIES_OVERRIDES_SHEET, ('series_id', 'alias')), - sheet_to_reader(NAME_TOKEN_ALIAS_SHEET, + sheet_to_reader(CONTENT_TOKEN_ALIAS_SHEET, ('tokens', 'alias')), sheet_to_reader(TREE_MODIFIER_OVERRIDE_SHEET, ('base_id', 'modifiers')), - sheet_to_reader(MODIFIER_OVERRIDE_SHEET, + sheet_to_reader(CARD_MODIFIER_OVERRIDE_SHEET, ('monster_id', 'modifiers', 'remove')), ) @@ -103,10 +109,10 @@ async def setup(self, graph: MonsterGraph): self.treename_overrides.add(evomid) if data['normal_prio']: for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=self.server)): - self.monster_id_to_nametokens[evomid].update(self._name_to_tokens(name)) + self.monster_id_to_name[evomid].update(self._name_to_tokens(name)) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")]\ + self.mwtoken_creators[name.lower().replace(" ", "")] \ .add(graph.get_monster(mid, server=self.server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) self.monster_id_to_treename[mid].add(name.lower().replace(" ", "")) @@ -127,15 +133,15 @@ async def setup(self, graph: MonsterGraph): self.issues.append(f"Invalid sheet settings for nickname `{name}`.") continue if data['normal_prio']: - self.monster_id_to_nametokens[mid].update(self._name_to_tokens(name)) + self.monster_id_to_name[mid].update(self._name_to_tokens(name)) if data['overrides']: self.treename_overrides.add(mid) else: if " " in name: - self.mwtoken_creators[name.lower().replace(" ", "")]\ + self.mwtoken_creators[name.lower().replace(" ", "")] \ .add(graph.get_monster(mid, server=self.server)) self.multi_word_tokens.add(tuple(name.lower().split(" "))) - self.monster_id_to_nickname[mid].add(name.lower().replace(" ", "")) + self.monster_id_to_cardname[mid].add(name.lower().replace(" ", "")) for data in pantheon_data: if data['series_id'].isdigit(): @@ -146,7 +152,7 @@ async def setup(self, graph: MonsterGraph): self.series_id_to_pantheon_nickname[sid].add(name.lower().replace(" ", "")) for data in nt_alias_data: - self.replacement_tokens[frozenset(re.split(r'[,\s]+', data['tokens']))].add(data['alias']) + self.content_token_aliases[frozenset(re.split(r'[,\s]+', data['tokens']))].add(data['alias']) for data in mod_data: if data['monster_id'].isdigit(): @@ -163,9 +169,9 @@ async def setup(self, graph: MonsterGraph): self.multi_word_tokens.add(tuple(mod.lower().split(" "))) mod = mod.lower().replace(" ", "") if data['remove']: - self.remove_mods[mid].update(mod) + self.manual_removed_modifiers[mid].update(mod) else: - self.manual_prefixes[mid].update(get_modifier_aliases(mod)) + self.manual_modifiers[mid].update(get_modifier_aliases(mod)) for data in treemod_data: if data['base_id'].isdigit() and graph.get_monster(int(data['base_id']), server=self.server): @@ -177,14 +183,14 @@ async def setup(self, graph: MonsterGraph): mod = mod.replace(" ", "") aliases = get_modifier_aliases(mod) for evomid in self.graph.get_alt_ids(self.graph.get_monster(mid, server=self.server)): - self.manual_prefixes[evomid].update(aliases) + self.manual_modifiers[evomid].update(aliases) self._known_mods = {x for xs in self.series_id_to_pantheon_nickname.values() for x in xs}.union(KNOWN_MODIFIERS) await self._build_monster_index(monsters) - self.manual = combine_tokens_dicts(self.manual_nick, self.manual_tree) + self.manual = combine_tokens_dicts(self.manual_cardnames, self.manual_treenames) self.all_name_tokens = combine_tokens_dicts(self.manual, self.fluff_tokens, self.name_tokens) self.all_modifiers = {p for ps in self.modifiers.values() for p in ps} self.suffixes = LEGAL_END_TOKENS @@ -197,9 +203,9 @@ async def _build_monster_index(self, monsters): self.modifiers[m] = await self.get_modifiers(m) # ID - self.manual_nick[str(m.monster_no_na)].add(m) + self.manual_cardnames[str(m.monster_no_na)].add(m) if m.monster_id > 10000: - self.manual_nick[str(m.monster_id)].add(m) + self.manual_cardnames[str(m.monster_id)].add(m) if m.monster_no_na != m.monster_no_jp: self.name_tokens['na' + str(m.monster_no_na)].add(m) self.name_tokens['jp' + str(m.monster_no_jp)].add(m) @@ -215,7 +221,7 @@ async def _build_monster_index(self, monsters): # Propagate name tokens throughout all evos for me in alt_monsters: - for t in self.monster_id_to_nametokens[me.monster_id]: + for t in self.monster_id_to_name[me.monster_id]: if t in nametokens: self.add_name_token(t, m) if me.is_equip or contains_ja(me.name_en): @@ -236,7 +242,7 @@ async def _build_monster_index(self, monsters): treenames.add(match.group(1)) # Add important tokens - for token in self.monster_id_to_nametokens[m.monster_id]: + for token in self.monster_id_to_name[m.monster_id]: self.add_name_token(token, m) if m.monster_id in self.treename_overrides: pass @@ -253,7 +259,7 @@ async def _build_monster_index(self, monsters): for mevo in alt_monsters: for token2 in possessives: if token2 in self._name_to_tokens(mevo.name_en.lower()) \ - and token2+"'s" not in self._name_to_tokens(mevo.name_en.lower()): + and token2 + "'s" not in self._name_to_tokens(mevo.name_en.lower()): self.add_name_token(token2, mevo) else: for mevo in alt_monsters: @@ -280,7 +286,7 @@ async def _build_monster_index(self, monsters): self.add_fluff_token(token, m) # Monster Nickname - for nick in self.monster_id_to_nickname[m.monster_id]: + for nick in self.monster_id_to_cardname[m.monster_id]: self.add_manual_nick_token(nick, m) # Tree Nickname @@ -297,10 +303,10 @@ def add_fluff_token(self, token, monster): self._add_content_token(self.fluff_tokens, token, monster) def add_manual_nick_token(self, token, monster): - self._add_content_token(self.manual_nick, token, monster) + self._add_content_token(self.manual_cardnames, token, monster) def add_manual_tree_token(self, token, monster): - self._add_content_token(self.manual_tree, token, monster) + self._add_content_token(self.manual_treenames, token, monster) def _add_content_token(self, token_dict, token, m, depth=5): if depth <= 0: @@ -312,8 +318,8 @@ def _add_content_token(self, token_dict, token, m, depth=5): self.modifiers[m].add(token.lower()) # Replacements - for ts in (k for k in self.replacement_tokens if token.lower() in k and all(m in token_dict[t] for t in k)): - for t in self.replacement_tokens[ts]: + for ts in (k for k in self.content_token_aliases if token.lower() in k and all(m in token_dict[t] for t in k)): + for t in self.content_token_aliases[ts]: self._add_content_token(token_dict, t, m, depth - 1) @staticmethod @@ -346,7 +352,7 @@ def _get_important_tokens(cls, oname, treenames=None): return cls._name_to_tokens(min(n1, n2, key=token_count)) async def get_modifiers(self, monster: MonsterModel): - modifiers = self.manual_prefixes[monster.monster_id].union({'monster'}) + modifiers = self.manual_modifiers[monster.monster_id].union({'monster'}) basemon = self.graph.get_base_monster(monster) @@ -537,7 +543,7 @@ def is_story(m, do_transform=True): if self.graph.evo_gem_monster(monster) is not None: modifiers.update(MISC_MAP[MiscModifiers.HAS_GEM]) - modifiers.difference_update(self.remove_mods[monster.monster_id]) + modifiers.difference_update(self.manual_removed_modifiers[monster.monster_id]) return modifiers diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e02a2f30..3a6c68dc 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1289,7 +1289,7 @@ async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, quer return base_monster = dbcog.database.graph.get_base_monster(mon) mods = dbcog.indexes[server].modifiers[mon] - manual_modifiers = dbcog.indexes[server].manual_prefixes[mon.monster_id] + manual_modifiers = dbcog.indexes[server].manual_modifiers[mon.monster_id] EVOANDTYPE = dbcog.token_maps.EVO_TOKENS.union(dbcog.token_maps.TYPE_TOKENS) ret = (f"[{mon.monster_id}] {mon.name_en}\n" f"Base: [{base_monster.monster_id}] {base_monster.name_en}\n" @@ -1297,8 +1297,8 @@ async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, quer f"[Name Tokens] {' '.join(sorted(t for t, ms in dbcog.indexes[server].name_tokens.items() if mon in ms))}\n" f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dbcog.indexes[server].fluff_tokens.items() if mon in ms))}\n\n" f"[Manual Tokens]\n" - f" Treenames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_tree.items() if mon in ms))}\n" - f" Nicknames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_nick.items() if mon in ms))}\n\n" + f" Treenames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_treenames.items() if mon in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_cardnames.items() if mon in ms))}\n\n" f"[Modifier Tokens]\n" f" Attribute: {' '.join(sorted(t for t in mods if t in dbcog.token_maps.COLOR_TOKENS))}\n" f" Awakening: {' '.join(sorted(t for t in mods if t in dbcog.token_maps.AWAKENING_TOKENS))}\n" From 8e587e68baa349e36e5a9b8057c1b4e820d8f0e0 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 7 Feb 2022 01:56:12 -0600 Subject: [PATCH 1052/1220] add note to readme about python 3.8 (#1592) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 00b94161..57974202 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # padbot-cogs -Red v3 Cogs developed originally for Miru Bot, now for Tsubaki Bot. - -Code should be PEP 8 formatted with a 120 character line limit. +**Important: You must use Python 3.8 for the setup!!** If you use an earlier or later version, things WILL break. It's not just convenient to be on the same version as us, it's mandatory. # Setting up your own Tsubaki instance for contributing code From b3df92b7da90f9bc2a99f1cfa436da358b8cd4c8 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 7 Feb 2022 17:24:01 -0700 Subject: [PATCH 1053/1220] Use the American spelling of defense (#1593) --- dbcog/dungeon_context.py | 6 +++--- dbcog/models/encounter_model.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dbcog/dungeon_context.py b/dbcog/dungeon_context.py index f831fb47..e23d1847 100644 --- a/dbcog/dungeon_context.py +++ b/dbcog/dungeon_context.py @@ -21,7 +21,7 @@ encounters.level, encounters.hp, encounters.atk, - encounters.defence, + encounters.defense, sub_dungeons.name_ja AS sub_name_ja, sub_dungeons.name_en AS sub_name_en, sub_dungeons.name_ko AS sub_name_ko, @@ -120,7 +120,7 @@ encounters.level, encounters.hp, encounters.atk, - encounters.defence + encounters.defense FROM encounters WHERE @@ -141,7 +141,7 @@ encounters.level, encounters.hp, encounters.atk, - encounters.defence + encounters.defense FROM encounters WHERE diff --git a/dbcog/models/encounter_model.py b/dbcog/models/encounter_model.py index ad2b6e94..a5b05ab5 100644 --- a/dbcog/models/encounter_model.py +++ b/dbcog/models/encounter_model.py @@ -14,7 +14,7 @@ def __init__(self, enemy_data_model: EnemyDataModel = None, **kwargs): self.level = kwargs['level'] self.hp = kwargs['hp'] self.atk = kwargs['atk'] - self.defense = kwargs['defence'] # We aren't Brittish + self.defense = kwargs['defense'] self.enemy_data = enemy_data_model def to_dict(self): From bb74efab6e91d92ab98dc9955c6577a14aa4db7c Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 10 Feb 2022 04:45:10 -0700 Subject: [PATCH 1054/1220] Fix ^events (#1594) --- padevents/events.py | 4 ++-- padevents/padevents.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/padevents/events.py b/padevents/events.py index 611b309e..1aeea54a 100644 --- a/padevents/events.py +++ b/padevents/events.py @@ -81,10 +81,10 @@ def end_from_now_full_min(self) -> str: return '{:2}m'.format(int(minutes)) def group_long_name(self): - return self.group.upper() if self.group is not None else "UNGROUPED" + return self.group.upper() if self.group is not None else "ALL" def to_partial_event(self, pe): - group = self.group_long_name()[0] + group = self.group_long_name()[0] if self.group is not None else " " if self.is_started(): return "`" + group + " " + self.clean_dungeon_name + " " * ( max(24 - len(self.clean_dungeon_name), 0)) + "-`" + self.end_from_now_discord() diff --git a/padevents/padevents.py b/padevents/padevents.py index 7878b8d5..1a35bd5b 100644 --- a/padevents/padevents.py +++ b/padevents/padevents.py @@ -404,7 +404,7 @@ async def do_partial(self, ctx, server: Server, group: StarterGroup = None): events = EventList(self.events) events = events.with_server(server) events = events.with_dungeon_type(DungeonType.SoloSpecial, DungeonType.Special) - events = events.is_grouped() + events = events.with_length(EventLength.limited) active_events = sorted(events.active_only(), key=lambda e: (e.open_datetime, e.dungeon_name), reverse=True) pending_events = sorted(events.pending_only(), key=lambda e: (e.open_datetime, e.dungeon_name), reverse=True) From 658891b66151e4e1d1c2da00e3218e362b23c41b Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sun, 13 Feb 2022 19:35:15 -0500 Subject: [PATCH 1055/1220] Tell the user which monster wasn't found by padbuildimg (#1595) --- padbuildimg/padbuildimg.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 6e2c8668..78e05553 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -513,7 +513,8 @@ async def process_card(self, card_str, is_assist=False): assist_str = tok.value ass_card = await self.dbcog.find_monster(tok.value, self.ctx.author.id) if ass_card is None: - raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') + raise commands.UserFeedbackCheckFailure(f'Lookup Error: Could not find a monster to match:' + f' {tok.value}.') elif tok.type == 'REPEAT': repeat = min(tok.value, MAX_LATENTS) elif tok.type == 'ID': @@ -523,7 +524,8 @@ async def process_card(self, card_str, is_assist=False): else: card = await self.dbcog.find_monster(tok.value, self.ctx.author.id) if card is None: - raise commands.UserFeedbackCheckFailure('Lookup Error: Monster not found.') + raise commands.UserFeedbackCheckFailure(f'Lookup Error: Could not find a monster to match:' + f' {tok.value}.') if not card.is_inheritable: if is_assist: return None, None From 0dd70ff93a38a22e2bd123470c4b4e2f8113ae38 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 14 Feb 2022 02:55:57 -0600 Subject: [PATCH 1056/1220] Add more dungeon boost names (#1596) --- dbcog/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 5c38da66..c66a0a91 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -151,7 +151,7 @@ class EvoTypes(Enum): AwokenSkills.ENHCOMBO10C: ('10c',), AwokenSkills.COMBOORB: ('co', 'corb'), AwokenSkills.VOICE: ('voice',), - AwokenSkills.DUNGEONBONUS: ('dgbonus', 'dgboost'), + AwokenSkills.DUNGEONBONUS: ('dgbonus', 'dgboost', 'dgb', 'dungeonboost', 'dungeonbonus'), AwokenSkills.REDUCEDHP: ('hp-',), AwokenSkills.REDUCEDATK: ('atk-',), AwokenSkills.REDUCEDRCV: ('rcv-',), From 5c05bd48c9166bb269fd1368ff244fed44041ba1 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 19 Feb 2022 07:09:48 -0600 Subject: [PATCH 1057/1220] brick for this awakening (#1597) --- dbcog/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index c66a0a91..d03da494 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -169,7 +169,7 @@ class EvoTypes(Enum): AwokenSkills.ATTR3BOOST: ('attr3', '3attr'), AwokenSkills.ATTR4BOOST: ('attr4', '4attr'), AwokenSkills.ATTR5BOOST: ('attr5', '5attr'), - AwokenSkills.BLOBBOOST: ('blobboost', 'blob'), + AwokenSkills.BLOBBOOST: ('blobboost', 'blob', 'brickboost', 'brick'), } From 60f3ec100c8c92c27cb8af619b3260b0344b86a6 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 23 Feb 2022 05:42:09 -0600 Subject: [PATCH 1058/1220] Straight refactor to use tsutils MonsterLink class (#1599) --- padinfo/common/external_links.py | 32 --------------------- padinfo/view/awakening_help.py | 5 ++-- padinfo/view/button_info.py | 5 ++-- padinfo/view/components/evo_scroll_mixin.py | 9 +++--- padinfo/view/components/monster/header.py | 12 ++++---- padinfo/view/evos.py | 5 ++-- padinfo/view/experience_curve.py | 9 +++--- padinfo/view/id.py | 5 ++-- padinfo/view/links.py | 13 ++++----- padinfo/view/lookup.py | 5 ++-- padinfo/view/materials.py | 8 ++---- padinfo/view/otherinfo.py | 4 +-- padinfo/view/pantheon.py | 5 ++-- padinfo/view/pic.py | 5 ++-- padinfo/view/transforminfo.py | 5 ++-- 15 files changed, 42 insertions(+), 85 deletions(-) delete mode 100644 padinfo/common/external_links.py diff --git a/padinfo/common/external_links.py b/padinfo/common/external_links.py deleted file mode 100644 index 9d933ac4..00000000 --- a/padinfo/common/external_links.py +++ /dev/null @@ -1,32 +0,0 @@ -import urllib.parse -from typing import TYPE_CHECKING - -from tsutils.pad import get_pdx_id - -if TYPE_CHECKING: - from dbcog.models.monster_model import MonsterModel - -INFO_PDX_TEMPLATE = 'http://www.puzzledragonx.com/en/monster.asp?n={}' -YT_SEARCH_TEMPLATE = 'https://www.youtube.com/results?search_query={}' -SKYOZORA_TEMPLATE = 'http://pad.skyozora.com/pets/{}' -ILMINA_TEMPLATE = 'https://ilmina.com/#/CARD/{}' - - -def puzzledragonx(m: "MonsterModel"): - return INFO_PDX_TEMPLATE.format(get_pdx_id(m)) - - -def youtube_search(m: "MonsterModel"): - return YT_SEARCH_TEMPLATE.format(urllib.parse.quote(m.name_ja)) - - -def skyozora(m: "MonsterModel"): - return SKYOZORA_TEMPLATE.format(m.monster_no_jp) - - -def ilmina(m: "MonsterModel"): - return ILMINA_TEMPLATE.format(m.monster_no_jp) - - -def ilmina_skill(m: "MonsterModel"): - return "https://ilmina.com/#/SKILL/{}".format(m.active_skill.active_skill_id) if m.active_skill else None diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 3d147dc8..6bf84de9 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -5,12 +5,11 @@ from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki import MonsterImage, MonsterLink from padinfo.common.emoji_map import get_awakening_emoji -from padinfo.common.external_links import puzzledragonx from padinfo.view.common import get_awoken_skill_description from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -77,7 +76,7 @@ def embed(state, props: AwakeningHelpViewProps): ), embed_author=EmbedAuthor( MonsterHeader.long_v2(monster).to_markdown(), - puzzledragonx(monster), + MonsterLink.puzzledragonx(monster), MonsterImage.icon(monster.monster_id) ), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 62bc69f9..99e287e7 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -8,13 +8,12 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings +from tsutils.tsubaki import MonsterImage, MonsterLink -from padinfo.common.external_links import puzzledragonx from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL, ButtonInfoStatSet, \ ButtonInfoResult from padinfo.view.components.evo_scroll_mixin import EvoScrollView, EvoScrollViewState, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -207,7 +206,7 @@ def embed(cls, state: ButtonInfoViewState): ), embed_author=EmbedAuthor( MonsterHeader.long_v2(monster).to_markdown(), - puzzledragonx(monster), + MonsterLink.puzzledragonx(monster), MonsterImage.icon(monster.monster_id) ), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/components/evo_scroll_mixin.py b/padinfo/view/components/evo_scroll_mixin.py index 8bddb6ae..ddb049fe 100644 --- a/padinfo/view/components/evo_scroll_mixin.py +++ b/padinfo/view/components/evo_scroll_mixin.py @@ -3,8 +3,7 @@ from discordmenu.embed.components import EmbedField from discordmenu.embed.text import HighlightableLinks, LinkedText - -from padinfo.common.external_links import puzzledragonx +from tsutils.tsubaki import MonsterLink if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -66,8 +65,10 @@ def evos_embed_field(state: EvoScrollViewState): return EmbedField( field_text + help_text, HighlightableLinks( - links=[LinkedText(EvoScrollView.alt_fmt(evo), puzzledragonx(evo.monster)) for evo in - state.alt_monsters], + links=[LinkedText( + EvoScrollView.alt_fmt(evo), + MonsterLink.puzzledragonx(evo.monster) + ) for evo in state.alt_monsters], highlighted=next(i for i, me in enumerate(state.alt_monsters) if state.monster.monster_id == me.monster.monster_id) ) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index 50d7205b..a96c1b8f 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -3,9 +3,9 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import LinkedText, Text from tsutils.enums import Server +from tsutils.tsubaki import MonsterLink from padinfo.common.emoji_map import get_attribute_emoji_by_monster -from padinfo.common.external_links import puzzledragonx if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -27,12 +27,12 @@ def jp_suffix(cls, m: "MonsterModel", is_jp_buffed=False, subname_on_override=Tr def short(cls, m: "MonsterModel", link=False): type_emojis = '' msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) - return '[{}]({})'.format(msg, puzzledragonx(m)) if link else msg + return '[{}]({})'.format(msg, MonsterLink.puzzledragonx(m)) if link else msg @classmethod def long(cls, m: "MonsterModel", link=False): msg = cls.short(m) + cls.jp_suffix(m) - return '[{}]({})'.format(msg, puzzledragonx(m)) if link else msg + return '[{}]({})'.format(msg, MonsterLink.puzzledragonx(m)) if link else msg @classmethod def name(cls, m: "MonsterModel", link=False, show_jp=False): @@ -40,12 +40,12 @@ def name(cls, m: "MonsterModel", link=False, show_jp=False): m.monster_no_na, m.name_en, cls.jp_suffix(m) if show_jp else '') - return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) + return LinkedText(msg, MonsterLink.puzzledragonx(m)) if link else Text(msg) @classmethod def long_v2(cls, m: "MonsterModel", link=False): msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, cls.jp_suffix(m)) - return LinkedText(msg, puzzledragonx(m)) if link else Text(msg) + return LinkedText(msg, MonsterLink.puzzledragonx(m)) if link else Text(msg) @classmethod def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed=False): @@ -73,7 +73,7 @@ def short_with_emoji(cls, m: "MonsterModel", link=True, prefix=None): return Box( prefix, Text(get_attribute_emoji_by_monster(m)), - LinkedText(msg, puzzledragonx(m)) if link else Text(msg), + LinkedText(msg, MonsterLink.puzzledragonx(m)) if link else Text(msg), Text(suffix) if suffix else None, delimiter=' ' ) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index ae8ea840..213656ba 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -6,13 +6,12 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings +from tsutils.tsubaki import MonsterImage, MonsterLink -from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -106,7 +105,7 @@ def embed(cls, state: EvosViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=puzzledragonx(state.monster)), + url=MonsterLink.puzzledragonx(state.monster)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index b280ebf3..cd9889b4 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -8,10 +8,9 @@ from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core.utils.chat_formatting import humanize_number from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki import MonsterImage, MonsterLink -from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -26,8 +25,8 @@ } TA2_EXP = 12_000_000 TA3_EXP = 50_000_000 - - + + class ExperienceCurveViewProps: def __init__(self, monster: "MonsterModel", low: int, high: int, offset: Union[float, int]): self.monster = monster @@ -120,7 +119,7 @@ def embed(state, props: ExperienceCurveViewProps): EmbedMain( color=state.color, title=MonsterHeader.fmt_id_header(props.monster, use_emoji=True), - url=puzzledragonx(props.monster), + url=MonsterLink.puzzledragonx(props.monster), description=Text(f'lv{props.low} -> lv{props.high} (' + (trunc_humanize(props.monster.exp_curve) if props.monster.exp_curve else "no") + f' curve)'), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 44adb56b..b2669bb4 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -8,14 +8,13 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings +from tsutils.tsubaki import MonsterImage, MonsterLink from padinfo.common.emoji_map import get_awakening_emoji, get_emoji -from padinfo.common.external_links import puzzledragonx from padinfo.view.common import get_monster_from_ims, invalid_monster_text from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -294,7 +293,7 @@ def embed(cls, state: IdViewState): title=MonsterHeader.fmt_id_header(m, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=puzzledragonx(m)), + url=MonsterLink.puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index f14b3353..cf6e56d9 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -4,10 +4,9 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView +from tsutils.tsubaki import MonsterImage, MonsterLink -from padinfo.common.external_links import puzzledragonx, youtube_search, skyozora, ilmina from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -19,10 +18,10 @@ class LinksView: @staticmethod def linksbox(m): return Box( - LinkedText('YouTube', youtube_search(m)), - LinkedText('Skyozora', skyozora(m)), - LinkedText('PDX', puzzledragonx(m)), - LinkedText('Ilmina', ilmina(m)), + LinkedText('YouTube', MonsterLink.youtube_search(m)), + LinkedText('Skyozora', MonsterLink.skyozora(m)), + LinkedText('PADIndex', MonsterLink.padindex(m)), + LinkedText('Ilmina', MonsterLink.ilmina(m)), delimiter=' | ') @staticmethod @@ -32,5 +31,5 @@ def embed(m: "MonsterModel", color): color=color, title=MonsterHeader.long_v2(m).to_markdown(), description=LinksView.linksbox(m), - url=puzzledragonx(m)), + url=MonsterLink.puzzledragonx(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index aad80d35..b1ba5067 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -2,10 +2,9 @@ from discordmenu.embed.components import EmbedMain, EmbedAuthor from discordmenu.embed.view import EmbedView +from tsutils.tsubaki import MonsterImage, MonsterLink -from padinfo.common.external_links import puzzledragonx from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -20,5 +19,5 @@ def embed(m: "MonsterModel", color): EmbedMain(color=color), embed_author=EmbedAuthor( MonsterHeader.long_v2(m).to_markdown(), - puzzledragonx(m), + MonsterLink.puzzledragonx(m), MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index be160f1a..cffcd05c 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -7,14 +7,12 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings +from tsutils.tsubaki import MonsterImage, MonsterLink -from padinfo.common.external_links import ilmina_skill -from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -88,7 +86,7 @@ async def do_query(dbcog, monster): gemusedin = db_context.graph.material_of_monsters(evo_gem) if evo_gem else [] skillups = [] skillup_evo_count = 0 - link = ilmina_skill(monster) + link = MonsterLink.ilmina(monster) if monster.active_skill: sums = [m for m in db_context.get_monsters_by_active(monster.active_skill.active_skill_id, @@ -159,7 +157,7 @@ def embed(cls, state: MaterialsViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=puzzledragonx(state.monster) + url=MonsterLink.puzzledragonx(state.monster) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 64bdb4cf..44bed239 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -7,8 +7,8 @@ from discordmenu.embed.view import EmbedView from redbot.core.utils.chat_formatting import box from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki import MonsterLink -from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.components.evo_scroll_mixin import EvoScrollView from padinfo.view.components.monster.header import MonsterHeader @@ -56,7 +56,7 @@ def embed(cls, state: OtherInfoViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=puzzledragonx(m)), + url=MonsterLink.puzzledragonx(m)), embed_footer=embed_footer_with_state(state), embed_fields=[ EmbedField( diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 5e02c06e..93469b36 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -6,15 +6,14 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings +from tsutils.tsubaki import MonsterImage, MonsterLink from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ get_rarity_emoji -from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -175,7 +174,7 @@ def embed(cls, state: PantheonViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=puzzledragonx(state.monster)), + url=MonsterLink.puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), embed_fields=fields, embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 709ffbbe..f0fa0b3d 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -3,12 +3,11 @@ from discordmenu.embed.text import LinkedText, Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki import MonsterImage, MonsterLink -from padinfo.common.external_links import puzzledragonx from padinfo.view.base import BaseIdView from padinfo.view.components.evo_scroll_mixin import EvoScrollView from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -57,7 +56,7 @@ def embed(cls, state: PicViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=puzzledragonx(state.monster)), + url=MonsterLink.puzzledragonx(state.monster)), embed_footer=embed_footer_with_state(state), embed_fields=fields, embed_body_image=EmbedBodyImage(url), diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index d27b8c80..f7d7c953 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -9,12 +9,11 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings +from tsutils.tsubaki import MonsterImage, MonsterLink from padinfo.common.emoji_map import get_emoji -from padinfo.common.external_links import puzzledragonx from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.monster.header import MonsterHeader -from tsutils.tsubaki import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -225,7 +224,7 @@ def embed(cls, state: TransformInfoViewState): title=MonsterHeader.fmt_id_header(transformed_mon, False, state.is_jp_buffed).to_markdown(), - url=puzzledragonx(transformed_mon) + url=MonsterLink.puzzledragonx(transformed_mon) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon.monster_id)), embed_footer=embed_footer_with_state(state), From 04780f794ea85211b0d13c2eda7589397c4af3e6 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 23 Feb 2022 06:40:14 -0600 Subject: [PATCH 1059/1220] Use header_link instead of puzzledragonx (#1600) --- padinfo/view/awakening_help.py | 3 +- padinfo/view/button_info.py | 2 +- padinfo/view/components/evo_scroll_mixin.py | 4 ++- padinfo/view/components/monster/header.py | 33 +++++++++++++-------- padinfo/view/evos.py | 2 +- padinfo/view/experience_curve.py | 3 +- padinfo/view/id.py | 2 +- padinfo/view/links.py | 3 +- padinfo/view/lookup.py | 3 +- padinfo/view/materials.py | 2 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 2 +- padinfo/view/pic.py | 2 +- padinfo/view/transforminfo.py | 2 +- 14 files changed, 39 insertions(+), 26 deletions(-) diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 6bf84de9..f1d28f2c 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -76,7 +76,8 @@ def embed(state, props: AwakeningHelpViewProps): ), embed_author=EmbedAuthor( MonsterHeader.long_v2(monster).to_markdown(), - MonsterLink.puzzledragonx(monster), + # TODO: add query_settings + MonsterLink.header_link(monster), MonsterImage.icon(monster.monster_id) ), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 99e287e7..50d2a098 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -206,7 +206,7 @@ def embed(cls, state: ButtonInfoViewState): ), embed_author=EmbedAuthor( MonsterHeader.long_v2(monster).to_markdown(), - MonsterLink.puzzledragonx(monster), + MonsterLink.header_link(monster, state.query_settings), MonsterImage.icon(monster.monster_id) ), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/components/evo_scroll_mixin.py b/padinfo/view/components/evo_scroll_mixin.py index ddb049fe..9d9d8640 100644 --- a/padinfo/view/components/evo_scroll_mixin.py +++ b/padinfo/view/components/evo_scroll_mixin.py @@ -3,6 +3,7 @@ from discordmenu.embed.components import EmbedField from discordmenu.embed.text import HighlightableLinks, LinkedText +from tsutils.query_settings import QuerySettings from tsutils.tsubaki import MonsterLink if TYPE_CHECKING: @@ -19,6 +20,7 @@ class EvoScrollViewState: alt_monster_ids: List[int] monster: "MonsterModel" alt_monsters: List["MonsterEvolution"] + query_settings: QuerySettings def decrement_monster(self, dbcog, ims: dict): @@ -67,7 +69,7 @@ def evos_embed_field(state: EvoScrollViewState): HighlightableLinks( links=[LinkedText( EvoScrollView.alt_fmt(evo), - MonsterLink.puzzledragonx(evo.monster) + MonsterLink.header_link(evo.monster, state.query_settings) ) for evo in state.alt_monsters], highlighted=next(i for i, me in enumerate(state.alt_monsters) if state.monster.monster_id == me.monster.monster_id) diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py index a96c1b8f..acf19b2d 100644 --- a/padinfo/view/components/monster/header.py +++ b/padinfo/view/components/monster/header.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from discordmenu.embed.base import Box from discordmenu.embed.text import LinkedText, Text from tsutils.enums import Server +from tsutils.query_settings import QuerySettings from tsutils.tsubaki import MonsterLink from padinfo.common.emoji_map import get_attribute_emoji_by_monster @@ -11,6 +12,7 @@ from dbcog.models.monster_model import MonsterModel +# TODO: add query_settings to all uses class MonsterHeader: @classmethod def jp_suffix(cls, m: "MonsterModel", is_jp_buffed=False, subname_on_override=True): @@ -24,31 +26,34 @@ def jp_suffix(cls, m: "MonsterModel", is_jp_buffed=False, subname_on_override=Tr return suffix @classmethod - def short(cls, m: "MonsterModel", link=False): + def short(cls, m: "MonsterModel", link=False, query_settings: Optional[QuerySettings] = None): type_emojis = '' msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) - return '[{}]({})'.format(msg, MonsterLink.puzzledragonx(m)) if link else msg + return '[{}]({})'.format(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else msg @classmethod - def long(cls, m: "MonsterModel", link=False): + def long(cls, m: "MonsterModel", link=False, query_settings: Optional[QuerySettings] = None): msg = cls.short(m) + cls.jp_suffix(m) - return '[{}]({})'.format(msg, MonsterLink.puzzledragonx(m)) if link else msg + return '[{}]({})'.format(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else msg @classmethod - def name(cls, m: "MonsterModel", link=False, show_jp=False): + def name(cls, m: "MonsterModel", link=False, show_jp=False, + query_settings: Optional[QuerySettings] = None): msg = '[{}] {}{}'.format( m.monster_no_na, m.name_en, cls.jp_suffix(m) if show_jp else '') - return LinkedText(msg, MonsterLink.puzzledragonx(m)) if link else Text(msg) + return LinkedText(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else Text(msg) @classmethod - def long_v2(cls, m: "MonsterModel", link=False): + def long_v2(cls, m: "MonsterModel", link=False, + query_settings: Optional[QuerySettings] = None): msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, cls.jp_suffix(m)) - return LinkedText(msg, MonsterLink.puzzledragonx(m)) if link else Text(msg) + return LinkedText(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else Text(msg) @classmethod - def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed=False): + def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed=False, + query_settings: Optional[QuerySettings] = None): """Returns long_v2 as well as an `!` if the monster is Tsubaki To celebrate 1000 issues/PRs in our main Tsubaki repo, we added this easter egg! Yay! @@ -60,20 +65,22 @@ def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed=False): cls.jp_suffix(m, is_jp_buffed)) @classmethod - def fmt_id_header(cls, m: "MonsterModel", is_tsubaki=False, is_jp_buffed=False, use_emoji=False): + def fmt_id_header(cls, m: "MonsterModel", is_tsubaki=False, is_jp_buffed=False, + use_emoji=False): return Text('{}{} {}'.strip().format( get_attribute_emoji_by_monster(m) if use_emoji else '', '\N{EARTH GLOBE AMERICAS}' if m.server_priority == Server.NA else '', cls.long_maybe_tsubaki(m, is_tsubaki, bool(is_jp_buffed)))) @classmethod - def short_with_emoji(cls, m: "MonsterModel", link=True, prefix=None): + def short_with_emoji(cls, m: "MonsterModel", link=True, prefix=None, + query_settings: Optional[QuerySettings] = None): msg = f"{m.monster_no_na} - {m.name_en}" suffix = cls.jp_suffix(m, False, False) return Box( prefix, Text(get_attribute_emoji_by_monster(m)), - LinkedText(msg, MonsterLink.puzzledragonx(m)) if link else Text(msg), + LinkedText(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else Text(msg), Text(suffix) if suffix else None, delimiter=' ' ) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 213656ba..da58aed4 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -105,7 +105,7 @@ def embed(cls, state: EvosViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=MonsterLink.puzzledragonx(state.monster)), + url=MonsterLink.header_link(state.monster, state.query_settings)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index cd9889b4..4438999f 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -119,7 +119,8 @@ def embed(state, props: ExperienceCurveViewProps): EmbedMain( color=state.color, title=MonsterHeader.fmt_id_header(props.monster, use_emoji=True), - url=MonsterLink.puzzledragonx(props.monster), + # TODO: add query_settings + url=MonsterLink.header_link(props.monster), description=Text(f'lv{props.low} -> lv{props.high} (' + (trunc_humanize(props.monster.exp_curve) if props.monster.exp_curve else "no") + f' curve)'), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index b2669bb4..65303762 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -293,7 +293,7 @@ def embed(cls, state: IdViewState): title=MonsterHeader.fmt_id_header(m, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=MonsterLink.puzzledragonx(m)), + url=MonsterLink.header_link(m, state.query_settings)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index cf6e56d9..d4c9f9d4 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -31,5 +31,6 @@ def embed(m: "MonsterModel", color): color=color, title=MonsterHeader.long_v2(m).to_markdown(), description=LinksView.linksbox(m), - url=MonsterLink.puzzledragonx(m)), + # TODO: add query_settings + url=MonsterLink.header_link(m)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index b1ba5067..19d4b09a 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -19,5 +19,6 @@ def embed(m: "MonsterModel", color): EmbedMain(color=color), embed_author=EmbedAuthor( MonsterHeader.long_v2(m).to_markdown(), - MonsterLink.puzzledragonx(m), + # TODO: add query_settings + MonsterLink.header_link(m), MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index cffcd05c..2e4ec070 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -157,7 +157,7 @@ def embed(cls, state: MaterialsViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=MonsterLink.puzzledragonx(state.monster) + url=MonsterLink.header_link(state.monster, state.query_settings) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 44bed239..b034906b 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -56,7 +56,7 @@ def embed(cls, state: OtherInfoViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=MonsterLink.puzzledragonx(m)), + url=MonsterLink.header_link(m, state.query_settings)), embed_footer=embed_footer_with_state(state), embed_fields=[ EmbedField( diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 93469b36..93216f12 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -174,7 +174,7 @@ def embed(cls, state: PantheonViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=MonsterLink.puzzledragonx(state.monster)), + url=MonsterLink.header_link(state.monster, state.query_settings)), embed_footer=embed_footer_with_state(state), embed_fields=fields, embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index f0fa0b3d..63cfa314 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -56,7 +56,7 @@ def embed(cls, state: PicViewState): title=MonsterHeader.fmt_id_header(state.monster, state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, state.is_jp_buffed).to_markdown(), - url=MonsterLink.puzzledragonx(state.monster)), + url=MonsterLink.header_link(state.monster, state.query_settings)), embed_footer=embed_footer_with_state(state), embed_fields=fields, embed_body_image=EmbedBodyImage(url), diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index f7d7c953..d659aa69 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -224,7 +224,7 @@ def embed(cls, state: TransformInfoViewState): title=MonsterHeader.fmt_id_header(transformed_mon, False, state.is_jp_buffed).to_markdown(), - url=MonsterLink.puzzledragonx(transformed_mon) + url=MonsterLink.header_link(transformed_mon, state.query_settings) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon.monster_id)), embed_footer=embed_footer_with_state(state), From 36c02c007031ca927cb3aab76ab53f1c74a35c05 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 24 Feb 2022 01:58:46 -0600 Subject: [PATCH 1060/1220] Add linktarget preference & apply it for links, lookup (#1601) --- padinfo/padinfo.py | 42 ++++++++++++++++++++++++++++++++++-------- padinfo/view/links.py | 6 +++--- padinfo/view/lookup.py | 6 +++--- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3a6c68dc..9a9ee3e1 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -17,7 +17,7 @@ from tsutils.cogs.donations import is_donor from tsutils.emoji import char_to_emoji from tsutils.enums import AltEvoSort, CardLevelModifier, CardModeModifier, CardPlusModifier, EvoGrouping, LsMultiplier, \ - Server + Server, MonsterLinkTarget from tsutils.json_utils import safe_read_json from tsutils.menu.components.config import BotConfig from tsutils.menu.simple_text import SimpleTextMenu @@ -553,7 +553,8 @@ async def links(self, ctx, *, query: str): return await self.log_id_result(ctx, monster.monster_id) color = await self.get_user_embed_color(ctx) - embed = LinksView.embed(monster, color).to_embed() + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + embed = LinksView.embed(monster, color, query_settings).to_embed() await ctx.send(embed=embed) @commands.command() @@ -567,7 +568,8 @@ async def lookup(self, ctx, *, query: str): return await self.log_id_result(ctx, monster.monster_id) color = await self.get_user_embed_color(ctx) - embed = LookupView.embed(monster, color).to_embed() + query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + embed = LookupView.embed(monster, color, query_settings).to_embed() await ctx.send(embed=embed) async def log_id_result(self, ctx, monster_id: int): @@ -1014,11 +1016,6 @@ async def idset_list(self, ctx): """`[p]id` settings list""" await ctx.send("This command is still in progress!") - @idset.command(name="scroll") - async def idset_scroll(self, ctx): - await ctx.send(f"We no longer support number scroll as a preference. " - f"Please use the `{ctx.prefix}scroll` command for numberscroll!") - @idset.command() async def survey(self, ctx, value): """How often you see the id survey @@ -1222,6 +1219,35 @@ async def evogrouping(self, ctx, value: str): await ctx.send( f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + @idset.command() + async def linktarget(self, ctx, value: str): + """Monster link targets in your `[p]ids` queries + + `[p]idset linktarget padindex`: [Default] Link to PADIndex always. + `[p]idset linktarget ilmina`: Link to Ilmina for cards that are in NA. + """ + setting_name = 'linktarget' + value1 = 'padindex' + value1_flag = 'padindex' + value2 = 'ilmina' + value2_flag = 'ilmina' + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: + value = value.lower() + if value == value1: + fm_flags[setting_name] = MonsterLinkTarget.padindex.value + not_value = value2 + not_value_flag = value2_flag + elif value == value2: + fm_flags[setting_name] = MonsterLinkTarget.ilmina.value + not_value = value1 + not_value_flag = value1_flag + else: + await ctx.send( + f'Please input an allowed value, either `{value1}` or `{value2}`.') + return + await ctx.send( + f"Your default `{ctx.prefix}id` {setting_name} preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + @commands.group() @checks.is_owner() async def padinfo(self, ctx): diff --git a/padinfo/view/links.py b/padinfo/view/links.py index d4c9f9d4..403fc56b 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -4,6 +4,7 @@ from discordmenu.embed.components import EmbedThumbnail, EmbedMain from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView +from tsutils.query_settings import QuerySettings from tsutils.tsubaki import MonsterImage, MonsterLink from padinfo.view.components.monster.header import MonsterHeader @@ -25,12 +26,11 @@ def linksbox(m): delimiter=' | ') @staticmethod - def embed(m: "MonsterModel", color): + def embed(m: "MonsterModel", color, query_settings: QuerySettings): return EmbedView( EmbedMain( color=color, title=MonsterHeader.long_v2(m).to_markdown(), description=LinksView.linksbox(m), - # TODO: add query_settings - url=MonsterLink.header_link(m)), + url=MonsterLink.header_link(m, query_settings)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index 19d4b09a..887dfb5d 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -2,6 +2,7 @@ from discordmenu.embed.components import EmbedMain, EmbedAuthor from discordmenu.embed.view import EmbedView +from tsutils.query_settings import QuerySettings from tsutils.tsubaki import MonsterImage, MonsterLink from padinfo.view.components.monster.header import MonsterHeader @@ -14,11 +15,10 @@ class LookupView: VIEW_TYPE = 'Lookup' @staticmethod - def embed(m: "MonsterModel", color): + def embed(m: "MonsterModel", color, query_settings: QuerySettings): return EmbedView( EmbedMain(color=color), embed_author=EmbedAuthor( MonsterHeader.long_v2(m).to_markdown(), - # TODO: add query_settings - MonsterLink.header_link(m), + MonsterLink.header_link(m, query_settings), MonsterImage.icon(m.monster_id))) From b6e42a3ad9250882836f2fecb18458bc878b60ac Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 24 Feb 2022 02:06:49 -0600 Subject: [PATCH 1061/1220] Use send confirmation, cancellation message for idset (#1602) --- padinfo/padinfo.py | 57 ++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9a9ee3e1..cee3d699 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -24,7 +24,7 @@ from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.menu.view.simple_text import SimpleTextViewState from tsutils.query_settings import QuerySettings -from tsutils.user_interaction import get_user_confirmation, send_cancellation_message +from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ get_attribute_emoji_by_monster, get_awakening_emoji, get_type_emoji @@ -1028,7 +1028,8 @@ async def survey(self, ctx, value): await self.config.user(ctx.author).survey_mode.set(vals.index(value)) await ctx.tick() else: - await ctx.send("value must be `always`, `sometimes`, or `never`") + await send_cancellation_message( + ctx, "value must be `always`, `sometimes`, or `never`") @is_donor() @idset.command() @@ -1047,7 +1048,8 @@ async def embedcolor(self, ctx, *, color): elif re.match(r"^#?[0-9a-fA-F]{6}$", color): await self.config.user(ctx.author).color.set(int(color.lstrip("#"), 16)) else: - await ctx.send("Invalid color! Valid colors are any hexcode and:\n" + ", ".join(COLORS)) + await send_cancellation_message( + ctx, "Invalid color! Valid colors are any hexcode and:\n" + ", ".join(COLORS)) return await ctx.tick() @@ -1057,7 +1059,8 @@ async def naprio(self, ctx, value: bool): async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: # The current na_prio enum has 0 and 1 instead of False and True as its values fm_flags['na_prio'] = int(value) - await ctx.send(f"NA monster prioritization has been **{'en' if value else 'dis'}abled**.") + await send_confirmation_message( + ctx, f"NA monster prioritization has been **{'en' if value else 'dis'}abled**.") @idset.command() async def server(self, ctx, server: str): @@ -1074,9 +1077,9 @@ async def server(self, ctx, server: str): fm_flags['server'] = server.upper() else: if dbcog.DEFAULT_SERVER == Server.COMBINED: - await ctx.send("Server must be `default` or `na`") + await send_cancellation_message(ctx, "Server must be `default` or `na`") else: - await ctx.send("Server must be `na` or `combined`") + await send_cancellation_message(ctx, "Server must be `na` or `combined`") return await ctx.tick() @@ -1094,10 +1097,12 @@ async def evosort(self, ctx, value: str): elif value == "numerical": fm_flags['evosort'] = AltEvoSort.numerical.value else: - await ctx.send( + await send_cancellation_message( + ctx, f'Please input an allowed value, either `{AltEvoSort.dfs.name}` or `{AltEvoSort.numerical.name}`.') return - await ctx.send(f"Your `{ctx.prefix}id` evo sort preference has been set to **{value}**.") + await send_confirmation_message( + ctx, f"Your `{ctx.prefix}id` evo sort preference has been set to **{value}**.") @idset.command() async def lsmultiplier(self, ctx, value: str): @@ -1117,10 +1122,12 @@ async def lsmultiplier(self, ctx, value: str): not_value = 'double' not_value_flag = 'lsdouble' else: - await ctx.send( + await send_cancellation_message( + ctx, f'Please input an allowed value, either `double` or `single`.') return - await ctx.send( + await send_confirmation_message( + ctx, f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() @@ -1141,10 +1148,12 @@ async def cardplus(self, ctx, value: str): not_value = '297' not_value_flag = 'plus297' else: - await ctx.send( + await send_cancellation_message( + ctx, f'Please input an allowed value, either `297` or `0`.') return - await ctx.send( + await send_confirmation_message( + ctx, f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() @@ -1165,10 +1174,12 @@ async def cardmode(self, ctx, value: str): not_value = 'solo' not_value_flag = 'solo' else: - await ctx.send( + await send_cancellation_message( + ctx, f'Please input an allowed value, either `coop` or `solo`.') return - await ctx.send( + await send_confirmation_message( + ctx, f"Your default `{ctx.prefix}id` cardmode preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() @@ -1189,10 +1200,12 @@ async def cardlevel(self, ctx, value: str): not_value = '120' not_value_flag = 'lv120' else: - await ctx.send( + await send_cancellation_message( + ctx, f'Please input an allowed value, either `110` or `120`.') return - await ctx.send( + await send_confirmation_message( + ctx, f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() @@ -1213,10 +1226,12 @@ async def evogrouping(self, ctx, value: str): not_value = 'split' not_value_flag = 'splitevos' else: - await ctx.send( + await send_cancellation_message( + ctx, f'Please input an allowed value, either `grouped` or `split`.') return - await ctx.send( + await send_confirmation_message( + ctx, f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @idset.command() @@ -1242,10 +1257,12 @@ async def linktarget(self, ctx, value: str): not_value = value1 not_value_flag = value1_flag else: - await ctx.send( + await send_cancellation_message( + ctx, f'Please input an allowed value, either `{value1}` or `{value2}`.') return - await ctx.send( + await send_confirmation_message( + ctx, f"Your default `{ctx.prefix}id` {setting_name} preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") @commands.group() From d42332c6c2334e2c653c89a888ff3cb3c95be75f Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Fri, 25 Feb 2022 12:22:33 -0500 Subject: [PATCH 1062/1220] Add padglobal prepend (#1604) --- padglobal/padglobal.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index ee2dab75..ec620fcb 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -296,9 +296,17 @@ async def delete(self, ctx, command: str): else: await ctx.send("PAD command doesn't exist.") + @padglobal.command() + async def prepend(self, ctx, command: str, *, addition): + """Prepend the additional text to an existing command before a blank line.""" + await self._concatenate(ctx, command, 'prepend', addition) + @padglobal.command() async def append(self, ctx, command: str, *, addition): """Append the additional text to an existing command after a blank line.""" + await self._concatenate(ctx, command, 'append', addition) + + async def _concatenate(self, ctx, command: str, operation: str, addition): # the same cleaning that padglobal add does command = command.lower() addition = clean_global_mentions(addition) @@ -318,15 +326,21 @@ async def append(self, ctx, command: str, *, addition): source_cmd = result result = self.c_commands[result] - result = "{}\n\n{}".format(result, addition) + if operation == 'prepend': + result = "{}\n\n{}".format(addition, result) + elif operation == 'append': + result = "{}\n\n{}".format(result, addition) + else: + raise KeyError("Invalid operation: Must be \'prepend\' or \'append\'") + if alias: self.c_commands[source_cmd] = result else: self.c_commands[corrected_cmd] = result json.dump(self.c_commands, open(self.file_path, 'w+')) - await ctx.send("Successfully appended to {}PAD command `{}`.".format("source " if alias else "", - source_cmd if alias else corrected_cmd)) + await ctx.send("Successfully {}ed to {}PAD command `{}`.".format(operation, "source " if alias else "", + source_cmd if alias else corrected_cmd)) @padglobal.command() async def eventswap(self, ctx): From f645565670d35c75a86fe104927871bdb43564b2 Mon Sep 17 00:00:00 2001 From: Turtle <72906298+turtleworks@users.noreply.github.com> Date: Sat, 26 Feb 2022 17:42:24 -0500 Subject: [PATCH 1063/1220] Fix allmats for monsters with no mats (#1606) --- padinfo/padinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index cee3d699..3486fd44 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -620,7 +620,7 @@ async def _do_idsearch(self, ctx, query, child_menu_type=None, query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id, query_settings) - if monster_list is None: + if not monster_list: await ctx.send("No monster matched.") return @@ -640,7 +640,7 @@ async def allmats(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return monster_list = await AllMatsViewState.do_query(dbcog, monster) - if monster_list is None: + if not monster_list: await ctx.send(inline("This monster is not a mat for anything nor does it have a gem")) return From c2f1b042adf78eb5c19dfb9ef4235de4a078b95a Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 28 Feb 2022 18:13:30 -0700 Subject: [PATCH 1064/1220] Move Cogs (#1609) --- README.md | 3 +- channelmirror/__init__.py | 8 - channelmirror/channelmirror.py | 565 --------------------------------- channelmirror/info.json | 29 -- crowddata/info.json | 2 +- crud/crud.py | 2 +- crud/info.json | 2 +- dbcog/info.json | 2 +- dungeoncog/info.json | 2 +- padglobal/info.json | 2 +- padglobal/padglobal.py | 2 +- padinfo/info.json | 2 +- pricecheck/info.json | 2 +- 13 files changed, 10 insertions(+), 613 deletions(-) delete mode 100644 channelmirror/__init__.py delete mode 100644 channelmirror/channelmirror.py delete mode 100644 channelmirror/info.json diff --git a/README.md b/README.md index 57974202..096f13d0 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type ^addpath path/to/repo/root ``` -> Note: you may have to do this for both `pad-cogs` and `misc-cogs`. +> Note: you may have to do this for `pad-cogs`, `core-cogs`, and `misc-cogs`. ## Loading cogs @@ -81,7 +81,6 @@ Most cogs here relate to the mobile game 'Puzzle & Dragons'. | Cog | Purpose | | --- | --- | | azurelane | **DISCONTINUED** Azur Lane card lookup | -| channelmirror | A better version of the built-in discord announcement channel | | crud | Database editor. Not useful for bots other than prod Tsubaki | | dbcog | Central cog to host all PAD data | | damagecalc | Simple attack damage calculator | diff --git a/channelmirror/__init__.py b/channelmirror/__init__.py deleted file mode 100644 index 6b346512..00000000 --- a/channelmirror/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .channelmirror import ChannelMirror - -__red_end_user_data_statement__ = "No personal data is stored." - - -def setup(bot): - n = ChannelMirror(bot) - bot.add_cog(n) diff --git a/channelmirror/channelmirror.py b/channelmirror/channelmirror.py deleted file mode 100644 index 6574f2ff..00000000 --- a/channelmirror/channelmirror.py +++ /dev/null @@ -1,565 +0,0 @@ -import logging -import re -import time -from io import BytesIO -from typing import List, Optional - -import discord -from redbot.core import Config, checks, commands -from redbot.core.utils.chat_formatting import box, inline, pagify -from tsutils.cog_settings import CogSettings -from tsutils.cogs.globaladmin import auth_check -from tsutils.emoji import fix_emojis_for_server, replace_emoji_names_with_code -from tsutils.helper_classes import DummyObject -from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message, \ - send_repeated_consecutive_messages - -logger = logging.getLogger('red.misc-cogs.channelmirror') - -# Three hour cooldown -ATTRIBUTION_TIME_SECONDS = 60 * 60 * 3 -MAX_MIRRORED_MESSAGES = 100 - -frMESSAGE_LINK = r'(https://discord\.com/channels/{0.guild.id}/{0.id}/(\d+)/?)' -MESSAGE_LINK = 'https://discord.com/channels/{0.guild.id}/{0.channel.id}/{0.id}' - - -class ChannelMirror(commands.Cog): - """Channel mirroring tools.""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.settings = ChannelMirrorSettings("channelmirror") - - self.config = Config.get_conf(self, identifier=3747737700) - self.config.register_channel(last_spoke=0, last_spoke_timestamp=0, mirrored_channels=[], mirrored_messages={}, - multiedit=False, mirroredit_target=None, nodeletion=False) - self.config.init_custom("message", 1) - self.config.register_custom("message", attribute=False) - - GACOG = self.bot.get_cog("GlobalAdmin") - if GACOG: - GACOG.register_perm("channelmirror") - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.group(aliases=['channelmod']) - async def channelmirror(self, ctx): - """Manage channel mirroring settings""" - - @channelmirror.command(aliases=['addmirror']) - @checks.is_owner() - async def add(self, ctx, source_channel_id: int, dest_channel_id: int, docheck: bool = True): - """Set mirroring between two channels.""" - if docheck and (not self.bot.get_channel(source_channel_id) or not self.bot.get_channel(dest_channel_id)): - await ctx.send('Check your channel IDs, or maybe the bot is not in those servers') - return - if not await get_user_confirmation(ctx, f"Set <#{dest_channel_id}> to mirror message from " - f"<#{source_channel_id}>?"): - await send_cancellation_message(ctx, "Action cancelled. No action was taken.") - return - async with self.config.channel_from_id(source_channel_id).mirrored_channels() as mirrored_channels: - if dest_channel_id not in mirrored_channels: - mirrored_channels.append(dest_channel_id) - await send_confirmation_message(ctx, f"Mirror from <#{source_channel_id}> to <#{dest_channel_id}> successful") - - @channelmirror.command(aliases=['rmmirror', 'rm', 'delete']) - @checks.is_owner() - async def remove(self, ctx, source_channel_id: int, dest_channel_id: int): - """Remove mirroring between two channels.""" - async with self.config.channel_from_id(source_channel_id).mirrored_channels() as mirrored_channels: - if dest_channel_id in mirrored_channels: - mirrored_channels.remove(dest_channel_id) - else: - return await ctx.send("That isn't an existing mirror.") - await ctx.tick() - - @channelmirror.command() - @checks.is_owner() - async def multiedit(self, ctx, channel: Optional[discord.TextChannel], enable: bool = True): - """Opt in a channel to multi-edit mode.""" - if channel is None: - channel = ctx.channel - await self.config.channel(channel).multiedit.set(enable) - await ctx.tick() - - @channelmirror.command() - @checks.is_owner() - async def nodeletion(self, ctx, channel: Optional[discord.TextChannel], enable: bool = True): - """Opt in a channel to no-deletion mode.""" - if channel is None: - channel = ctx.channel - await self.config.channel(channel).nodeletion.set(enable) - await ctx.tick() - - @channelmirror.command(aliases=['mirrorconfig']) - @checks.is_owner() - async def config(self, ctx, server_id: int = None): - """List mirror config.""" - gchs = set() - if server_id: - guild = self.bot.get_guild(server_id) - if guild is None: - await ctx.send("Invalid server id.") - return - gchs = {c.id for c in self.bot.get_guild(server_id).channels} - msg = 'Mirrored channels\n' - for mc_id, config in (await self.config.all_channels()).items(): - if server_id is not None and mc_id not in gchs and not gchs.intersection(config['mirrored_channels']): - continue - channel = self.bot.get_channel(mc_id) - channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - multiedit = await self.config.channel_from_id(mc_id).multiedit() - msg += '\n\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) - for channel_id in config['mirrored_channels']: - if server_id is not None and mc_id not in gchs and not channel_id not in gchs: - continue - channel = self.bot.get_channel(channel_id) - channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - msg += '\n\t{} ({})'.format(channel_id, channel_name) - msg += '\n\n* indicates multi-edit' - for page in pagify(msg): - await ctx.send(box(page)) - - @channelmirror.command(aliases=['guildmirrorconfig']) - @checks.is_owner() - async def guildconfig(self, ctx, server_id: int): - """List mirror config for a guild.""" - gchs = set() - if server_id: - guild = self.bot.get_guild(server_id) - if guild is None: - await ctx.send("Invalid server id.") - return - gchs = {c.id for c in self.bot.get_guild(server_id).channels} - - msg = 'From:' - for mc_id, config in (await self.config.all_channels()).items(): - if mc_id not in gchs: - continue - channel = self.bot.get_channel(mc_id) - channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - multiedit = await self.config.channel_from_id(mc_id).multiedit() - msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) - for channel_id in config['mirrored_channels']: - channel = self.bot.get_channel(channel_id) - channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - msg += '\n\t{} ({})'.format(channel_id, channel_name) - for page in pagify(msg): - await ctx.send(box(page)) - - msg = 'To:' - for mc_id, config in (await self.config.all_channels()).items(): - if not gchs.intersection(config['mirrored_channels']): - continue - channel = self.bot.get_channel(mc_id) - channel_name = f"{channel.guild.name}/{channel.name}" if channel else 'unknown' - multiedit = await self.config.channel_from_id(mc_id).multiedit() - msg += '\n{}{} ({})'.format(mc_id, '*' if multiedit else '', channel_name) - for channel_id in config['mirrored_channels']: - if channel_id not in gchs: - continue - channel = self.bot.get_channel(channel_id) - channel_name = channel.name if channel else 'unknown' - msg += '\n\t{} ({})'.format(channel_id, channel_name) - for page in pagify(msg): - await ctx.send(box(page)) - - await ctx.send(inline('* indicates multi-edit')) - - @channelmirror.command() - async def countreactions(self, ctx, message: discord.Message): - """Count reactions on a message and all of its mirrors. - - The message can be a link, a message id (if used in the same - channel as the message), or the channel_id and message_id - separated by a dash (channel_id-message-id)""" - mirrored_messages = (await self.config.channel(message.channel).mirrored_messages())[str(message.id)] - if not mirrored_messages: - await ctx.send("This message isn't mirrored!") - reacts = {} - for react in message.reactions: - reacts[str(react)] = react.count - 1 - for (chid, mids) in mirrored_messages: - dest_channel = self.bot.get_channel(chid) - if not dest_channel: - logger.warning('could not locate channel {}'.format(chid)) - continue - dest_messages = [await dest_channel.fetch_message(mid) for mid in mids] - if not all(dest_messages): - logger.warning('could not locate messages {}'.format(mids)) - continue - for dest_message in dest_messages: - dest_reaction = discord.utils.find(lambda r: r == react, dest_message.reactions) - if not dest_reaction: - logger.warning('could not locate reaction {}'.format(react)) - continue - reacts[str(react)] += dest_reaction.count - 1 - o = "" - maxlen = len(str(max(reacts.values(), key=lambda x: len(str(x))))) - for reaction, count in reacts.items(): - o += "{{}}: {{:{}}}\n".format(maxlen).format(reaction, count) - await ctx.send(o) - - @channelmirror.command() - @auth_check('channelmirror') - async def catchup(self, ctx, channel, from_message, to_message=None): - """Catch up a mirror for all messages after from_message (inclusive)""" - if channel.isdigit(): - channel = self.bot.get_channel(int(channel)) - else: - channel = await self.catchup.do_conversion(ctx, discord.TextChannel, channel, "channel") - - if from_message.isdigit(): - from_message = await channel.fetch_message(int(from_message)) - else: - from_message = await self.catchup.do_conversion(ctx, discord.Message, from_message, "from_message") - - if to_message is None: - pass - elif to_message.isdigit(): - to_message = await channel.fetch_message(int(to_message)) - else: - to_message = await self.catchup.do_conversion(ctx, discord.Message, to_message, "to_message") - - async with ctx.typing(): - await self.mirror_msg(from_message) - async for message in channel.history(limit=None, after=from_message, before=to_message): - await self.mirror_msg(message) - if to_message: - await self.mirror_msg(to_message) - await ctx.tick() - - @commands.Cog.listener('on_message') - async def mirror_msg(self, message): - author = message.author - - if author.bot: - return - - if (await self.bot.get_context(message)).prefix is not None: - return - - channel = message.channel - mirrored_channels = await self.config.channel(channel).mirrored_channels() - multiedit = await self.config.channel(channel).multiedit() - - if not mirrored_channels: - return - - if multiedit and len(message.content) > 2000: - return await message.channel.send("I can't send this message as it's longer than 2000 characters.") - - last_spoke = await self.config.channel(channel).last_spoke() - last_spoke_timestamp = await self.config.channel(channel).last_spoke_timestamp() - attribution_required = last_spoke != author.id - attribution_required |= time.time() - last_spoke_timestamp > ATTRIBUTION_TIME_SECONDS - attribution_required &= not multiedit - - await self.config.custom('message', message.id).attribute.set(attribution_required) - await self.config.channel(channel).last_spoke.set(author.id) - await self.config.channel(channel).last_spoke_timestamp.set(time.time()) - - attachment_bytes = [(BytesIO(await attachment.read()), attachment.filename) - for attachment in message.attachments] - - if multiedit: - await message.delete() - idmess = await channel.send("Pending...") - attachments = message.attachments - try: - message = await channel.send(message.content, - files=[await a.to_file() for a in attachments]) - except discord.HTTPException: - try: - message = await channel.send(content=message.content) - for a in attachments: - await channel.send(file=await a.to_file()) - except discord.HTTPException: - if message.content: - message = await channel.send(message.content) - await channel.send( - f"<{author.mention} File too large for this channel. Other attachments not shown>") - - await idmess.edit(content=str(message.id)) - - linked_messages = [] - - for dest_channel_id in mirrored_channels: - dest_channel = self.bot.get_channel(dest_channel_id) - if not dest_channel: - continue - try: - if not (attachment_bytes or message.content): - logger.warning('Failed to mirror message from {} no action to take'.format(channel.id)) - continue - - message.content = await self.mformat(message.content, message.channel, dest_channel) - fmessages = await self.split_message(message) - - [b.seek(0) for b, fn in attachment_bytes] - try: - dest_messages = [await dest_channel.send( - files=[discord.File(b, fn) for b, fn in attachment_bytes], - content=fmessage) for fmessage in fmessages] - except discord.HTTPException: - dest_messages = [await dest_channel.send(fmessage) for fmessage in fmessages] - try: - for b, fn in attachment_bytes: - await dest_channel.send(file=discord.File(b, fn)) - except discord.HTTPException: - await dest_channel.send("") - - linked_messages.append((dest_channel.id, [m.id for m in dest_messages])) - - except discord.Forbidden: - if dest_channel.guild.owner: - try: - notify = ("Hi, {1.guild.owner}! This is an automated message from the Tsubaki team to let" - " you know that your server, {1.guild.name}, has been configured to mirror" - " messages from {0.name} (from {0.guild.name}) to {1.name}, but your channel" - " doesn't give me manage message permissions! Please do make sure to allow" - " me permissions to send messages, embed links, and attach files! It's also" - " okay to turn off message mirroring from your channel. If you need help, contact" - " us via `{2}feedback`!" - "").format(channel, dest_channel, (await self.bot.get_valid_prefixes())[0]) - - fctx = await self.bot.get_context(message) - fctx.send = dest_channel.guild.owner.send - fctx.history = dest_channel.guild.owner.history - await send_repeated_consecutive_messages(fctx, notify) - except Exception: - logger.exception("Owner message failed.") - except Exception as ex: - logger.exception( - 'Failed to mirror message from {} to {}: {}'.format(channel.id, dest_channel_id, str(ex))) - - async with self.config.channel(channel).mirrored_messages() as mirrored_messages: - mirrored_messages[str(message.id)] = linked_messages - if len(mirrored_messages) > MAX_MIRRORED_MESSAGES: - mirrored_messages.pop(min(mirrored_messages)) # This works because snowflakes sort by time - [b.close() for b, fn in attachment_bytes] - - @commands.Cog.listener('on_raw_message_edit') - async def mirror_msg_edit(self, payload): - if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): - return - message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) - if 'content' in payload.data: - await self.mirror_msg_mod(message, new_message_content=payload.data['content']) - - @commands.Cog.listener('on_raw_message_delete') - async def mirror_msg_delete(self, payload): - if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): - return - if not await self.config.channel(self.bot.get_channel(payload.channel_id)).nodeletion(): - fmessage = DummyObject(id=payload.message_id, channel=self.bot.get_channel(payload.channel_id)) - await self.mirror_msg_mod(fmessage, delete_message_content=True) - - @commands.Cog.listener('on_raw_reaction_add') - async def mirror_reaction_add(self, payload): - if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): - return - message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) - if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): - return - await self.mirror_msg_mod(message, new_message_reaction=payload.emoji) - - @commands.Cog.listener('on_raw_reaction_remove') - async def mirror_reaction_remove(self, payload): - if str(payload.message_id) not in await self.config.channel_from_id(payload.channel_id).mirrored_messages(): - return - message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) - if message.author.id != payload.user_id and not await self.config.channel(message.channel).multiedit(): - return - await self.mirror_msg_mod(message, delete_message_reaction=payload.emoji) - - async def split_message(self, message: discord.Message, fit_in: Optional[int] = None) -> List[str]: - attribute = await self.config.custom('message', message.id).attribute() - content = (self.makeheader(message) if attribute else '') + message.content - if fit_in is None: - return list(pagify(content, delims=['\n\n', '\n'], shorten_by=0, page_length=1750)) - - messages = list(pagify(content, delims=['\n\n', '\n'], shorten_by=0, page_length=2000)) - if len(messages) > fit_in: - messages = messages[:fit_in] - messages[-1] = messages[-1][:1971] + "... *(Continued in original)*" - elif len(messages) < fit_in: - messages += ["[Placeholder - This message used to be longer!]"] * (fit_in - len(messages)) - return messages - - async def mirror_msg_mod(self, message, - new_message_content: str = None, - delete_message_content: bool = False, - new_message_reaction=None, - delete_message_reaction=None): - if isinstance(message.channel, discord.abc.PrivateChannel): - return - - channel = message.channel - mirrored_messages = (await self.config.channel(channel).mirrored_messages())[str(message.id)] - for (dest_channel_id, dest_message_ids) in mirrored_messages: - try: - dest_channel = self.bot.get_channel(dest_channel_id) - if not dest_channel: - logger.warning('could not locate channel to mod') - continue - dest_messages = [await dest_channel.fetch_message(m_id) for m_id in dest_message_ids] - if not all(dest_messages): - logger.warning('could not locate message to mod') - continue - - if new_message_content: - message.content = await self.mformat(new_message_content, channel, dest_channel) - fcontents = await self.split_message(message, fit_in=len(dest_message_ids)) - for dest_message, content in zip(dest_messages, fcontents): - await dest_message.edit(content=content) - elif new_message_reaction: - try: - await dest_messages[-1].add_reaction(new_message_reaction) - except discord.HTTPException: - pass - elif delete_message_content: - for dest_message in dest_messages: - await dest_message.delete() - elif delete_message_reaction: - try: - await dest_messages[-1].remove_reaction(delete_message_reaction, dest_channel.me) - except discord.HTTPException: - pass - except Exception as ex: - logger.exception('Failed to mirror message edit from {} to {}:'.format(channel.id, dest_channel_id)) - - def makeheader(self, message): - return 'Posted by **{}** in *{} - #{}*:\n{}\n'.format(message.author.name, - message.guild.name, - message.channel.name, - message.jump_url) - - async def mformat(self, text, from_channel, dest_channel): - # LINKS - for link, mid in re.findall(frMESSAGE_LINK.format(from_channel), text): - from_link = await from_channel.fetch_message(mid) - if not from_link: - logger.warning('could not locate link to copy') - continue - mirrored_messages = (await self.config.channel(from_channel).mirrored_messages())[str(from_link.id)] - to_link_ids = [dmids for dcid, dmids in mirrored_messages if dcid == dest_channel.id] - if not to_link_ids: - logger.warning('could not locate link to mod') - continue - to_link_id = to_link_ids[0][0] - dest_link = await dest_channel.fetch_message(to_link_id) - - newlink = MESSAGE_LINK.format(dest_link) - text = text.replace(link, newlink) - - # ROLES - for rtext, rid in re.findall(r'(<@&(\d+)>)', text): - target = from_channel.guild.get_role(int(rid)) - if target is None: - logger.warning('could not locate role to mod') - continue - dest = discord.utils.get(dest_channel.guild.roles, name=target.name) - if dest is None: - repl = "\\@" + target.name - else: - repl = "<@&{}>".format(dest.id) - text = text.replace(rtext, repl) - - # MENTIONS - for utext, uid in re.findall(r'(<@!(\d+)>)', text): - target = from_channel.guild.get_member(int(uid)) - if target is None: - logger.warning('could not locate user to mod') - continue - text = text.replace(utext, target.name) - - # CHANNELS - for ctext, cid in re.findall(r'(<#(\d+)>)', text): - target = from_channel.guild.get_channel(int(cid)) - if target is None: - logger.warning('could not locate channel to mod') - continue - text = text.replace(ctext, "\\#" + target.name) - - # EVERYONE - text = re.sub(r"@everyone\b", "@\u200beveryone", text) - text = re.sub(r"@here\b", "@\u200bhere", text) - - # EMOJI - text = self.emojify(text) - return text - - def emojify(self, message): - emojis = list() - for guild in self.bot.guilds: - emojis.extend(guild.emojis) - message = replace_emoji_names_with_code(emojis, message) - return fix_emojis_for_server(emojis, message) - - @channelmirror.command(aliases=['setmirroreditchannel']) - @checks.is_owner() - async def seteditchannel(self, ctx, channel: discord.TextChannel): - """Sets the edit_target for the current channel to `channelid`""" - await self.config.channel(ctx.channel).mirroredit_target.set(channel.id) - await ctx.tick() - - @channelmirror.command(aliases=['rmmirroreditchannel']) - @checks.is_owner() - async def rmeditchannel(self, ctx): - """Removes the mirroredit_target for the current channel""" - await self.config.channel(ctx.channel).mirroredit_target.set(None) - await ctx.tick() - - @commands.command(aliases=['medit']) - @checks.mod_or_permissions(manage_messages=True) - async def mirroredit(self, ctx, message, *, content): - """Given a message ID from the mirroredit-configured channel, replaces it. - This is a TEMPORARY command until the alias command is fixed to respect newlines. - At that point, we will just use aliases instead of configuring channel IDs here. - """ - try: - message = await commands.MessageConverter().convert(ctx, message) - except commands.MessageNotFound as e: - channel_id = await self.config.channel(ctx.channel).mirroredit_target() - channel = self.bot.get_channel(channel_id) - if channel is None: - await ctx.send( - "Invalid mirroredit channel. Please add one with `{0.prefix}setmirroreditchannel`".format(ctx)) - return - try: - message = await channel.fetch_message(int(message)) - except (discord.NotFound, ValueError): - raise e - if message.author != ctx.me: - await ctx.send("I can't edit a message that's not my own") - return - # Even if permissions change in the target channel in between the bot sending and - # editing the message, the bot will still be able to edit the message, so testing - # for discord.Forbidden is not actually required here (this is in contrast to - # human users lacking an edit button when channels are read-only but they previously) - # had sent a message in them. Tested 2020-12-02. - await message.edit(content=content) - await ctx.tick() - - -class ChannelMirrorSettings(CogSettings): - def make_default_settings(self): - config = { - 'servers': {}, - 'mirrored_channels': {}, - 'max_mirrored_messages': 20, - } - return config diff --git a/channelmirror/info.json b/channelmirror/info.json deleted file mode 100644 index d367786d..00000000 --- a/channelmirror/info.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Allows you to mirror channels across servers.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]channelmirror to get started", - "short": "Mirror channels across servers.", - "min_bot_version": "3.4.0", - "max_bot_version": "0.0.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "required_cogs": { - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" - }, - "requirements": [ - "tsutils>=5.0.0", - "aiohttp" - ], - "tags": [ - "administration", - "mirror" - ] -} diff --git a/crowddata/info.json b/crowddata/info.json index 7c710acb..0ae35c0a 100644 --- a/crowddata/info.json +++ b/crowddata/info.json @@ -2,7 +2,7 @@ "author": ["Aradia Megido#2552"], "description": "Allows users to set up custom preferences to be used cross-cog.", "end_user_data_statement": "All explicitly stored user preferences are kept persistantly.", - "install_msg": "See for a full list of commands.", + "install_msg": "See for a full list of commands.", "short": "User Preferences", "min_bot_version": "3.4.0", "max_bot_version": "0.0.0", diff --git a/crud/crud.py b/crud/crud.py index a1c00161..5451eea6 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -63,7 +63,7 @@ def __init__(self, bot, *args, **kwargs): GADMIN_COG.register_perm("crud") else: raise errors.CogLoadError("Global Administration cog must be loaded. Make sure it's " - "installed from misc-cogs and load it via `^load globaladmin`") + "installed from core-cogs and load it via `^load globaladmin`") async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" diff --git a/crud/info.json b/crud/info.json index e64d0a3f..93f3336f 100644 --- a/crud/info.json +++ b/crud/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" + "globaladmin": "https://github.com/TsubakiBotPad/core-cogs" }, "requirements": [ "tsutils>=5.0.0", diff --git a/dbcog/info.json b/dbcog/info.json index bcbb230a..921fb9f6 100644 --- a/dbcog/info.json +++ b/dbcog/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs" + "globaladmin": "https://github.com/TsubakiBotPad/core-cogs" }, "requirements": [ "tsutils>=5.0.0", diff --git a/dungeoncog/info.json b/dungeoncog/info.json index 46883343..8f23a36b 100644 --- a/dungeoncog/info.json +++ b/dungeoncog/info.json @@ -18,7 +18,7 @@ "disabled": false, "required_cogs": { "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", - "menulistener": "https://github.com/TsubakiBotPad/misc-cogs" + "menulistener": "https://github.com/TsubakiBotPad/core-cogs" }, "requirements": [ "tsutils>=3.4.0", diff --git a/padglobal/info.json b/padglobal/info.json index 682b3ab3..43445900 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": { "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", + "globaladmin": "https://github.com/TsubakiBotPad/core-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, "requirements": [ diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index ec620fcb..72b55618 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -94,7 +94,7 @@ def __init__(self, bot, *args, **kwargs): GADMIN_COG.register_perm("contentadmin") else: raise errors.CogLoadError("Global Administration cog must be loaded. Make sure it's " - "installed from misc-cogs and load it via `^load globaladmin`") + "installed from core-cogs and load it via `^load globaladmin`") self.file_path = _data_file('commands.json') self.c_commands = safe_read_json(self.file_path) diff --git a/padinfo/info.json b/padinfo/info.json index 319eb9c3..b507411f 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -17,7 +17,7 @@ "disabled": false, "required_cogs": { "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", - "speech": "https://github.com/TsubakiBotPad/padbot-cogs" + "speech": "https://github.com/TsubakiBotPad/core-cogs" }, "requirements": [ "tsutils>=4.0.0", diff --git a/pricecheck/info.json b/pricecheck/info.json index de6531e5..d5cc4bc7 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "globaladmin": "https://github.com/TsubakiBotPad/misc-cogs", + "globaladmin": "https://github.com/TsubakiBotPad/core-cogs", "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" }, From 90b6cfc142d71d973701bea52da76bbb857be16a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 1 Mar 2022 20:06:13 -0600 Subject: [PATCH 1065/1220] compatibility update for tsutils 7.0.0 (#1611) --- crowddata/info.json | 4 +- crowddata/mixins/adpem/adpem.py | 11 ++- crowddata/mixins/adpem/view/show_stats.py | 2 +- dbcog/dbcog.py | 2 +- padbuildimg/padbuildimg.py | 2 +- padglobal/padglobal.py | 2 +- padinfo/padinfo.py | 9 +- padinfo/view/awakening_help.py | 6 +- padinfo/view/button_info.py | 6 +- padinfo/view/common.py | 6 +- padinfo/view/components/base_id_main_view.py | 2 +- padinfo/view/components/evo_scroll_mixin.py | 2 +- padinfo/view/components/monster/__init__.py | 0 padinfo/view/components/monster/header.py | 86 -------------------- padinfo/view/evos.py | 16 ++-- padinfo/view/experience_curve.py | 7 +- padinfo/view/id.py | 10 +-- padinfo/view/id_traceback.py | 4 +- padinfo/view/leader_skill.py | 6 +- padinfo/view/leader_skill_single.py | 4 +- padinfo/view/links.py | 7 +- padinfo/view/lookup.py | 7 +- padinfo/view/materials.py | 29 ++++--- padinfo/view/monster_list/monster_list.py | 12 +-- padinfo/view/otherinfo.py | 10 +-- padinfo/view/pantheon.py | 16 ++-- padinfo/view/pic.py | 13 +-- padinfo/view/series_scroll.py | 13 +-- padinfo/view/transforminfo.py | 10 +-- 29 files changed, 110 insertions(+), 194 deletions(-) delete mode 100644 padinfo/view/components/monster/__init__.py delete mode 100644 padinfo/view/components/monster/header.py diff --git a/crowddata/info.json b/crowddata/info.json index 0ae35c0a..bd5778c2 100644 --- a/crowddata/info.json +++ b/crowddata/info.json @@ -9,7 +9,9 @@ "min_python_version": [3, 8, 1], "hidden": false, "disabled": false, - "required_cogs": {}, + "required_cogs": { + "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" + }, "requirements": [], "tags": [] } diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index 0da467d0..0d66f3f2 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -11,6 +11,7 @@ from tsutils.emoji import NO_EMOJI, char_to_emoji from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.time import NA_TIMEZONE, NEW_DAY, get_last_time +from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, get_user_reaction from crowddata.mixins.adpem.menu.closable_embed import ClosableEmbedMenu @@ -76,8 +77,7 @@ async def report_at_time(self, ctx, pulls, offset): f"Valid input: `{ctx.prefix}adpem report Enoch, Facet, D Globe, B Super King, Facet`") dbcog: Any = ctx.bot.get_cog("DBCog") - pdicog: Any = ctx.bot.get_cog("PadInfo") - if dbcog is None or pdicog is None: + if dbcog is None: return await ctx.send("Required cogs not loaded. Please alert a bot owner.") await dbcog.wait_until_ready() @@ -91,7 +91,7 @@ async def report_at_time(self, ctx, pulls, offset): unknown = '\n\t'.join(s for s, m in zip(pulls_split, monsters) if m is None) return await ctx.send(f"Not all monsters were valid. The following could not be processed:\n\t{unknown}") - check = '\n\t'.join(pdicog.monster_header.fmt_id_header(m, use_emoji=True).to_markdown() + check = '\n\t'.join(MonsterHeader.menu_title(m, use_emoji=True).to_markdown() for m in monsters) confirmation = await get_user_confirmation(ctx, f"Are these monsters correct?\n\t{check}", timeout=30, force_delete=False, show_feedback=True) @@ -148,8 +148,7 @@ async def removeyesterday(self, ctx): async def remove_at_time(self, ctx, midnight): dbcog: Any = ctx.bot.get_cog("DBCog") - pdicog: Any = ctx.bot.get_cog("PadInfo") - if dbcog is None or pdicog is None: + if dbcog is None: return await ctx.send("Required cogs not loaded. Please alert a bot owner.") await dbcog.wait_until_ready() @@ -165,7 +164,7 @@ async def remove_at_time(self, ctx, midnight): pulltext.append(c + ' ' + pull) else: pulltext.append(c + '\n\t' + '\n\t'.join( - pdicog.monster_header.fmt_id_header(dbcog.get_monster(mid), use_emoji=True).to_markdown() + MonsterHeader.menu_title(dbcog.get_monster(mid), use_emoji=True).to_markdown() for mid in pull) ) pulltext = '\n\n'.join(pulltext) diff --git a/crowddata/mixins/adpem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py index fcafe509..548e2fb7 100644 --- a/crowddata/mixins/adpem/view/show_stats.py +++ b/crowddata/mixins/adpem/view/show_stats.py @@ -7,7 +7,7 @@ from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.closable_embed import ClosableEmbedViewState -from tsutils.tsubaki import MonsterImage +from tsutils.tsubaki.links import MonsterImage if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 3e0d1228..04d186ea 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -22,7 +22,7 @@ from tsutils.cogs.globaladmin import auth_check from tsutils.enums import Server from tsutils.json_utils import async_cached_dadguide_request, safe_read_json -from tsutils.tsubaki import CLOUDFRONT_URL +from tsutils.tsubaki.links import CLOUDFRONT_URL from tsutils.user_interaction import StatusManager, send_confirmation_message from . import token_mappings diff --git a/padbuildimg/padbuildimg.py b/padbuildimg/padbuildimg.py index 78e05553..b679461e 100644 --- a/padbuildimg/padbuildimg.py +++ b/padbuildimg/padbuildimg.py @@ -13,7 +13,7 @@ from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, inline from tsutils.cog_settings import CogSettings -from tsutils.tsubaki import CLOUDFRONT_URL +from tsutils.tsubaki.links import CLOUDFRONT_URL HELP_MSG = """ ^buildimg diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 72b55618..5fc14a8b 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -21,7 +21,7 @@ from tsutils.json_utils import safe_read_json from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.time import NA_TIMEZONE -from tsutils.tsubaki import CLOUDFRONT_URL +from tsutils.tsubaki.links import CLOUDFRONT_URL from tsutils.user_interaction import get_user_confirmation, get_user_reaction from padglobal.menu.closable_embed import ClosableEmbedMenu diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 3486fd44..58efd3a5 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -24,6 +24,7 @@ from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.menu.view.simple_text import SimpleTextViewState from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ @@ -49,7 +50,6 @@ from padinfo.view.awakening_list import AwakeningListSortTypes, AwakeningListViewState from padinfo.view.button_info import ButtonInfoToggles, ButtonInfoViewState from padinfo.view.common import invalid_monster_text -from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.evos import EvosViewState from padinfo.view.experience_curve import ExperienceCurveView, ExperienceCurveViewProps from padinfo.view.id import IdViewState @@ -124,7 +124,6 @@ def __init__(self, bot, *args, **kwargs): self.awoken_emoji_names = {v: k for k, v in AWAKENING_ID_TO_EMOJI_NAME_MAP.items()} self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster - self.monster_header = MonsterHeader self.settings = settings async def red_get_data_for_user(self, *, user_id): @@ -185,7 +184,7 @@ async def jpname(self, ctx, *, query: str): dbcog = await self.get_dbcog() monster = await dbcog.find_monster(query, ctx.author.id) if monster is not None: - await ctx.send(MonsterHeader.short(monster)) + await ctx.send(MonsterHeader.text_with_emoji(monster)) await ctx.send(box(monster.name_ja)) else: await self.send_id_failure_message(ctx, query) @@ -998,7 +997,7 @@ async def padsay(self, ctx, server, *, query: str = None): return base_dir = settings.voiceDir() voice_file = os.path.join(base_dir, server, '{0:03d}.wav'.format(voice_id)) - header = '{} ({})'.format(MonsterHeader.short(monster), server) + header = '{} ({})'.format(MonsterHeader.text_with_emoji(monster), server) if not os.path.exists(voice_file): await ctx.send(inline('Could not find voice for ' + header)) return @@ -1591,7 +1590,7 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): if start <= 0 or end > 120 or end < start or (start == end and offset): return await send_cancellation_message(ctx, f"Invalid bounds ({start}[{offset}] - {end}).") - header = MonsterHeader.fmt_id_header(monster, use_emoji=True).to_markdown() + header = MonsterHeader.menu_title(monster, use_emoji=True).to_markdown() if not monster.limit_mult and end > 99: return await send_cancellation_message(ctx, f"{header} cannot limit break.") if monster.level < end <= 99: diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index f1d28f2c..97a903b3 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -5,11 +5,11 @@ from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.common.emoji_map import get_awakening_emoji from padinfo.view.common import get_awoken_skill_description -from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -75,7 +75,7 @@ def embed(state, props: AwakeningHelpViewProps): description='This monster has no awakenings.' if not monster.awakenings else '' ), embed_author=EmbedAuthor( - MonsterHeader.long_v2(monster).to_markdown(), + MonsterHeader.menu_title(monster).to_markdown(), # TODO: add query_settings MonsterLink.header_link(monster), MonsterImage.icon(monster.monster_id) diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 50d2a098..66e36b8f 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -8,12 +8,12 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL, ButtonInfoStatSet, \ ButtonInfoResult from padinfo.view.components.evo_scroll_mixin import EvoScrollView, EvoScrollViewState, MonsterEvolution -from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -205,7 +205,7 @@ def embed(cls, state: ButtonInfoViewState): description='(Co-op mode)' if is_coop else '(Singleplayer mode)' ), embed_author=EmbedAuthor( - MonsterHeader.long_v2(monster).to_markdown(), + MonsterHeader.menu_title(monster).to_markdown(), MonsterLink.header_link(monster, state.query_settings), MonsterImage.icon(monster.monster_id) ), diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 5f6e52c6..1eca50f0 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -3,9 +3,9 @@ from discordmenu.embed.base import Box from discordmenu.embed.text import Text, BoldText from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.common.emoji_map import get_awakening_emoji -from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -35,6 +35,6 @@ def get_awoken_skill_description(awoken_skill: "AwokenSkillModel"): ) -def invalid_monster_text(query: str, monster: "MonsterModel", append_text: str, link: bool = False): - monster_name = MonsterHeader.short_with_emoji(monster, link=link).to_markdown() +def invalid_monster_text(query: str, monster: "MonsterModel", append_text: str): + monster_name = MonsterHeader.text_with_emoji(monster) return f'Your query `{query}` found {monster_name}{append_text}.' diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 5b727e04..337c7a7f 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -6,8 +6,8 @@ from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache -from tsutils.tsubaki import number_emoji_small from tsutils.enums import LsMultiplier +from tsutils.tsubaki.custom_emoji import number_emoji_small from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text diff --git a/padinfo/view/components/evo_scroll_mixin.py b/padinfo/view/components/evo_scroll_mixin.py index 9d9d8640..71f5eb60 100644 --- a/padinfo/view/components/evo_scroll_mixin.py +++ b/padinfo/view/components/evo_scroll_mixin.py @@ -4,7 +4,7 @@ from discordmenu.embed.components import EmbedField from discordmenu.embed.text import HighlightableLinks, LinkedText from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterLink +from tsutils.tsubaki.links import MonsterLink if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/components/monster/__init__.py b/padinfo/view/components/monster/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/padinfo/view/components/monster/header.py b/padinfo/view/components/monster/header.py deleted file mode 100644 index acf19b2d..00000000 --- a/padinfo/view/components/monster/header.py +++ /dev/null @@ -1,86 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -from discordmenu.embed.base import Box -from discordmenu.embed.text import LinkedText, Text -from tsutils.enums import Server -from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterLink - -from padinfo.common.emoji_map import get_attribute_emoji_by_monster - -if TYPE_CHECKING: - from dbcog.models.monster_model import MonsterModel - - -# TODO: add query_settings to all uses -class MonsterHeader: - @classmethod - def jp_suffix(cls, m: "MonsterModel", is_jp_buffed=False, subname_on_override=True): - suffix = "" - if m.roma_subname and (subname_on_override or m.name_en_override is None): - suffix += ' [{}]'.format(m.roma_subname) - if not m.on_na: - suffix += ' (JP only)' - if is_jp_buffed: - suffix += ' (JP buffed)' - return suffix - - @classmethod - def short(cls, m: "MonsterModel", link=False, query_settings: Optional[QuerySettings] = None): - type_emojis = '' - msg = '[{}] {}{}'.format(m.monster_no_na, type_emojis, m.name_en) - return '[{}]({})'.format(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else msg - - @classmethod - def long(cls, m: "MonsterModel", link=False, query_settings: Optional[QuerySettings] = None): - msg = cls.short(m) + cls.jp_suffix(m) - return '[{}]({})'.format(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else msg - - @classmethod - def name(cls, m: "MonsterModel", link=False, show_jp=False, - query_settings: Optional[QuerySettings] = None): - msg = '[{}] {}{}'.format( - m.monster_no_na, - m.name_en, - cls.jp_suffix(m) if show_jp else '') - return LinkedText(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else Text(msg) - - @classmethod - def long_v2(cls, m: "MonsterModel", link=False, - query_settings: Optional[QuerySettings] = None): - msg = '[{}] {}{}'.format(m.monster_no_na, m.name_en, cls.jp_suffix(m)) - return LinkedText(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else Text(msg) - - @classmethod - def long_maybe_tsubaki(cls, m: "MonsterModel", is_tsubaki, is_jp_buffed=False, - query_settings: Optional[QuerySettings] = None): - """Returns long_v2 as well as an `!` if the monster is Tsubaki - - To celebrate 1000 issues/PRs in our main Tsubaki repo, we added this easter egg! Yay! - """ - return '[{}] {}{}{}'.format( - m.monster_no_na, - m.name_en, - '!' if is_tsubaki else '', - cls.jp_suffix(m, is_jp_buffed)) - - @classmethod - def fmt_id_header(cls, m: "MonsterModel", is_tsubaki=False, is_jp_buffed=False, - use_emoji=False): - return Text('{}{} {}'.strip().format( - get_attribute_emoji_by_monster(m) if use_emoji else '', - '\N{EARTH GLOBE AMERICAS}' if m.server_priority == Server.NA else '', - cls.long_maybe_tsubaki(m, is_tsubaki, bool(is_jp_buffed)))) - - @classmethod - def short_with_emoji(cls, m: "MonsterModel", link=True, prefix=None, - query_settings: Optional[QuerySettings] = None): - msg = f"{m.monster_no_na} - {m.name_en}" - suffix = cls.jp_suffix(m, False, False) - return Box( - prefix, - Text(get_attribute_emoji_by_monster(m)), - LinkedText(msg, MonsterLink.header_link(m, query_settings=query_settings)) if link else Text(msg), - Text(suffix) if suffix else None, - delimiter=' ' - ) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index da58aed4..39deafe7 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -6,12 +6,12 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import MonsterEvolution -from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -77,11 +77,11 @@ class EvosView(BaseIdView): VIEW_TYPE = 'Evos' @staticmethod - def _evo_lines(monsters, current_monster): + def _evo_lines(monsters, current_monster, query_settings): if not len(monsters): return [] return [ - MonsterHeader.short_with_emoji(ae, link=ae.monster_id != current_monster.monster_id) + MonsterHeader.box_with_emoji(ae, link=ae.monster_id != current_monster.monster_id, query_settings=query_settings) for ae in sorted(monsters, key=lambda x: int(x.monster_id)) ] @@ -90,7 +90,7 @@ def embed(cls, state: EvosViewState): fields = [ EmbedField( ("{} evolution" if len(state.alt_versions) == 1 else "{} evolutions").format(len(state.alt_versions)), - Box(*EvosView._evo_lines(state.alt_versions, state.monster)))] + Box(*EvosView._evo_lines(state.alt_versions, state.monster, state.query_settings)))] if state.gem_versions: fields.append( @@ -102,9 +102,9 @@ def embed(cls, state: EvosViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.is_jp_buffed).to_markdown(), + title=MonsterHeader.menu_title(state.monster, + is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + is_jp_buffed=state.is_jp_buffed).to_markdown(), url=MonsterLink.header_link(state.monster, state.query_settings)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index 4438999f..78c73a27 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -8,9 +8,8 @@ from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core.utils.chat_formatting import humanize_number from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.tsubaki import MonsterImage, MonsterLink - -from padinfo.view.components.monster.header import MonsterHeader +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -118,7 +117,7 @@ def embed(state, props: ExperienceCurveViewProps): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(props.monster, use_emoji=True), + title=MonsterHeader.menu_title(props.monster, use_emoji=True), # TODO: add query_settings url=MonsterLink.header_link(props.monster), description=Text(f'lv{props.low} -> lv{props.high} (' diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 65303762..61e12b60 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -8,13 +8,13 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.view.common import get_monster_from_ims, invalid_monster_text from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution -from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -290,9 +290,9 @@ def embed(cls, state: IdViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(m, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.is_jp_buffed).to_markdown(), + title=MonsterHeader.menu_title(m, + is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + is_jp_buffed=state.is_jp_buffed).to_markdown(), url=MonsterLink.header_link(m, state.query_settings)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id)), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index 1f41d18c..477fa1f3 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -5,9 +5,9 @@ from discordmenu.embed.text import LabeledText from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.common.emoji_map import get_attribute_emoji_by_monster -from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -44,7 +44,7 @@ def embed(state, props: IdTracebackViewProps): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(props.monster, use_emoji=True), + title=MonsterHeader.menu_title(props.monster, use_emoji=True), description=get_description(props.score) ), embed_fields=[ diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index deecf6b5..8bea97bb 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -8,10 +8,10 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.core.leader_skills import leaderskill_query from padinfo.core.leader_skills import ls_multiplier_text -from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: pass @@ -63,9 +63,9 @@ def embed(state: LeaderSkillViewState): embed_main=EmbedMain( title=ls_multiplier_text(lls, rls), description=Box( - BoldText(MonsterHeader.name(state.l_mon, link=True, show_jp=True)), + BoldText(MonsterHeader.box_with_emoji(state.l_mon, query_settings=state.l_query_settings)), Text(lls.desc if lls else 'None'), - BoldText(MonsterHeader.name(state.r_mon, link=True, show_jp=True)), + BoldText(MonsterHeader.box_with_emoji(state.r_mon, query_settings=state.r_query_settings)), Text(rls.desc if rls else 'None')), color=state.color), embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index b09b58bd..a80faaa5 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -8,10 +8,10 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.core.leader_skills import ls_single_multiplier_text from padinfo.view.common import get_monster_from_ims -from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: pass @@ -54,7 +54,7 @@ def embed(state: LeaderSkillSingleViewState): embed_main=EmbedMain( title=ls_single_multiplier_text(ls), description=Box( - BoldText(MonsterHeader.name(state.mon, link=True, show_jp=True)), + BoldText(MonsterHeader.box_with_emoji(state.mon, query_settings=state.query_settings)), Text(ls.desc if ls else 'None')), color=state.color), embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 403fc56b..e7dd11f2 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -5,9 +5,8 @@ from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink - -from padinfo.view.components.monster.header import MonsterHeader +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -30,7 +29,7 @@ def embed(m: "MonsterModel", color, query_settings: QuerySettings): return EmbedView( EmbedMain( color=color, - title=MonsterHeader.long_v2(m).to_markdown(), + title=MonsterHeader.menu_title(m).to_markdown(), description=LinksView.linksbox(m), url=MonsterLink.header_link(m, query_settings)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index 887dfb5d..814776d5 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -3,9 +3,8 @@ from discordmenu.embed.components import EmbedMain, EmbedAuthor from discordmenu.embed.view import EmbedView from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink - -from padinfo.view.components.monster.header import MonsterHeader +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -19,6 +18,6 @@ def embed(m: "MonsterModel", color, query_settings: QuerySettings): return EmbedView( EmbedMain(color=color), embed_author=EmbedAuthor( - MonsterHeader.long_v2(m).to_markdown(), + MonsterHeader.menu_title(m).to_markdown(), MonsterLink.header_link(m, query_settings), MonsterImage.icon(m.monster_id))) diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 2e4ec070..77fd8d9a 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -7,12 +7,12 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution -from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -114,7 +114,7 @@ async def do_query(dbcog, monster): return mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override -def mat_use_field(mons, title, max_mons=MAX_MONS_TO_SHOW): +def mat_use_field(mons, title, max_mons=MAX_MONS_TO_SHOW, query_settings: Optional[QuerySettings] = None): text = None if len(mons) == 0: text = "None" @@ -122,10 +122,11 @@ def mat_use_field(mons, title, max_mons=MAX_MONS_TO_SHOW): text = f"({len(mons) - max_mons} more monster{'s' if len(mons) - max_mons > 1 else ''}, see `^allmats` for full list)" return EmbedField( title, - Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:max_mons]), text)) + Box(*(MonsterHeader.box_with_emoji( + em, query_settings=query_settings) for em in mons[:max_mons]), text)) -def skillup_field(mons, sec, link): +def skillup_field(mons, sec, link, query_settings): text = None text2 = None if len(mons) == 0: @@ -142,7 +143,8 @@ def skillup_field(mons, sec, link): return EmbedField( "Skillups", - Box(*(MonsterHeader.short_with_emoji(em) for em in mons[:MAX_MONS_TO_SHOW]), text, text2)) + Box(*(MonsterHeader.box_with_emoji( + em, query_settings=query_settings) for em in mons[:MAX_MONS_TO_SHOW]), text, text2)) class MaterialsView(BaseIdView, EvoScrollView): @@ -154,22 +156,23 @@ def embed(cls, state: MaterialsViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.is_jp_buffed).to_markdown(), + title=MonsterHeader.menu_title(state.monster, + is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + is_jp_buffed=state.is_jp_buffed).to_markdown(), url=MonsterLink.header_link(state.monster, state.query_settings) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id)), embed_footer=embed_footer_with_state(state), embed_fields=[f for f in [ - mat_use_field(state.mats, "Evo materials") + mat_use_field(state.mats, "Evo materials", query_settings=state.query_settings) if state.mats or not (state.monster.is_stackable or state.gem_override) else None, - mat_use_field(state.usedin, "Material for", 10) + mat_use_field(state.usedin, "Material for", 10, state.query_settings) if state.usedin else None, mat_use_field(state.gemusedin, "Evo gem ({}) is mat for".format(state.gemid), - 10 if state.gem_override else 5) + 10 if state.gem_override else 5, state.query_settings) if state.gemusedin else None, - skillup_field(state.skillups, state.skillup_evo_count, state.link) + skillup_field(state.skillups, state.skillup_evo_count, + state.link, state.query_settings) if not (state.monster.is_stackable or state.gem_override) else None ] if f is not None] + [cls.evos_embed_field(state)] ) diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 5498af60..ee26c38b 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -9,8 +9,7 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings - -from padinfo.view.components.monster.header import MonsterHeader +from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -192,11 +191,12 @@ class MonsterListView: VIEW_TYPE = 'MonsterList' @classmethod - def monster_list(cls, monsters: List["MonsterModel"], current_monster_id: int): + def monster_list(cls, monsters: List["MonsterModel"], current_monster_id: int, query_settings: QuerySettings): if not len(monsters): return [] - return [MonsterHeader.short_with_emoji( - mon, link=True, prefix=cls.get_emoji(i, current_monster_id)) for i, mon in enumerate(monsters)] + return [MonsterHeader.box_with_emoji( + mon, link=True, prefix=cls.get_emoji(i, current_monster_id), + query_settings=query_settings) for i, mon in enumerate(monsters)] @classmethod def get_emoji(cls, i: int, _current_monster_id: int): @@ -206,7 +206,7 @@ def get_emoji(cls, i: int, _current_monster_id: int): def embed(cls, state: MonsterListViewState): fields = [ EmbedField(state.title, - Box(*cls.monster_list(state.monster_list, state.current_monster_id))), + Box(*cls.monster_list(state.monster_list, state.current_monster_id, state.query_settings))), EmbedField(BoldText('Page'), Box('{} of {}'.format(state.current_page + 1, state.page_count)), inline=True diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index b034906b..621b1223 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -7,11 +7,11 @@ from discordmenu.embed.view import EmbedView from redbot.core.utils.chat_formatting import box from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.tsubaki import MonsterLink +from tsutils.tsubaki.links import MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.view.base import BaseIdView from padinfo.view.components.evo_scroll_mixin import EvoScrollView -from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId from padinfo.view.links import LinksView @@ -53,9 +53,9 @@ def embed(cls, state: OtherInfoViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.is_jp_buffed).to_markdown(), + title=MonsterHeader.menu_title(state.monster, + is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + is_jp_buffed=state.is_jp_buffed).to_markdown(), url=MonsterLink.header_link(m, state.query_settings)), embed_footer=embed_footer_with_state(state), embed_fields=[ diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 93216f12..1b636af0 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -6,14 +6,14 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ get_rarity_emoji from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution -from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId if TYPE_CHECKING: @@ -148,11 +148,11 @@ async def do_query(cls, dbcog, monster): return pantheon_list, cls.make_series_name(series_name, filters), base_mon -def _pantheon_lines(monsters, base_monster): +def _pantheon_lines(monsters, base_monster, query_settings): if not len(monsters): return [] return [ - MonsterHeader.short_with_emoji(mon, link=mon.monster_id != base_monster.monster_id) + MonsterHeader.box_with_emoji(mon, link=mon.monster_id != base_monster.monster_id, query_settings=query_settings) for mon in sorted(monsters, key=lambda x: int(x.monster_id)) ] @@ -164,16 +164,16 @@ class PantheonView(BaseIdView, EvoScrollView): def embed(cls, state: PantheonViewState): fields = [EmbedField( 'Pantheon: {}'.format(state.series_name), - Box(*_pantheon_lines(state.pantheon_list, state.base_monster)) + Box(*_pantheon_lines(state.pantheon_list, state.base_monster, state.query_settings)) ), cls.evos_embed_field(state)] return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.is_jp_buffed).to_markdown(), + title=MonsterHeader.menu_title(state.monster, + is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + is_jp_buffed=state.is_jp_buffed).to_markdown(), url=MonsterLink.header_link(state.monster, state.query_settings)), embed_footer=embed_footer_with_state(state), embed_fields=fields, diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 63cfa314..7ac2d0b8 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -3,11 +3,11 @@ from discordmenu.embed.text import LinkedText, Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.view.base import BaseIdView from padinfo.view.components.evo_scroll_mixin import EvoScrollView -from padinfo.view.components.monster.header import MonsterHeader from padinfo.view.components.view_state_base_id import ViewStateBaseId @@ -53,10 +53,11 @@ def embed(cls, state: PicViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(state.monster, - state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, - state.is_jp_buffed).to_markdown(), - url=MonsterLink.header_link(state.monster, state.query_settings)), + title=MonsterHeader.menu_title(state.monster, + is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, + is_jp_buffed=state.is_jp_buffed).to_markdown(), + url=MonsterLink.header_link(state.monster, state.query_settings) + ), embed_footer=embed_footer_with_state(state), embed_fields=fields, embed_body_image=EmbedBodyImage(url), diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index af1b4df2..7a378f75 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -9,8 +9,7 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings - -from padinfo.view.components.monster.header import MonsterHeader +from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -220,7 +219,8 @@ def embed(state: SeriesScrollViewState): EmbedField(BoldText('Current rarity: {}'.format(state.rarity)), Box(*SeriesScrollView._monster_list( state.monster_list, - state.current_index))), + state.current_index, + state.query_settings))), EmbedField(BoldText('Rarities'), Box( SeriesScrollView._all_rarity_text(state), @@ -251,14 +251,15 @@ def _all_rarity_text(state): return ', '.join(lines) @staticmethod - def _monster_list(monsters, current_index): + def _monster_list(monsters, current_index, query_settings: QuerySettings): if not len(monsters): return [] return [ - MonsterHeader.short_with_emoji( + MonsterHeader.box_with_emoji( mon, link=SeriesScrollView._is_linked(i, current_index), - prefix=char_to_emoji(str(i)) + prefix=char_to_emoji(str(i)), + query_settings=query_settings ) for i, mon in enumerate(monsters) ] diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index d659aa69..d5cd6a27 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -9,11 +9,11 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings -from tsutils.tsubaki import MonsterImage, MonsterLink +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.common.emoji_map import get_emoji from padinfo.view.components.base_id_main_view import BaseIdMainView -from padinfo.view.components.monster.header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -221,9 +221,9 @@ def embed(cls, state: TransformInfoViewState): return EmbedView( EmbedMain( color=state.color, - title=MonsterHeader.fmt_id_header(transformed_mon, - False, - state.is_jp_buffed).to_markdown(), + title=MonsterHeader.menu_title(transformed_mon, + is_tsubaki=False, + is_jp_buffed=state.is_jp_buffed).to_markdown(), url=MonsterLink.header_link(transformed_mon, state.query_settings) ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(transformed_mon.monster_id)), From 2cb4e3dad25ccb65257b1f85ca43605fbf35bef5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 1 Mar 2022 20:29:03 -0600 Subject: [PATCH 1066/1220] Update info.json with right repo url (#1612) --- crowddata/info.json | 2 +- dungeoncog/info.json | 2 +- padbuildimg/info.json | 4 ++-- padevents/info.json | 2 +- padglobal/info.json | 4 ++-- padinfo/info.json | 2 +- padmonitor/info.json | 2 +- padsearch/info.json | 2 +- pricecheck/info.json | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crowddata/info.json b/crowddata/info.json index bd5778c2..cdbfcac6 100644 --- a/crowddata/info.json +++ b/crowddata/info.json @@ -10,7 +10,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs" }, "requirements": [], "tags": [] diff --git a/dungeoncog/info.json b/dungeoncog/info.json index 8f23a36b..720cb657 100644 --- a/dungeoncog/info.json +++ b/dungeoncog/info.json @@ -17,7 +17,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs", "menulistener": "https://github.com/TsubakiBotPad/core-cogs" }, "requirements": [ diff --git a/padbuildimg/info.json b/padbuildimg/info.json index 86ece959..ee00b5d9 100644 --- a/padbuildimg/info.json +++ b/padbuildimg/info.json @@ -17,8 +17,8 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs", + "padinfo": "https://github.com/TsubakiBotPad/pad-cogs" }, "requirements": [ "tsutils", diff --git a/padevents/info.json b/padevents/info.json index 723769c4..2308c86f 100644 --- a/padevents/info.json +++ b/padevents/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs" }, "requirements": [ "tsutils>=4.0.7", diff --git a/padglobal/info.json b/padglobal/info.json index 43445900..37243a34 100644 --- a/padglobal/info.json +++ b/padglobal/info.json @@ -16,9 +16,9 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs", "globaladmin": "https://github.com/TsubakiBotPad/core-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + "padinfo": "https://github.com/TsubakiBotPad/pad-cogs" }, "requirements": [ "tsutils>=4.0.0", diff --git a/padinfo/info.json b/padinfo/info.json index b507411f..7cd32991 100644 --- a/padinfo/info.json +++ b/padinfo/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs", "speech": "https://github.com/TsubakiBotPad/core-cogs" }, "requirements": [ diff --git a/padmonitor/info.json b/padmonitor/info.json index 02065ead..4f4837a3 100644 --- a/padmonitor/info.json +++ b/padmonitor/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs" }, "requirements": [ "tsutils>=3.0.0" diff --git a/padsearch/info.json b/padsearch/info.json index 32984638..422108f9 100644 --- a/padsearch/info.json +++ b/padsearch/info.json @@ -16,7 +16,7 @@ "hidden": false, "disabled": false, "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs" }, "requirements": [ "tsutils", diff --git a/pricecheck/info.json b/pricecheck/info.json index d5cc4bc7..ec6b785a 100644 --- a/pricecheck/info.json +++ b/pricecheck/info.json @@ -17,8 +17,8 @@ "disabled": false, "required_cogs": { "globaladmin": "https://github.com/TsubakiBotPad/core-cogs", - "dbcog": "https://github.com/TsubakiBotPad/padbot-cogs", - "padinfo": "https://github.com/TsubakiBotPad/padbot-cogs" + "dbcog": "https://github.com/TsubakiBotPad/pad-cogs", + "padinfo": "https://github.com/TsubakiBotPad/pad-cogs" }, "requirements": [ "tsutils" From 6e19120f4e496412b17e649c94de421c1e6d2f82 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 1 Mar 2022 20:49:31 -0600 Subject: [PATCH 1067/1220] Use custom_emoji import from tsutils (#1613) --- padglobal/padglobal.py | 13 +- padinfo/common/__init__.py | 0 padinfo/common/emoji_map.py | 136 ------------------- padinfo/core/leader_skills.py | 2 +- padinfo/padinfo.py | 4 +- padinfo/view/awakening_help.py | 2 +- padinfo/view/common.py | 2 +- padinfo/view/components/base_id_main_view.py | 3 +- padinfo/view/id.py | 2 +- padinfo/view/id_traceback.py | 3 +- padinfo/view/pantheon.py | 4 +- padinfo/view/transforminfo.py | 2 +- 12 files changed, 16 insertions(+), 157 deletions(-) delete mode 100644 padinfo/common/__init__.py delete mode 100644 padinfo/common/emoji_map.py diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 5fc14a8b..3be4b124 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -21,6 +21,7 @@ from tsutils.json_utils import safe_read_json from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.time import NA_TIMEZONE +from tsutils.tsubaki.custom_emoji import get_attribute_emoji_by_monster from tsutils.tsubaki.links import CLOUDFRONT_URL from tsutils.user_interaction import get_user_confirmation, get_user_reaction @@ -639,7 +640,6 @@ async def bosslist(self, ctx): async def lookup_boss(self, term, ctx): dbcog = await self.get_dbcog() - pdicog = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') m = await dbcog.find_monster(term, ctx.author.id) @@ -647,7 +647,7 @@ async def lookup_boss(self, term, ctx): return None, None m = dbcog.database.graph.get_base_monster(m) - name = pdicog.get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() + name = get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() monster_id = m.monster_id definition = self.settings.boss().get(monster_id, None) @@ -768,7 +768,6 @@ async def which(self, ctx, *, term: str = None): async def _resolve_which(self, ctx, term): dbcog = await self.get_dbcog() db_context = dbcog.database - padinfo = self.bot.get_cog("PadInfo") term = term.lower().replace('?', '') m = await dbcog.find_monster(term, ctx.author.id) @@ -778,7 +777,7 @@ async def _resolve_which(self, ctx, term): m = db_context.graph.get_base_monster(m) - name = padinfo.get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() + name = get_attribute_emoji_by_monster(m) + " " + m.name_en.split(",")[-1].strip() monster_id = m.monster_id definition = self.settings.which().get(monster_id, None) timestamp = UNKNOWN_EDIT_TIMESTAMP @@ -869,7 +868,6 @@ async def pwhich_edit(self, ctx, term, *, definition): async def _pwhich_add(self, ctx, term, definition, need_confirm=True): dbcog = await self.get_dbcog() - pdicog = self.bot.get_cog("PadInfo") term = term.lower() m = await dbcog.find_monster(term, ctx.author.id) @@ -889,7 +887,7 @@ async def _pwhich_add(self, ctx, term, definition, need_confirm=True): if not is_int or op == 'edited' and need_confirm: if not await get_user_confirmation(ctx, "Are you sure you want to {} which info for {} [{}] {}?".format( 'edit the' if op == 'edited' else 'add new', - pdicog.get_attribute_emoji_by_monster(m), + get_attribute_emoji_by_monster(m), m.monster_no_na, m.name_en)): return @@ -934,7 +932,6 @@ async def pwhich_append(self, ctx, term: str, *, addition): async def _concatenate_which(self, ctx, term: str, operation: str, addition): dbcog = await self.get_dbcog() - pdicog = self.bot.get_cog("PadInfo") term = term.lower() m = await dbcog.find_monster(term, ctx.author.id) @@ -953,7 +950,7 @@ async def _concatenate_which(self, ctx, term: str, operation: str, addition): if not await get_user_confirmation(ctx, 'Are you sure you want to {} to the which info for {} [{}] {}?'.format( operation, - pdicog.get_attribute_emoji_by_monster(m), + get_attribute_emoji_by_monster(m), m.monster_no_na, m.name_en)): return diff --git a/padinfo/common/__init__.py b/padinfo/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/padinfo/common/emoji_map.py b/padinfo/common/emoji_map.py deleted file mode 100644 index b354127d..00000000 --- a/padinfo/common/emoji_map.py +++ /dev/null @@ -1,136 +0,0 @@ -from enum import Enum -from typing import Union, Optional, Literal, TYPE_CHECKING - -if TYPE_CHECKING: - from dbcog.models.monster_model import MonsterModel - from dbcog.models.enum_types import AwakeningRestrictedLatent, MonsterType - -from discordmenu.emoji.emoji_cache import emoji_cache - -AWAKENING_ID_TO_EMOJI_NAME_MAP = { - 1: 'boost_hp', - 2: 'boost_atk', - 3: 'boost_rcv', - 4: 'reduce_fire', - 5: 'reduce_water', - 6: 'reduce_wood', - 7: 'reduce_light', - 8: 'reduce_dark', - 9: 'misc_autoheal', - 10: 'res_bind', - 11: 'res_blind', - 12: 'res_jammer', - 13: 'res_poison', - 14: 'oe_fire', - 15: 'oe_water', - 16: 'oe_wood', - 17: 'oe_light', - 18: 'oe_dark', - 19: 'misc_te', - 20: 'misc_bindclear', - 21: 'misc_sb', - 22: 'row_fire', - 23: 'row_water', - 24: 'row_wood', - 25: 'row_light', - 26: 'row_dark', - 27: 'misc_tpa', - 28: 'res_skillbind', - 29: 'oe_heart', - 30: 'misc_multiboost', - 31: 'killer_dragon', - 32: 'killer_god', - 33: 'killer_devil', - 34: 'killer_machine', - 35: 'killer_balance', - 36: 'killer_attacker', - 37: 'killer_physical', - 38: 'killer_healer', - 39: 'killer_evomat', - 40: 'killer_awoken', - 41: 'killer_enhancemat', - 42: 'killer_vendor', - 43: 'misc_comboboost', - 44: 'misc_guardbreak', - 45: 'misc_extraattack', - 46: 'teamboost_hp', - 47: 'teamboost_rcv', - 48: 'misc_voidshield', - 49: 'misc_assist', - 50: 'misc_super_extraattack', - 51: 'misc_skillcharge', - 52: 'res_bind_super', - 53: 'misc_te_super', - 54: 'res_cloud', - 55: 'res_seal', - 56: 'misc_sb_super', - 57: 'attack_boost_high', - 58: 'attack_boost_low', - 59: 'l_shield', - 60: 'l_attack', - 61: 'misc_super_comboboost', - 62: 'orb_combo', - 63: 'misc_voice', - 64: 'misc_dungeonbonus', - 65: 'reduce_hp', - 66: 'reduce_atk', - 67: 'reduce_rcv', - 68: 'res_blind_super', - 69: 'res_jammer_super', - 70: 'res_poison_super', - 71: 'misc_jammerboost', - 72: 'misc_poisonboost', - 73: 'cc_fire', - 74: 'cc_water', - 75: 'cc_wood', - 76: 'cc_light', - 77: 'cc_dark', - 78: 'cross_attack', - 79: 'boost_attr3', - 80: 'boost_attr4', - 81: 'boost_attr5', - 82: 'blob_boost', -} - -AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP = { - 606: 'unmatchable_clear', - 607: 'spinner_clear', - 608: 'absorb_pierce', -} - - -def awakening_restricted_latent_emoji(latent: "AwakeningRestrictedLatent"): - return get_emoji('latent_{}'.format(AWAKENING_RESTRICTED_LATENT_VALUE_TO_EMOJI_NAME_MAP[latent.value])) - - -def get_type_emoji(mons_type: "MonsterType"): - return get_emoji('mons_type_' + mons_type.name.lower()) - - -def get_attribute_emoji_by_monster(monster: "MonsterModel"): - attr1 = monster.attr1.name.lower() - attr2 = monster.attr2.name.lower() - emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) - return get_emoji(emoji) - - -def get_attribute_emoji_by_enum(type1: Union[Enum, Literal[False]], type2: Optional[Union[Enum, Literal[False]]] = None): - attr1 = 'nil' if type1 is False else type1.name.lower() - attr2 = 'nil' if type2 is False else type2.name.lower() if type2 else attr1 - emoji = "{}_{}".format(attr1, attr2) if attr1 != attr2 else 'orb_{}'.format(attr1) - return get_emoji(emoji) - - -def get_rarity_emoji(rarity: int): - emoji = 'star_0{}'.format(rarity) if rarity < 10 else 'star_{}'.format(rarity) - return get_emoji(emoji) - - -def get_awakening_emoji(awid: Union[Enum, int], default: str = None): - return get_emoji(AWAKENING_ID_TO_EMOJI_NAME_MAP.get(awid if isinstance(awid, int) else awid.value, default)) - - -def get_emoji(name): - if isinstance(name, int): - name = AWAKENING_ID_TO_EMOJI_NAME_MAP[name] - return emoji_cache.get_emoji(name) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index fb0c97eb..a52ab3de 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -1,6 +1,6 @@ import re -from padinfo.common.emoji_map import get_emoji +from tsutils.tsubaki.custom_emoji import get_emoji def _humanize_number(number, sigfigs=2): diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 58efd3a5..22ac341c 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -24,11 +24,11 @@ from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.menu.view.simple_text import SimpleTextViewState from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.custom_emoji import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ + get_attribute_emoji_by_monster, get_awakening_emoji, get_type_emoji from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message -from padinfo.common.emoji_map import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ - get_attribute_emoji_by_monster, get_awakening_emoji, get_type_emoji from padinfo.core.button_info import button_info from padinfo.core.leader_skills import leaderskill_query from padinfo.core.padinfo_settings import settings diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 97a903b3..77d5a2e0 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -5,10 +5,10 @@ from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki.custom_emoji import get_awakening_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.common.emoji_map import get_awakening_emoji from padinfo.view.common import get_awoken_skill_description if TYPE_CHECKING: diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 1eca50f0..c6b2cdac 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -5,7 +5,7 @@ from tsutils.query_settings import QuerySettings from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.common.emoji_map import get_awakening_emoji +from tsutils.tsubaki.custom_emoji import get_awakening_emoji if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 337c7a7f..193cad8c 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -7,9 +7,8 @@ from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache from tsutils.enums import LsMultiplier -from tsutils.tsubaki.custom_emoji import number_emoji_small +from tsutils.tsubaki.custom_emoji import get_awakening_emoji, get_emoji, number_emoji_small -from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text from padinfo.view.base import BaseIdView from padinfo.view.components.view_state_base_id import ViewStateBaseId diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 61e12b60..ac3fd358 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -8,10 +8,10 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.custom_emoji import get_awakening_emoji, get_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.common.emoji_map import get_awakening_emoji, get_emoji from padinfo.view.common import get_monster_from_ims, invalid_monster_text from padinfo.view.components.base_id_main_view import BaseIdMainView from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index 477fa1f3..ee1f24b9 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -5,10 +5,9 @@ from discordmenu.embed.text import LabeledText from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki.custom_emoji import get_attribute_emoji_by_monster from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.common.emoji_map import get_attribute_emoji_by_monster - if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index 1b636af0..a5bd1da9 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -6,11 +6,11 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.custom_emoji import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ + get_rarity_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.common.emoji_map import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ - get_rarity_emoji from padinfo.view.base import BaseIdView from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollView, MonsterEvolution diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index d5cd6a27..a18d50d9 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -9,10 +9,10 @@ from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings import QuerySettings +from tsutils.tsubaki.custom_emoji import get_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.common.emoji_map import get_emoji from padinfo.view.components.base_id_main_view import BaseIdMainView if TYPE_CHECKING: From c93ed081f12b508ddc00ab0e8ec22b2e185e6724 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 3 Mar 2022 00:18:24 -0700 Subject: [PATCH 1068/1220] Update TSUtils to 7.0.1 (#1614) --- crowddata/crowddata.py | 3 +- dbcog/find_monster.py | 2 +- dbcog/monster_graph.py | 2 +- padevents/autoevent_mixin.py | 4 +- padevents/events.py | 9 ++--- padinfo/core/button_info.py | 2 +- padinfo/menu/awakening_list.py | 6 +-- padinfo/menu/button_info.py | 4 +- padinfo/menu/id.py | 4 +- padinfo/menu/leader_skill.py | 3 +- padinfo/menu/leader_skill_single.py | 2 +- padinfo/menu/monster_list.py | 2 +- padinfo/menu/na_diff.py | 7 ++-- padinfo/menu/scroll.py | 2 +- padinfo/menu/series_scroll.py | 6 +-- padinfo/menu/transforminfo.py | 2 +- padinfo/padinfo.py | 37 ++++++------------- padinfo/reaction_list.py | 2 +- padinfo/view/awakening_help.py | 2 +- padinfo/view/awakening_list.py | 2 +- padinfo/view/button_info.py | 10 ++--- padinfo/view/common.py | 7 ++-- padinfo/view/components/base_id_main_view.py | 2 +- padinfo/view/components/evo_scroll_mixin.py | 4 +- padinfo/view/components/view_state_base_id.py | 6 +-- padinfo/view/evos.py | 4 +- padinfo/view/id.py | 7 ++-- padinfo/view/leader_skill.py | 5 +-- padinfo/view/leader_skill_single.py | 2 +- padinfo/view/links.py | 4 +- padinfo/view/lookup.py | 4 +- padinfo/view/materials.py | 6 +-- padinfo/view/monster_list/all_mats.py | 2 +- padinfo/view/monster_list/id_search.py | 6 +-- padinfo/view/monster_list/monster_list.py | 6 +-- padinfo/view/monster_list/scroll.py | 6 +-- .../view/monster_list/static_monster_list.py | 2 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 9 ++--- padinfo/view/pic.py | 2 +- padinfo/view/series_scroll.py | 6 +-- padinfo/view/transforminfo.py | 4 +- 42 files changed, 97 insertions(+), 112 deletions(-) diff --git a/crowddata/crowddata.py b/crowddata/crowddata.py index 7453f7a0..23048a18 100644 --- a/crowddata/crowddata.py +++ b/crowddata/crowddata.py @@ -1,5 +1,6 @@ import logging from io import BytesIO +from typing import Any from redbot.core import Config from redbot.core.bot import Red @@ -38,7 +39,7 @@ async def red_delete_data_for_user(self, *, requester, user_id): async def register_menu(self): await self.bot.wait_until_ready() - menulistener = self.bot.get_cog("MenuListener") + menulistener: Any = self.bot.get_cog("MenuListener") if menulistener is None: logger.warning("MenuListener is not loaded.") return diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index fcc55d3d..7c8ce6fe 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -5,7 +5,7 @@ from Levenshtein import jaro_winkler from tsutils.formatting import rmdiacritics -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.enums import Server from dbcog.find_monster_tokens import Token, SPECIAL_TOKEN_TYPES diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index f7341c69..6d54dfe3 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -9,7 +9,7 @@ from tsutils.enums import Server from .database_manager import DBCogDatabase -from .models.active_skill_model import ActivePartModel, ActiveSkillModel, ActiveSubskillModel +from .models.active_skill_model import ActiveSkillModel from .models.awakening_model import AwakeningModel from .models.awoken_skill_model import AwokenSkillModel from .models.base_model import BaseModel diff --git a/padevents/autoevent_mixin.py b/padevents/autoevent_mixin.py index 89196946..f5052dea 100644 --- a/padevents/autoevent_mixin.py +++ b/padevents/autoevent_mixin.py @@ -1,9 +1,9 @@ import logging import re -from typing import Any, Callable, Dict, List, Optional, Set, TypeVar, Union +import time +from typing import Any, Callable, Dict, Optional, Set, TypeVar, Union import discord -import time from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import box, humanize_timedelta, inline diff --git a/padevents/events.py b/padevents/events.py index 1aeea54a..26c5a8a0 100644 --- a/padevents/events.py +++ b/padevents/events.py @@ -1,11 +1,10 @@ import datetime from datetime import timedelta -from typing import Callable, Collection, List, TYPE_CHECKING +from typing import Callable, Collection, TYPE_CHECKING -import itertools import pytz from tsutils.formatting import normalize_server_name -from tsutils.time import JP_TIMEZONE, NA_TIMEZONE, KR_TIMEZONE +from tsutils.time import JP_TIMEZONE, KR_TIMEZONE, NA_TIMEZONE from padevents.enums import DungeonType, EventLength @@ -24,8 +23,8 @@ class Event: def __init__(self, scheduled_event: "ScheduledEventModel"): self.key = scheduled_event.event_id self.server = SUPPORTED_SERVERS[scheduled_event.server_id] - self.open_datetime = scheduled_event.open_datetime - self.close_datetime = scheduled_event.close_datetime + self.open_datetime: datetime = scheduled_event.open_datetime + self.close_datetime: datetime = scheduled_event.close_datetime self.group = scheduled_event.group_name self.dungeon = scheduled_event.dungeon self.dungeon_name = self.dungeon.name_en if self.dungeon else 'unknown_dungeon' diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index 48e90a31..c1968bcc 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -1,5 +1,5 @@ from collections import namedtuple -from typing import TYPE_CHECKING, Optional +from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index 9786db48..2297b40e 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -3,10 +3,10 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from tsutils.enums import ChildMenuType -from tsutils.menu.components.panes import emoji_buttons, MenuPanes +from tsutils.menu.components.panes import MenuPanes, emoji_buttons +from tsutils.query_settings.enums import ChildMenuType -from padinfo.view.awakening_list import AwakeningListViewState, AwakeningListView, AwakeningListSortTypes +from padinfo.view.awakening_list import AwakeningListSortTypes, AwakeningListView, AwakeningListViewState class AwakeningListEmoji: diff --git a/padinfo/menu/button_info.py b/padinfo/menu/button_info.py index c085e291..db32aed1 100644 --- a/padinfo/menu/button_info.py +++ b/padinfo/menu/button_info.py @@ -4,10 +4,10 @@ from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu from tsutils.emoji import char_to_emoji -from tsutils.menu.components.panes import emoji_buttons, MenuPanes +from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.menu.components.evo_scroll_mixin import EvoScrollMenu -from padinfo.view.button_info import ButtonInfoOptions, ButtonInfoToggles, ButtonInfoViewState, ButtonInfoView +from padinfo.view.button_info import ButtonInfoOptions, ButtonInfoToggles, ButtonInfoView, ButtonInfoViewState class ButtonInfoEmoji: diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index d5ade24e..e1f75096 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -1,11 +1,11 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.embed.menu import EmbedControl, EmbedMenu from tsutils.emoji import char_to_emoji -from tsutils.enums import ChildMenuType from tsutils.menu.components.panes import MenuPanes from tsutils.menu.simple_text import SimpleTextMenu +from tsutils.query_settings.enums import ChildMenuType from padinfo.menu.components.evo_scroll_mixin import EvoScrollMenu from padinfo.view.components.view_state_base_id import ViewStateBaseId diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index 7d7b6531..ad88ed26 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -1,9 +1,8 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.embed.menu import EmbedControl, EmbedMenu from tsutils.emoji import char_to_emoji - from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index fff3a651..ac39c34e 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -1,7 +1,7 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.embed.menu import EmbedControl, EmbedMenu from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index b09ea329..bc3a605b 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -11,7 +11,7 @@ from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState from padinfo.view.monster_list.monster_list import MonsterListView, MonsterListViewState -from padinfo.view.monster_list.scroll import ScrollViewState, ScrollView +from padinfo.view.monster_list.scroll import ScrollView, ScrollViewState from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index 1285257c..3648cb60 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -3,12 +3,13 @@ from discord import Message from discordmenu.embed.control import EmbedControl from discordmenu.embed.menu import EmbedMenu -from tsutils.enums import Server, ChildMenuType +from tsutils.enums import Server from tsutils.menu.components.panes import MenuPanes from tsutils.menu.simple_text import SimpleTextMenu -from tsutils.menu.view.simple_text import SimpleTextViewState, SimpleTextView +from tsutils.menu.view.simple_text import SimpleTextView, SimpleTextViewState +from tsutils.query_settings.enums import ChildMenuType -from padinfo.view.id import IdViewState, IdView +from padinfo.view.id import IdView, IdViewState class NaDiffEmoji: diff --git a/padinfo/menu/scroll.py b/padinfo/menu/scroll.py index 7a86863f..c77e16b6 100644 --- a/padinfo/menu/scroll.py +++ b/padinfo/menu/scroll.py @@ -1,6 +1,6 @@ from typing import List -from padinfo.menu.monster_list import MonsterListMenuPanes, MonsterListEmoji +from padinfo.menu.monster_list import MonsterListEmoji, MonsterListMenuPanes class ScrollMenuPanes(MonsterListMenuPanes): diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 84663144..79a30721 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -1,11 +1,11 @@ -from typing import Optional, List +from typing import List, Optional from discord import Message -from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.embed.menu import EmbedControl, EmbedMenu from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes, emoji_buttons -from padinfo.menu.id import IdMenu, IdMenuPanes, IdMenuEmoji +from padinfo.menu.id import IdMenu, IdMenuEmoji, IdMenuPanes from padinfo.view.id import IdView from padinfo.view.series_scroll import SeriesScrollView, SeriesScrollViewState diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index 72953f5b..ad735456 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -1,7 +1,7 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedMenu, EmbedControl +from discordmenu.embed.menu import EmbedControl, EmbedMenu from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes, emoji_buttons diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 22ac341c..6a4d616d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -16,14 +16,17 @@ from tabulate import tabulate from tsutils.cogs.donations import is_donor from tsutils.emoji import char_to_emoji -from tsutils.enums import AltEvoSort, CardLevelModifier, CardModeModifier, CardPlusModifier, EvoGrouping, LsMultiplier, \ - Server, MonsterLinkTarget +from tsutils.enums import Server from tsutils.json_utils import safe_read_json from tsutils.menu.components.config import BotConfig from tsutils.menu.simple_text import SimpleTextMenu from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.menu.view.simple_text import SimpleTextViewState -from tsutils.query_settings import QuerySettings +from tsutils.query_settings import validators +from tsutils.query_settings.enums import AltEvoSort, CardLevelModifier, CardModeModifier, CardPlusModifier, EvoGrouping, \ + LsMultiplier, \ + MonsterLinkTarget +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.custom_emoji import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ get_attribute_emoji_by_monster, get_awakening_emoji, get_type_emoji from tsutils.tsubaki.monster_header import MonsterHeader @@ -88,19 +91,6 @@ def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='padinfo')), file_name) -COLORS = { - **{c: getattr(discord.Colour, c)().value for c in discord.Colour.__dict__ if - isinstance(discord.Colour.__dict__[c], classmethod) and - discord.Colour.__dict__[c].__func__.__code__.co_argcount == 1 and - isinstance(getattr(discord.Colour, c)(), discord.Colour)}, - 'pink': 0xffa1dd, - - # Special - 'random': 'random', - 'clear': 0, -} - - class PadInfo(commands.Cog): """Info for PAD Cards""" @@ -1032,7 +1022,7 @@ async def survey(self, ctx, value): @is_donor() @idset.command() - async def embedcolor(self, ctx, *, color): + async def embedcolor(self, ctx, *, color: validators.Color): """(DONOR ONLY) The color of all your `[p]id` embeds! Examples: @@ -1042,14 +1032,11 @@ async def embedcolor(self, ctx, *, color): Picking random will choose a random hex code every time you use [p]id! """ - if color in COLORS: - await self.config.user(ctx.author).color.set(COLORS[color]) - elif re.match(r"^#?[0-9a-fA-F]{6}$", color): - await self.config.user(ctx.author).color.set(int(color.lstrip("#"), 16)) - else: - await send_cancellation_message( - ctx, "Invalid color! Valid colors are any hexcode and:\n" + ", ".join(COLORS)) - return + # Remove this line once the Menu2 stuff is done + await self.config.user(ctx.author).color.set(color) + + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: + fm_flags['color'] = color await ctx.tick() @idset.command(usage="") diff --git a/padinfo/reaction_list.py b/padinfo/reaction_list.py index ec528ad6..4e50daa2 100644 --- a/padinfo/reaction_list.py +++ b/padinfo/reaction_list.py @@ -1,7 +1,7 @@ from typing import List, Optional, TYPE_CHECKING from padinfo.core.padinfo_settings import settings -from padinfo.menu.id import IdMenuPanes, IdMenuEmoji +from padinfo.menu.id import IdMenuEmoji, IdMenuPanes from padinfo.view.evos import EvosViewState from padinfo.view.materials import MaterialsViewState from padinfo.view.pantheon import PantheonViewState diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 77d5a2e0..8cd93539 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField, EmbedAuthor +from discordmenu.embed.components import EmbedAuthor, EmbedField, EmbedMain from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index f0162cd7..eb1db831 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -1,7 +1,7 @@ from typing import List from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.view import EmbedView from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 66e36b8f..2fdfabfa 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -1,18 +1,18 @@ -from typing import TYPE_CHECKING, List +from typing import List, TYPE_CHECKING from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedAuthor, EmbedField, EmbedMain -from discordmenu.embed.text import Text, BlockText +from discordmenu.embed.text import BlockText, Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.core.button_info import button_info, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL, ButtonInfoStatSet, \ - ButtonInfoResult +from padinfo.core.button_info import ButtonInfoResult, ButtonInfoStatSet, LIMIT_BREAK_LEVEL, SUPER_LIMIT_BREAK_LEVEL, \ + button_info from padinfo.view.components.evo_scroll_mixin import EvoScrollView, EvoScrollViewState, MonsterEvolution if TYPE_CHECKING: diff --git a/padinfo/view/common.py b/padinfo/view/common.py index c6b2cdac..1745298f 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -1,11 +1,10 @@ from typing import TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.text import Text, BoldText -from tsutils.query_settings import QuerySettings -from tsutils.tsubaki.monster_header import MonsterHeader - +from discordmenu.embed.text import BoldText, Text +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.custom_emoji import get_awakening_emoji +from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index 193cad8c..f3108cd9 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -6,7 +6,7 @@ from discordmenu.embed.text import BoldText, Text from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache -from tsutils.enums import LsMultiplier +from tsutils.query_settings.enums import LsMultiplier from tsutils.tsubaki.custom_emoji import get_awakening_emoji, get_emoji, number_emoji_small from padinfo.core.leader_skills import ls_multiplier_text, ls_single_multiplier_text diff --git a/padinfo/view/components/evo_scroll_mixin.py b/padinfo/view/components/evo_scroll_mixin.py index 71f5eb60..ef070a19 100644 --- a/padinfo/view/components/evo_scroll_mixin.py +++ b/padinfo/view/components/evo_scroll_mixin.py @@ -1,9 +1,9 @@ from abc import abstractmethod -from typing import List, TYPE_CHECKING, NamedTuple, Optional +from typing import List, NamedTuple, Optional, TYPE_CHECKING from discordmenu.embed.components import EmbedField from discordmenu.embed.text import HighlightableLinks, LinkedText -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.links import MonsterLink if TYPE_CHECKING: diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 6b8b0980..173cf64b 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -1,9 +1,9 @@ -from typing import TYPE_CHECKING, List +from typing import List, TYPE_CHECKING from discordmenu.embed.view_state import ViewState -from tsutils.enums import AltEvoSort from tsutils.menu.components.config import UserConfig -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.enums import AltEvoSort +from tsutils.query_settings.query_settings import QuerySettings from padinfo.view.common import get_monster_from_ims from padinfo.view.components.evo_scroll_mixin import EvoScrollViewState, MonsterEvolution diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 39deafe7..3dfb7032 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -1,11 +1,11 @@ from typing import List, TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.view import EmbedView from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader diff --git a/padinfo/view/id.py b/padinfo/view/id.py index ac3fd358..2284c9a5 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -1,13 +1,14 @@ -from typing import List, Optional, TYPE_CHECKING, Dict +from typing import Dict, List, Optional, TYPE_CHECKING from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.text import BoldText, LabeledText, Text from discordmenu.embed.view import EmbedView -from tsutils.enums import CardLevelModifier, CardModeModifier, CardPlusModifier, Server +from tsutils.enums import Server from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.enums import CardLevelModifier, CardModeModifier, CardPlusModifier +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.custom_emoji import get_awakening_emoji, get_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 8bea97bb..83c9c526 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -7,11 +7,10 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.monster_header import MonsterHeader -from padinfo.core.leader_skills import leaderskill_query -from padinfo.core.leader_skills import ls_multiplier_text +from padinfo.core.leader_skills import leaderskill_query, ls_multiplier_text if TYPE_CHECKING: pass diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index a80faaa5..c1b7ed90 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -7,7 +7,7 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.monster_header import MonsterHeader from padinfo.core.leader_skills import ls_single_multiplier_text diff --git a/padinfo/view/links.py b/padinfo/view/links.py index e7dd11f2..72dc75e4 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -1,10 +1,10 @@ from typing import TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedThumbnail, EmbedMain +from discordmenu.embed.components import EmbedMain, EmbedThumbnail from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index 814776d5..28eb3546 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING -from discordmenu.embed.components import EmbedMain, EmbedAuthor +from discordmenu.embed.components import EmbedAuthor, EmbedMain from discordmenu.embed.view import EmbedView -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 77fd8d9a..160ba066 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -1,12 +1,12 @@ -from typing import List, TYPE_CHECKING, Optional +from typing import List, Optional, TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.text import LinkedText from discordmenu.embed.view import EmbedView from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader diff --git a/padinfo/view/monster_list/all_mats.py b/padinfo/view/monster_list/all_mats.py index 2f44fe3c..17d33576 100644 --- a/padinfo/view/monster_list/all_mats.py +++ b/padinfo/view/monster_list/all_mats.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional, TYPE_CHECKING from padinfo.view.materials import MaterialsViewState from padinfo.view.monster_list.monster_list import MonsterListViewState diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py index c0d59e98..c27e3c43 100644 --- a/padinfo/view/monster_list/id_search.py +++ b/padinfo/view/monster_list/id_search.py @@ -1,7 +1,7 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional, TYPE_CHECKING -from tsutils.enums import EvoGrouping -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.enums import EvoGrouping +from tsutils.query_settings.query_settings import QuerySettings from padinfo.view.monster_list.monster_list import MonsterListViewState diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index ee26c38b..7f9e2e1b 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -1,14 +1,14 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional, TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils.emoji import char_to_emoji from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: diff --git a/padinfo/view/monster_list/scroll.py b/padinfo/view/monster_list/scroll.py index 62c8d015..930f419c 100644 --- a/padinfo/view/monster_list/scroll.py +++ b/padinfo/view/monster_list/scroll.py @@ -1,9 +1,9 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional, TYPE_CHECKING from tsutils.menu.components.config import UserConfig -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings -from padinfo.view.monster_list.monster_list import MonsterListViewState, MonsterListView +from padinfo.view.monster_list.monster_list import MonsterListView, MonsterListViewState if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel diff --git a/padinfo/view/monster_list/static_monster_list.py b/padinfo/view/monster_list/static_monster_list.py index ab3b89a9..951b7652 100644 --- a/padinfo/view/monster_list/static_monster_list.py +++ b/padinfo/view/monster_list/static_monster_list.py @@ -1,6 +1,6 @@ from typing import List, TYPE_CHECKING -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from padinfo.view.monster_list.monster_list import MonsterListViewState diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 621b1223..bc462f20 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -2,7 +2,7 @@ import prettytable from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.text import LabeledText, Text from discordmenu.embed.view import EmbedView from redbot.core.utils.chat_formatting import box diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index a5bd1da9..f3f3c7bc 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -1,13 +1,12 @@ -from typing import TYPE_CHECKING, List +from typing import List, TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField, EmbedThumbnail +from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.view import EmbedView from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state -from tsutils.query_settings import QuerySettings -from tsutils.tsubaki.custom_emoji import get_attribute_emoji_by_monster, get_attribute_emoji_by_enum, \ - get_rarity_emoji +from tsutils.query_settings.query_settings import QuerySettings +from tsutils.tsubaki.custom_emoji import get_attribute_emoji_by_enum, get_attribute_emoji_by_monster, get_rarity_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 7ac2d0b8..8de04857 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -1,5 +1,5 @@ from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField, EmbedBodyImage +from discordmenu.embed.components import EmbedBodyImage, EmbedField, EmbedMain from discordmenu.embed.text import LinkedText, Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index 7a378f75..d4ed961e 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -1,14 +1,14 @@ -from typing import TYPE_CHECKING, List +from typing import List, TYPE_CHECKING from discordmenu.embed.base import Box -from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.components import EmbedField, EmbedMain from discordmenu.embed.text import BoldText from discordmenu.embed.view import EmbedView from tsutils.emoji import char_to_emoji from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.monster_header import MonsterHeader if TYPE_CHECKING: diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index a18d50d9..84b34891 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -4,11 +4,11 @@ from discordmenu.embed.components import EmbedField, EmbedMain, EmbedThumbnail from discordmenu.embed.text import BoldText, LabeledText, Text from discordmenu.embed.view import EmbedView -from tsutils.enums import LsMultiplier from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase -from tsutils.query_settings import QuerySettings +from tsutils.query_settings.enums import LsMultiplier +from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.custom_emoji import get_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader From aaf72e1fe8244cea09ed0f1a3f76b7a415c1a217 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 4 Mar 2022 18:06:41 -0600 Subject: [PATCH 1069/1220] Record historic lookups after querying (#1615) --- dbcog/dbcog.py | 2 -- dbcog/find_monster.py | 3 --- padinfo/padinfo.py | 48 ++++++++++++++++++++++++++++++------------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 04d186ea..7c0bdcde 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -66,8 +66,6 @@ def __init__(self, bot, *args, **kwargs): self.config.register_user(lastaction=None, fm_flags={}) self.db_file_path = _data_file('dadguide.sqlite') - self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') - self.historic_lookups = safe_read_json(self.historic_lookups_file_path) self.monster_stats = monster_stats self.MonsterStatModifierInput = MonsterStatModifierInput diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index 7c8ce6fe..60015223 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -319,9 +319,6 @@ async def find_monster_debug(self, query: str) -> \ key=lambda t: t[1].get(t[0], MonsterMatch()).score ) - self.dbcog.historic_lookups[query] = best_monster.monster_id if best_monster else -1 - json.dump(self.dbcog.historic_lookups, open(self.dbcog.historic_lookups_file_path, "w+")) - return best_monster, matches_dict, valid_monsters, 0 async def find_monster(self, query: str) -> Optional[MonsterModel]: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 6a4d616d..e4db7727 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1,4 +1,5 @@ import asyncio +import json import logging import os import random @@ -110,7 +111,8 @@ def __init__(self, bot, *args, **kwargs): self.config.register_user(survey_mode=0, color=None, beta_id3=False, id_history=[]) self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False) - self.historic_lookups = safe_read_json(_data_file('historic_lookups_id3.json')) + self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') + self.historic_lookups = safe_read_json(self.historic_lookups_file_path) self.awoken_emoji_names = {v: k for k, v in AWAKENING_ID_TO_EMOJI_NAME_MAP.items()} self.get_attribute_emoji_by_monster = get_attribute_emoji_by_monster @@ -161,6 +163,10 @@ async def get_dbcog(self) -> "DBCog": await dbcog.wait_until_ready() return dbcog + def save_historic_data(self, query, monster: Optional["MonsterModel"]): + self.historic_lookups[query] = monster.monster_id if monster else -1 + json.dump(self.historic_lookups, open(self.historic_lookups_file_path, "w+")) + async def get_menu_default_data(self, ims): data = { 'dbcog': await self.get_dbcog(), @@ -247,10 +253,9 @@ async def _do_id(self, ctx, query: str): raw_query = query if (monster := await self._get_monster(ctx, query)) is None: + self.save_historic_data(query, monster) return - await self.log_id_result(ctx, monster.monster_id) - # id3 messaging stuff if monster and monster.monster_no_na != monster.monster_no_jp: await ctx.send(f"The NA ID and JP ID of this card differ! The JP ID is {monster.monster_id}, so " @@ -273,6 +278,8 @@ async def _do_id(self, ctx, query: str): reaction_list=initial_reaction_list) menu = IdMenu.menu() await menu.create(ctx, state) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) async def send_survey_after(self, ctx, query, result_monster): dbcog = await self.get_dbcog() @@ -340,9 +347,9 @@ async def nadiff(self, ctx, *, query: str): original_author_id = ctx.message.author.id monster = await dbcog.find_monster(raw_query, ctx.author.id) - await self.log_id_result(ctx, monster.monster_id) if monster is None: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) @@ -360,6 +367,8 @@ async def nadiff(self, ctx, *, query: str): raw_query, color, message) menu = NaDiffMenu.menu(initial_control=NaDiffMenu.message_control) await menu.create(ctx, state) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) @commands.command(name="evos") @checks.bot_has_permissions(embed_links=True) @@ -374,10 +383,9 @@ async def evos(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return - await self.log_id_result(ctx, monster.monster_id) - alt_monsters = EvosViewState.get_alt_monsters_and_evos(dbcog, monster) alt_versions, gem_versions = await EvosViewState.do_query(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) @@ -397,6 +405,8 @@ async def evos(self, ctx, *, query: str): ) menu = IdMenu.menu(initial_control=IdMenu.evos_control) await menu.create(ctx, state) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) @commands.command(name="mats", aliases=['evomats', 'evomat', 'skillups']) @checks.bot_has_permissions(embed_links=True) @@ -410,10 +420,9 @@ async def evomats(self, ctx, *, query: str): if not monster: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return - await self.log_id_result(ctx, monster.monster_id) - mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override = \ await MaterialsViewState.do_query(dbcog, monster) @@ -434,6 +443,8 @@ async def evomats(self, ctx, *, query: str): ) menu = IdMenu.menu(initial_control=IdMenu.mats_control) await menu.create(ctx, state) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) @commands.command(aliases=["series", "panth"]) @checks.bot_has_permissions(embed_links=True) @@ -448,10 +459,9 @@ async def pantheon(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return - await self.log_id_result(ctx, monster.monster_id) - pantheon_list, series_name, base_monster = await PantheonViewState.do_query(dbcog, monster) if pantheon_list is None: await ctx.send('Unable to find a pantheon for the result of your query,' @@ -470,6 +480,8 @@ async def pantheon(self, ctx, *, query: str): ) menu = IdMenu.menu(initial_control=IdMenu.pantheon_control) await menu.create(ctx, state) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) @commands.command(aliases=['img']) @checks.bot_has_permissions(embed_links=True) @@ -484,10 +496,9 @@ async def pic(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return - await self.log_id_result(ctx, monster.monster_id) - alt_monsters = PicViewState.get_alt_monsters_and_evos(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) @@ -500,6 +511,8 @@ async def pic(self, ctx, *, query: str): ) menu = IdMenu.menu(initial_control=IdMenu.pic_control) await menu.create(ctx, state) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) @commands.command(aliases=['stats']) @checks.bot_has_permissions(embed_links=True) @@ -514,10 +527,9 @@ async def otherinfo(self, ctx, *, query: str): if monster is None: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return - await self.log_id_result(ctx, monster.monster_id) - alt_monsters = PicViewState.get_alt_monsters_and_evos(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) @@ -530,6 +542,8 @@ async def otherinfo(self, ctx, *, query: str): ) menu = IdMenu.menu(initial_control=IdMenu.otherinfo_control) await menu.create(ctx, state) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -539,12 +553,15 @@ async def links(self, ctx, *, query: str): monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return await self.log_id_result(ctx, monster.monster_id) color = await self.get_user_embed_color(ctx) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) embed = LinksView.embed(monster, color, query_settings).to_embed() await ctx.send(embed=embed) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -554,12 +571,15 @@ async def lookup(self, ctx, *, query: str): monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) + self.save_historic_data(query, monster) return await self.log_id_result(ctx, monster.monster_id) color = await self.get_user_embed_color(ctx) query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) embed = LookupView.embed(monster, color, query_settings).to_embed() await ctx.send(embed=embed) + await self.log_id_result(ctx, monster.monster_id) + self.save_historic_data(query, monster) async def log_id_result(self, ctx, monster_id: int): history = await self.config.user(ctx.author).id_history() From a42086b5b52764e627b4778c769b3200b6859115 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 6 Mar 2022 09:58:02 -0600 Subject: [PATCH 1070/1220] QuerySettings instead of Color in menus (#1617) --- crowddata/mixins/adpem/adpem.py | 4 +- dungeoncog/dungeoncog.py | 4 +- dungeoncog/menu/dungeon.py | 21 ++-- dungeoncog/menu/simple.py | 6 +- dungeoncog/view/dungeon.py | 9 +- dungeoncog/view/simple.py | 9 +- padglobal/padglobal.py | 7 +- padglobal/view/which.py | 5 +- padinfo/padinfo.py | 114 ++++++++---------- padinfo/view/awakening_help.py | 2 +- padinfo/view/awakening_list.py | 12 +- padinfo/view/button_info.py | 7 +- padinfo/view/components/view_state_base_id.py | 5 +- padinfo/view/evos.py | 8 +- padinfo/view/experience_curve.py | 2 +- padinfo/view/id.py | 8 +- padinfo/view/id_traceback.py | 2 +- padinfo/view/leader_skill.py | 7 +- padinfo/view/leader_skill_single.py | 7 +- padinfo/view/links.py | 4 +- padinfo/view/lookup.py | 4 +- padinfo/view/materials.py | 10 +- padinfo/view/monster_list/monster_list.py | 7 +- padinfo/view/monster_list/scroll.py | 6 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 8 +- padinfo/view/pic.py | 2 +- padinfo/view/series_scroll.py | 7 +- padinfo/view/transforminfo.py | 7 +- 29 files changed, 131 insertions(+), 165 deletions(-) diff --git a/crowddata/mixins/adpem/adpem.py b/crowddata/mixins/adpem/adpem.py index 0d66f3f2..eb6de403 100644 --- a/crowddata/mixins/adpem/adpem.py +++ b/crowddata/mixins/adpem/adpem.py @@ -10,6 +10,7 @@ from tsutils.cog_mixins import CogMixin from tsutils.emoji import NO_EMOJI, char_to_emoji from tsutils.menu.view.closable_embed import ClosableEmbedViewState +from tsutils.query_settings.query_settings import QuerySettings from tsutils.time import NA_TIMEZONE, NEW_DAY, get_last_time from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, get_user_reaction @@ -265,8 +266,9 @@ async def showstats(self, ctx, *, query): most_common = Counter(m for m in total if m in monsters).most_common(1)[0][0] menu = ClosableEmbedMenu.menu() props = ShowStatsViewProps(total, adj, you, valid, most_common) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query.replace('"', ''), - Color.default(), ShowStatsView.VIEW_TYPE, props) + query_settings, ShowStatsView.VIEW_TYPE, props) await menu.create(ctx, state) diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 79be1993..162416d8 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -1,7 +1,6 @@ import logging from typing import TYPE_CHECKING -from discord import Color from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import commands from redbot.core.utils.chat_formatting import pagify @@ -45,7 +44,6 @@ async def register_menu(self): async def get_menu_default_data(self, ims): data = { 'dbcog': await self.get_dbcog(), - 'color': Color.default() } return data @@ -139,7 +137,7 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): original_author_id = ctx.message.author.id test_list = ['1', '2', '3', '4'] # print(pm_dungeon[0]) - view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, Color.default(), pm_dungeon[0][0], + view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, pm_dungeon[0][0], dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, len(pm_dungeon[0]), 0, int(dungeon.sub_dungeons[0].technical), dbcog.database, verbose=False) diff --git a/dungeoncog/menu/dungeon.py b/dungeoncog/menu/dungeon.py index 0cbf18e8..c51eedc3 100644 --- a/dungeoncog/menu/dungeon.py +++ b/dungeoncog/menu/dungeon.py @@ -25,16 +25,14 @@ def menu(): @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): dbcog = data.get('dbcog') - color = data.get('color') - view_state = await DungeonViewState.deserialize(dbcog, color, ims) + view_state = await DungeonViewState.deserialize(dbcog, ims) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_verbose(message: Optional[Message], ims, **data): dbcog = data.get('dbcog') - color = data.get('color') - view_state = await DungeonViewState.deserialize(dbcog, color, ims, verbose_toggle=True) + view_state = await DungeonViewState.deserialize(dbcog, ims, verbose_toggle=True) control = DungeonMenu.message_control(view_state) return control @@ -45,45 +43,40 @@ async def respond_with_delete(message: Optional[Message], ims, **data): @staticmethod async def respond_with_previous_monster(message: Optional[Message], ims, **data): dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, -1) + view_state = await DungeonViewState.deserialize(dbcog, ims, 0, -1) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_monster(message: Optional[Message], ims, **data): dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, 0, 1) + view_state = await DungeonViewState.deserialize(dbcog, ims, 0, 1) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_previous_floor(message: Optional[Message], ims, **data): dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, -1, 0, reset_spawn=True) + view_state = await DungeonViewState.deserialize(dbcog, ims, -1, 0, reset_spawn=True) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_floor(message: Optional[Message], ims, **data): dbcog = data['dbcog'] - color = data['color'] - view_state = await DungeonViewState.deserialize(dbcog, color, ims, 1, 0, reset_spawn=True) + view_state = await DungeonViewState.deserialize(dbcog, ims, 1, 0, reset_spawn=True) control = DungeonMenu.message_control(view_state) return control @staticmethod async def respond_with_next_page(message: Optional[Message], ims, **data): dbcog = data['dbcog'] - color = data['color'] page = ims.get('page') if page == 0: page = 1 else: page = 0 - view_state = await DungeonViewState.deserialize(dbcog, color, ims, page=page) + view_state = await DungeonViewState.deserialize(dbcog, ims, page=page) control = DungeonMenu.message_control(view_state) return control diff --git a/dungeoncog/menu/simple.py b/dungeoncog/menu/simple.py index cc5bb6eb..b72fb7bd 100644 --- a/dungeoncog/menu/simple.py +++ b/dungeoncog/menu/simple.py @@ -25,8 +25,7 @@ def menu(): @staticmethod async def respond_with_message(message: Optional[Message], ims, **data): dbcog = data.get('dbcog') - color = data.get('color') - view_state = await SimpleViewState.deserialize(dbcog, color, ims, 0) + view_state = await SimpleViewState.deserialize(dbcog, ims, 0) control = SimpleMenu.message_control(view_state) return control @@ -37,8 +36,7 @@ async def respond_with_delete(message: Optional[Message], ims, **data): @staticmethod async def respond_with_right(message: Optional[Message], ims, **data): dbcog = data.get('dbcog') - color = data.get('color') - view_state = await SimpleViewState.deserialize(dbcog, color, ims, 1) + view_state = await SimpleViewState.deserialize(dbcog, ims, 1) control = SimpleMenu.message_control(view_state) return control diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index 0f73f70a..162754ee 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, List import discord -from discord import Color, Embed +from discord import Embed from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView @@ -18,7 +18,7 @@ class DungeonViewState(ViewState): - def __init__(self, original_author_id: int, menu_type: str, raw_query: str, color: Color, + def __init__(self, original_author_id: int, menu_type: str, raw_query: str, encounter: "EncounterModel", sub_dungeon_id: int, num_floors: int, floor: int, num_spawns: int, floor_index: int, technical: int, database, page: int = 0, verbose: bool = False): @@ -30,7 +30,6 @@ def __init__(self, original_author_id: int, menu_type: str, raw_query: str, colo self.floor_index = floor_index self.technical = technical self.database = database - self.color = color self.num_spawns = num_spawns self.page = page self.verbose = verbose @@ -50,7 +49,7 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dbcog, color, ims: dict, inc_floor: int = 0, inc_index: int = 0, + async def deserialize(cls, dbcog, ims: dict, inc_floor: int = 0, inc_index: int = 0, verbose_toggle: bool = False, page: int = 0, reset_spawn: bool = False): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] @@ -88,7 +87,7 @@ async def deserialize(cls, dbcog, color, ims: dict, inc_floor: int = 0, inc_inde encounter_model = floor_models[floor_index] - return cls(original_author_id, menu_type, raw_query, color, encounter_model, sub_dungeon_id, + return cls(original_author_id, menu_type, raw_query, encounter_model, sub_dungeon_id, num_floors, floor, len(floor_models), floor_index, technical, dbcog.database, verbose=verbose, page=page) diff --git a/dungeoncog/view/simple.py b/dungeoncog/view/simple.py index 9b1c3679..d389b2a5 100644 --- a/dungeoncog/view/simple.py +++ b/dungeoncog/view/simple.py @@ -7,11 +7,10 @@ class SimpleViewState(ViewState): def __init__(self, original_author_id, menu_type, raw_query, - color, message_list, index): + message_list, index): super().__init__(original_author_id, menu_type, raw_query) self.message_list = message_list self.index = index - self.color = color def serialize(self): ret = super().serialize() @@ -22,11 +21,11 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dbcog, color, ims: dict, inc): + async def deserialize(cls, dbcog, ims: dict, inc): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] raw_query = ims.get('raw_query') - return cls(original_author_id, menu_type, raw_query, color, ims.get('message_list'), ims.get('index') + inc) + return cls(original_author_id, menu_type, raw_query, ims.get('message_list'), ims.get('index') + inc) class SimpleView: @@ -39,7 +38,7 @@ def embed(state: SimpleViewState): ] return EmbedView( EmbedMain( - color=state.color, + color="black", description=state.message_list[state.index] ), embed_fields=fields, diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 3be4b124..0a900ab1 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -11,7 +11,6 @@ import discord import prettytable import pytz -from discord import Color from redbot.core import checks, commands, data_manager, errors from redbot.core.utils.chat_formatting import bold, box, humanize_timedelta, inline, pagify from tsutils.cog_settings import CogSettings @@ -20,6 +19,7 @@ from tsutils.formatting import clean_global_mentions, strip_right_multiline from tsutils.json_utils import safe_read_json from tsutils.menu.view.closable_embed import ClosableEmbedViewState +from tsutils.query_settings.query_settings import QuerySettings from tsutils.time import NA_TIMEZONE from tsutils.tsubaki.custom_emoji import get_attribute_emoji_by_monster from tsutils.tsubaki.links import CLOUDFRONT_URL @@ -756,13 +756,12 @@ async def which(self, ctx, *, term: str = None): name, definition, timestamp, success = await self._resolve_which(ctx, term) if name is None or definition is None: return - # TODO: maybe support different colors one day if the configuration gets moved out of padinfo - color = Color.default() + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, term) original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() props = WhichViewProps(name=name, definition=definition, timestamp=timestamp, success=success) state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, term, - color, WhichView.VIEW_TYPE, props) + query_settings, WhichView.VIEW_TYPE, props) await menu.create(ctx, state) async def _resolve_which(self, ctx, term): diff --git a/padglobal/view/which.py b/padglobal/view/which.py index 56e30be7..eba3cc17 100644 --- a/padglobal/view/which.py +++ b/padglobal/view/which.py @@ -4,6 +4,7 @@ from discordmenu.embed.view import EmbedView from discordmenu.emoji.emoji_cache import emoji_cache from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.closable_embed import ClosableEmbedViewState STAR = '* ' BULLET_EMOJIS = ['db', 'dp', 'dg'] @@ -66,7 +67,7 @@ class WhichView: VIEW_TYPE = 'Which' @staticmethod - def embed(state, props: WhichViewProps): + def embed(state: ClosableEmbedViewState, props: WhichViewProps): name = props.name definition = props.definition timestamp = props.timestamp @@ -76,7 +77,7 @@ def embed(state, props: WhichViewProps): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title='Which {}'.format(name), description=get_description(definition, timestamp, success) ), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e4db7727..488acdb8 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -248,7 +248,6 @@ async def send_message(): async def _do_id(self, ctx, query: str): dbcog = await self.get_dbcog() - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id raw_query = query @@ -271,9 +270,9 @@ async def _do_id(self, ctx, query: str): full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) - state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, id_queried_props, reaction_list=initial_reaction_list) menu = IdMenu.menu() @@ -343,7 +342,6 @@ async def nadiff(self, ctx, *, query: str): """Show differences between NA & JP versions of a card""" dbcog = await self.get_dbcog() raw_query = query - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id monster = await dbcog.find_monster(raw_query, ctx.author.id) @@ -356,15 +354,15 @@ async def nadiff(self, ctx, *, query: str): id_queried_props = await IdViewState.do_query(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) - state = IdViewState(original_author_id, NaDiffMenu.MENU_TYPE, raw_query, query, color, monster, + state = IdViewState(original_author_id, NaDiffMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, id_queried_props) menu = NaDiffMenu.menu() message = state.get_na_diff_invalid_message() if message: state = SimpleTextViewState(original_author_id, NaDiffMenu.MENU_TYPE, - raw_query, color, message) + raw_query, query_settings, message) menu = NaDiffMenu.menu(initial_control=NaDiffMenu.message_control) await menu.create(ctx, state) await self.log_id_result(ctx, monster.monster_id) @@ -376,7 +374,6 @@ async def evos(self, ctx, *, query: str): """Monster info (evolutions tab)""" dbcog = await self.get_dbcog() raw_query = query - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id monster = await dbcog.find_monster(raw_query, ctx.author.id) @@ -389,7 +386,7 @@ async def evos(self, ctx, *, query: str): alt_monsters = EvosViewState.get_alt_monsters_and_evos(dbcog, monster) alt_versions, gem_versions = await EvosViewState.do_query(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) if alt_versions is None: await self.send_invalid_monster_message(ctx, query, monster, ', which has no alt evos or gems') @@ -398,7 +395,7 @@ async def evos(self, ctx, *, query: str): full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) - state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + state = EvosViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, alt_versions, gem_versions, reaction_list=initial_reaction_list @@ -414,7 +411,6 @@ async def evomats(self, ctx, *, query: str): """Monster info (evo materials tab)""" dbcog = await self.get_dbcog() raw_query = query - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id monster = await dbcog.find_monster(raw_query, ctx.author.id) @@ -432,11 +428,11 @@ async def evomats(self, ctx, *, query: str): alt_monsters = MaterialsViewState.get_alt_monsters_and_evos(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) - state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + state = MaterialsViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, gem_override, reaction_list=initial_reaction_list @@ -452,7 +448,6 @@ async def pantheon(self, ctx, *, query: str): """Monster info (pantheon tab)""" dbcog = await self.get_dbcog() raw_query = query - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id monster = await dbcog.find_monster(raw_query, ctx.author.id) @@ -469,11 +464,11 @@ async def pantheon(self, ctx, *, query: str): return alt_monsters = PantheonViewState.get_alt_monsters_and_evos(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) - state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + state = PantheonViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, pantheon_list, series_name, base_monster, reaction_list=initial_reaction_list @@ -489,7 +484,6 @@ async def pic(self, ctx, *, query: str): """Monster info (full image tab)""" dbcog = await self.get_dbcog() raw_query = query - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id monster = await dbcog.find_monster(raw_query, ctx.author.id) @@ -501,11 +495,11 @@ async def pic(self, ctx, *, query: str): alt_monsters = PicViewState.get_alt_monsters_and_evos(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) - state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + state = PicViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=initial_reaction_list ) @@ -520,7 +514,6 @@ async def otherinfo(self, ctx, *, query: str): """Monster info (misc info tab)""" dbcog = await self.get_dbcog() raw_query = query - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id monster = await dbcog.find_monster(raw_query, ctx.author.id) @@ -532,11 +525,11 @@ async def otherinfo(self, ctx, *, query: str): alt_monsters = PicViewState.get_alt_monsters_and_evos(dbcog, monster) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) full_reaction_list = IdMenuPanes.emoji_names() initial_reaction_list = await get_id_menu_initial_reaction_list(ctx, dbcog, monster, full_reaction_list) - state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + state = OtherInfoViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=initial_reaction_list ) @@ -556,9 +549,8 @@ async def links(self, ctx, *, query: str): self.save_historic_data(query, monster) return await self.log_id_result(ctx, monster.monster_id) - color = await self.get_user_embed_color(ctx) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - embed = LinksView.embed(monster, color, query_settings).to_embed() + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) + embed = LinksView.embed(monster, query_settings).to_embed() await ctx.send(embed=embed) await self.log_id_result(ctx, monster.monster_id) self.save_historic_data(query, monster) @@ -574,9 +566,8 @@ async def lookup(self, ctx, *, query: str): self.save_historic_data(query, monster) return await self.log_id_result(ctx, monster.monster_id) - color = await self.get_user_embed_color(ctx) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - embed = LookupView.embed(monster, color, query_settings).to_embed() + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) + embed = LookupView.embed(monster, query_settings).to_embed() await ctx.send(embed=embed) await self.log_id_result(ctx, monster.monster_id) self.save_historic_data(query, monster) @@ -600,13 +591,12 @@ async def buttoninfo(self, ctx, *, query: str): await self.send_id_failure_message(ctx, query) return - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id info = button_info.get_info(dbcog, monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) display_options = ButtonInfoToggles() alt_monsters = ButtonInfoViewState.get_alt_monsters_and_evos(dbcog, monster) - state = ButtonInfoViewState(original_author_id, ButtonInfoMenu.MENU_TYPE, query, color, display_options, + state = ButtonInfoViewState(original_author_id, ButtonInfoMenu.MENU_TYPE, query, display_options, monster, alt_monsters, info, query_settings, reaction_list=ButtonInfoMenuPanes.get_user_reaction_list(display_options)) menu = ButtonInfoMenu.menu() @@ -626,7 +616,7 @@ async def nadiffs(self, ctx, *, query): async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): dbcog = await self.get_dbcog() - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id, query_settings) if not monster_list: @@ -681,8 +671,7 @@ async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterM ): raw_query = query original_author_id = ctx.message.author.id - color = await self.get_user_embed_color(ctx) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' @@ -691,7 +680,7 @@ async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterM _, child_panes_class = padinfo_menu_map[child_menu_type] child_reaction_list = child_panes_class.emoji_names() - state = view_state_type(original_author_id, view_state_type.VIEW_STATE_TYPE, query, color, + state = view_state_type(original_author_id, view_state_type.VIEW_STATE_TYPE, query, monster_list, query_settings, title, instruction_message, child_menu_type=child_menu_type, @@ -707,7 +696,8 @@ async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterM 'dbcog': dbcog, 'user_config': user_config, } - child_state = SimpleTextViewState(original_author_id, view_state_type.VIEW_STATE_TYPE, raw_query, color, + child_state = SimpleTextViewState(original_author_id, view_state_type.VIEW_STATE_TYPE, + raw_query, query_settings, instruction_message, reaction_list=[] ) @@ -735,12 +725,11 @@ async def scroll(self, ctx, *, query: str): title = 'Monster Book Scroll' raw_query = query original_author_id = ctx.message.author.id - color = await self.get_user_embed_color(ctx) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) initial_reaction_list = ScrollMenuPanes.get_initial_reaction_list(len(monster_list)) instruction_message = 'Click a reaction to see monster details!' - state = ScrollViewState(original_author_id, ScrollViewState.VIEW_STATE_TYPE, query, color, + state = ScrollViewState(original_author_id, ScrollViewState.VIEW_STATE_TYPE, query, monster_list, query_settings, title, instruction_message, monster.monster_id, child_menu_type=IdMenu.MENU_TYPE, @@ -761,9 +750,8 @@ async def scroll(self, ctx, *, query: str): id_queried_props = await IdViewState.do_query(dbcog, monster) full_reaction_list = IdMenuPanes.emoji_names() is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) - child_state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, color, monster, + child_state = IdViewState(original_author_id, IdMenu.MENU_TYPE, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, id_queried_props, reaction_list=full_reaction_list) child_menu = IdMenu.menu() @@ -790,7 +778,7 @@ async def seriesscroll(self, ctx, *, query): series_object: "SeriesModel" = monster.series title = series_object.name_en paginated_monsters = None - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) rarity = None for rarity in SeriesScrollMenu.RARITY_INITIAL_TRY_ORDER: paginated_monsters = await SeriesScrollViewState.do_query(dbcog, monster.series_id, @@ -801,11 +789,10 @@ async def seriesscroll(self, ctx, *, query): raw_query = query original_author_id = ctx.message.author.id - color = await self.get_user_embed_color(ctx) initial_reaction_list = SeriesScrollMenuPanes.get_initial_reaction_list(len(paginated_monsters)) instruction_message = 'Click a reaction to see monster details!' - state = SeriesScrollViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, query, color, + state = SeriesScrollViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, query, series_id, paginated_monsters, 0, int(rarity), query_settings, all_rarities, @@ -820,7 +807,8 @@ async def seriesscroll(self, ctx, *, query): 'dbcog': dbcog, 'user_config': user_config, } - child_state = SimpleTextViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, raw_query, color, + child_state = SimpleTextViewState(original_author_id, SeriesScrollMenu.MENU_TYPE, + raw_query, query_settings, instruction_message, reaction_list=[] ) @@ -858,12 +846,11 @@ async def leaderskill(self, ctx, *, raw_query): await ctx.send(inline(err_msg.format('Right', r_query))) return - l_query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), l_query) - r_query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), r_query or l_query) + l_query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, l_query) + r_query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, r_query) - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id - state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, color, l_mon, r_mon, + state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, l_mon, r_mon, l_query, r_query, l_query_settings, r_query_settings) menu = LeaderSkillMenu.menu() await menu.create(ctx, state) @@ -894,12 +881,11 @@ async def leaderskillsingle(self, ctx, *, query): await self.send_id_failure_message(ctx, query) return - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id state = LeaderSkillSingleViewState(original_author_id, LeaderSkillSingleMenu.MENU_TYPE, query, query_settings, - color, monster) + monster) menu = LeaderSkillSingleMenu.menu() await menu.create(ctx, state) @@ -918,15 +904,14 @@ async def transforminfo(self, ctx, *, query): await self.send_invalid_monster_message(ctx, query, base_mon, ', which has no evos that transform') return - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id tfinfo_queried_props = await TransformInfoViewState.do_query(dbcog, transformed_mon) reaction_list = TransformInfoMenuPanes.get_user_reaction_list(len(monster_ids)) is_jp_buffed = dbcog.database.graph.monster_is_discrepant(base_mon) - query_settings = QuerySettings.extract(await self.get_fm_flags(ctx.author), query) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) state = TransformInfoViewState(original_author_id, TransformInfoMenu.MENU_TYPE, query, - color, base_mon, transformed_mon, tfinfo_queried_props, monster_ids, + base_mon, transformed_mon, tfinfo_queried_props, monster_ids, is_jp_buffed, query_settings, reaction_list=reaction_list) @@ -940,13 +925,13 @@ async def awakenings(self, ctx, *, query=None): Leave blank to see a list of every awakening in the game.""" dbcog = await self.get_dbcog() - color = await self.get_user_embed_color(ctx) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query or '') if not query: sort_type = AwakeningListSortTypes.numerical paginated_skills = await AwakeningListViewState.do_query(dbcog, sort_type) menu = AwakeningListMenu.menu() - state = AwakeningListViewState(ctx.message.author.id, AwakeningListMenu.MENU_TYPE, color, + state = AwakeningListViewState(ctx.message.author.id, AwakeningListMenu.MENU_TYPE, query_settings, sort_type, paginated_skills, 0, reaction_list=AwakeningListMenuPanes.get_user_reaction_list(sort_type)) await menu.create(ctx, state) @@ -962,7 +947,7 @@ async def awakenings(self, ctx, *, query=None): menu = ClosableEmbedMenu.menu() props = AwakeningHelpViewProps(monster=monster) state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, - color, AwakeningHelpView.VIEW_TYPE, props) + query_settings, AwakeningHelpView.VIEW_TYPE, props) await menu.create(ctx, state) @commands.command(aliases=['idhist']) @@ -1042,7 +1027,7 @@ async def survey(self, ctx, value): @is_donor() @idset.command() - async def embedcolor(self, ctx, *, color: validators.Color): + async def embedcolor(self, ctx, *, color: validators.EmbedColor): """(DONOR ONLY) The color of all your `[p]id` embeds! Examples: @@ -1056,7 +1041,7 @@ async def embedcolor(self, ctx, *, color: validators.Color): await self.config.user(ctx.author).color.set(color) async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: - fm_flags['color'] = color + fm_flags['embedcolor'] = color await ctx.tick() @idset.command(usage="") @@ -1561,12 +1546,11 @@ async def idtraceback(self, ctx, *, query): f" {t[2]}" for t in sorted(ntokens)) - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() props = IdTracebackViewProps(monster=monster, score=score, name_tokens=ntokenstr, modifier_tokens=mtokenstr, lower_priority_monsters=lpstr if lower_prio else "None") - state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, color, + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, IdTracebackView.VIEW_TYPE, props) await menu.create(ctx, state) @@ -1604,13 +1588,9 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): return await send_cancellation_message(ctx, f"{header} cannot get to level {end} " f"(max level {monster.level}).") - color = await self.get_user_embed_color(ctx) original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() props = ExperienceCurveViewProps(monster=monster, low=start, high=end, offset=offset) - state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, color, + state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, ExperienceCurveView.VIEW_TYPE, props) await menu.create(ctx, state) - - async def get_fm_flags(self, user: discord.User): - return await self.bot.get_cog("DBCog").config.user(user).fm_flags() diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 8cd93539..e71f472a 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -71,7 +71,7 @@ def embed(state, props: AwakeningHelpViewProps): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), description='This monster has no awakenings.' if not monster.awakenings else '' ), embed_author=EmbedAuthor( diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index eb1db831..f5cf67bb 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -6,6 +6,7 @@ from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase +from tsutils.query_settings.query_settings import QuerySettings from padinfo.view.common import get_awoken_skill_description @@ -18,7 +19,8 @@ class AwakeningListSortTypes: class AwakeningListViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 10 - def __init__(self, original_author_id, menu_type, color, sort_type, paginated_skills, current_page, + def __init__(self, original_author_id, menu_type, query_settings: QuerySettings, + sort_type, paginated_skills, current_page, extra_state=None, reaction_list: List[str] = None): super().__init__(original_author_id, menu_type, '', extra_state=extra_state, reaction_list=reaction_list) @@ -26,7 +28,7 @@ def __init__(self, original_author_id, menu_type, color, sort_type, paginated_sk self.paginated_skills = paginated_skills self.total_pages = len(self.paginated_skills) self.current_page = current_page - self.color = color + self.query_settings = query_settings def serialize(self): ret = super().serialize() @@ -34,6 +36,7 @@ def serialize(self): 'current_page': self.current_page, 'total_pages': self.total_pages, 'sort_type': self.sort_type, + 'query_settings': self.query_settings.serialize(), }) return ret @@ -45,7 +48,8 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): current_page = ims['current_page'] menu_type = ims['menu_type'] reaction_list = ims['reaction_list'] - return AwakeningListViewState(original_author_id, menu_type, user_config.color, sort_type, paginated_skills, + query_settings = QuerySettings.deserialize(ims.get('query_settings')) + return AwakeningListViewState(original_author_id, menu_type, query_settings, sort_type, paginated_skills, current_page, reaction_list=reaction_list) @classmethod @@ -76,7 +80,7 @@ def embed(state: AwakeningListViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), ), embed_footer=embed_footer_with_state(state), embed_fields=fields diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 2fdfabfa..9c92c645 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -37,12 +37,11 @@ def __init__(self, players_setting=ButtonInfoOptions.coop, device_setting=Button class ButtonInfoViewState(ViewStateBase, EvoScrollViewState): - def __init__(self, original_author_id, menu_type, raw_query, color, display_options: ButtonInfoToggles, + def __init__(self, original_author_id, menu_type, raw_query, display_options: ButtonInfoToggles, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], info: ButtonInfoResult, query_settings: QuerySettings, reaction_list: List[str] = None): super().__init__(original_author_id, menu_type, raw_query, extra_state=None, reaction_list=reaction_list) - self.color = color self.display_options = display_options self.monster = monster self.info = info @@ -74,7 +73,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): alt_monsters = cls.get_alt_monsters_and_evos(dbcog, monster) info = button_info.get_info(dbcog, monster) reaction_list = ims['reaction_list'] - return ButtonInfoViewState(original_author_id, menu_type, raw_query, user_config.color, display_options, + return ButtonInfoViewState(original_author_id, menu_type, raw_query, display_options, monster, alt_monsters, info, query_settings, reaction_list=reaction_list) def set_player_count(self, new_count): @@ -201,7 +200,7 @@ def embed(cls, state: ButtonInfoViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), description='(Co-op mode)' if is_coop else '(Singleplayer mode)' ), embed_author=EmbedAuthor( diff --git a/padinfo/view/components/view_state_base_id.py b/padinfo/view/components/view_state_base_id.py index 173cf64b..ed38fadb 100644 --- a/padinfo/view/components/view_state_base_id.py +++ b/padinfo/view/components/view_state_base_id.py @@ -13,7 +13,7 @@ class ViewStateBaseId(ViewState, EvoScrollViewState): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, reaction_list: List[str] = None, extra_state=None): @@ -21,7 +21,6 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, monst extra_state=extra_state) self.dfs_alt_monsters = alt_monsters self.reaction_list = reaction_list - self.color = color self.monster = monster self.is_jp_buffed = is_jp_buffed self.query = query @@ -60,7 +59,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=reaction_list, extra_state=ims) diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 3dfb7032..3b80fef0 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -19,12 +19,12 @@ class EvosViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, alt_versions: List["MonsterModel"], gem_versions: List["MonsterModel"], reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + super().__init__(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=reaction_list, extra_state=extra_state) @@ -56,7 +56,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, alt_versions, gem_versions, reaction_list=reaction_list, @@ -101,7 +101,7 @@ def embed(cls, state: EvosViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index 78c73a27..9c83f361 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -116,7 +116,7 @@ def embed(state, props: ExperienceCurveViewProps): is_light = props.monster.full_damage_attr.value == 3 return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(props.monster, use_emoji=True), # TODO: add query_settings url=MonsterLink.header_link(props.monster), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index 2284c9a5..ebc4b753 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -42,12 +42,12 @@ class IdViewState(ViewStateBaseId): nadiff_jp_only_text = ', which is only in JP' nadiff_identical_text = ', which is the same in NA & JP' - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, id_queried_props: IdQueriedProps, fallback_message: str = None, reaction_list: List[str] = None, is_child: bool = False, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + super().__init__(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=reaction_list, extra_state=extra_state) @@ -90,7 +90,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): is_child = ims.get('is_child') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, id_queried_props, fallback_message=fallback_message, @@ -290,7 +290,7 @@ def embed(cls, state: IdViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(m, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index ee1f24b9..de7c7fc5 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -42,7 +42,7 @@ class IdTracebackView: def embed(state, props: IdTracebackViewProps): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(props.monster, use_emoji=True), description=get_description(props.score) ), diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index 83c9c526..c105e0ce 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -17,10 +17,9 @@ class LeaderSkillViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, l_mon, r_mon, l_query, r_query, + def __init__(self, original_author_id, menu_type, raw_query, l_mon, r_mon, l_query, r_query, l_query_settings, r_query_settings): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) - self.color = color self.l_mon = l_mon self.l_query = l_query self.l_query_settings = l_query_settings @@ -47,7 +46,7 @@ async def deserialize(dbcog, user_config: UserConfig, ims: dict): r_query_settings = QuerySettings.deserialize(ims['r_query_settings']) l_mon, l_query, r_mon, r_query = await leaderskill_query(dbcog, raw_query, ims['original_author_id']) - return LeaderSkillViewState(original_author_id, menu_type, raw_query, user_config.color, l_mon, r_mon, l_query, + return LeaderSkillViewState(original_author_id, menu_type, raw_query, l_mon, r_mon, l_query, r_query, l_query_settings, r_query_settings) @@ -66,5 +65,5 @@ def embed(state: LeaderSkillViewState): Text(lls.desc if lls else 'None'), BoldText(MonsterHeader.box_with_emoji(state.r_mon, query_settings=state.r_query_settings)), Text(rls.desc if rls else 'None')), - color=state.color), + color=state.l_query_settings.get_embedcolor()), embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index c1b7ed90..9210f511 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -18,9 +18,8 @@ class LeaderSkillSingleViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, query_settings, color, mon): + def __init__(self, original_author_id, menu_type, raw_query, query_settings, mon): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) - self.color = color self.mon = mon self.query_settings = query_settings @@ -40,7 +39,7 @@ async def deserialize(dbcog, user_config: UserConfig, ims: dict): mon = await get_monster_from_ims(dbcog, ims) return LeaderSkillSingleViewState(original_author_id, menu_type, - raw_query, query_settings, user_config.color, + raw_query, query_settings, mon) @@ -56,5 +55,5 @@ def embed(state: LeaderSkillSingleViewState): description=Box( BoldText(MonsterHeader.box_with_emoji(state.mon, query_settings=state.query_settings)), Text(ls.desc if ls else 'None')), - color=state.color), + color=state.query_settings.get_embedcolor()), embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 72dc75e4..4066da7f 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -25,10 +25,10 @@ def linksbox(m): delimiter=' | ') @staticmethod - def embed(m: "MonsterModel", color, query_settings: QuerySettings): + def embed(m: "MonsterModel", query_settings: QuerySettings): return EmbedView( EmbedMain( - color=color, + color=query_settings.get_embedcolor(), title=MonsterHeader.menu_title(m).to_markdown(), description=LinksView.linksbox(m), url=MonsterLink.header_link(m, query_settings)), diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index 28eb3546..e0d4c3ee 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -14,9 +14,9 @@ class LookupView: VIEW_TYPE = 'Lookup' @staticmethod - def embed(m: "MonsterModel", color, query_settings: QuerySettings): + def embed(m: "MonsterModel", query_settings: QuerySettings): return EmbedView( - EmbedMain(color=color), + EmbedMain(color=query_settings.get_embedcolor()), embed_author=EmbedAuthor( MonsterHeader.menu_title(m).to_markdown(), MonsterLink.header_link(m, query_settings), diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 160ba066..56572739 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -22,14 +22,14 @@ class MaterialsViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", alt_monsters: List[MonsterEvolution], is_jp_buffed: bool, query_settings: QuerySettings, mats: List["MonsterModel"], usedin: List["MonsterModel"], gemid: Optional[str], gemusedin: List["MonsterModel"], skillups: List["MonsterModel"], skillup_evo_count: int, link: str, gem_override: bool, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + super().__init__(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=reaction_list, extra_state=extra_state) @@ -70,7 +70,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link, stackable, reaction_list=reaction_list, @@ -152,10 +152,10 @@ class MaterialsView(BaseIdView, EvoScrollView): @classmethod def embed(cls, state: MaterialsViewState): - # m: "MonsterModel", color, mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link + # m: "MonsterModel", mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 7f9e2e1b..090d21a5 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -19,7 +19,7 @@ class MonsterListViewState(ViewStateBase): VIEW_STATE_TYPE: str MAX_ITEMS_PER_PANE = 11 - def __init__(self, original_author_id, menu_type, query, color, + def __init__(self, original_author_id, menu_type, query, monster_list: List["MonsterModel"], query_settings: QuerySettings, title, message, *, @@ -44,7 +44,6 @@ def __init__(self, original_author_id, menu_type, query, color, self.title = title self.paginated_monsters = paginated_monsters self.reaction_list = reaction_list - self.color = color self.query = query self.query_settings = query_settings @@ -106,7 +105,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): child_menu_type = ims.get('child_menu_type') child_reaction_list = ims.get('child_reaction_list') idle_message = ims.get('idle_message') - return cls(original_author_id, menu_type, query, user_config.color, + return cls(original_author_id, menu_type, query, monster_list, query_settings, title, idle_message, current_page=current_page, @@ -215,7 +214,7 @@ def embed(cls, state: MonsterListViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), ), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/monster_list/scroll.py b/padinfo/view/monster_list/scroll.py index 930f419c..08b1f3a9 100644 --- a/padinfo/view/monster_list/scroll.py +++ b/padinfo/view/monster_list/scroll.py @@ -13,7 +13,7 @@ class ScrollViewState(MonsterListViewState): VIEW_STATE_TYPE = "Scroll" - def __init__(self, original_author_id, menu_type, query, color, + def __init__(self, original_author_id, menu_type, query, monster_list: List["MonsterModel"], query_settings: QuerySettings, title, message, current_monster_id: int, *, @@ -25,7 +25,7 @@ def __init__(self, original_author_id, menu_type, query, color, extra_state=None, child_message_id=None ): - super().__init__(original_author_id, menu_type, query, color, monster_list, query_settings, title, message, + super().__init__(original_author_id, menu_type, query, monster_list, query_settings, title, message, current_page=current_page, current_index=current_index, child_menu_type=child_menu_type, child_reaction_list=child_reaction_list, reaction_list=reaction_list, extra_state=extra_state, child_message_id=child_message_id) @@ -63,7 +63,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): child_menu_type = ims.get('child_menu_type') child_reaction_list = ims.get('child_reaction_list') idle_message = ims.get('idle_message') - return ScrollViewState(original_author_id, menu_type, query, user_config.color, + return ScrollViewState(original_author_id, menu_type, query, monster_list, query_settings, title, idle_message, ims['current_monster_id'], current_page=current_page, diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index bc462f20..7fcf9ba9 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -52,7 +52,7 @@ def embed(cls, state: OtherInfoViewState): m: "MonsterModel" = state.monster return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index f3f3c7bc..faafaac4 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -24,12 +24,12 @@ class PantheonViewState(ViewStateBaseId): - def __init__(self, original_author_id, menu_type, raw_query, query, color, monster: "MonsterModel", + def __init__(self, original_author_id, menu_type, raw_query, query, monster: "MonsterModel", alt_monsters, is_jp_buffed, query_settings, pantheon_list: List[MonsterEvolution], series_name: str, base_monster, reaction_list: List[str] = None, extra_state=None): - super().__init__(original_author_id, menu_type, raw_query, query, color, monster, + super().__init__(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, reaction_list=reaction_list, extra_state=extra_state) @@ -63,7 +63,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): reaction_list = ims.get('reaction_list') is_jp_buffed = dbcog.database.graph.monster_is_discrepant(monster) - return cls(original_author_id, menu_type, raw_query, query, user_config.color, monster, + return cls(original_author_id, menu_type, raw_query, query, monster, alt_monsters, is_jp_buffed, query_settings, pantheon_list, series_name, base_monster, reaction_list=reaction_list, @@ -169,7 +169,7 @@ def embed(cls, state: PantheonViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 8de04857..0461eb3e 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -52,7 +52,7 @@ def embed(cls, state: PicViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index d4ed961e..a0fe335d 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -19,7 +19,7 @@ class SeriesScrollViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 11 - def __init__(self, original_author_id, menu_type, raw_query, query, color, series_id, + def __init__(self, original_author_id, menu_type, raw_query, query, series_id, paginated_monsters: List[List["MonsterModel"]], current_page, rarity: int, query_settings: QuerySettings, all_rarities: List[int], @@ -41,7 +41,6 @@ def __init__(self, original_author_id, menu_type, raw_query, query, color, serie self.child_message_id = child_message_id self.title = title self.reaction_list = reaction_list - self.color = color self.query = query self._max_len_so_far = max(max_len_so_far or len(self.monster_list), len(self.monster_list)) @@ -115,7 +114,7 @@ async def deserialize(dbcog, user_config: UserConfig, ims: dict): max_len_so_far = max(ims['max_len_so_far'] or len(current_monster_list), len(current_monster_list)) idle_message = ims.get('idle_message') - return SeriesScrollViewState(original_author_id, menu_type, raw_query, query, user_config.color, series_id, + return SeriesScrollViewState(original_author_id, menu_type, raw_query, query, series_id, paginated_monsters, current_page, rarity, query_settings, all_rarities, title, idle_message, @@ -235,7 +234,7 @@ def embed(state: SeriesScrollViewState): return EmbedView( EmbedMain( title=state.title, - color=state.color, + color=state.query_settings.get_embedcolor(), ), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 84b34891..41cd22da 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -28,11 +28,10 @@ def __init__(self, acquire_raw, previous_transforms, awoken_skill_map): class TransformInfoViewState(ViewStateBase): - def __init__(self, original_author_id, menu_type, raw_query, color, base_mon, transformed_mon, + def __init__(self, original_author_id, menu_type, raw_query, base_mon, transformed_mon, tfinfo_queried_props: TransformInfoQueriedProps, monster_ids, is_jp_buffed, query_settings, reaction_list): super().__init__(original_author_id, menu_type, raw_query, extra_state=None) - self.color = color self.base_mon = base_mon self.transformed_mon = transformed_mon self.acquire_raw = tfinfo_queried_props.acquire_raw @@ -70,7 +69,7 @@ async def deserialize(dbcog, user_config: UserConfig, ims: dict): is_jp_buffed = dbcog.database.graph.monster_is_discrepant( base_mon) or dbcog.database.graph.monster_is_discrepant(transformed_mon) - return TransformInfoViewState(original_author_id, menu_type, raw_query, user_config.color, + return TransformInfoViewState(original_author_id, menu_type, raw_query, base_mon, transformed_mon, tfinfo_queried_props, monster_ids, is_jp_buffed, query_settings, reaction_list=reaction_list) @@ -220,7 +219,7 @@ def embed(cls, state: TransformInfoViewState): return EmbedView( EmbedMain( - color=state.color, + color=state.query_settings.get_embedcolor(), title=MonsterHeader.menu_title(transformed_mon, is_tsubaki=False, is_jp_buffed=state.is_jp_buffed).to_markdown(), From 185ad159572e269ebfd66cb0f2fe8299c530c8ff Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 6 Mar 2022 10:13:05 -0600 Subject: [PATCH 1071/1220] Stop storing padinfo embedcolor (#1618) --- padinfo/padinfo.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 488acdb8..fb782448 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1037,8 +1037,6 @@ async def embedcolor(self, ctx, *, color: validators.EmbedColor): Picking random will choose a random hex code every time you use [p]id! """ - # Remove this line once the Menu2 stuff is done - await self.config.user(ctx.author).color.set(color) async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: fm_flags['embedcolor'] = color From 14703f6affc71ecf128c19daa95c64178c945171 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 6 Mar 2022 10:47:00 -0600 Subject: [PATCH 1072/1220] add color to showstats (#1619) --- crowddata/mixins/adpem/view/show_stats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crowddata/mixins/adpem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py index 548e2fb7..7d6e5ad9 100644 --- a/crowddata/mixins/adpem/view/show_stats.py +++ b/crowddata/mixins/adpem/view/show_stats.py @@ -58,12 +58,13 @@ def embed(cls, state: ClosableEmbedViewState, props: ShowStatsViewProps): return EmbedView( EmbedMain( + color=state.query_settings.get_embedcolor(), title=f"AdPEM Data for query: {state.raw_query}", description=Box( LabeledText("Net", ShowStatsView.get_count(props.total, *props.valid)), LabeledText("Adj", ShowStatsView.get_count(props.adj, *props.valid)), LabeledText("You", ShowStatsView.get_count(props.you, *props.valid)) - ) + ), ), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(props.most_commmon.monster_id)), embed_fields=fields, From 2f2c228a22d302ddb08a4a99a980184cf295552f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 7 Mar 2022 18:56:30 -0600 Subject: [PATCH 1073/1220] Fix leaderskill when no right query (#1620) --- padinfo/padinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index fb782448..24295a93 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -847,7 +847,7 @@ async def leaderskill(self, ctx, *, raw_query): return l_query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, l_query) - r_query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, r_query) + r_query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, r_query or l_query) original_author_id = ctx.message.author.id state = LeaderSkillViewState(original_author_id, LeaderSkillMenu.MENU_TYPE, raw_query, l_mon, r_mon, From 9d4521cb54406dd2ce1ae9a1470a76570c309846 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 16 Mar 2022 11:54:16 -0700 Subject: [PATCH 1074/1220] Create subdungeonlist (#1623) --- padguidedb/padguidedb.py | 50 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index 2c5fa588..bb30c7c2 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -101,9 +101,9 @@ async def searchdungeon(self, ctx, *, search_text): """Search for a dungeon via its jp or na name""" search_text = '%{}%'.format(search_text) with self.get_cursor() as cursor: - sql = ('select dungeon_id, name_en, name_ja, visible from dungeons' - ' where lower(name_en) like %s or lower(name_ja) like %s' - ' order by dungeon_id desc limit 20') + sql = ('SELECT dungeon_id, name_en, name_ja, visible FROM dungeons' + ' WHERE LOWER(name_en) LIKE %s OR LOWER(name_ja) LIKE %s' + ' ORDER BY dungeon_id DESC LIMIT 20') cursor.execute(sql, [search_text, search_text]) results = list(cursor.fetchall()) msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False) @@ -111,6 +111,48 @@ async def searchdungeon(self, ctx, *, search_text): for page in pagify(msg): await ctx.send(box(page)) + @padguidedb.command() + async def subdungeonlist(self, ctx, dungeon_id): + """Show the subdungeon ids of a dungeon""" + with self.get_cursor() as cursor: + cursor.execute('SELECT name_en FROM dungeons' + ' WHERE dungeon_id = %s', (dungeon_id,)) + dg_name = next(iter(cursor.fetchall()), {'name_en': None})['name_en'] + if dg_name is None: + return await ctx.send(f"No dungeon found with id {dungeon_id}") + + cursor.execute('SELECT sub_dungeon_id, name_en FROM sub_dungeons' + ' WHERE dungeon_id = %s' + ' ORDER BY sub_dungeon_id', (dungeon_id,)) + results = list(cursor.fetchall()) + msg = f'{dg_name}: {dungeon_id}\n\t' + '\n\t'.join(f"{row['name_en']}: {row['sub_dungeon_id']}" + for row in results) + for page in pagify(msg): + await ctx.send(box(page)) + + @padguidedb.command() + async def searchsubdungeons(self, ctx, *, search_text): + """Show the subdungeon ids of all matching dungeons""" + search_text = f"%{search_text}%" + with self.get_cursor() as cursor: + cursor.execute('SELECT dungeon_id, name_en FROM dungeons' + ' WHERE LOWER(name_en) LIKE %s OR LOWER(name_ja) LIKE %s' + ' ORDER BY dungeon_id LIMIT 20', (search_text, search_text)) + dgs = cursor.fetchall() + if not dgs: + return await ctx.send(f"No dungeons found") + + msg = [] + for dg in dgs: + cursor.execute('SELECT sub_dungeon_id, name_en FROM sub_dungeons' + ' WHERE dungeon_id = %s' + ' ORDER BY sub_dungeon_id', (dg['dungeon_id'],)) + results = list(cursor.fetchall()) + msg.append(f"{dg['name_en']} - {dg['dungeon_id']}\n\t" + + '\n\t'.join(f"{row['name_en']} - {row['sub_dungeon_id']}" + for row in results)) + await ctx.send_interactive(map(box, pagify('\n\n'.join(msg), delims=['\n\n']))) + @padguidedb.command() @is_padguidedb_admin() async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): @@ -225,7 +267,7 @@ async def pipeline(self, context): @is_padguidedb_admin() async def fulletl(self, ctx, server='COMBINED'): """Runs a job which downloads pad data, and updates the padguide database.""" - await self.runetl(ctx, '--server='+server) + await self.runetl(ctx, '--server=' + server) @pipeline.command() @is_padguidedb_admin() From 41709d090ce4e1b9e79a01a8055075873bf75f9c Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 16 Mar 2022 14:30:45 -0700 Subject: [PATCH 1075/1220] Update for TSUtils 7.2 (#1622) --- crowddata/mixins/adpem/view/show_stats.py | 2 +- padglobal/view/which.py | 2 +- padinfo/padinfo.py | 4 ++-- padinfo/view/awakening_help.py | 2 +- padinfo/view/awakening_list.py | 2 +- padinfo/view/button_info.py | 2 +- padinfo/view/evos.py | 2 +- padinfo/view/experience_curve.py | 2 +- padinfo/view/id.py | 2 +- padinfo/view/id_traceback.py | 2 +- padinfo/view/leader_skill.py | 2 +- padinfo/view/leader_skill_single.py | 2 +- padinfo/view/links.py | 2 +- padinfo/view/lookup.py | 2 +- padinfo/view/materials.py | 2 +- padinfo/view/monster_list/monster_list.py | 2 +- padinfo/view/otherinfo.py | 2 +- padinfo/view/pantheon.py | 2 +- padinfo/view/pic.py | 2 +- padinfo/view/series_scroll.py | 2 +- padinfo/view/transforminfo.py | 2 +- 21 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crowddata/mixins/adpem/view/show_stats.py b/crowddata/mixins/adpem/view/show_stats.py index 7d6e5ad9..69525152 100644 --- a/crowddata/mixins/adpem/view/show_stats.py +++ b/crowddata/mixins/adpem/view/show_stats.py @@ -58,7 +58,7 @@ def embed(cls, state: ClosableEmbedViewState, props: ShowStatsViewProps): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=f"AdPEM Data for query: {state.raw_query}", description=Box( LabeledText("Net", ShowStatsView.get_count(props.total, *props.valid)), diff --git a/padglobal/view/which.py b/padglobal/view/which.py index eba3cc17..cddff73b 100644 --- a/padglobal/view/which.py +++ b/padglobal/view/which.py @@ -77,7 +77,7 @@ def embed(state: ClosableEmbedViewState, props: WhichViewProps): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title='Which {}'.format(name), description=get_description(definition, timestamp, success) ), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 24295a93..36f80d01 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -23,7 +23,7 @@ from tsutils.menu.simple_text import SimpleTextMenu from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.menu.view.simple_text import SimpleTextViewState -from tsutils.query_settings import validators +from tsutils.query_settings import converters from tsutils.query_settings.enums import AltEvoSort, CardLevelModifier, CardModeModifier, CardPlusModifier, EvoGrouping, \ LsMultiplier, \ MonsterLinkTarget @@ -1027,7 +1027,7 @@ async def survey(self, ctx, value): @is_donor() @idset.command() - async def embedcolor(self, ctx, *, color: validators.EmbedColor): + async def embedcolor(self, ctx, *, color: converters.EmbedColor): """(DONOR ONLY) The color of all your `[p]id` embeds! Examples: diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index e71f472a..143aeec4 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -71,7 +71,7 @@ def embed(state, props: AwakeningHelpViewProps): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, description='This monster has no awakenings.' if not monster.awakenings else '' ), embed_author=EmbedAuthor( diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index f5cf67bb..9d86a614 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -80,7 +80,7 @@ def embed(state: AwakeningListViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, ), embed_footer=embed_footer_with_state(state), embed_fields=fields diff --git a/padinfo/view/button_info.py b/padinfo/view/button_info.py index 9c92c645..301fc3aa 100644 --- a/padinfo/view/button_info.py +++ b/padinfo/view/button_info.py @@ -200,7 +200,7 @@ def embed(cls, state: ButtonInfoViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, description='(Co-op mode)' if is_coop else '(Singleplayer mode)' ), embed_author=EmbedAuthor( diff --git a/padinfo/view/evos.py b/padinfo/view/evos.py index 3b80fef0..0fe91d7c 100644 --- a/padinfo/view/evos.py +++ b/padinfo/view/evos.py @@ -101,7 +101,7 @@ def embed(cls, state: EvosViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index 9c83f361..1d3216ce 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -116,7 +116,7 @@ def embed(state, props: ExperienceCurveViewProps): is_light = props.monster.full_damage_attr.value == 3 return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(props.monster, use_emoji=True), # TODO: add query_settings url=MonsterLink.header_link(props.monster), diff --git a/padinfo/view/id.py b/padinfo/view/id.py index ebc4b753..ae7fbe3b 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -290,7 +290,7 @@ def embed(cls, state: IdViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(m, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index de7c7fc5..c6d5e354 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -42,7 +42,7 @@ class IdTracebackView: def embed(state, props: IdTracebackViewProps): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(props.monster, use_emoji=True), description=get_description(props.score) ), diff --git a/padinfo/view/leader_skill.py b/padinfo/view/leader_skill.py index c105e0ce..9d67041a 100644 --- a/padinfo/view/leader_skill.py +++ b/padinfo/view/leader_skill.py @@ -65,5 +65,5 @@ def embed(state: LeaderSkillViewState): Text(lls.desc if lls else 'None'), BoldText(MonsterHeader.box_with_emoji(state.r_mon, query_settings=state.r_query_settings)), Text(rls.desc if rls else 'None')), - color=state.l_query_settings.get_embedcolor()), + color=state.l_query_settings.embedcolor), embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/leader_skill_single.py b/padinfo/view/leader_skill_single.py index 9210f511..9af09ae4 100644 --- a/padinfo/view/leader_skill_single.py +++ b/padinfo/view/leader_skill_single.py @@ -55,5 +55,5 @@ def embed(state: LeaderSkillSingleViewState): description=Box( BoldText(MonsterHeader.box_with_emoji(state.mon, query_settings=state.query_settings)), Text(ls.desc if ls else 'None')), - color=state.query_settings.get_embedcolor()), + color=state.query_settings.embedcolor), embed_footer=embed_footer_with_state(state)) diff --git a/padinfo/view/links.py b/padinfo/view/links.py index 4066da7f..6ba57673 100644 --- a/padinfo/view/links.py +++ b/padinfo/view/links.py @@ -28,7 +28,7 @@ def linksbox(m): def embed(m: "MonsterModel", query_settings: QuerySettings): return EmbedView( EmbedMain( - color=query_settings.get_embedcolor(), + color=query_settings.embedcolor, title=MonsterHeader.menu_title(m).to_markdown(), description=LinksView.linksbox(m), url=MonsterLink.header_link(m, query_settings)), diff --git a/padinfo/view/lookup.py b/padinfo/view/lookup.py index e0d4c3ee..bc7c12d4 100644 --- a/padinfo/view/lookup.py +++ b/padinfo/view/lookup.py @@ -16,7 +16,7 @@ class LookupView: @staticmethod def embed(m: "MonsterModel", query_settings: QuerySettings): return EmbedView( - EmbedMain(color=query_settings.get_embedcolor()), + EmbedMain(color=query_settings.embedcolor), embed_author=EmbedAuthor( MonsterHeader.menu_title(m).to_markdown(), MonsterLink.header_link(m, query_settings), diff --git a/padinfo/view/materials.py b/padinfo/view/materials.py index 56572739..ac8cb061 100644 --- a/padinfo/view/materials.py +++ b/padinfo/view/materials.py @@ -155,7 +155,7 @@ def embed(cls, state: MaterialsViewState): # m: "MonsterModel", mats, usedin, gemid, gemusedin, skillups, skillup_evo_count, link return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 090d21a5..4267f8bf 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -214,7 +214,7 @@ def embed(cls, state: MonsterListViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, ), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/otherinfo.py b/padinfo/view/otherinfo.py index 7fcf9ba9..2a223256 100644 --- a/padinfo/view/otherinfo.py +++ b/padinfo/view/otherinfo.py @@ -52,7 +52,7 @@ def embed(cls, state: OtherInfoViewState): m: "MonsterModel" = state.monster return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/pantheon.py b/padinfo/view/pantheon.py index faafaac4..ccff6161 100644 --- a/padinfo/view/pantheon.py +++ b/padinfo/view/pantheon.py @@ -169,7 +169,7 @@ def embed(cls, state: PantheonViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/pic.py b/padinfo/view/pic.py index 0461eb3e..fed17c81 100644 --- a/padinfo/view/pic.py +++ b/padinfo/view/pic.py @@ -52,7 +52,7 @@ def embed(cls, state: PicViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(state.monster, is_tsubaki=state.alt_monsters[0].monster.monster_id == cls.TSUBAKI, is_jp_buffed=state.is_jp_buffed).to_markdown(), diff --git a/padinfo/view/series_scroll.py b/padinfo/view/series_scroll.py index a0fe335d..3e23bf34 100644 --- a/padinfo/view/series_scroll.py +++ b/padinfo/view/series_scroll.py @@ -234,7 +234,7 @@ def embed(state: SeriesScrollViewState): return EmbedView( EmbedMain( title=state.title, - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, ), embed_footer=embed_footer_with_state(state), embed_fields=fields) diff --git a/padinfo/view/transforminfo.py b/padinfo/view/transforminfo.py index 41cd22da..bbc17806 100644 --- a/padinfo/view/transforminfo.py +++ b/padinfo/view/transforminfo.py @@ -219,7 +219,7 @@ def embed(cls, state: TransformInfoViewState): return EmbedView( EmbedMain( - color=state.query_settings.get_embedcolor(), + color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(transformed_mon, is_tsubaki=False, is_jp_buffed=state.is_jp_buffed).to_markdown(), From 9b34114becef03f214654311b0f67772d34fa08d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 20 Mar 2022 22:27:48 -0500 Subject: [PATCH 1076/1220] Use MonsterHeader for monidlistener messages (#1625) --- monidlistener/monidlistener.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 2a0af782..96170003 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -2,6 +2,7 @@ from io import BytesIO from redbot.core import Config, checks, commands +from tsutils.tsubaki.monster_header import MonsterHeader class MonIdListener(commands.Cog): @@ -50,12 +51,12 @@ async def on_message(self, message): matches = re.findall(r'\b\d{3,4}\b', content) ret = "" for i in matches: - if (i == "100"): # skip when people say "is 100 mp or over" ~~and ryan's posts~~ + if i == "100": # skip when people say "is 100 mp or over" ~~and ryan's posts~~ continue m = await dbcog.find_monster(i, message.author.id) if not m: # monster not found continue - ret += "[{}] {}{}\n".format(i, m.name_en, " (untradable)" if m.sell_mp >= 100 else "") + ret += "{}{}\n".format(MonsterHeader.text_with_emoji(m), " (untradable)" if m.sell_mp >= 100 else "") if ret != "": await channel.send(ret) From 5fb68610af97a5aae18850969d4fca0d247ad824 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 20 Mar 2022 23:20:40 -0500 Subject: [PATCH 1077/1220] Use MonsterHeader in dgcog (#1626) --- dungeoncog/dungeon_monster.py | 4 ++-- dungeoncog/processors.py | 4 ++-- dungeoncog/view/dungeon.py | 13 +++++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/dungeoncog/dungeon_monster.py b/dungeoncog/dungeon_monster.py index 77fa180b..20ddb87f 100644 --- a/dungeoncog/dungeon_monster.py +++ b/dungeoncog/dungeon_monster.py @@ -8,8 +8,7 @@ class DungeonMonster(object): A class that symbolizes an encounter in a dungeon. Why is it not a model? Good question. """ - def __init__(self, name: str, hp, atk, defense, turns, level, error=None): - self.name = name + def __init__(self, hp, atk, defense, turns, level, monster, error=None): self.hp = hp self.atk = atk self.defense = defense @@ -18,6 +17,7 @@ def __init__(self, name: str, hp, atk, defense, turns, level, error=None): self.level = level self.groups: List[GroupedSkills] = [] self.am_invade = False + self.monster = monster self.error = error diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index 6c8f091d..ad6f27ca 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -235,12 +235,12 @@ def process_monster(behavior: MonsterBehavior, encounter: "EncounterModel", data ) monster_model = database.graph.get_monster(encounter.monster_id) monster = DungeonMonster( - name=monster_model.name_en, hp=encounter.hp, atk=encounter.atk, defense=encounter.defense, turns=encounter.turns, - level=encounter.level + level=encounter.level, + monster=monster_model ) if behavior is None: return monster diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index 162754ee..6ac92c21 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -8,6 +8,7 @@ from discordmenu.embed.view import EmbedView from discordmenu.embed.view_state import ViewState from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.tsubaki.monster_header import MonsterHeader from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skills_pb2 import MonsterBehavior @@ -208,14 +209,18 @@ def make_embed(dungeon_monster: DungeonMonster, verbose: bool = False, spawn: Li # We create two pages as monsters at max will only ever require two pages of embeds if spawn is not None: embed = discord.Embed( - title="{} Spawn:{}/{} Floor:{}/{} Page:".format(dungeon_monster.name, - spawn[0], spawn[1], - floor[0], floor[1]), + title="{} Floor:{}/{} Spawn:{}/{} Page:".format( + MonsterHeader.text_with_emoji(dungeon_monster.monster), + floor[0], floor[1], + spawn[0], spawn[1] + ), description=DungeonView.make_embed_description(dungeon_monster, desc) ) else: embed = discord.Embed( - title="Enemy:{} at Level: {}".format(dungeon_monster.name, dungeon_monster.level), + title="Enemy:{} at Level: {}".format( + MonsterHeader.text_with_emoji(dungeon_monster.monster), + dungeon_monster.level), description=DungeonView.make_embed_description(dungeon_monster, desc) ) From 54838b0859004a1d7da6cd7c779068e115012ba5 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 20 Mar 2022 23:30:50 -0500 Subject: [PATCH 1078/1220] Adjust dgid ui (#1627) --- dungeoncog/view/dungeon.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index 6ac92c21..39835947 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -146,13 +146,6 @@ def embed_helper(level, names, values, line): values.append("") DungeonView.embed_helper(level, names, values, line) - @staticmethod - def make_embed_description(dungeon_monster, desc=""): - return "HP:{} ATK:{} DEF:{} CD:{}{}".format(f'{dungeon_monster.hp:,}', - f'{dungeon_monster.atk:,}', - f'{dungeon_monster.defense:,}', - f'{dungeon_monster.turns:,}', desc) - @staticmethod def embed(state: DungeonViewState): fields = [] @@ -190,6 +183,13 @@ def embed(state: DungeonViewState): embed_footer=embed_footer_with_state(state) ) + @staticmethod + def make_embed_description(dungeon_monster, desc=""): + return "HP: {} ATK: {} DEF: {} CD: {}{}".format(f'{dungeon_monster.hp:,}', + f'{dungeon_monster.atk:,}', + f'{dungeon_monster.defense:,}', + f'{dungeon_monster.turns:,}', desc) + @staticmethod def make_embed(dungeon_monster: DungeonMonster, verbose: bool = False, spawn: List[int] = None, floor: List[int] = None, technical: int = None) -> List[discord.Embed]: @@ -209,12 +209,13 @@ def make_embed(dungeon_monster: DungeonMonster, verbose: bool = False, spawn: Li # We create two pages as monsters at max will only ever require two pages of embeds if spawn is not None: embed = discord.Embed( - title="{} Floor:{}/{} Spawn:{}/{} Page:".format( - MonsterHeader.text_with_emoji(dungeon_monster.monster), + title="Floor: {}/{} Spawn: {}/{} Page: ".format( floor[0], floor[1], spawn[0], spawn[1] ), - description=DungeonView.make_embed_description(dungeon_monster, desc) + description='{}\n{}'.format( + MonsterHeader.text_with_emoji(dungeon_monster.monster), + DungeonView.make_embed_description(dungeon_monster, desc)) ) else: embed = discord.Embed( From 6f61c7f1514fa22ef7b0a773bcf71cd8bb1f03de Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 24 Mar 2022 07:13:37 -0700 Subject: [PATCH 1079/1220] Add subquery tokens (#1628) --- dbcog/database_context.py | 34 +++++-- dbcog/dbcog.py | 22 ++--- dbcog/find_monster.py | 169 ++++++++++++++++++++--------------- dbcog/find_monster_tokens.py | 154 +++++++++++++++++++++++-------- padinfo/padinfo.py | 36 ++++---- padinfo/view/id_traceback.py | 17 +--- 6 files changed, 275 insertions(+), 157 deletions(-) diff --git a/dbcog/database_context.py b/dbcog/database_context.py index c6f9a1de..44a2ee54 100644 --- a/dbcog/database_context.py +++ b/dbcog/database_context.py @@ -1,4 +1,4 @@ -from typing import Callable, Iterable, List, Optional +from typing import Any, Callable, Hashable, Iterable, List, Optional, Union from tsutils.enums import Server @@ -29,12 +29,23 @@ def __init__(self, database: DBCogDatabase, graph: MonsterGraph, dungeon: Dungeo self.graph = graph self.dungeon = dungeon + self.cached_monster_ids = None + self.cached_monsters = {} + self.cached_filters = {} self.debug_monster_ids = debug_monster_ids self.awoken_skill_map = {awsk.awoken_skill_id: awsk for awsk in self.get_all_awoken_skills()} - def get_monsters_where(self, f: Callable[[MonsterModel], bool], *, server: Server) -> List[MonsterModel]: - return [m for m in self.get_all_monsters(server) if f(m)] + def get_monsters_where(self, f: Callable[[MonsterModel], bool], *, server: Server, cache_key: Hashable = None) \ + -> List[MonsterModel]: + if cache_key is None: + return [m for m in self.get_all_monsters(server) if f(m)] + + if (cache_key, server) in self.cached_filters: + return self.cached_filters[(cache_key, server)] + + self.cached_filters[(cache_key, server)] = self.get_monsters_where(f, server=server) + return self.cached_filters[(cache_key, server)] def get_monsters_by_series(self, series_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.series_id == series_id, server=server) @@ -45,20 +56,29 @@ def get_monsters_by_active(self, active_skill_id: int, *, server: Server) -> Lis def get_monsters_by_leader(self, leader_skill_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.leader_skill_id == leader_skill_id, server=server) - def get_all_monster_ids_query(self, server: Server) -> Iterable[int]: + def get_all_monster_ids(self, server: Server) -> List[int]: # We don't need to query if we're in debug mode. We already know exactly which monsters we're working with if self.debug_monster_ids is not None: return self.debug_monster_ids + if self.cached_monster_ids is not None: + return self.cached_monster_ids + table = 'monsters_na' if server == Server.NA else 'monsters' query = self.database.query_many( self.database.select_builder(tables={table: ('monster_id',)}), (), as_generator=True) - return map(lambda m: m.monster_id, query) + self.cached_monster_ids = [m.monster_id for m in query] + return self.cached_monster_ids + + def get_all_monsters(self, server: Server = DEFAULT_SERVER) -> List[MonsterModel]: + if server in self.cached_monsters: + return self.cached_monsters[server] - def get_all_monsters(self, server: Server = DEFAULT_SERVER) -> Iterable[MonsterModel]: - return (self.graph.get_monster(mid, server=server) for mid in self.get_all_monster_ids_query(server)) + self.cached_monsters[server] = [self.graph.get_monster(mid, server=server) + for mid in self.get_all_monster_ids(server)] + return self.cached_monsters[server] def get_all_events(self) -> Iterable[ScheduledEventModel]: result = self.database.query_many(SCHEDULED_EVENT_QUERY) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 7c0bdcde..2f92586a 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -10,24 +10,24 @@ import logging import os import shutil -from functools import reduce - import time +from functools import reduce from io import BytesIO -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union import discord from redbot.core import Config, checks, commands, data_manager from redbot.core.utils.chat_formatting import box, pagify from tsutils.cogs.globaladmin import auth_check from tsutils.enums import Server -from tsutils.json_utils import async_cached_dadguide_request, safe_read_json +from tsutils.json_utils import async_cached_dadguide_request from tsutils.tsubaki.links import CLOUDFRONT_URL from tsutils.user_interaction import StatusManager, send_confirmation_message from . import token_mappings +from .database_context import DbContext from .database_loader import load_database -from .find_monster import FindMonster, MatchMap +from .find_monster import ExtraInfo, FindMonster, MonsterInfo from .idtest_mixin import IdTest from .models.enum_types import DEFAULT_SERVER, SERVERS from .models.monster_model import MonsterModel @@ -50,8 +50,8 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self._is_ready = asyncio.Event() - self.database = None - self.indexes = { + self.database: Optional[DbContext] = None + self.indexes: Dict[Server, MonsterIndex] = { Server.COMBINED: MonsterIndex(Server.COMBINED), Server.NA: MonsterIndex(Server.NA), } @@ -319,12 +319,14 @@ async def get_fm_flags(self, author_id): return {**self.fm_flags_default, **(await self.config.user_from_id(author_id).fm_flags())} async def find_monster(self, query: str, author_id: int = 0) -> Optional[MonsterModel]: - return await FindMonster(self, await self.get_fm_flags(author_id)).find_monster(query) + monster, e_info = await FindMonster(self, await self.get_fm_flags(author_id)).find_monster(query) + return monster async def find_monsters(self, query: str, author_id: int = 0) -> List[MonsterModel]: - return await FindMonster(self, await self.get_fm_flags(author_id)).find_monsters(query) + monsters, e_info = await FindMonster(self, await self.get_fm_flags(author_id)).find_monsters(query) + return monsters - async def _find_monster_debug(self, query: str) -> Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel], int]: + async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: return await FindMonster(self, self.fm_flags_default).find_monster_debug(query) @staticmethod diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index 60015223..7d41f4d8 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -1,14 +1,13 @@ -import json import re from collections import defaultdict -from typing import Set, List, Tuple, Optional, Mapping, Union, Iterable, Any, Dict +from typing import Any, DefaultDict, Dict, Iterable, List, Mapping, NamedTuple, Optional, Set, Tuple, Union from Levenshtein import jaro_winkler +from tsutils.enums import Server from tsutils.formatting import rmdiacritics from tsutils.query_settings.query_settings import QuerySettings -from tsutils.enums import Server -from dbcog.find_monster_tokens import Token, SPECIAL_TOKEN_TYPES +from dbcog.find_monster_tokens import MatchData, SPECIAL_TOKEN_TYPES, SpecialToken, Token from dbcog.models.monster_model import MonsterModel SERIES_TYPE_PRIORITY = { @@ -22,13 +21,23 @@ } +class NameMatch(NamedTuple): + token: str + matched: str + match_type: str + + +class ModifierMatch(NamedTuple): + token: str + matched: str + match_data: MatchData + + class MonsterMatch: - def __init__(self, score=0, name=None, mod=None): - self.score = score - if name is None: - self.name = set() - if mod is None: - self.mod = set() + def __init__(self): + self.score: float = 0 + self.name: Set[NameMatch] = set() + self.mod: Set[ModifierMatch] = set() def __repr__(self): return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) @@ -37,6 +46,16 @@ def __repr__(self): MatchMap = Mapping[MonsterModel, MonsterMatch] +class MonsterInfo(NamedTuple): + matched_monster: Optional[MonsterModel] + monster_matches: MatchMap + valid_monsters: Set[MonsterModel] + + +class ExtraInfo(NamedTuple): + return_code: int + + class FindMonster: MODIFIER_JW_DISTANCE = .95 TOKEN_JW_DISTANCE = .8 @@ -73,41 +92,43 @@ def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: break else: skip = len(mwt) - 1 - result.append("".join(tokens[c1:c1+len(mwt)])) + result.append("".join(tokens[c1:c1 + len(mwt)])) break else: result.append(token1) return result - def _monster_has_modifier(self, monster: MonsterModel, token: Token, matches: MatchMap) -> bool: - if not token.matches(monster): + async def _monster_has_modifier(self, monster: MonsterModel, token: Token, matches: MatchMap) -> bool: + if len(token.value) < 6 or isinstance(token, SpecialToken): + matched_token = '' if isinstance(token, SpecialToken) else token.value + ratio = 1 + if matched_token and matched_token not in self.index.modifiers[monster]: + return False + else: + matched_token = max(self.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(token, m)) + ratio = self.calc_ratio_modifier(matched_token, token.value) + if ratio <= self.MODIFIER_JW_DISTANCE: + return False + + mult, data = await token.matches(monster) + if mult == 0: return False - if len(token.value) < 6: - if token.value in self.index.modifiers[monster]: - matches[monster].mod.add((token.value, token.value)) - matches[monster].score += 1 - return True - else: - closest = max(self.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(token, m)) - ratio = self.calc_ratio_modifier(closest, token.value) - if ratio > self.MODIFIER_JW_DISTANCE: - matches[monster].mod.add((token.value, closest)) - matches[monster].score += ratio - return True - return False - - def _string_to_token(self, value: str) -> Token: + matches[monster].mod.add(ModifierMatch(token.full_value, matched_token, data)) + matches[monster].score += ratio * mult + return True + + async def _string_to_token(self, value: str) -> Token: if (negated := value.startswith('-')): value = value.lstrip('-') if (exact := bool(re.fullmatch(r'".+"', value))): value = value[1:-1] for special in SPECIAL_TOKEN_TYPES: if re.fullmatch(special.RE_MATCH, value): - return special(value, negated=negated, exact=exact, database=self.dbcog.database) + return await special(value, negated=negated, exact=exact, dbcog=self.dbcog).prepare() return Token(value, negated=negated, exact=exact) - def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: + async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: modifiers = [] name = set() longmods = [p for p in self.index.all_modifiers if len(p) > 8] @@ -115,10 +136,10 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ # Suffixes for i, value in enumerate(tokenized_query[::-1]): - token = self._string_to_token(value) + token = await self._string_to_token(value) if any(self.calc_ratio_modifier(m, token.value.split('-')[0], .1) > self.MODIFIER_JW_DISTANCE - for m in self.index.suffixes): + for m in self.index.suffixes) or not token.value: # TODO: Store this as a list of regexes. Don't split on '-' anymore. modifiers.append(token) else: @@ -128,9 +149,9 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ # Prefixes for i, value in enumerate(tokenized_query): - token = self._string_to_token(value) + token = await self._string_to_token(value) - if token.value in self.index.all_modifiers or ( + if token.value in self.index.all_modifiers or not token.value or ( any(self.calc_ratio_modifier(token, m, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) and token.value not in self.index.all_name_tokens and len(token.value) >= 8): @@ -144,7 +165,7 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ # Name Tokens for value in tokenized_query: - name.add(self._string_to_token(value)) + name.add(await self._string_to_token(value)) # Allow modifiers to match as name tokens if they're alone (Fix for hel and cloud) if not name and modifiers and lastmodpos: @@ -154,18 +175,18 @@ def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[ return set(modifiers), name - def _process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) -> Optional[Set[MonsterModel]]: + async def _process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) -> Optional[Set[MonsterModel]]: matched_mons = None for name_token in name_query_tokens: if name_token.negated: - invalid = self._get_valid_monsters_from_name_token(name_token, matches, mult=-10) + invalid = await self._get_valid_monsters_from_name_token(name_token, matches, mult=-10) if matched_mons is not None: matched_mons.difference_update(invalid) else: matched_mons = set(self.dbcog.database.get_all_monsters(self.index.server)).difference(invalid) else: - valid = self._get_valid_monsters_from_name_token(name_token, matches) + valid = await self._get_valid_monsters_from_name_token(name_token, matches) if matched_mons is not None: matched_mons.intersection_update(valid) else: @@ -173,8 +194,8 @@ def _process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) return matched_mons - def _get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, - mult: Union[int, float] = 1) -> Set[MonsterModel]: + async def _get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, + mult: Union[int, float] = 1) -> Set[MonsterModel]: valid_monsters = set() all_monsters_name_tokens_scores = {nt: self.calc_ratio_name(token, nt) for nt in self.index.all_name_tokens} @@ -183,29 +204,25 @@ def _get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, matched_tokens += [t for t in self.index.all_name_tokens if t.startswith(token.value)] for match in matched_tokens: score = all_monsters_name_tokens_scores[match] - for matched_monster in self.index.manual[match]: - if matched_monster not in valid_monsters and token.matches(matched_monster): - matches[matched_monster].name.add((token.value, match, '(manual)')) - matches[matched_monster].score += (score + .001) * mult - valid_monsters.add(matched_monster) - for matched_monster in self.index.name_tokens[match]: - if matched_monster not in valid_monsters and token.matches(matched_monster): - matches[matched_monster].name.add((token.value, match, '(name)')) - matches[matched_monster].score += score * mult - valid_monsters.add(matched_monster) - for matched_monster in self.index.fluff_tokens[match]: - if matched_monster not in valid_monsters and token.matches(matched_monster): - matches[matched_monster].name.add((token.value, match, '(fluff)')) - matches[matched_monster].score += score * mult / 2 - valid_monsters.add(matched_monster) + + async def do_matching(monsters: Set[MonsterModel], value: float, name: str) -> None: + for matched_monster in monsters: + if matched_monster not in valid_monsters and await token.matches(matched_monster): + matches[matched_monster].name.add(NameMatch(token.value, match, name)) + matches[matched_monster].score += value + valid_monsters.add(matched_monster) + + await do_matching(self.index.manual[match], (score + .001) * mult, '(manual)') + await do_matching(self.index.name_tokens[match], score * mult, '(name)') + await do_matching(self.index.fluff_tokens[match], score * mult / 2, '(fluff)') return valid_monsters - def _process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[MonsterModel], - matches: MatchMap) -> Set[MonsterModel]: + async def _process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[MonsterModel], + matches: MatchMap) -> Set[MonsterModel]: for mod_token in mod_tokens: potential_evos = {m for m in potential_evos if - self._monster_has_modifier(m, mod_token, matches) ^ mod_token.negated} + await self._monster_has_modifier(m, mod_token, matches) ^ mod_token.negated} if not potential_evos: return set() return potential_evos @@ -250,7 +267,7 @@ def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) matches[evo].score = matches[monster].score - .003 return monster_evos - + def get_most_eligable_monster(self, monsters: Iterable[MonsterModel], tokenized_query: List[str] = None, matches: MatchMap = None) -> MonsterModel: """Get the most eligable monster from a list of monsters and debug info from a query""" @@ -263,10 +280,17 @@ def get_most_eligable_monster(self, monsters: Iterable[MonsterModel], tokenized_ async def _find_monster_search(self, tokenized_query: List[str]) -> \ Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel]]: - mod_tokens, name_query_tokens = self._interpret_query(tokenized_query) + mod_tokens, name_query_tokens = await self._interpret_query(tokenized_query) name_query_tokens.difference_update({'|'}) + def merge_dicts(*dicts: Dict[str, Set]) -> DefaultDict[str, Set]: + o = defaultdict(set) + for d in dicts: + for k, vs in d.items(): + o[k].update(vs) + return o + for mod_token in mod_tokens: if mod_token.value not in self.index.all_modifiers: async with self.dbcog.config.typo_mods() as typo_mods: @@ -274,7 +298,7 @@ async def _find_monster_search(self, tokenized_query: List[str]) -> \ matches = defaultdict(MonsterMatch) if name_query_tokens: - matched_mons = self._process_name_tokens(name_query_tokens, matches) + matched_mons = await self._process_name_tokens(name_query_tokens, matches) if not matched_mons: # No monsters match the given name tokens return None, {}, set() @@ -285,7 +309,7 @@ async def _find_monster_search(self, tokenized_query: List[str]) -> \ monster_score = defaultdict(int) # Expand search to the evo tree - matched_mons = self._process_modifiers(mod_tokens, matched_mons, matches) + matched_mons = await self._process_modifiers(mod_tokens, matched_mons, matches) if not matched_mons: # no modifiers match any monster in the evo tree return None, {}, set() @@ -295,8 +319,7 @@ async def _find_monster_search(self, tokenized_query: List[str]) -> \ return mon, matches, matched_mons - async def find_monster_debug(self, query: str) -> \ - Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel], int]: + async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: """Get debug info from a search. This gives info that isn't necessary for non-debug functions. Consider using @@ -306,7 +329,7 @@ async def find_monster_debug(self, query: str) -> \ query = query.split('//')[0] # Remove comments query = rmdiacritics(query).lower().replace(",", "") query = re.sub(r'(\s|^)\'(\S+)\'(\s|$)', r'\1"\2"\3', query) # Replace ' with " around tokens - query = re.sub(r':=?r?([\'"])[^\1]+\1', lambda m: m.group(0).replace(' ', '\0'), query) # Keep modded spaces + query = re.sub(r':=?r?("[^"]+"|\'[^\']+\')', lambda m: m.group(0).replace(' ', '\0'), query) # Keep some spaces tokenized_query = self._process_settings(query).split() tokenized_query = [token.replace('\0', ' ') for token in tokenized_query] mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) @@ -319,17 +342,19 @@ async def find_monster_debug(self, query: str) -> \ key=lambda t: t[1].get(t[0], MonsterMatch()).score ) - return best_monster, matches_dict, valid_monsters, 0 + return MonsterInfo(best_monster, matches_dict, valid_monsters), ExtraInfo(0) - async def find_monster(self, query: str) -> Optional[MonsterModel]: + async def find_monster(self, query: str) -> Tuple[Optional[MonsterModel], ExtraInfo]: """Get the best matching monster for a query. Returns None if no eligable monsters exist.""" - matched_monster, _, _, _ = await self.find_monster_debug(query) - return matched_monster + m_info, e_info = await self.find_monster_debug(query) + return m_info.matched_monster, e_info - async def find_monsters(self, query: str) -> List[MonsterModel]: + async def find_monsters(self, query: str) -> Tuple[List[MonsterModel], ExtraInfo]: """Get a list of monsters sorted by how well they match""" - _, matches, monsters, _ = await self.find_monster_debug(query) - return sorted(monsters, key=lambda m: self.get_priority_tuple(m, matches=matches), reverse=True) + m_info, e_info = await self.find_monster_debug(query) + return sorted(m_info.valid_monsters, + key=lambda m: self.get_priority_tuple(m, matches=m_info.monster_matches), + reverse=True), e_info def calc_ratio_modifier(self, s1: Union[Token, str], s2: str, prefix_weight: float = .05) -> float: """Calculate the modifier distance between two tokens""" diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index fe6409b6..322b9d60 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -1,6 +1,10 @@ +from abc import ABC, abstractmethod from fnmatch import fnmatch +from typing import Iterable, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union import regex as re +from tsutils.helper_classes import DummyObject +from tsutils.tsubaki.monster_header import MonsterHeader from dbcog.models.enum_types import AwokenSkills from dbcog.models.monster_model import MonsterModel @@ -10,19 +14,34 @@ NUMERIC_MONSTER_ATTRIBUTE_NAMES, \ PLUS_AWOKENSKILL_MAP, STRING_MONSTER_ATTRIBUTE_ALIASES, STRING_MONSTER_ATTRIBUTE_NAMES +if TYPE_CHECKING: + from dbcog import DBCog + +T = TypeVar("T") + + +class MatchData(NamedTuple): + subquery_result: Optional[MonsterModel] = None + + def __repr__(self): + ret = [] + if self.subquery_result is not None: + ret.append(f"[Subquery: {MonsterHeader.text_with_emoji(self.subquery_result)}]") + return " ".join(ret) + def regexlist(tokens): return '(?:' + '|'.join(re.escape(t) for t in tokens) + ")" class Token: - def __init__(self, value, *, negated=False, exact=False): + def __init__(self, value: str, *, negated: bool = False, exact: bool = False): self.value = self.full_value = value self.negated = negated self.exact = exact - def matches(self, monster: MonsterModel) -> bool: - return True + async def matches(self, monster: MonsterModel) -> Tuple[Union[bool, float], MatchData]: + return True, MatchData() def __eq__(self, other): if isinstance(other, Token): @@ -39,36 +58,39 @@ def __repr__(self): class SpecialToken(Token): - RE_MATCH = r"" + RE_MATCH: str - def __init__(self, value, *, negated=False, exact=False, database): - self.database = database + def __init__(self, value='', *, negated=False, exact=False, dbcog: "DBCog"): + self.dbcog = dbcog super().__init__(value, negated=negated, exact=exact) - def matches(self, monster: MonsterModel) -> bool: - return False + async def prepare(self: T) -> T: + return self + + async def matches(self, monster: MonsterModel): + return False, MatchData() class MultipleAwakeningToken(SpecialToken): RE_MATCH = rf"(\d+)-(sa-)?-?({regexlist(AWAKENING_TOKENS)})" - def __init__(self, fullvalue, *, negated=False, exact=False, database): + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): count, sa, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() self.minimum_count = int(count) self.allows_super_awakenings = bool(sa) - super().__init__(value, negated=negated, exact=exact, database=database) + super().__init__(value, negated=negated, exact=exact, dbcog=dbcog) self.full_value = fullvalue - def matches(self, monster): + async def matches(self, monster): monster_total_awakenings_matching_token = 0 for awakening in monster.awakenings: if awakening.is_super and not self.allows_super_awakenings: - return False + return False, MatchData() # Keep track of whether we matched this cycle for SA check at the end matched = True - for awoken_skill in (self.database.awoken_skill_map[aws.value] + for awoken_skill in (self.dbcog.database.awoken_skill_map[aws.value] for aws, tokens in AWOKEN_SKILL_MAP.items() if self.value in tokens): if (equivalence := PLUS_AWOKENSKILL_MAP.get(AwokenSkills(awakening.awoken_skill_id))) \ @@ -82,28 +104,28 @@ def matches(self, monster): matched = False if monster_total_awakenings_matching_token >= self.minimum_count: - return True + return True, MatchData() # If we already matched an SA and didn't return True, fail immediately. # We only allow one SA to count towards the total for each MultipleAwakeningToken if awakening.is_super and matched: - return False - return False + return False, MatchData() + return False, MatchData() class MonsterAttributeNumeric(SpecialToken): RE_MATCH = rf"({regexlist(NUMERIC_MONSTER_ATTRIBUTE_NAMES)}):([<>=]+)?(\d+)([kmb])?" - def __init__(self, fullvalue, *, negated=False, exact=False, database): + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): c_attr, ineq, value, mult = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() self.monster_class_attributes = {ats for ats, aliases in NUMERIC_MONSTER_ATTRIBUTE_ALIASES.items() if c_attr in aliases}.pop() self.operator = ineq or "=" self.rhs = int(value) * (1e9 if mult == 'b' else 1e6 if mult == 'm' else 1e3 if mult == 'k' else 1) - super().__init__('monster', negated=negated, exact=exact, database=database) + super().__init__(negated=negated, exact=exact, dbcog=dbcog) self.full_value = fullvalue - def matches(self, monster): + async def matches(self, monster): for class_attrs in self.monster_class_attributes: val: MonsterModel = monster for class_attr in class_attrs: @@ -112,27 +134,27 @@ def matches(self, monster): continue # Test each character of the equality operator because I don't want to use the eval function here if "<" in self.operator and val < self.rhs: - return True + return True, MatchData() if ">" in self.operator and val > self.rhs: - return True + return True, MatchData() if "=" in self.operator and val == self.rhs: - return True - return False + return True, MatchData() + return False, MatchData() class MonsterAttributeString(SpecialToken): RE_MATCH = rf"({regexlist(STRING_MONSTER_ATTRIBUTE_NAMES)}):([=rg]?)([\"']?)(.+)\3" - def __init__(self, fullvalue, *, negated=False, exact=False, database): + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): c_attr, match, _, string = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() self.monster_class_attributes = {ats for ats, aliases in STRING_MONSTER_ATTRIBUTE_ALIASES.items() if c_attr in aliases}.pop() self.match = match self.string = string - super().__init__('monster', negated=negated, exact=exact, database=database) + super().__init__(negated=negated, exact=exact, dbcog=dbcog) self.full_value = fullvalue - def matches(self, monster): + async def matches(self, monster): for class_attrs in self.monster_class_attributes: val: MonsterModel = monster for class_attr in class_attrs: @@ -141,28 +163,28 @@ def matches(self, monster): continue val: str = val.lower() if self.match == "=" and val == self.string: # Exact match - return True + return True, MatchData() elif self.match == "r" and bool(re.search(self.string, val)): # Regex match - return True + return True, MatchData() elif self.match == "g" and fnmatch(val, '*' + self.string + '*'): # Glob match - return True + return True, MatchData() elif self.string in val: - return True - return False + return True, MatchData() + return False, MatchData() class MonsterAttributeBool(SpecialToken): RE_MATCH = rf"({regexlist(BOOL_MONSTER_ATTRIBUTE_NAMES)}):(.+)" - def __init__(self, fullvalue, *, negated=False, exact=False, database): + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): c_attr, raw_bool_value = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() self.monster_class_attributes = {ats for ats, aliases in BOOL_MONSTER_ATTRIBUTE_ALIASES.items() if c_attr in aliases}.pop() self.bool_value = raw_bool_value not in ('0', 'false', 'no') - super().__init__('monster', negated=negated, exact=exact, database=database) + super().__init__(negated=negated, exact=exact, dbcog=dbcog) self.full_value = fullvalue - def matches(self, monster): + async def matches(self, monster): for class_attrs in self.monster_class_attributes: val: MonsterModel = monster for class_attr in class_attrs: @@ -170,13 +192,69 @@ def matches(self, monster): if val is None: continue if val == self.bool_value: - return True - return False + return True, MatchData() + return False, MatchData() + + +class SubqueryToken(ABC, SpecialToken): + def __init__(self, fullvalue, subquery, *, negated=False, exact=False, dbcog): + self.subquery = subquery + self.match_data = None + self.valid_monsters = None + self.max_score = None + + super().__init__(negated=negated, exact=exact, dbcog=dbcog) + self.full_value = fullvalue + + async def prepare(self): + m_info, _ = await self.dbcog.find_monster_debug(self.subquery) + self.match_data = m_info.monster_matches + self.valid_monsters = m_info.valid_monsters + self.max_score = max((self.match_data[mon].score for mon in self.valid_monsters), default=0) + return self + + @abstractmethod + def get_matching_monsters(self, monster: MonsterModel) -> Iterable[MonsterModel]: + ... + + async def matches(self, monster): + mats = self.valid_monsters.intersection(self.get_matching_monsters(monster)) + if not mats: + return False, MatchData() + matched, score = max(((mat, self.match_data.get(mat, DummyObject(score=0)).score) for mat in mats), + key=lambda x: x[1]) + return score / self.max_score, MatchData(subquery_result=matched) + + +class HasMaterial(SubqueryToken): + RE_MATCH = r"hasmat:([\"']?)(.+)\1" + + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): + _, subquery = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() + super().__init__(fullvalue, subquery, negated=negated, exact=exact, dbcog=dbcog) + + def get_matching_monsters(self, monster): + return self.dbcog.database.graph.evo_mats(monster) + + +class SeriesOf(SubqueryToken): + RE_MATCH = r"seriesof:([\"']?)(.+)\1" + + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): + _, subquery = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() + super().__init__(fullvalue, subquery, negated=negated, exact=exact, dbcog=dbcog) + + def get_matching_monsters(self, monster): + return self.dbcog.database.get_monsters_where(lambda m: m.series_id == monster.series_id, + server=monster.server_priority, + cache_key=('series_id', monster.series_id)) -SPECIAL_TOKEN_TYPES = [ +SPECIAL_TOKEN_TYPES: Set[Type[SpecialToken]] = { MultipleAwakeningToken, MonsterAttributeNumeric, MonsterAttributeString, MonsterAttributeBool, -] + HasMaterial, + SeriesOf, +} diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 36f80d01..e86d255d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1512,44 +1512,49 @@ async def idtraceback(self, ctx, *, query): dbcog = await self.get_dbcog() - bestmatch, matches, _, _ = await dbcog.mon_finder.find_monster_debug(query) + m_info, e_info = await dbcog.find_monster_debug(query) - if bestmatch is None: + if m_info.matched_monster is None: await ctx.send("No monster matched.") return if selected_monster_id is not None: - selected = {m for m in matches if m.monster_id == selected_monster_id} + selected = {m for m in m_info.valid_monsters if m.monster_id == selected_monster_id} if not selected: await ctx.send("The requested monster was not found as a result of the query.") return monster = selected.pop() else: - monster = bestmatch - - score = matches[monster].score - ntokens = matches[monster].name - mtokens = matches[monster].mod - lower_prio = {m for m in matches if matches[m].score == matches[monster].score}.difference({monster}) + monster = m_info.matched_monster + + score = m_info.monster_matches[monster].score + ntokens = m_info.monster_matches[monster].name + mtokens = m_info.monster_matches[monster].mod + lower_prio = {m for m in m_info.monster_matches + if m_info.monster_matches[m].score == m_info.monster_matches[monster].score + }.difference({monster}) if len(lower_prio) > 20: lpstr = f"{len(lower_prio)} other monsters." else: - lpstr = "\n".join(f"{get_attribute_emoji_by_monster(m)} {m.name_en} ({m.monster_id})" for m in lower_prio) + lpstr = "\n".join(f"{MonsterHeader.text_with_emoji(m)}" for m in lower_prio) - mtokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" - f" ({round(dbcog.mon_finder.calc_ratio_modifier(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" + mtokenstr = '\n'.join((f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] and t[1] else ''}" + + ('' if not t[1] else " (exact)" if t[0] == t[1] + else f" ({round(dbcog.mon_finder.calc_ratio_modifier(t[0], t[1]), 2)})") + + f" {t[2]}").strip() for t in sorted(mtokens)) ntokenstr = '\n'.join(f"{inline(t[0])}{(': ' + t[1]) if t[0] != t[1] else ''}" f" ({round(dbcog.mon_finder.calc_ratio_name(t[0], t[1]), 2) if t[0] != t[1] else 'exact'})" - f" {t[2]}" + f" {t[2]}".strip() for t in sorted(ntokens)) original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) props = IdTracebackViewProps(monster=monster, score=score, name_tokens=ntokenstr, modifier_tokens=mtokenstr, lower_priority_monsters=lpstr if lower_prio else "None") state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, - IdTracebackView.VIEW_TYPE, props) + query_settings, IdTracebackView.VIEW_TYPE, props) await menu.create(ctx, state) @commands.command() @@ -1588,7 +1593,8 @@ async def expcurve(self, ctx, start, end: Optional[int], *, query=''): original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) props = ExperienceCurveViewProps(monster=monster, low=start, high=end, offset=offset) state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, - ExperienceCurveView.VIEW_TYPE, props) + query_settings, ExperienceCurveView.VIEW_TYPE, props) await menu.create(ctx, state) diff --git a/padinfo/view/id_traceback.py b/padinfo/view/id_traceback.py index c6d5e354..33271b8c 100644 --- a/padinfo/view/id_traceback.py +++ b/padinfo/view/id_traceback.py @@ -13,7 +13,7 @@ class IdTracebackViewProps: - def __init__(self, monster: "MonsterModel", score: int, name_tokens: str, + def __init__(self, monster: "MonsterModel", score: float, name_tokens: str, modifier_tokens: str, lower_priority_monsters: str): self.lower_priority_monsters = lower_priority_monsters self.modifier_tokens = modifier_tokens @@ -22,19 +22,6 @@ def __init__(self, monster: "MonsterModel", score: int, name_tokens: str, self.monster = monster -def get_title(monster: "MonsterModel"): - return f"{get_attribute_emoji_by_monster(monster)} {monster.name_en} ({monster.monster_id})" - - -def get_description(score: int): - return Box( - LabeledText( - 'Total score', - str(round(score, 2)) - ) - ) - - class IdTracebackView: VIEW_TYPE = 'IdTraceback' @@ -44,7 +31,7 @@ def embed(state, props: IdTracebackViewProps): EmbedMain( color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(props.monster, use_emoji=True), - description=get_description(props.score) + description=Box(LabeledText('Total score', str(round(props.score, 2)))) ), embed_fields=[ EmbedField('Matched Name Tokens', Box(props.name_tokens)), From 0c2ba1c0367b12429b8d2572fe425969dbdc4d12 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 24 Mar 2022 13:00:11 -0700 Subject: [PATCH 1080/1220] Let attr tokens fall back to matching subattr (#1629) --- dbcog/find_monster.py | 7 +++++-- dbcog/find_monster_tokens.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index 7d41f4d8..b3ed824f 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -111,6 +111,9 @@ async def _monster_has_modifier(self, monster: MonsterModel, token: Token, match return False mult, data = await token.matches(monster) + if token.full_value in self.index.modifiers[monster] and mult <= 1: + mult, data = True, MatchData() + matched_token = token.full_value if mult == 0: return False @@ -229,8 +232,8 @@ async def _process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[M def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = None, matches: MatchMap = None) -> Tuple[Any, ...]: - """Get the priority tuple for a monster. - + """Get the priority tuple for a monster. + This is a comparable tuple that's used to sort how well a monster matches a query. """ if matches is None: diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index 322b9d60..d4b18cc5 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -6,7 +6,7 @@ from tsutils.helper_classes import DummyObject from tsutils.tsubaki.monster_header import MonsterHeader -from dbcog.models.enum_types import AwokenSkills +from dbcog.models.enum_types import Attribute, AwokenSkills from dbcog.models.monster_model import MonsterModel from dbcog.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, BOOL_MONSTER_ATTRIBUTE_ALIASES, \ BOOL_MONSTER_ATTRIBUTE_NAMES, \ @@ -22,11 +22,14 @@ class MatchData(NamedTuple): subquery_result: Optional[MonsterModel] = None + subattr_match: Optional[bool] = None def __repr__(self): ret = [] if self.subquery_result is not None: ret.append(f"[Subquery: {MonsterHeader.text_with_emoji(self.subquery_result)}]") + if self.subattr_match: + ret.append("[Subattr]") return " ".join(ret) @@ -250,6 +253,34 @@ def get_matching_monsters(self, monster): cache_key=('series_id', monster.series_id)) +class AttributeToken(SpecialToken): + RE_MATCH = r"[rbgldx]|red|fire|blue|water|green|wood|light|yellow|dark|purple|nil|none|null|white" + + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): + if fullvalue in ('r', 'red', 'fire'): + self.attr = Attribute.Fire + elif fullvalue in ('b', 'blue', 'water'): + self.attr = Attribute.Water + elif fullvalue in ('g', 'green', 'wood'): + self.attr = Attribute.Wood + elif fullvalue in ('l', 'light', 'yellow'): + self.attr = Attribute.Light + elif fullvalue in ('d', 'dark', 'purple'): + self.attr = Attribute.Dark + else: + self.attr = Attribute.Nil + + super().__init__(negated=negated, exact=exact, dbcog=dbcog) + self.full_value = fullvalue + + async def matches(self, monster): + if monster.attr1 == self.attr: + return True, MatchData() + if monster.attr2 == self.attr: + return .999, MatchData(subattr_match=True) + return False, MatchData() + + SPECIAL_TOKEN_TYPES: Set[Type[SpecialToken]] = { MultipleAwakeningToken, MonsterAttributeNumeric, @@ -257,4 +288,5 @@ def get_matching_monsters(self, monster): MonsterAttributeBool, HasMaterial, SeriesOf, + AttributeToken, } From 9ed4449f5904e64bb48912587da8e9c74d189255 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 24 Mar 2022 14:30:40 -0700 Subject: [PATCH 1081/1220] Add hazmat as a hasmat alias (#1630) --- dbcog/find_monster_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index d4b18cc5..18cda971 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -230,7 +230,7 @@ async def matches(self, monster): class HasMaterial(SubqueryToken): - RE_MATCH = r"hasmat:([\"']?)(.+)\1" + RE_MATCH = r"ha[sz]mat:([\"']?)(.+)\1" def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): _, subquery = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() From e68e09db906546e5630aafa5200e7bd6270809cf Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Tue, 29 Mar 2022 03:53:22 -0500 Subject: [PATCH 1082/1220] Add ^idset list contents (#1633) --- padinfo/padinfo.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index e86d255d..9eceb129 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1008,7 +1008,19 @@ async def idset(self, ctx): @idset.command(name="list") async def idset_list(self, ctx): """`[p]id` settings list""" - await ctx.send("This command is still in progress!") + fm_flags = await self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() + settings = (f"Here are your current `{ctx.prefix}id` preference settings:\n" + f"\tcardlevel: {'110' if 'cardlevel' not in fm_flags.keys() else CardLevelModifier(fm_flags['cardlevel']).name[2:]}\n" + f"\tcardmode: {'Solo' if 'cardmode' not in fm_flags.keys() else CardModeModifier(fm_flags['cardmode']).name.title()}\n" + f"\tcardplus: {'297' if 'cardplus' not in fm_flags.keys() else CardPlusModifier(fm_flags['cardplus']).name[4:]}\n" + f"\tevogrouping: {'Grouped' if 'evogrouping' not in fm_flags.keys() or fm_flags['evogrouping']==1 else 'Split'}\n" + f"\tevosort: {'Numerical' if 'evosort' not in fm_flags.keys() else AltEvoSort(fm_flags['evosort']).name.title()}\n" + f"\tlinktarget: {'PADIndex' if 'linktarget' not in fm_flags.keys() or fm_flags['linktarget']==0 else MonsterLinkTarget(fm_flags['linktarget']).name.title()}\n" + f"\tlsmultiplier: {'Double' if 'lsmultiplier' not in fm_flags.keys() else LsMultiplier(fm_flags['lsmultiplier']).name[2:].title()}\n" + f"\tnaprio: {'On' if 'na_prio' not in fm_flags.keys() or fm_flags['na_prio']==1 else 'Off'}\n" + f"\tserver: {'Default' if 'server' not in fm_flags.keys() or fm_flags['server']=='COMBINED' else fm_flags['server']}\n" + f"\t(Donor Only) embedcolor: {'Default' if 'embedcolor' not in fm_flags.keys() else fm_flags['embedcolor'].title()}") + await ctx.send(settings) @idset.command() async def survey(self, ctx, value): From 4cfb08f74b855296ea9ef80c90a03cba35c08f2f Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 31 Mar 2022 03:12:59 -0500 Subject: [PATCH 1083/1220] Fix order of cooldown text for transforms (#1634) --- padinfo/view/components/base_id_main_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index f3108cd9..d8eb5c76 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -94,7 +94,7 @@ def active_skill_header(cls, m: "MonsterModel", previous_transforms: List["Monst # we can assume skill is not None because the monster transforms cooldown_text = '({}cd)'.format(str(skill.cooldown_turns_max)) if skill.cooldown_turns_min != skill.cooldown_turns_max: - cooldown_text = '{} -> {}'.format(skill.cooldown_turns_min, skill.cooldown_turns_max) + cooldown_text = '{} -> {}'.format(skill.cooldown_turns_max, skill.cooldown_turns_min) skill_texts.append( '{}{}'.format(get_emoji(cls.transform_emoji_names[i % len(cls.transform_emoji_names)]), cooldown_text)) From edecb024d929f1232493ff5ea9509971c4858169 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 31 Mar 2022 04:39:25 -0500 Subject: [PATCH 1084/1220] Say whether a monster has a(n) (un)tradable evo gem (#1635) --- monidlistener/monidlistener.py | 78 ++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 96170003..f5939ede 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -1,10 +1,16 @@ import re from io import BytesIO +from typing import TYPE_CHECKING from redbot.core import Config, checks, commands from tsutils.tsubaki.monster_header import MonsterHeader +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + from dbcog.monster_graph import MonsterGraph + + class MonIdListener(commands.Cog): """Monster Name from ID""" @@ -32,33 +38,51 @@ async def on_message(self, message): channel = message.channel content = message.content content = re.sub(r'297', '', content) - if await self.config.channel(channel).enabled(): - if message.guild is None: # dms - return - if message.author == self.bot.user: # dont reply to self - return - if await self.is_command(message): # skip commands - return - dbcog = self.bot.get_cog("DBCog") - if dbcog is None: - await channel.send("Error: DBCog Cog not loaded. Please alert a bot owner.") - return - if re.search(r'\b\d{3}[ -,]{0,2}\d{3}[ -,]{0,2}\d{3}\b', content): # friend code - return - if "+" in content or "plus" in content: - return - if re.search(r'\b\d{3,4}\b', content): - matches = re.findall(r'\b\d{3,4}\b', content) - ret = "" - for i in matches: - if i == "100": # skip when people say "is 100 mp or over" ~~and ryan's posts~~ - continue - m = await dbcog.find_monster(i, message.author.id) - if not m: # monster not found - continue - ret += "{}{}\n".format(MonsterHeader.text_with_emoji(m), " (untradable)" if m.sell_mp >= 100 else "") - if ret != "": - await channel.send(ret) + if not await self.config.channel(channel).enabled(): + return + if message.guild is None: # dms + return + if message.author == self.bot.user: # dont reply to self + return + if await self.is_command(message): # skip commands + return + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: + await channel.send("Error: DBCog Cog not loaded. Please alert a bot owner.") + return + if re.search(r'\b\d{3}[ -,]{0,2}\d{3}[ -,]{0,2}\d{3}\b', content): # friend code + return + if "+" in content or "plus" in content: + return + if re.search(r'\b\d{3,4}\b', content): + matches = re.findall(r'\b\d{3,4}\b', content) + ret = "" + for i in matches: + if i == "100": # skip when people say "is 100 mp or over" ~~and ryan's posts~~ + continue + m: "MonsterModel" = dbcog.get_monster(int(i)) + if m is None: # monster not found + continue + dbcog.database.graph: "MonsterGraph" + if m.sell_mp < 100: + tradeable_text = '' + else: + evo_gem_text = '' + if m.evo_gem_id is not None: + evo_gem: "MonsterModel" = dbcog.get_monster(m.evo_gem_id) + if evo_gem.sell_mp < 100: + evo_gem_text = ', but it has a tradable evo gem!' + else: + evo_gem_text = ', and its evo gem is also not tradable' + + tradeable_text = " (untradable{})".format(evo_gem_text) + + ret = "{}{}\n".format( + MonsterHeader.text_with_emoji(m), + tradeable_text + ) + if ret != "": + await channel.send(ret) @commands.group(aliases=['monidlistener']) async def midlistener(self, ctx): From ed3b347aab0431da52117b1591f196bb3a7738a1 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Thu, 31 Mar 2022 04:42:24 -0500 Subject: [PATCH 1085/1220] Readd ability to post multiple ids in the same message (#1636) --- monidlistener/monidlistener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index f5939ede..2ef27560 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -77,7 +77,7 @@ async def on_message(self, message): tradeable_text = " (untradable{})".format(evo_gem_text) - ret = "{}{}\n".format( + ret += "{}{}\n".format( MonsterHeader.text_with_emoji(m), tradeable_text ) From cfd72960e025cfccfeb92eebaf7b96d7dfa2a76f Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 31 Mar 2022 18:32:49 -0700 Subject: [PATCH 1086/1220] Don't cache monster ids (#1637) --- dbcog/database_context.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/dbcog/database_context.py b/dbcog/database_context.py index 44a2ee54..0f0c9677 100644 --- a/dbcog/database_context.py +++ b/dbcog/database_context.py @@ -56,21 +56,14 @@ def get_monsters_by_active(self, active_skill_id: int, *, server: Server) -> Lis def get_monsters_by_leader(self, leader_skill_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.leader_skill_id == leader_skill_id, server=server) - def get_all_monster_ids(self, server: Server) -> List[int]: + def get_all_monster_ids(self, server: Server) -> Iterable[int]: # We don't need to query if we're in debug mode. We already know exactly which monsters we're working with if self.debug_monster_ids is not None: return self.debug_monster_ids - if self.cached_monster_ids is not None: - return self.cached_monster_ids - - table = 'monsters_na' if server == Server.NA else 'monsters' - query = self.database.query_many( - self.database.select_builder(tables={table: ('monster_id',)}), - (), - as_generator=True) - self.cached_monster_ids = [m.monster_id for m in query] - return self.cached_monster_ids + suffix = '_na' if server == Server.NA else '' + query = self.database.query_many(f"SELECT monster_id FROM monsters{suffix}", as_generator=True) + return (m.monster_id for m in query) def get_all_monsters(self, server: Server = DEFAULT_SERVER) -> List[MonsterModel]: if server in self.cached_monsters: From b8fe6a7f57aada3da4dbdf152b5651cc4a9063d9 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Apr 2022 00:04:16 -0700 Subject: [PATCH 1087/1220] Remove every last trace of id1 for real this time i swear (#1638) --- dbcog/token_mappings.py | 4 -- padinfo/padinfo.py | 106 ++-------------------------------------- 2 files changed, 4 insertions(+), 106 deletions(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index d03da494..8a43dd37 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -292,10 +292,6 @@ class MiscModifiers(Enum): "sherias", } -ID1_SUPPORTED = {'hw', 'h', 'x', 'ny', 'gh', 'v', 'np', 'ma', 'a', 'r', 'rr', 'rg', 'rb', 'rl', 'rd', 'rx', 'b', 'br', - 'bg', 'bb', 'bl', 'bd', 'bx', 'g', 'gr', 'gg', 'gb', 'gl', 'gd', 'gx', 'l', 'lr', 'lg', 'lb', 'll', - 'ld', 'lx', 'd', 'dr', 'dg', 'db', 'dl', 'dd', 'dx', 'x', 'xr', 'xg', 'xb', 'xl', 'xd', 'xx'} - # This probably doesn't belong in here PlusAwakeningData = namedtuple("PlusAwakeningData", "awoken_skill value") PLUS_AWOKENSKILL_MAP = { diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9eceb129..639450f5 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -108,8 +108,8 @@ def __init__(self, bot, *args, **kwargs): self.remove_emoji = '\N{CROSS MARK}' self.config = Config.get_conf(self, identifier=9401770) - self.config.register_user(survey_mode=0, color=None, beta_id3=False, id_history=[]) - self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[], do_survey=False) + self.config.register_user(color=None, beta_id3=False, id_history=[]) + self.config.register_global(sometimes_perc=20, good=0, bad=0, bad_queries=[]) self.historic_lookups_file_path = _data_file('historic_lookups_id3.json') self.historic_lookups = safe_read_json(self.historic_lookups_file_path) @@ -209,38 +209,10 @@ async def id3(self, ctx, *, query: str): """Monster info (main tab)""" await self._do_id(ctx, query) - async def _get_monster(self, ctx, query: str) -> Optional["MonsterModel"]: + async def _get_monster(self, ctx, query) -> Optional["MonsterModel"]: dbcog = await self.get_dbcog() - raw_query = query - - goodquery = None - ID1_SUPPORTED = dbcog.token_maps.ID1_SUPPORTED - if query[0] in ID1_SUPPORTED and query[1:] in dbcog.indexes[Server.COMBINED].all_name_tokens: - goodquery = [query[0], query[1:]] - elif query[:2] in ID1_SUPPORTED and query[2:] in dbcog.indexes[Server.COMBINED].all_name_tokens: - goodquery = [query[:2], query[2:]] - - if goodquery: - bad = False - for monster in dbcog.indexes[Server.COMBINED].all_name_tokens[goodquery[1]]: - for modifier in dbcog.indexes[Server.COMBINED].modifiers[monster]: - if modifier == 'xm' and goodquery[0] == 'x': - goodquery[0] = 'xm' - if modifier == goodquery[0]: - bad = True - if bad and query not in dbcog.indexes[Server.COMBINED].all_name_tokens: - async def send_message(): - await asyncio.sleep(1) - await ctx.send(f"Uh oh, it looks like you tried a query that isn't supported anymore!" - f" Try using `{' '.join(goodquery)}` (with a space) instead! For more" - f" info about the new id changes, check out" - f" <{IDGUIDE}>!") - - asyncio.create_task(send_message()) - async with self.config.bad_queries() as bad_queries: - bad_queries.append((raw_query, ctx.author.id)) - monster = await dbcog.find_monster(raw_query, ctx.author.id) + monster = await dbcog.find_monster(query, ctx.author.id) if monster is None: await self.send_id_failure_message(ctx, query) @@ -262,9 +234,6 @@ async def _do_id(self, ctx, query: str): (" Make sure you use the **JP id number** when updating the Google doc!!!!!" if ctx.author.id in self.bot.get_cog("PadGlobal").settings.bot_settings['admins'] else "")) - if await self.config.do_survey(): - asyncio.create_task(self.send_survey_after(ctx, query, monster)) - alt_monsters = IdViewState.get_alt_monsters_and_evos(dbcog, monster) id_queried_props = await IdViewState.do_query(dbcog, monster) full_reaction_list = IdMenuPanes.emoji_names() @@ -280,58 +249,6 @@ async def _do_id(self, ctx, query: str): await self.log_id_result(ctx, monster.monster_id) self.save_historic_data(query, monster) - async def send_survey_after(self, ctx, query, result_monster): - dbcog = await self.get_dbcog() - survey_mode = await self.config.user(ctx.author).survey_mode() - survey_chance = (1, await self.config.sometimes_perc() / 100, 0)[survey_mode] - if random.random() < survey_chance: - mid1 = self.historic_lookups.get(query) - m1 = mid1 and dbcog.get_monster(mid1) - id1res = "Not Historic" if mid1 is None else f"{m1.name_en} ({m1.monster_id})" if mid1 > 0 else "None" - id3res = f"{result_monster.name_en} ({result_monster.monster_id})" if result_monster else "None" - params = urllib.parse.urlencode( - {'usp': 'pp_url', - 'entry.154088017': query, - 'entry.173096863': id3res, - 'entry.1016180044': id1res, - 'entry.1787446565': str(ctx.author)}) - url = "https://docs.google.com/forms/d/e/1FAIpQLSeA2EBYiZTOYfGLNtTHqYdL6gMZrfurFZonZ5dRQa3XPHP9yw/viewform?" + params - await asyncio.sleep(1) - userres = await get_user_confirmation(ctx, "Was this the monster you were looking for?", - yes_emoji=char_to_emoji('y'), no_emoji=char_to_emoji('n')) - if userres is True: - await self.config.good.set(await self.config.good() + 1) - elif userres is False: - await self.config.bad.set(await self.config.bad() + 1) - message = await ctx.send(f"Oh no! You can help the Tsubaki team give better results" - f" by filling out this survey!\nPRO TIP: Use `{ctx.prefix}idset" - f" survey` to adjust how often this shows.\n\n<{url}>") - await asyncio.sleep(15) - await message.delete() - - @commands.group() - async def idsurvey(self, ctx): - """Commands pertaining to the id survey""" - - @idsurvey.command() - async def dosurvey(self, ctx, do_survey: bool): - """Toggle the survey avalibility""" - await self.config.do_survey.set(do_survey) - await ctx.tick() - - @idsurvey.command() - async def sometimesperc(self, ctx, percent: int): - """Change what 'sometimes' means""" - await self.config.sometimes_perc.set(percent) - await ctx.tick() - - @idsurvey.command() - async def checkbadness(self, ctx): - """Check how good id is according to end users""" - good = await self.config.good() - bad = await self.config.bad() - await ctx.send(f"{bad}/{good + bad} ({int(round(bad / (good + bad) * 100)) if good or bad else 'NaN'}%)") - @staticmethod async def send_invalid_monster_message(ctx, query: str, monster: "MonsterModel", append_text: str): await ctx.send(invalid_monster_text(query, monster, append_text)) @@ -1022,21 +939,6 @@ async def idset_list(self, ctx): f"\t(Donor Only) embedcolor: {'Default' if 'embedcolor' not in fm_flags.keys() else fm_flags['embedcolor'].title()}") await ctx.send(settings) - @idset.command() - async def survey(self, ctx, value): - """How often you see the id survey - - [p]idset survey always (Always see survey after using id) - [p]idset survey sometimes (See survey some of the time after using id) - [p]idset survey never (Never see survey after using id D:)""" - vals = ['always', 'sometimes', 'never'] - if value in vals: - await self.config.user(ctx.author).survey_mode.set(vals.index(value)) - await ctx.tick() - else: - await send_cancellation_message( - ctx, "value must be `always`, `sometimes`, or `never`") - @is_donor() @idset.command() async def embedcolor(self, ctx, *, color: converters.EmbedColor): From c1dee891671862775d90a64b8de7fef6a3f82bec Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Apr 2022 18:16:34 -0700 Subject: [PATCH 1088/1220] Add OrToken (#1639) --- dbcog/dbcog.py | 2 +- dbcog/find_monster.py | 155 +++++++++----------------- dbcog/find_monster_tokens.py | 208 +++++++++++++++++++++++++++-------- dbcog/token_mappings.py | 2 +- 4 files changed, 216 insertions(+), 151 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 2f92586a..2b2e0cb1 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -59,7 +59,7 @@ def __init__(self, bot, *args, **kwargs): self.fir_lock = asyncio.Lock() self.fir3_lock = asyncio.Lock() - self.fm_flags_default = {'na_prio': True, 'server': DEFAULT_SERVER} + self.fm_flags_default = {'na_prio': True, 'server': DEFAULT_SERVER, 'ormod_prio': False} self.config = Config.get_conf(self, identifier=64667103) self.config.register_global(datafile='', indexlog=0, test_suite={}, fluff_suite=[], typo_mods=[], debug_mode=False, debug_mode_monsters=[3260]) diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index b3ed824f..ad25e6a3 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -2,12 +2,14 @@ from collections import defaultdict from typing import Any, DefaultDict, Dict, Iterable, List, Mapping, NamedTuple, Optional, Set, Tuple, Union -from Levenshtein import jaro_winkler from tsutils.enums import Server from tsutils.formatting import rmdiacritics from tsutils.query_settings.query_settings import QuerySettings -from dbcog.find_monster_tokens import MatchData, SPECIAL_TOKEN_TYPES, SpecialToken, Token +from dbcog.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, QueryToken, SpecialToken, \ + TOKEN_JW_DISTANCE, \ + TokenMatch, \ + calc_ratio_modifier, calc_ratio_name, string_to_token from dbcog.models.monster_model import MonsterModel SERIES_TYPE_PRIORITY = { @@ -21,23 +23,11 @@ } -class NameMatch(NamedTuple): - token: str - matched: str - match_type: str - - -class ModifierMatch(NamedTuple): - token: str - matched: str - match_data: MatchData - - class MonsterMatch: def __init__(self): self.score: float = 0 - self.name: Set[NameMatch] = set() - self.mod: Set[ModifierMatch] = set() + self.name: Set[TokenMatch] = set() + self.mod: Set[TokenMatch] = set() def __repr__(self): return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) @@ -57,9 +47,6 @@ class ExtraInfo(NamedTuple): class FindMonster: - MODIFIER_JW_DISTANCE = .95 - TOKEN_JW_DISTANCE = .8 - def __init__(self, dbcog, flags: Dict[str, Any]): self.dbcog = dbcog self.flags = flags @@ -88,7 +75,7 @@ def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: continue for c2, token2 in enumerate(mwt): if (tokens[c1 + c2] != token2 and len(token2) < 5) \ - or self.calc_ratio_modifier(tokens[c1 + c2], token2) < self.TOKEN_JW_DISTANCE: + or self.calc_ratio_modifier(tokens[c1 + c2], token2) < TOKEN_JW_DISTANCE: break else: skip = len(mwt) - 1 @@ -98,40 +85,16 @@ def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: result.append(token1) return result - async def _monster_has_modifier(self, monster: MonsterModel, token: Token, matches: MatchMap) -> bool: - if len(token.value) < 6 or isinstance(token, SpecialToken): - matched_token = '' if isinstance(token, SpecialToken) else token.value - ratio = 1 - if matched_token and matched_token not in self.index.modifiers[monster]: - return False - else: - matched_token = max(self.index.modifiers[monster], key=lambda m: self.calc_ratio_modifier(token, m)) - ratio = self.calc_ratio_modifier(matched_token, token.value) - if ratio <= self.MODIFIER_JW_DISTANCE: - return False - - mult, data = await token.matches(monster) - if token.full_value in self.index.modifiers[monster] and mult <= 1: - mult, data = True, MatchData() - matched_token = token.full_value - if mult == 0: + async def _monster_has_modifier(self, monster: MonsterModel, token: QueryToken, matches: MatchMap) -> bool: + score, match = await token.matches(monster, self.index) + if not score: return False - matches[monster].mod.add(ModifierMatch(token.full_value, matched_token, data)) - matches[monster].score += ratio * mult + matches[monster].score += score + matches[monster].mod.add(match) return True - async def _string_to_token(self, value: str) -> Token: - if (negated := value.startswith('-')): - value = value.lstrip('-') - if (exact := bool(re.fullmatch(r'".+"', value))): - value = value[1:-1] - for special in SPECIAL_TOKEN_TYPES: - if re.fullmatch(special.RE_MATCH, value): - return await special(value, negated=negated, exact=exact, dbcog=self.dbcog).prepare() - return Token(value, negated=negated, exact=exact) - - async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token], Set[Token]]: + async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[QueryToken], Set[QueryToken]]: modifiers = [] name = set() longmods = [p for p in self.index.all_modifiers if len(p) > 8] @@ -139,10 +102,10 @@ async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token] # Suffixes for i, value in enumerate(tokenized_query[::-1]): - token = await self._string_to_token(value) + token = await string_to_token(value, self.dbcog) - if any(self.calc_ratio_modifier(m, token.value.split('-')[0], .1) > self.MODIFIER_JW_DISTANCE - for m in self.index.suffixes) or not token.value: + if any(self.calc_ratio_modifier(m, token.value.split('-')[0], .1) > MODIFIER_JW_DISTANCE + for m in self.index.suffixes) or isinstance(token, SpecialToken): # TODO: Store this as a list of regexes. Don't split on '-' anymore. modifiers.append(token) else: @@ -152,10 +115,10 @@ async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token] # Prefixes for i, value in enumerate(tokenized_query): - token = await self._string_to_token(value) + token = await string_to_token(value, self.dbcog) - if token.value in self.index.all_modifiers or not token.value or ( - any(self.calc_ratio_modifier(token, m, .1) > self.MODIFIER_JW_DISTANCE for m in longmods) + if isinstance(token, SpecialToken) or token.value in self.index.all_modifiers \ + or (any(self.calc_ratio_modifier(token, m, .1) > MODIFIER_JW_DISTANCE for m in longmods) and token.value not in self.index.all_name_tokens and len(token.value) >= 8): lastmodpos = not token.negated @@ -168,17 +131,18 @@ async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[Token] # Name Tokens for value in tokenized_query: - name.add(await self._string_to_token(value)) + name.add(await string_to_token(value, self.dbcog)) # Allow modifiers to match as name tokens if they're alone (Fix for hel and cloud) if not name and modifiers and lastmodpos: - if self.index.manual[modifiers[-1].full_value]: + if self.index.manual[modifiers[-1].value]: name.add(modifiers[-1]) modifiers = modifiers[:-1] return set(modifiers), name - async def _process_name_tokens(self, name_query_tokens: Set[Token], matches: MatchMap) -> Optional[Set[MonsterModel]]: + async def _process_name_tokens(self, name_query_tokens: Set[QueryToken], matches: MatchMap) \ + -> Optional[Set[MonsterModel]]: matched_mons = None for name_token in name_query_tokens: @@ -197,31 +161,32 @@ async def _process_name_tokens(self, name_query_tokens: Set[Token], matches: Mat return matched_mons - async def _get_valid_monsters_from_name_token(self, token: Token, matches: MatchMap, + async def _get_valid_monsters_from_name_token(self, token: QueryToken, matches: MatchMap, mult: Union[int, float] = 1) -> Set[MonsterModel]: valid_monsters = set() all_monsters_name_tokens_scores = {nt: self.calc_ratio_name(token, nt) for nt in self.index.all_name_tokens} - matched_tokens = sorted((nt for nt, s in all_monsters_name_tokens_scores.items() if s > self.TOKEN_JW_DISTANCE), + matched_tokens = sorted((nt for nt, s in all_monsters_name_tokens_scores.items() if s > TOKEN_JW_DISTANCE), key=lambda nt: all_monsters_name_tokens_scores[nt], reverse=True) matched_tokens += [t for t in self.index.all_name_tokens if t.startswith(token.value)] for match in matched_tokens: score = all_monsters_name_tokens_scores[match] - async def do_matching(monsters: Set[MonsterModel], value: float, name: str) -> None: + async def do_matching(monsters: Set[MonsterModel], value: float, name_type: str) -> None: for matched_monster in monsters: - if matched_monster not in valid_monsters and await token.matches(matched_monster): - matches[matched_monster].name.add(NameMatch(token.value, match, name)) + if matched_monster not in valid_monsters and await token.matches(matched_monster, self.index): + matches[matched_monster].name.add( + TokenMatch(token.value, match, MatchData(name_type=name_type))) matches[matched_monster].score += value valid_monsters.add(matched_monster) - await do_matching(self.index.manual[match], (score + .001) * mult, '(manual)') - await do_matching(self.index.name_tokens[match], score * mult, '(name)') - await do_matching(self.index.fluff_tokens[match], score * mult / 2, '(fluff)') + await do_matching(self.index.manual[match], (score + EPSILON) * mult, 'manual') + await do_matching(self.index.name_tokens[match], score * mult, 'name') + await do_matching(self.index.fluff_tokens[match], score * mult / 2, 'fluff') return valid_monsters - async def _process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[MonsterModel], + async def _process_modifiers(self, mod_tokens: Set[QueryToken], potential_evos: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: for mod_token in mod_tokens: potential_evos = {m for m in potential_evos if @@ -230,8 +195,8 @@ async def _process_modifiers(self, mod_tokens: Set[Token], potential_evos: Set[M return set() return potential_evos - def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = None, - matches: MatchMap = None) -> Tuple[Any, ...]: + def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = None, matches: MatchMap = None) \ + -> Tuple[Any, ...]: """Get the priority tuple for a monster. This is a comparable tuple that's used to sort how well a monster matches a query. @@ -241,7 +206,12 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = if tokenized_query is None: tokenized_query = [] + or_prio_score = sum(match.match_data.or_index[0] + for match in matches[monster].mod + if match.match_data.or_index is not None) + return (matches[monster].score, + -or_prio_score if self.flags['ormod_prio'] else 0, not self.dbcog.database.graph.monster_is_evo_gem(monster), # Don't deprio evos with new modifier not monster.is_equip if not {m[0] for m in matches[monster].mod}.intersection( @@ -257,6 +227,7 @@ def get_priority_tuple(self, monster: MonsterModel, tokenized_query: List[str] = monster.on_na if self.flags['na_prio'] else True, not monster.is_equip, self.dbcog.database.graph.get_adjusted_rarity(monster), + -or_prio_score, monster.monster_no_na) def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) -> Set[MonsterModel]: @@ -267,7 +238,7 @@ def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) if matches[evo].score < matches[monster].score: matches[evo].name = {(t[0], t[1], f'(from evo {monster.monster_id})') for t in matches[monster].name} - matches[evo].score = matches[monster].score - .003 + matches[evo].score = matches[monster].score - EPSILON * 3 return monster_evos @@ -332,7 +303,10 @@ async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: query = query.split('//')[0] # Remove comments query = rmdiacritics(query).lower().replace(",", "") query = re.sub(r'(\s|^)\'(\S+)\'(\s|$)', r'\1"\2"\3', query) # Replace ' with " around tokens - query = re.sub(r':=?r?("[^"]+"|\'[^\']+\')', lambda m: m.group(0).replace(' ', '\0'), query) # Keep some spaces + # Keep some spaces + query = re.sub(r':=?r?("[^"]+"|\'[^\']+\')', lambda m: m.group(0).replace(' ', '\0'), query) + query = re.sub(r'\[[^[]+]', lambda m: m.group(0).replace(' ', '\0'), query) + tokenized_query = self._process_settings(query).split() tokenized_query = [token.replace('\0', ' ') for token in tokenized_query] mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) @@ -359,38 +333,9 @@ async def find_monsters(self, query: str) -> Tuple[List[MonsterModel], ExtraInfo key=lambda m: self.get_priority_tuple(m, matches=m_info.monster_matches), reverse=True), e_info - def calc_ratio_modifier(self, s1: Union[Token, str], s2: str, prefix_weight: float = .05) -> float: - """Calculate the modifier distance between two tokens""" - if isinstance(s1, Token): - if s1.exact: - return 1.0 if s1.value == s2 else 0.0 - s1 = s1.value + def calc_ratio_modifier(self, s1: Union[QueryToken, str], s2: str, prefix_weight: float = .05) -> float: + return calc_ratio_modifier(s1, s2, prefix_weight) - return jaro_winkler(s1, s2, prefix_weight) - - def calc_ratio_name(self, token: Union[Token, str], full_word: str, prefix_weight: float = .05) -> float: + def calc_ratio_name(self, token: Union[QueryToken, str], full_word: str, prefix_weight: float = .05) -> float: """Calculate the name distance between two tokens""" - string = token.value if isinstance(token, Token) else token - - mw = self.index.mwt_to_len[full_word] != 1 - jw = jaro_winkler(string, full_word, prefix_weight) - - if isinstance(token, Token) and token.exact and string != full_word: - return 0.0 - - if string.isdigit() and full_word.isdigit() and string != full_word: - return 0.0 - - if full_word == string: - score = 1.0 - elif len(string) >= 3 and full_word.startswith(string): - score = .995 - if mw and jw < score: - return score - else: - score = jw - - if mw: - score = score ** 10 * self.index.mwt_to_len[full_word] - - return score + return calc_ratio_name(token, full_word, prefix_weight, self.index) diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index 18cda971..5f8f6031 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -1,13 +1,15 @@ from abc import ABC, abstractmethod from fnmatch import fnmatch -from typing import Iterable, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union +from typing import Iterable, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union import regex as re +from Levenshtein import jaro_winkler from tsutils.helper_classes import DummyObject from tsutils.tsubaki.monster_header import MonsterHeader from dbcog.models.enum_types import Attribute, AwokenSkills from dbcog.models.monster_model import MonsterModel +from dbcog.monster_index import MonsterIndex from dbcog.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, BOOL_MONSTER_ATTRIBUTE_ALIASES, \ BOOL_MONSTER_ATTRIBUTE_NAMES, \ NUMERIC_MONSTER_ATTRIBUTE_ALIASES, \ @@ -18,37 +20,63 @@ from dbcog import DBCog T = TypeVar("T") +MODIFIER_JW_DISTANCE = .95 +TOKEN_JW_DISTANCE = .8 +EPSILON = .001 class MatchData(NamedTuple): + name_type: Optional[str] = None subquery_result: Optional[MonsterModel] = None subattr_match: Optional[bool] = None + or_index: Optional[Tuple[int, str]] = None def __repr__(self): ret = [] + if self.name_type is not None: + ret.append(f"({self.name_type})") if self.subquery_result is not None: ret.append(f"[Subquery: {MonsterHeader.text_with_emoji(self.subquery_result)}]") - if self.subattr_match: + if self.subattr_match is not None: ret.append("[Subattr]") + if self.or_index is not None: + ret.append(f"[Matched `{self.or_index[1]}`]") return " ".join(ret) + def __or__(self, other) -> "MatchData": + if other is None: + return self + kwargs = {} + for field in self._fields: + kwargs[field] = getattr(self, field) + if kwargs[field] is None: + kwargs[field] = getattr(other, field) + return MatchData(**kwargs) + + +class TokenMatch(NamedTuple): + token: str + matched: str + match_data: MatchData + def regexlist(tokens): return '(?:' + '|'.join(re.escape(t) for t in tokens) + ")" -class Token: +class QueryToken(ABC): def __init__(self, value: str, *, negated: bool = False, exact: bool = False): - self.value = self.full_value = value + self.value = value self.negated = negated self.exact = exact - async def matches(self, monster: MonsterModel) -> Tuple[Union[bool, float], MatchData]: - return True, MatchData() + @abstractmethod + async def matches(self, monster: MonsterModel, index: MonsterIndex) -> Tuple[float, Optional[TokenMatch]]: + ... def __eq__(self, other): - if isinstance(other, Token): - return self.__dict__ == other.__dict__ + if isinstance(other, QueryToken): + return self.value == self.value elif isinstance(other, str): return self.value == other @@ -56,35 +84,56 @@ def __hash__(self): return hash(self.value) def __repr__(self): - token = ("-" if self.negated else "") + (repr(self.full_value) if self.exact else self.full_value) + token = ("-" if self.negated else "") + (repr(self.value) if self.exact else self.value) return f"{self.__class__.__name__}<{token}>" -class SpecialToken(Token): +class RegularToken(QueryToken): + async def matches(self, monster, index): + if len(self.value) < 6: + matched_token = self.value + else: + matched_token = max(index.modifiers[monster], key=lambda m: calc_ratio_modifier(self, m)) + + ratio = calc_ratio_modifier(matched_token, self.value) + if matched_token not in index.modifiers[monster] or ratio <= MODIFIER_JW_DISTANCE: + return 0.0, None + + return ratio, TokenMatch(self.value, matched_token, MatchData()) + + +class SpecialToken(QueryToken): RE_MATCH: str - def __init__(self, value='', *, negated=False, exact=False, dbcog: "DBCog"): + def __init__(self, value, *, negated=False, exact=False, dbcog: "DBCog"): self.dbcog = dbcog super().__init__(value, negated=negated, exact=exact) - async def prepare(self: T) -> T: - return self + async def special_matches(self, monster: MonsterModel) -> Tuple[Union[bool, float], MatchData]: + return True, MatchData() - async def matches(self, monster: MonsterModel): - return False, MatchData() + async def matches(self, monster, index): + mult, data = await self.special_matches(monster) + if self.value in index.modifiers[monster] and mult <= 1: + # If the full value is actually a modifier + return 1.0, TokenMatch(self.value, self.value, MatchData()) + return mult, TokenMatch(self.value, '', data) + + async def prepare(self): + return self class MultipleAwakeningToken(SpecialToken): RE_MATCH = rf"(\d+)-(sa-)?-?({regexlist(AWAKENING_TOKENS)})" def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): - count, sa, value = re.fullmatch(self.RE_MATCH, fullvalue).groups() + count, sa, awo = re.fullmatch(self.RE_MATCH, fullvalue).groups() self.minimum_count = int(count) self.allows_super_awakenings = bool(sa) - super().__init__(value, negated=negated, exact=exact, dbcog=dbcog) - self.full_value = fullvalue + self.awo = awo + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) - async def matches(self, monster): + async def special_matches(self, monster): monster_total_awakenings_matching_token = 0 for awakening in monster.awakenings: if awakening.is_super and not self.allows_super_awakenings: @@ -92,10 +141,9 @@ async def matches(self, monster): # Keep track of whether we matched this cycle for SA check at the end matched = True - for awoken_skill in (self.dbcog.database.awoken_skill_map[aws.value] for aws, tokens in AWOKEN_SKILL_MAP.items() - if self.value in tokens): + if self.awo in tokens): if (equivalence := PLUS_AWOKENSKILL_MAP.get(AwokenSkills(awakening.awoken_skill_id))) \ and equivalence.awoken_skill.value == awoken_skill.awoken_skill_id: monster_total_awakenings_matching_token += equivalence.value @@ -125,10 +173,9 @@ def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): if c_attr in aliases}.pop() self.operator = ineq or "=" self.rhs = int(value) * (1e9 if mult == 'b' else 1e6 if mult == 'm' else 1e3 if mult == 'k' else 1) - super().__init__(negated=negated, exact=exact, dbcog=dbcog) - self.full_value = fullvalue + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) - async def matches(self, monster): + async def special_matches(self, monster): for class_attrs in self.monster_class_attributes: val: MonsterModel = monster for class_attr in class_attrs: @@ -154,10 +201,9 @@ def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): if c_attr in aliases}.pop() self.match = match self.string = string - super().__init__(negated=negated, exact=exact, dbcog=dbcog) - self.full_value = fullvalue + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) - async def matches(self, monster): + async def special_matches(self, monster): for class_attrs in self.monster_class_attributes: val: MonsterModel = monster for class_attr in class_attrs: @@ -184,10 +230,9 @@ def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): self.monster_class_attributes = {ats for ats, aliases in BOOL_MONSTER_ATTRIBUTE_ALIASES.items() if c_attr in aliases}.pop() self.bool_value = raw_bool_value not in ('0', 'false', 'no') - super().__init__(negated=negated, exact=exact, dbcog=dbcog) - self.full_value = fullvalue + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) - async def matches(self, monster): + async def special_matches(self, monster): for class_attrs in self.monster_class_attributes: val: MonsterModel = monster for class_attr in class_attrs: @@ -199,32 +244,30 @@ async def matches(self, monster): return False, MatchData() -class SubqueryToken(ABC, SpecialToken): +class SubqueryToken(SpecialToken): def __init__(self, fullvalue, subquery, *, negated=False, exact=False, dbcog): self.subquery = subquery - self.match_data = None + self.matches = None self.valid_monsters = None self.max_score = None - super().__init__(negated=negated, exact=exact, dbcog=dbcog) - self.full_value = fullvalue + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) async def prepare(self): m_info, _ = await self.dbcog.find_monster_debug(self.subquery) - self.match_data = m_info.monster_matches + self.matches = m_info.monster_matches self.valid_monsters = m_info.valid_monsters - self.max_score = max((self.match_data[mon].score for mon in self.valid_monsters), default=0) - return self + self.max_score = max((self.matches[mon].score for mon in self.valid_monsters), default=0) @abstractmethod def get_matching_monsters(self, monster: MonsterModel) -> Iterable[MonsterModel]: ... - async def matches(self, monster): - mats = self.valid_monsters.intersection(self.get_matching_monsters(monster)) - if not mats: + async def special_matches(self, monster): + mons = self.valid_monsters.intersection(self.get_matching_monsters(monster)) + if not mons: return False, MatchData() - matched, score = max(((mat, self.match_data.get(mat, DummyObject(score=0)).score) for mat in mats), + matched, score = max(((mon, self.matches.get(mon, DummyObject(score=0)).score) for mon in mons), key=lambda x: x[1]) return score / self.max_score, MatchData(subquery_result=matched) @@ -270,10 +313,9 @@ def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): else: self.attr = Attribute.Nil - super().__init__(negated=negated, exact=exact, dbcog=dbcog) - self.full_value = fullvalue + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) - async def matches(self, monster): + async def special_matches(self, monster): if monster.attr1 == self.attr: return True, MatchData() if monster.attr2 == self.attr: @@ -281,6 +323,83 @@ async def matches(self, monster): return False, MatchData() +class OrToken(SpecialToken): + RE_MATCH = r"\[.+ .+\]" + + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): + self._strings = re.sub(r'"[^"]+"|\'[^\']+\'', + lambda m: m.group(0).replace(' ', ''), + fullvalue[1:-1]).split() + + self.tokens: Optional[List[QueryToken]] = None + + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) + + async def prepare(self): + self.tokens = [await string_to_token(s, self.dbcog) for s in self._strings] + + async def matches(self, monster, index): + best_score, best_data = 0.0, None + for idx, token in enumerate(self.tokens): + score, data = await token.matches(monster, index) + print(token, score, data, monster) + if score > best_score: + best_score = score + best_data = TokenMatch(self.value, '', data.match_data | MatchData(or_index=(idx, token.value))) + return best_score, best_data + + +def calc_ratio_modifier(s1: Union[QueryToken, str], s2: str, prefix_weight: float = .05) -> float: + """Calculate the modifier distance between two tokens""" + if isinstance(s1, QueryToken): + if s1.exact: + return 1.0 if s1.value == s2 else 0.0 + s1 = s1.value + + return jaro_winkler(s1, s2, prefix_weight) + + +def calc_ratio_name(token: Union[QueryToken, str], full_word: str, prefix_weight: float, index: MonsterIndex) -> float: + """Calculate the name distance between two tokens""" + string = token.value if isinstance(token, QueryToken) else token + + mw = index.mwt_to_len[full_word] != 1 + jw = jaro_winkler(string, full_word, prefix_weight) + + if string != full_word: + if isinstance(token, QueryToken) and token.exact: + return 0.0 + if string.isdigit() and full_word.isdigit(): + return 0.0 + + if full_word == string: + score = 1.0 + elif len(string) >= 3 and full_word.startswith(string): + score = .995 + if mw and jw < score: + return score + else: + score = jw + + if mw: + score = score ** 10 * index.mwt_to_len[full_word] + + return score + + +async def string_to_token(string: str, dbcog: "DBCog") -> QueryToken: + if (negated := string.startswith('-')): + string = string.lstrip('-') + if (exact := bool(re.fullmatch(r'".+"', string))): + string = string[1:-1] + for special in SPECIAL_TOKEN_TYPES: + if re.fullmatch(special.RE_MATCH, string): + token = special(string, negated=negated, exact=exact, dbcog=dbcog) + await token.prepare() + return token + return RegularToken(string, negated=negated, exact=exact) + + SPECIAL_TOKEN_TYPES: Set[Type[SpecialToken]] = { MultipleAwakeningToken, MonsterAttributeNumeric, @@ -289,4 +408,5 @@ async def matches(self, monster): HasMaterial, SeriesOf, AttributeToken, + OrToken, } diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 8a43dd37..8e1d2dbc 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -131,7 +131,7 @@ class EvoTypes(Enum): AwokenSkills.FODDERKILLER: ('enhancekiller', 'enhk', 'a2killer'), AwokenSkills.REDEEMKILLER: ('vendorkiller', 'vendork', 'a2killer'), AwokenSkills.ENHCOMBO7C: ('7c',), - AwokenSkills.GUARDBREAK: ('gb', 'guardbreak'), + AwokenSkills.GUARDBREAK: ('guardbreak',), AwokenSkills.FUA: ('fua',), AwokenSkills.ENHTEAMHP: ('teamhp', 'thp'), AwokenSkills.ENHTEAMRCV: ('teamrcv', 'trcv'), From f3a246b434f43f9feb27634ce025b7cec420e4c8 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 2 Apr 2022 20:34:08 -0500 Subject: [PATCH 1089/1220] add gbr for guardbreak (#1640) --- dbcog/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 8e1d2dbc..74bceaa7 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -131,7 +131,7 @@ class EvoTypes(Enum): AwokenSkills.FODDERKILLER: ('enhancekiller', 'enhk', 'a2killer'), AwokenSkills.REDEEMKILLER: ('vendorkiller', 'vendork', 'a2killer'), AwokenSkills.ENHCOMBO7C: ('7c',), - AwokenSkills.GUARDBREAK: ('guardbreak',), + AwokenSkills.GUARDBREAK: ('guardbreak', 'gbr'), AwokenSkills.FUA: ('fua',), AwokenSkills.ENHTEAMHP: ('teamhp', 'thp'), AwokenSkills.ENHTEAMRCV: ('teamrcv', 'trcv'), From d4b43e88a42f11742ea671b6c0f1debad9372677 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 2 Apr 2022 20:44:42 -0500 Subject: [PATCH 1090/1220] Add last query settings (#1641) --- padinfo/view/awakening_help.py | 3 +-- padinfo/view/experience_curve.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index 143aeec4..d74b659d 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -76,8 +76,7 @@ def embed(state, props: AwakeningHelpViewProps): ), embed_author=EmbedAuthor( MonsterHeader.menu_title(monster).to_markdown(), - # TODO: add query_settings - MonsterLink.header_link(monster), + MonsterLink.header_link(monster, query_settings=state.query_settings), MonsterImage.icon(monster.monster_id) ), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/experience_curve.py b/padinfo/view/experience_curve.py index 1d3216ce..d26b3155 100644 --- a/padinfo/view/experience_curve.py +++ b/padinfo/view/experience_curve.py @@ -118,8 +118,7 @@ def embed(state, props: ExperienceCurveViewProps): EmbedMain( color=state.query_settings.embedcolor, title=MonsterHeader.menu_title(props.monster, use_emoji=True), - # TODO: add query_settings - url=MonsterLink.header_link(props.monster), + url=MonsterLink.header_link(props.monster, query_settings=state.query_settings), description=Text(f'lv{props.low} -> lv{props.high} (' + (trunc_humanize(props.monster.exp_curve) if props.monster.exp_curve else "no") + f' curve)'), From cb089a299527866b574d0f471edd4f22dbe8773d Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 2 Apr 2022 22:32:30 -0500 Subject: [PATCH 1091/1220] Awakening documentation (#1642) --- dbcog/dbcog.py | 5 ++++- dbcog/token_mappings.py | 2 ++ padinfo/padinfo.py | 7 ++++--- padinfo/view/awakening_help.py | 35 +++++++++++++++++++++------------- padinfo/view/awakening_list.py | 21 ++++++++++++++++---- padinfo/view/common.py | 14 ++++++++++++-- 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 2b2e0cb1..65415b7b 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -33,7 +33,7 @@ from .models.monster_model import MonsterModel from .models.monster_stats import MonsterStatModifierInput, monster_stats from .monster_index import MonsterIndex -from .token_mappings import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES +from .token_mappings import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES, AWOKEN_SKILL_MAP logger = logging.getLogger('red.padbot-cogs.dbcog') @@ -73,6 +73,9 @@ def __init__(self, bot, *args, **kwargs): self.DEFAULT_SERVER = DEFAULT_SERVER self.SERVERS = SERVERS + # expose for documentation command + self.AWOKEN_SKILL_TOKEN_MAP = AWOKEN_SKILL_MAP + self.mon_finder = None # type: Optional[FindMonster] gadmin: Any = self.bot.get_cog("GlobalAdmin") diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 74bceaa7..b046eddd 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -87,6 +87,8 @@ class EvoTypes(Enum): EvoTypes.NONPIXEL: ('nonpixel', 'np'), } +# awakening view state & awakening help props in padinfo are transforming this dict to `int: tuple` +# so rewrite those two if this gets refactored at some point in the future! AWOKEN_SKILL_MAP = { AwokenSkills.ENHANCEDHP: ('hp+', 'hp'), AwokenSkills.ENHANCEDATK: ('atk+', 'atk'), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 639450f5..d0f09628 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -844,12 +844,13 @@ async def awakenings(self, ctx, *, query=None): dbcog = await self.get_dbcog() query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query or '') - if not query: + # TODO: Fix this absolutely awful way of finding if the query is empty but has a QS + if not query or query.startswith('--'): sort_type = AwakeningListSortTypes.numerical paginated_skills = await AwakeningListViewState.do_query(dbcog, sort_type) menu = AwakeningListMenu.menu() state = AwakeningListViewState(ctx.message.author.id, AwakeningListMenu.MENU_TYPE, query_settings, - sort_type, paginated_skills, 0, + sort_type, paginated_skills, 0, dbcog.AWOKEN_SKILL_TOKEN_MAP, reaction_list=AwakeningListMenuPanes.get_user_reaction_list(sort_type)) await menu.create(ctx, state) return @@ -862,7 +863,7 @@ async def awakenings(self, ctx, *, query=None): original_author_id = ctx.message.author.id menu = ClosableEmbedMenu.menu() - props = AwakeningHelpViewProps(monster=monster) + props = AwakeningHelpViewProps(monster=monster, token_map=dbcog.AWOKEN_SKILL_TOKEN_MAP) state = ClosableEmbedViewState(original_author_id, ClosableEmbedMenu.MENU_TYPE, query, query_settings, AwakeningHelpView.VIEW_TYPE, props) await menu.create(ctx, state) diff --git a/padinfo/view/awakening_help.py b/padinfo/view/awakening_help.py index d74b659d..3cbc7314 100644 --- a/padinfo/view/awakening_help.py +++ b/padinfo/view/awakening_help.py @@ -5,6 +5,7 @@ from discordmenu.embed.text import Text from discordmenu.embed.view import EmbedView from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.tsubaki.custom_emoji import get_awakening_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader @@ -19,11 +20,16 @@ class AwakeningHelpViewProps: - def __init__(self, monster: "MonsterModel"): + def __init__(self, monster: "MonsterModel", token_map: dict): self.monster = monster + # this is the list of allowed modifiers from token_mappings dbcog file (cannot import it bc redbot rules) + self.token_map = {} + for k in token_map.keys(): + self.token_map[k.value] = token_map[k] -def _get_short_desc(prev_index: int, awakening: "AwakeningModel"): + +def _get_repeated_desc(prev_index: int, awakening: "AwakeningModel"): emoji_text = get_awakening_emoji(awakening.awoken_skill_id, awakening.name) return Box( Text(emoji_text), @@ -32,41 +38,44 @@ def _get_short_desc(prev_index: int, awakening: "AwakeningModel"): ) -def _get_all_awakening_descs(awakening_list): +def _get_all_awakening_descs(awakening_list, show_help, token_map): appearances = {} awakening_descs = [] for index, awakening in enumerate(awakening_list): if awakening.name not in appearances: appearances[awakening.name] = index - awakening_descs.append(get_awoken_skill_description(awakening.awoken_skill)) + awakening_descs.append( + get_awoken_skill_description( + awakening.awoken_skill, show_help=show_help, token_map=token_map)) else: - awakening_descs.append(_get_short_desc(appearances[awakening.name], awakening)) + awakening_descs.append(_get_repeated_desc(appearances[awakening.name], awakening)) return Box(*awakening_descs) -def get_normal_awakenings(monster: "MonsterModel"): +def get_normal_awakenings(monster: "MonsterModel", show_help: bool, token_map: dict): normal_awakening_count = len(monster.awakenings) - monster.superawakening_count normal_awakenings = monster.awakenings[:normal_awakening_count] - return _get_all_awakening_descs(normal_awakenings) + return _get_all_awakening_descs(normal_awakenings, show_help, token_map) -def get_super_awakenings(monster: "MonsterModel"): +def get_super_awakenings(monster: "MonsterModel", show_help: bool, token_map: dict): normal_awakening_count = len(monster.awakenings) - monster.superawakening_count super_awakenings = monster.awakenings[normal_awakening_count:] - return _get_all_awakening_descs(super_awakenings) + return _get_all_awakening_descs(super_awakenings, show_help, token_map) class AwakeningHelpView: VIEW_TYPE = 'AwakeningHelp' @staticmethod - def embed(state, props: AwakeningHelpViewProps): + def embed(state: ClosableEmbedViewState, props: AwakeningHelpViewProps): monster = props.monster - + show_help = state.query_settings.showhelp.value + token_map = props.token_map fields = [ - EmbedField('Normal Awakenings', get_normal_awakenings(monster)), - EmbedField('Super Awakenings', get_super_awakenings(monster)) + EmbedField('Normal Awakenings', get_normal_awakenings(monster, show_help, token_map)), + EmbedField('Super Awakenings', get_super_awakenings(monster, show_help, token_map)) ] return EmbedView( diff --git a/padinfo/view/awakening_list.py b/padinfo/view/awakening_list.py index 9d86a614..0d5dc341 100644 --- a/padinfo/view/awakening_list.py +++ b/padinfo/view/awakening_list.py @@ -21,6 +21,7 @@ class AwakeningListViewState(ViewStateBase): def __init__(self, original_author_id, menu_type, query_settings: QuerySettings, sort_type, paginated_skills, current_page, + token_map: dict, extra_state=None, reaction_list: List[str] = None): super().__init__(original_author_id, menu_type, '', extra_state=extra_state, reaction_list=reaction_list) @@ -30,6 +31,11 @@ def __init__(self, original_author_id, menu_type, query_settings: QuerySettings, self.current_page = current_page self.query_settings = query_settings + # this is the list of allowed modifiers from token_mappings dbcog file (cannot import it bc redbot rules) + self.token_map = {} + for k in token_map.keys(): + self.token_map[k.value] = token_map[k] + def serialize(self): ret = super().serialize() ret.update({ @@ -41,16 +47,17 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): + async def deserialize(cls, dbcog, _user_config: UserConfig, ims: dict): sort_type = ims['sort_type'] paginated_skills = await cls.do_query(dbcog, sort_type) original_author_id = ims['original_author_id'] current_page = ims['current_page'] menu_type = ims['menu_type'] reaction_list = ims['reaction_list'] + token_map = dbcog.AWOKEN_SKILL_TOKEN_MAP query_settings = QuerySettings.deserialize(ims.get('query_settings')) return AwakeningListViewState(original_author_id, menu_type, query_settings, sort_type, paginated_skills, - current_page, reaction_list=reaction_list) + current_page, token_map, reaction_list=reaction_list) @classmethod async def do_query(cls, dbcog, sort_type): @@ -71,8 +78,14 @@ class AwakeningListView: @staticmethod def embed(state: AwakeningListViewState): fields = [ - EmbedField('Awakenings - by {}'.format('name' if state.sort_type == AwakeningListSortTypes.alphabetical else 'id number'), - Box(*[get_awoken_skill_description(awo) for awo in state.paginated_skills[state.current_page]])), + EmbedField( + 'Awakenings - by {}'.format( + 'name' if state.sort_type == AwakeningListSortTypes.alphabetical else 'id number'), + Box(*[get_awoken_skill_description( + awo, + show_help=state.query_settings.showhelp.value, + token_map=state.token_map) for awo in state.paginated_skills[state.current_page]] + )), EmbedField('Page', Box( '{} of {}'.format(str(state.current_page + 1), str(len(state.paginated_skills))) )) diff --git a/padinfo/view/common.py b/padinfo/view/common.py index 1745298f..f92470af 100644 --- a/padinfo/view/common.py +++ b/padinfo/view/common.py @@ -23,9 +23,19 @@ async def get_monster_from_ims(dbcog, ims: dict): return monster -def get_awoken_skill_description(awoken_skill: "AwokenSkillModel"): +def get_awoken_skill_description(awoken_skill: "AwokenSkillModel", show_help: bool = False, token_map: dict = None): + if token_map is None: + token_map = {} emoji_text = get_awakening_emoji(awoken_skill.awoken_skill_id, awoken_skill.name) - desc = awoken_skill.desc_en + if not show_help: + desc = awoken_skill.desc_en + else: + if awoken_skill.awoken_skill_id not in token_map: + desc = "No modifiers found. Please contact Tsubaki admins for support." + else: + tokens = token_map[awoken_skill.awoken_skill_id] + tokens: set + desc = ', '.join('`{}`'.format(token) for token in list(tokens)) return Box( Text(emoji_text), BoldText(awoken_skill.name_en), From 679ce3ca12022996642c1fb5f4f52454dbd5af77 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 2 Apr 2022 23:02:14 -0500 Subject: [PATCH 1092/1220] Move exportmodifiers to dbcog (#1644) --- dbcog/dbcog.py | 41 ++++++++++++++++++++++++++++++++++++++++- padinfo/padinfo.py | 45 ++------------------------------------------- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 65415b7b..d532fdfd 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -17,7 +17,9 @@ import discord from redbot.core import Config, checks, commands, data_manager -from redbot.core.utils.chat_formatting import box, pagify +from redbot.core.commands import Literal as LiteralConverter +from redbot.core.utils.chat_formatting import box, pagify, inline, text_to_file +from tabulate import tabulate from tsutils.cogs.globaladmin import auth_check from tsutils.enums import Server from tsutils.json_utils import async_cached_dadguide_request @@ -367,3 +369,40 @@ async def attr(self, ctx, attr, *, query): if k.endswith('_en'): return await ctx.send(v) return await ctx.send('\n'.join([f"**{k}**: {v}" for k, v in data.items()])) + + @commands.command() + async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] = "COMBINED"): + server = Server(server) + maps = self.token_maps + awakenings = {a.awoken_skill_id: a for a in self.database.get_all_awoken_skills()} + series = {s.series_id: s for s in self.database.get_all_series()} + + ret = ("Jump to:\n\n" + "* [Types](#types)\n" + "* [Evolutions](#evolutions)\n" + "* [Misc](#misc)\n" + "* [Awakenings](#awakenings)\n" + "* [Series](#series)\n" + "* [Attributes](#attributes)\n\n\n\n") + + etable = [(k.value, ", ".join(map(inline, v))) for k, v in maps.EVO_MAP.items()] + ret += "\n\n### Evolutions\n\n" + tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") + ttable = [(k.name, ", ".join(map(inline, v))) for k, v in maps.TYPE_MAP.items()] + ret += "\n\n### Types\n\n" + tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") + mtable = [(k.value, ", ".join(map(inline, v))) for k, v in maps.MISC_MAP.items()] + ret += "\n\n### Misc\n\n" + tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") + atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_SKILL_MAP.items()] + ret += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") + stable = [(series[k].name_en, ", ".join(map(inline, v))) + for k, v in self.indexes[server].series_id_to_pantheon_nickname.items()] + ret += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") + ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in maps.COLOR_MAP.items()] + ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in + maps.SUB_COLOR_MAP.items()] + for k, v in maps.DUAL_COLOR_MAP.items(): + k0name = k[0].name.replace("Nil", "None") + k1name = k[1].name.replace("Nil", "None") + ctable.append((k0name + "/" + k1name, ", ".join(map(inline, v)))) + ret += "### Attributes\n\n" + tabulate(ctable, headers=["Meaning", "Tokens"], tablefmt="github") + + await ctx.send(file=text_to_file(ret, filename="table.md")) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d0f09628..d54b851e 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -4,7 +4,6 @@ import os import random import re -import urllib.parse from io import BytesIO from typing import List, Optional, TYPE_CHECKING, Type @@ -12,9 +11,7 @@ from discord import Color from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import Config, checks, commands, data_manager -from redbot.core.commands import Literal as LiteralConverter -from redbot.core.utils.chat_formatting import bold, box, inline, pagify, text_to_file -from tabulate import tabulate +from redbot.core.utils.chat_formatting import bold, box, inline, pagify from tsutils.cogs.donations import is_donor from tsutils.emoji import char_to_emoji from tsutils.enums import Server @@ -31,7 +28,7 @@ from tsutils.tsubaki.custom_emoji import AWAKENING_ID_TO_EMOJI_NAME_MAP, get_attribute_emoji_by_enum, \ get_attribute_emoji_by_monster, get_awakening_emoji, get_type_emoji from tsutils.tsubaki.monster_header import MonsterHeader -from tsutils.user_interaction import get_user_confirmation, send_cancellation_message, send_confirmation_message +from tsutils.user_interaction import send_cancellation_message, send_confirmation_message from padinfo.core.button_info import button_info from padinfo.core.leader_skills import leaderskill_query @@ -1287,44 +1284,6 @@ async def idhelp(self, ctx, *, query=""): if query: await self.debugid(ctx, query=query) - @commands.command() - async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] = "COMBINED"): - server = Server(server) - DGCOG = await self.get_dbcog() - maps = DGCOG.token_maps - awakenings = {a.awoken_skill_id: a for a in DGCOG.database.get_all_awoken_skills()} - series = {s.series_id: s for s in DGCOG.database.get_all_series()} - - ret = ("Jump to:\n\n" - "* [Types](#types)\n" - "* [Evolutions](#evolutions)\n" - "* [Misc](#misc)\n" - "* [Awakenings](#awakenings)\n" - "* [Series](#series)\n" - "* [Attributes](#attributes)\n\n\n\n") - - etable = [(k.value, ", ".join(map(inline, v))) for k, v in maps.EVO_MAP.items()] - ret += "\n\n### Evolutions\n\n" + tabulate(etable, headers=["Meaning", "Tokens"], tablefmt="github") - ttable = [(k.name, ", ".join(map(inline, v))) for k, v in maps.TYPE_MAP.items()] - ret += "\n\n### Types\n\n" + tabulate(ttable, headers=["Meaning", "Tokens"], tablefmt="github") - mtable = [(k.value, ", ".join(map(inline, v))) for k, v in maps.MISC_MAP.items()] - ret += "\n\n### Misc\n\n" + tabulate(mtable, headers=["Meaning", "Tokens"], tablefmt="github") - atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_SKILL_MAP.items()] - ret += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") - stable = [(series[k].name_en, ", ".join(map(inline, v))) - for k, v in DGCOG.indexes[server].series_id_to_pantheon_nickname.items()] - ret += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") - ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in maps.COLOR_MAP.items()] - ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in - maps.SUB_COLOR_MAP.items()] - for k, v in maps.DUAL_COLOR_MAP.items(): - k0name = k[0].name.replace("Nil", "None") - k1name = k[1].name.replace("Nil", "None") - ctable.append((k0name + "/" + k1name, ", ".join(map(inline, v)))) - ret += "### Attributes\n\n" + tabulate(ctable, headers=["Meaning", "Tokens"], tablefmt="github") - - await ctx.send(file=text_to_file(ret, filename="table.md")) - @commands.command(aliases=["idcheckmod", "lookupmod", "idlookupmod", "luid", "idlu"]) async def idmeaning(self, ctx, token, server: Optional[Server] = Server.COMBINED): """Get all the meanings of a token (bold signifies base of a tree)""" From 46a6eb01ef2c30b7560d88339bacb1bb61653c48 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 3 Apr 2022 00:13:46 -0500 Subject: [PATCH 1093/1220] Use _do_idsetting & add ormod_prio setting (#1645) --- dbcog/find_monster_tokens.py | 2 +- padinfo/padinfo.py | 175 ++++++++----------------- padinfo/view/monster_list/id_search.py | 2 +- 3 files changed, 55 insertions(+), 124 deletions(-) diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index 5f8f6031..0a8d30fa 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -342,7 +342,7 @@ async def matches(self, monster, index): best_score, best_data = 0.0, None for idx, token in enumerate(self.tokens): score, data = await token.matches(monster, index) - print(token, score, data, monster) + # print(token, score, data, monster) if score > best_score: best_score = score best_data = TokenMatch(self.value, '', data.match_data | MatchData(or_index=(idx, token.value))) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index d54b851e..4304ed00 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -4,6 +4,7 @@ import os import random import re +from enum import EnumMeta from io import BytesIO from typing import List, Optional, TYPE_CHECKING, Type @@ -924,18 +925,23 @@ async def idset(self, ctx): async def idset_list(self, ctx): """`[p]id` settings list""" fm_flags = await self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() - settings = (f"Here are your current `{ctx.prefix}id` preference settings:\n" - f"\tcardlevel: {'110' if 'cardlevel' not in fm_flags.keys() else CardLevelModifier(fm_flags['cardlevel']).name[2:]}\n" - f"\tcardmode: {'Solo' if 'cardmode' not in fm_flags.keys() else CardModeModifier(fm_flags['cardmode']).name.title()}\n" - f"\tcardplus: {'297' if 'cardplus' not in fm_flags.keys() else CardPlusModifier(fm_flags['cardplus']).name[4:]}\n" - f"\tevogrouping: {'Grouped' if 'evogrouping' not in fm_flags.keys() or fm_flags['evogrouping']==1 else 'Split'}\n" - f"\tevosort: {'Numerical' if 'evosort' not in fm_flags.keys() else AltEvoSort(fm_flags['evosort']).name.title()}\n" - f"\tlinktarget: {'PADIndex' if 'linktarget' not in fm_flags.keys() or fm_flags['linktarget']==0 else MonsterLinkTarget(fm_flags['linktarget']).name.title()}\n" - f"\tlsmultiplier: {'Double' if 'lsmultiplier' not in fm_flags.keys() else LsMultiplier(fm_flags['lsmultiplier']).name[2:].title()}\n" - f"\tnaprio: {'On' if 'na_prio' not in fm_flags.keys() or fm_flags['na_prio']==1 else 'Off'}\n" - f"\tserver: {'Default' if 'server' not in fm_flags.keys() or fm_flags['server']=='COMBINED' else fm_flags['server']}\n" - f"\t(Donor Only) embedcolor: {'Default' if 'embedcolor' not in fm_flags.keys() else fm_flags['embedcolor'].title()}") - await ctx.send(settings) + intro = f"Here are your current `{ctx.prefix}id` preference settings:\n" + user_settings = { + "cardlevel": '110' if 'cardlevel' not in fm_flags else CardLevelModifier(fm_flags['cardlevel']).name[2:], + "cardmode": 'Solo' if 'cardmode' not in fm_flags else CardModeModifier(fm_flags['cardmode']).name.title(), + "cardplus": '297' if 'cardplus' not in fm_flags else CardPlusModifier(fm_flags['cardplus']).name[4:], + "evogrouping": 'Grouped' if 'evogrouping' not in fm_flags or fm_flags['evogrouping'] == 1 else 'Split', + "evosort": 'Numerical' if 'evosort' not in fm_flags else AltEvoSort(fm_flags['evosort']).name.title(), + "linktarget": 'PADIndex' if 'linktarget' not in fm_flags or fm_flags[ + 'linktarget'] == 0 else MonsterLinkTarget(fm_flags['linktarget']).name.title(), + "lsmultiplier": 'Double' if 'lsmultiplier' not in fm_flags else LsMultiplier(fm_flags['lsmultiplier']).name[ + 2:].title(), + "naprio": 'On' if 'na_prio' not in fm_flags or fm_flags['na_prio'] == 1 else 'Off', + "ormod prio": 'On' if 'ormod_prio' not in fm_flags or fm_flags['ormod_prio'] is True else 'Off', + "server": 'Default' if 'server' not in fm_flags or fm_flags['server'] == 'COMBINED' else fm_flags['server'], + "(Donor Only) embedcolor": 'Default' if 'embedcolor' not in fm_flags else fm_flags['embedcolor'].title(), + } + await ctx.send(intro + '\n'.join(["\t{}: {}".format(k, v) for k, v in user_settings.items()])) @is_donor() @idset.command() @@ -963,6 +969,14 @@ async def naprio(self, ctx, value: bool): await send_confirmation_message( ctx, f"NA monster prioritization has been **{'en' if value else 'dis'}abled**.") + @idset.command() + async def ormodprio(self, ctx, value: bool): + """Whether `[p]id` will be order-sensitive with respect to your "or" clauses""" + async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: + fm_flags['ormod_prio'] = bool(value) + await send_confirmation_message( + ctx, f"Order in or clause prioritization has been **{'en' if value else 'dis'}abled**.") + @idset.command() async def server(self, ctx, server: str): """The server used for your `[p]id` queries @@ -991,19 +1005,9 @@ async def evosort(self, ctx, value: str): `[p]idset evosort numerical`: Show alt evos sorted by card ID number. `[p]idset evosort dfs`: Show alt evos in a depth-first-sort order, starting with the base of the tree. """ - async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: - value = value.lower() - if value == "dfs": - fm_flags['evosort'] = AltEvoSort.dfs.value - elif value == "numerical": - fm_flags['evosort'] = AltEvoSort.numerical.value - else: - await send_cancellation_message( - ctx, - f'Please input an allowed value, either `{AltEvoSort.dfs.name}` or `{AltEvoSort.numerical.name}`.') - return - await send_confirmation_message( - ctx, f"Your `{ctx.prefix}id` evo sort preference has been set to **{value}**.") + await self._do_idsetting(ctx, 'evosort', AltEvoSort, value, + 'dfs', 'dfs', + 'numerical', 'numerical', ) @idset.command() async def lsmultiplier(self, ctx, value: str): @@ -1012,24 +1016,9 @@ async def lsmultiplier(self, ctx, value: str): `[p]idset lsmultiplier double`: [Default] Show leader skill multipliers assuming the card is paired with itself. `[p]idset lsmultiplier single`: Show leader skill multipliers of just the single card. """ - async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: - value = value.lower() - if value == "double": - fm_flags['lsmultiplier'] = LsMultiplier.lsdouble.value - not_value = 'single' - not_value_flag = 'lssingle' - elif value == "single": - fm_flags['lsmultiplier'] = LsMultiplier.lssingle.value - not_value = 'double' - not_value_flag = 'lsdouble' - else: - await send_cancellation_message( - ctx, - f'Please input an allowed value, either `double` or `single`.') - return - await send_confirmation_message( - ctx, - f"Your default `{ctx.prefix}id` lsmultiplier preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await self._do_idsetting(ctx, 'lsmultiplier', LsMultiplier, value, + 'double', 'lsdouble', + 'single', 'lssingle', ) @idset.command() async def cardplus(self, ctx, value: str): @@ -1038,24 +1027,9 @@ async def cardplus(self, ctx, value: str): `[p]idset cardplus 297`: [Default] Show cards with +297 stats. `[p]idset cardplus 0`: Show cards with +0 stats. """ - async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: - value = value.lower() - if value == "297": - fm_flags['cardplus'] = CardPlusModifier.plus297.value - not_value = '0' - not_value_flag = 'plus0' - elif value == "0": - fm_flags['cardplus'] = CardPlusModifier.plus0.value - not_value = '297' - not_value_flag = 'plus297' - else: - await send_cancellation_message( - ctx, - f'Please input an allowed value, either `297` or `0`.') - return - await send_confirmation_message( - ctx, - f"Your default `{ctx.prefix}id` cardplus preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await self._do_idsetting(ctx, 'cardplus', CardPlusModifier, value, + '297', 'plus297', + '0', 'plus0', ) @idset.command() async def cardmode(self, ctx, value: str): @@ -1064,24 +1038,9 @@ async def cardmode(self, ctx, value: str): `[p]idset mode solo`: [Default] Show cards with stats in solo `[p]idset mode coop`: Show cards with stats in coop (i.e. multiboost) """ - async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: - value = value.lower() - if value == "solo": - fm_flags['cardmode'] = CardModeModifier.solo.value - not_value = 'coop' - not_value_flag = 'coop' - elif value == "coop": - fm_flags['cardmode'] = CardModeModifier.coop.value - not_value = 'solo' - not_value_flag = 'solo' - else: - await send_cancellation_message( - ctx, - f'Please input an allowed value, either `coop` or `solo`.') - return - await send_confirmation_message( - ctx, - f"Your default `{ctx.prefix}id` cardmode preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await self._do_idsetting(ctx, 'cardmode', CardModeModifier, value, + 'solo', 'solo', + 'coop', 'coop', ) @idset.command() async def cardlevel(self, ctx, value: str): @@ -1090,24 +1049,9 @@ async def cardlevel(self, ctx, value: str): `[p]idset cardlevel 110`: [Default] Show LB stats at 110. `[p]idset cardlevel 120`: Show LB stats at 120. """ - async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: - value = value.lower() - if value == "120": - fm_flags['cardlevel'] = CardLevelModifier.lv120.value - not_value = '110' - not_value_flag = 'lv110' - elif value == "110": - fm_flags['cardlevel'] = CardLevelModifier.lv110.value - not_value = '120' - not_value_flag = 'lv120' - else: - await send_cancellation_message( - ctx, - f'Please input an allowed value, either `110` or `120`.') - return - await send_confirmation_message( - ctx, - f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await self._do_idsetting(ctx, 'cardlevel', CardLevelModifier, value, + '120', 'lv120', + '110', 'lv110', ) @idset.command() async def evogrouping(self, ctx, value: str): @@ -1116,24 +1060,9 @@ async def evogrouping(self, ctx, value: str): `[p]idset evogrouping grouped`: [Default] Show trees grouped. `[p]idset evogrouping split`: Show individual evos of trees split apart. """ - async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: - value = value.lower() - if value == "split": - fm_flags['evogrouping'] = EvoGrouping.splitevos.value - not_value = 'grouped' - not_value_flag = 'groupevos' - elif value == "grouped": - fm_flags['evogrouping'] = EvoGrouping.groupevos.value - not_value = 'split' - not_value_flag = 'splitevos' - else: - await send_cancellation_message( - ctx, - f'Please input an allowed value, either `grouped` or `split`.') - return - await send_confirmation_message( - ctx, - f"Your default `{ctx.prefix}id` cardlevel preference has been set to **{value}**. You can temporarily access `{not_value}` with the flag `--{not_value_flag}` in your queries.") + await self._do_idsetting(ctx, 'evogrouping', EvoGrouping, value, + 'split', 'splitevos', + 'grouped', 'groupevos') @idset.command() async def linktarget(self, ctx, value: str): @@ -1142,19 +1071,21 @@ async def linktarget(self, ctx, value: str): `[p]idset linktarget padindex`: [Default] Link to PADIndex always. `[p]idset linktarget ilmina`: Link to Ilmina for cards that are in NA. """ - setting_name = 'linktarget' - value1 = 'padindex' - value1_flag = 'padindex' - value2 = 'ilmina' - value2_flag = 'ilmina' + await self._do_idsetting(ctx, 'linktarget', MonsterLinkTarget, value, + 'padindex', 'padindex', + 'ilmina', 'ilmina') + + async def _do_idsetting(self, ctx, setting_name, enum_type: EnumMeta, value, + value1, value1_flag, + value2, value2_flag): async with self.bot.get_cog("DBCog").config.user(ctx.author).fm_flags() as fm_flags: value = value.lower() if value == value1: - fm_flags[setting_name] = MonsterLinkTarget.padindex.value + fm_flags[setting_name] = enum_type[value1_flag].value not_value = value2 not_value_flag = value2_flag elif value == value2: - fm_flags[setting_name] = MonsterLinkTarget.ilmina.value + fm_flags[setting_name] = enum_type[value2_flag].value not_value = value1 not_value_flag = value1_flag else: diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py index c27e3c43..400a7005 100644 --- a/padinfo/view/monster_list/id_search.py +++ b/padinfo/view/monster_list/id_search.py @@ -20,7 +20,7 @@ async def do_query(cls, dbcog, query, original_author_id, query_settings: QueryS if not found_monsters: return None - print(query_settings.serialize()) + # print(query_settings.serialize()) if query_settings.evogrouping == EvoGrouping.splitevos: return found_monsters used = set() From de54f0958a7cabe021307514d14c86c09c190d78 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 9 Apr 2022 23:18:51 -0500 Subject: [PATCH 1094/1220] use any nickname for crud cog awo skill editing (#1650) --- crud/crud.py | 31 +++++++++++++++++++++++-------- dbcog/dbcog.py | 4 +++- dbcog/token_mappings.py | 5 +++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 5451eea6..96f0ebe3 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -4,6 +4,7 @@ import re from datetime import datetime from io import BytesIO +from typing import TYPE_CHECKING import aiofiles import aiomysql @@ -14,6 +15,9 @@ from tsutils.cogs.globaladmin import auth_check from tsutils.user_interaction import get_user_confirmation, send_cancellation_message +if TYPE_CHECKING: + from dbcog import DBCog + logger = logging.getLogger('red.padbot-cogs.crud') SERIES_KEYS = { @@ -82,6 +86,13 @@ async def get_cursor(self): async with aiofiles.open(await self.config.config_file(), 'r') as db_config: return (await self.connect(json.loads(await db_config.read()))).cursor() + async def get_dbcog(self) -> "DBCog": + dbcog = self.bot.get_cog("DBCog") + if dbcog is None: + raise ValueError("DBCog cog is not loaded") + await dbcog.wait_until_ready() + return dbcog + def connect(self, db_config): return aiomysql.connect(host=db_config['host'], user=db_config['user'], @@ -361,12 +372,18 @@ async def awokenskill(self, ctx): @awokenskill.command(name="search") async def awokenskill_search(self, ctx, *, search_text): """Search for a awoken skill via its jp or na name""" - search_text = '%{}%'.format(search_text).lower() + dbcog = await self.get_dbcog() + if search_text in dbcog.KNOWN_AWOKEN_SKILL_TOKENS: + awoken_skill_id = dbcog.KNOWN_AWOKEN_SKILL_TOKENS[search_text].value + where = f'awoken_skill_id = {awoken_skill_id}' + else: + search_text = '%{}%'.format(search_text).lower() + where = f"lower(name_en) LIKE {search_text} OR lower(name_ja) LIKE {search_text}" sql = ('SELECT awoken_skill_id, name_en, name_ja, name_ko, desc_en,' ' desc_ja, desc_ko FROM awoken_skills' - ' WHERE lower(name_en) LIKE %s OR lower(name_ja) LIKE %s' + ' WHERE %s' ' ORDER BY awoken_skill_id DESC LIMIT 20') - await self.execute_read(ctx, sql, [search_text, search_text]) + await self.execute_read(ctx, sql, [where]) @awokenskill.command(name="add") @checks.is_owner() @@ -442,11 +459,9 @@ async def awokenskill_edit(self, ctx, awoken_skill, *elements): [p]crud awokenskill edit 100 key1 "Value1" key2 "Value2" [p]crud awokenskill edit misc_comboboost key1 "Value1" key2 "Value2" """ - pdicog = self.bot.get_cog("PadInfo") - emoji_to_awid = getattr(pdicog, 'awoken_emoji_names', {}) - - if awoken_skill in emoji_to_awid: - awoken_skill_id = emoji_to_awid[awoken_skill] + dbcog = await self.get_dbcog() + if awoken_skill in dbcog.KNOWN_AWOKEN_SKILL_TOKENS: + awoken_skill_id = dbcog.KNOWN_AWOKEN_SKILL_TOKENS[awoken_skill].value elif awoken_skill.isdigit(): awoken_skill_id = int(awoken_skill) else: diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index d532fdfd..4775364b 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -35,7 +35,8 @@ from .models.monster_model import MonsterModel from .models.monster_stats import MonsterStatModifierInput, monster_stats from .monster_index import MonsterIndex -from .token_mappings import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES, AWOKEN_SKILL_MAP +from .token_mappings import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES, AWOKEN_SKILL_MAP, \ + KNOWN_AWOKEN_SKILL_TOKENS logger = logging.getLogger('red.padbot-cogs.dbcog') @@ -77,6 +78,7 @@ def __init__(self, bot, *args, **kwargs): # expose for documentation command self.AWOKEN_SKILL_TOKEN_MAP = AWOKEN_SKILL_MAP + self.KNOWN_AWOKEN_SKILL_TOKENS = KNOWN_AWOKEN_SKILL_TOKENS self.mon_finder = None # type: Optional[FindMonster] diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index b046eddd..12871b83 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -353,3 +353,8 @@ class MiscModifiers(Enum): **STRING_MONSTER_ATTRIBUTE_ALIASES, **BOOL_MONSTER_ATTRIBUTE_ALIASES, }.items() for v in vs} + +KNOWN_AWOKEN_SKILL_TOKENS = {} +for skill, tokens in enumerate(KNOWN_AWOKEN_SKILL_TOKENS): + for token in list(tokens): + KNOWN_AWOKEN_SKILL_TOKENS[token] = skill From ec25f847cebe795dffde96404a36b373f7e74ce0 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 9 Apr 2022 23:29:42 -0500 Subject: [PATCH 1095/1220] Fix KNOWN_AWOKEN_SKILL_TOKENS (#1651) --- dbcog/token_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 12871b83..b1a41c67 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -355,6 +355,6 @@ class MiscModifiers(Enum): }.items() for v in vs} KNOWN_AWOKEN_SKILL_TOKENS = {} -for skill, tokens in enumerate(KNOWN_AWOKEN_SKILL_TOKENS): +for skill, tokens in AWOKEN_SKILL_MAP.items(): for token in list(tokens): KNOWN_AWOKEN_SKILL_TOKENS[token] = skill From 3317a93dd5f5914ad38daa8e86b3167ac0cb3d4a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 18 Apr 2022 20:42:06 -0500 Subject: [PATCH 1096/1220] Stop saying "dadguide" in dbcog logging msgs (#1656) --- dbcog/dbcog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 4775364b..f1fe01a3 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -232,20 +232,20 @@ async def reload_data_task(self): async def download_and_refresh_nicknames(self): if await self.config.datafile(): - logger.info('Copying dg data file') + logger.info('Copying database file') shutil.copy2(await self.config.datafile(), self.db_file_path) else: - logger.info('Downloading dg data files') + logger.info('Downloading database files') await async_cached_dadguide_request(self.db_file_path, CLOUDFRONT_URL + '/db/dadguide.sqlite', 1 * 60 * 60) - logger.info('Loading dg database') + logger.info('Loading database') self.database = load_database(self.database, await self.get_debug_monsters()) - logger.info('Building dg monster index') + logger.info('Building monster index') await self.create_index() - logger.info('Done refreshing dg data') + logger.info('Done refreshing database') @commands.group() @checks.is_owner() From 0808c0adefa8bd0c25b135df40880aba9334a99b Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:03:12 -0500 Subject: [PATCH 1097/1220] Display friendly error if dgid data is missing (#1657) --- dungeoncog/dungeoncog.py | 102 ++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 162416d8..6399e1e3 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -4,6 +4,7 @@ from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import commands from redbot.core.utils.chat_formatting import pagify +from tsutils.user_interaction import send_cancellation_message from dungeoncog.enemy_skills_pb2 import MonsterBehavior from dungeoncog.menu.dungeon import DungeonMenu @@ -62,13 +63,9 @@ async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", di if not dungeons: dungeons = database.get_dungeons_from_name(name) if len(dungeons) == 0: - await ctx.send("No dungeons found!") - return + return None if len(dungeons) > 1: - header = "Multiple Dungeons Found, please be more specific:\n" - for page in pagify(header + '\n'.join(d.name_en for d in dungeons)): - await ctx.send(page) - return + return dungeons dungeon = dungeons.pop() sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty) sub_dungeon_model = None @@ -95,56 +92,63 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): Difficulty: Difficulty level/name of floor (eg. for A1, "Bipolar Goddess") """ if bad: - await ctx.send("Too many arguments. Make sure to surround all" - " arguments with spaces in quotes.") + await send_cancellation_message(ctx, "Too many arguments. Make sure to surround all" + " arguments with spaces in quotes.") return # load dbcog cog for database access dbcog = await self.get_dbcog() dungeon = await self.find_dungeon_from_name(ctx, name, dbcog.database.dungeon, difficulty) - if dungeon is not None: + if dungeon is None: + return await send_cancellation_message(ctx, "No dungeons found!") + if type(dungeon) == list: + header = "Multiple Dungeons Found, please be more specific:\n" + for page in pagify(header + '\n'.join(d.name_en for d in dungeon)): + await ctx.send(page) + return + # await ctx.send(format_overview(test_result)) + current_stage = 0 + pm_dungeon = [] + # check for invades: + invades = [] + + # print(dungeon.sub_dungeons[0].encounter_models) + for enc_model in dungeon.sub_dungeons[0].encounter_models: + behavior_test = MonsterBehavior() + if (enc_model.enemy_data is not None) and (enc_model.enemy_data.behavior is not None): + behavior_test.ParseFromString(enc_model.enemy_data.behavior) + else: + behavior_test = None + # await ctx.send(format_overview(test_result)) - current_stage = 0 - pm_dungeon = [] - # check for invades: - invades = [] - - # print(dungeon.sub_dungeons[0].encounter_models) - for enc_model in dungeon.sub_dungeons[0].encounter_models: - behavior_test = MonsterBehavior() - if (enc_model.enemy_data is not None) and (enc_model.enemy_data.behavior is not None): - behavior_test.ParseFromString(enc_model.enemy_data.behavior) - else: - behavior_test = None - - # await ctx.send(format_overview(test_result)) - # pm = process_monster(behavior_test, enc_model, db_cog.database) - if enc_model.stage < 0: - # pm.am_invade = True - invades.append(enc_model) - elif enc_model.stage > current_stage: - current_stage = enc_model.stage - floor = [enc_model] - pm_dungeon.append(floor) - else: - pm_dungeon[current_stage - 1].append(enc_model) - for f in pm_dungeon: - if pm_dungeon.index(f) != (len(pm_dungeon) - 1): - f.extend(invades) - - menu = DungeonMenu.menu() - original_author_id = ctx.message.author.id - test_list = ['1', '2', '3', '4'] - # print(pm_dungeon[0]) - view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, pm_dungeon[0][0], - dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, - len(pm_dungeon[0]), 0, - int(dungeon.sub_dungeons[0].technical), dbcog.database, verbose=False) - await ctx.send( - "EN: {}({})\nJP: {}({})".format(dungeon.name_en, dungeon.sub_dungeons[0].name_en, dungeon.name_ja, - dungeon.sub_dungeons[0].name_ja)) - message = await menu.create(ctx, view_state) + # pm = process_monster(behavior_test, enc_model, db_cog.database) + if enc_model.stage < 0: + # pm.am_invade = True + invades.append(enc_model) + elif enc_model.stage > current_stage: + current_stage = enc_model.stage + floor = [enc_model] + pm_dungeon.append(floor) + else: + pm_dungeon[current_stage - 1].append(enc_model) + for f in pm_dungeon: + if pm_dungeon.index(f) != (len(pm_dungeon) - 1): + f.extend(invades) + + if len(pm_dungeon) == 0: + return await send_cancellation_message(ctx, "This dungeon exists, but we have no data for it") + + menu = DungeonMenu.menu() + original_author_id = ctx.message.author.id + view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, pm_dungeon[0][0], + dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, + len(pm_dungeon[0]), 0, + int(dungeon.sub_dungeons[0].technical), dbcog.database, verbose=False) + await ctx.send( + "EN: {}({})\nJP: {}({})".format(dungeon.name_en, dungeon.sub_dungeons[0].name_en, dungeon.name_ja, + dungeon.sub_dungeons[0].name_ja)) + await menu.create(ctx, view_state) ''' @commands.command() From 11b8ee1a2ad738cb7699d7de29eaba9b69fe57b7 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 18 Apr 2022 20:09:56 -0700 Subject: [PATCH 1098/1220] Add stream-safe flag (#1658) --- padguidedb/padguidedb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padguidedb/padguidedb.py b/padguidedb/padguidedb.py index bb30c7c2..20dac899 100644 --- a/padguidedb/padguidedb.py +++ b/padguidedb/padguidedb.py @@ -188,13 +188,14 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): '--floor_id={}'.format(dungeon_floor_id), '--user_uuid={}'.format(self.settings.userUuidFor(server)), '--user_intid={}'.format(self.settings.userIntidFor(server)), + '--stream_safe', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() - if stderr: + if b"Traceback" in stderr: logger.error("Dungeon Load Error:\n{}\n\n{}".format(stdout.decode(), stderr.decode())) await send_repeated_consecutive_messages(ctx, inline( 'Load for {} {} {} failed'.format(server, dungeon_id, dungeon_floor_id))) From 0e4ba061cae46648409b9ecda4a357aff9127820 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 22 Apr 2022 14:15:57 -0500 Subject: [PATCH 1099/1220] Fix subqueries in id3 (#1660) --- dbcog/find_monster_tokens.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index 0a8d30fa..0caeea58 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -247,7 +247,7 @@ async def special_matches(self, monster): class SubqueryToken(SpecialToken): def __init__(self, fullvalue, subquery, *, negated=False, exact=False, dbcog): self.subquery = subquery - self.matches = None + self.sub_matches = None self.valid_monsters = None self.max_score = None @@ -255,9 +255,9 @@ def __init__(self, fullvalue, subquery, *, negated=False, exact=False, dbcog): async def prepare(self): m_info, _ = await self.dbcog.find_monster_debug(self.subquery) - self.matches = m_info.monster_matches + self.sub_matches = m_info.monster_matches self.valid_monsters = m_info.valid_monsters - self.max_score = max((self.matches[mon].score for mon in self.valid_monsters), default=0) + self.max_score = max((self.sub_matches[mon].score for mon in self.valid_monsters), default=0) @abstractmethod def get_matching_monsters(self, monster: MonsterModel) -> Iterable[MonsterModel]: @@ -267,7 +267,7 @@ async def special_matches(self, monster): mons = self.valid_monsters.intersection(self.get_matching_monsters(monster)) if not mons: return False, MatchData() - matched, score = max(((mon, self.matches.get(mon, DummyObject(score=0)).score) for mon in mons), + matched, score = max(((mon, self.sub_matches.get(mon, DummyObject(score=0)).score) for mon in mons), key=lambda x: x[1]) return score / self.max_score, MatchData(subquery_result=matched) From 032b37e4daf6f2fc5a0164476ad593b424f2ee4a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 27 Apr 2022 18:27:17 -0500 Subject: [PATCH 1100/1220] Create skyo command (#1659) --- dungeoncog/dungeoncog.py | 66 ++++++++++++++++++++----------- dungeoncog/menu/closable_embed.py | 9 +++++ dungeoncog/menu/menu_map.py | 6 ++- dungeoncog/view/skyo_links.py | 50 +++++++++++++++++++++++ 4 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 dungeoncog/menu/closable_embed.py create mode 100644 dungeoncog/view/skyo_links.py diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 6399e1e3..7c46429c 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -3,15 +3,20 @@ from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import commands -from redbot.core.utils.chat_formatting import pagify +from redbot.core.utils.chat_formatting import pagify, box +from tsutils.menu.view.closable_embed import ClosableEmbedViewState +from tsutils.query_settings.query_settings import QuerySettings from tsutils.user_interaction import send_cancellation_message from dungeoncog.enemy_skills_pb2 import MonsterBehavior +from dungeoncog.menu.closable_embed import ClosableEmbedMenu from dungeoncog.menu.dungeon import DungeonMenu from dungeoncog.menu.menu_map import dungeon_menu_map from dungeoncog.view.dungeon import DungeonViewState +from dungeoncog.view.skyo_links import SkyoLinksViewProps, SkyoLinksView if TYPE_CHECKING: + from dbcog.database_manager import DBCogDatabase from dbcog.dungeon_context import DungeonContext logger = logging.getLogger('red.padbot-cogs.dungeoncog') @@ -150,24 +155,41 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): dungeon.sub_dungeons[0].name_ja)) await menu.create(ctx, view_state) - -''' @commands.command() - async def spinner_help(self, ctx, spin_time, move_time): - """ - spin_time: The cycle the spinner is set at (typically changes once every second) - move_time: How much time you have to move orbs - """ - embed = discord.Embed(title="Spinner Helper {}s Rotation {}s Move Time".format(spin_time, move_time), - description="Assuming orbs are set and are NOT moved during movement:") - casino = ["🔥", "🌊", "🌿", "💡", "🌙", "🩹"] - cycles = float(move_time) / float(spin_time) - rounded = int(cycles) - casino_dict = OrderedDict() - for c in casino: - casino_dict.update({c: casino[(casino.index(c) + rounded) % 6]}) - casino_dict.update({"Other": casino[(rounded - 1) % 6]}) - output = "" - for k, v in casino_dict.items(): - output += "\n{} {} {}".format(k, "➡", v) - embed.add_field(name="Original -> Final", value=output) - await ctx.send(embed=embed)''' + @commands.command() + async def skyo(self, ctx, *, search_text): + """Show the subdungeon ids of all matching dungeons""" + dbcog = await self.get_dbcog() + db: "DBCogDatabase" = dbcog.database.database + + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, search_text) + + formatted_text = f'%{search_text}%' + dgs = db.query_many( + 'SELECT dungeon_id, name_en FROM dungeons' + ' WHERE LOWER(name_en) LIKE ? OR LOWER(name_ja) LIKE ?' + ' ORDER BY dungeon_id LIMIT 20', (formatted_text, formatted_text)) + if not dgs: + return await ctx.send(f"No dungeons found") + + dungeons = [] + for dg in dgs: + subdgs = db.query_many("SELECT sub_dungeon_id, name_en FROM sub_dungeons" + " WHERE dungeon_id = ?" + " ORDER BY sub_dungeon_id", (dg['dungeon_id'],)) + dg_dict = { + 'name': dg['name_en'], + 'idx': dg['dungeon_id'], + 'subdungeons': [], + } + for subdg in subdgs: + dg_dict['subdungeons'].append({ + 'name': subdg['name_en'], + 'idx': subdg['sub_dungeon_id'], + }) + dungeons.append(dg_dict) + + menu = ClosableEmbedMenu.menu() + props = SkyoLinksViewProps(dungeons) + state = ClosableEmbedViewState(ctx.message.author.id, ClosableEmbedMenu.MENU_TYPE, search_text, + query_settings, SkyoLinksView.VIEW_TYPE, props) + return await menu.create(ctx, state) diff --git a/dungeoncog/menu/closable_embed.py b/dungeoncog/menu/closable_embed.py new file mode 100644 index 00000000..0feb46cc --- /dev/null +++ b/dungeoncog/menu/closable_embed.py @@ -0,0 +1,9 @@ +from tsutils.menu.closable_embed_base import ClosableEmbedMenuBase + +from dungeoncog.view.skyo_links import SkyoLinksView + + +class ClosableEmbedMenu(ClosableEmbedMenuBase): + view_types = { + SkyoLinksView.VIEW_TYPE: SkyoLinksView + } diff --git a/dungeoncog/menu/menu_map.py b/dungeoncog/menu/menu_map.py index 7f9ccfc0..dd960e09 100644 --- a/dungeoncog/menu/menu_map.py +++ b/dungeoncog/menu/menu_map.py @@ -1,7 +1,11 @@ +from tsutils.menu.closable_embed_base import ClosableEmbedMenuPanes + +from dungeoncog.menu.closable_embed import ClosableEmbedMenu from dungeoncog.menu.dungeon import DungeonMenu, DungeonMenuPanes from dungeoncog.menu.simple import SimpleMenu, SimpleMenuPanes dungeon_menu_map = { SimpleMenu.MENU_TYPE: (SimpleMenu, SimpleMenuPanes), - DungeonMenu.MENU_TYPE: (DungeonMenu, DungeonMenuPanes) + DungeonMenu.MENU_TYPE: (DungeonMenu, DungeonMenuPanes), + ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), } diff --git a/dungeoncog/view/skyo_links.py b/dungeoncog/view/skyo_links.py new file mode 100644 index 00000000..ad0cae4b --- /dev/null +++ b/dungeoncog/view/skyo_links.py @@ -0,0 +1,50 @@ +from typing import List + +from discordmenu.embed.base import Box +from discordmenu.embed.components import EmbedMain, EmbedAuthor, EmbedField +from discordmenu.embed.text import LinkedText +from discordmenu.embed.view import EmbedView +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.closable_embed import ClosableEmbedViewState + + +class SkyoLinksViewProps: + def __init__(self, dungeons: List[dict]): + self.dungeons = dungeons + + +class SkyoLinksView: + VIEW_TYPE = 'SkyoLinks' + dungeon_link = 'https://skyo.tsubakibot.com/{}' + subdungeon_link = 'https://skyo.tsubakibot.com/{}' + bad_chars = ['[', ']'] + + @classmethod + def embed_fields(cls, dungeons): + ret = [] + for dungeon in dungeons: + ret.append(LinkedText(cls.escape_name(dungeon['name']), cls.dungeon_link.format(dungeon['idx']))) + for subdungeon in dungeon['subdungeons']: + ret.append(Box( + "\u200b \u200b \u200b \u200b \u200b ", + LinkedText(cls.escape_name(subdungeon['name']), cls.subdungeon_link.format(subdungeon['idx'])), + delimiter='' + )) + return [EmbedField('Dungeons', Box(*ret, delimiter='\n'))] + + @classmethod + def escape_name(cls, name): + for char in cls.bad_chars: + name = name.replace(char, '') + return name + + @classmethod + def embed(cls, state: ClosableEmbedViewState, props: SkyoLinksViewProps): + return EmbedView( + EmbedMain( + color=state.query_settings.embedcolor, + description='The following dungeons were found.' + ), + embed_footer=embed_footer_with_state(state), + embed_fields=cls.embed_fields(props.dungeons) + ) From 86940f9ce045abd8a053dc6c8e447f8252399357 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 28 Apr 2022 09:06:46 -0700 Subject: [PATCH 1101/1220] Change Some Token Types (#1662) Closes #1655 Closes #1163 Closes #954 --- dbcog/monster_index.py | 6 +++--- dbcog/token_mappings.py | 8 +++++--- padinfo/padinfo.py | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 35320eac..5a7cd153 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -443,12 +443,12 @@ async def get_modifiers(self, monster: MonsterModel): logger.warning(f"Invalid awoken skill ID: {aw.awoken_skill_id}") self.issues.append(f"Invalid awoken skill ID: {aw.awoken_skill_id}") - # Numbered Equips - self.add_numbered_modifier(monster, modifiers, AWOKEN_SKILL_MAP[AwokenSkills.EQUIP], + # Equips + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.EQUIP], lambda m: m.is_equip) # Chibi - self.add_numbered_modifier(monster, modifiers, MISC_MAP[MiscModifiers.CHIBI], + self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.CHIBI], lambda m: (m.name_en == m.name_en.lower() and m.name_en != m.name_ja or 'ミニ' in m.name_ja or '(chibi)' in m.name_en)) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index b1a41c67..c482776e 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -62,6 +62,7 @@ class EvoTypes(Enum): EVO = 'Evolved' UVO = 'Ulimate' UUVO = 'Super Ultimate' + EQUIP = 'Equip' BASETRANS = 'Base Transform' TRANS = 'Transform' AWOKEN = 'Awoken' @@ -70,6 +71,7 @@ class EvoTypes(Enum): SREVO = 'Super Reincarnated' PIXEL = 'Pixel' NONPIXEL = 'Nonpixel' + CHIBI = 'Chibi' EVO_MAP = { @@ -77,6 +79,7 @@ class EvoTypes(Enum): EvoTypes.EVO: ('evo', 'evolved'), EvoTypes.UVO: ('uvo', 'ult', 'ultimate', 'uevo'), EvoTypes.UUVO: ('uuvo', 'uult', 'uultimate', 'uuevo', 'suvo'), + EvoTypes.EQUIP: ('equip', 'assist', 'eq'), EvoTypes.BASETRANS: ('transformbase', 'transbase'), EvoTypes.TRANS: ('transform', 'trans', 'transformed', 'xf', 'xform', 'tf'), EvoTypes.AWOKEN: ('awoken', 'awo', 'a'), @@ -85,6 +88,7 @@ class EvoTypes(Enum): EvoTypes.SREVO: ('srevo', 'super', 'sr', 'superreincarnated'), EvoTypes.PIXEL: ('pixel', 'p', 'dot', 'px'), EvoTypes.NONPIXEL: ('nonpixel', 'np'), + EvoTypes.CHIBI: ('chibi', 'mini'), } # awakening view state & awakening help props in padinfo are transforming this dict to `int: tuple` @@ -138,7 +142,7 @@ class EvoTypes(Enum): AwokenSkills.ENHTEAMHP: ('teamhp', 'thp'), AwokenSkills.ENHTEAMRCV: ('teamrcv', 'trcv'), AwokenSkills.VDP: ('vdp', 'box'), - AwokenSkills.EQUIP: ('equip', 'assist', 'eq'), + AwokenSkills.EQUIP: (), # Equip is an evo type AwokenSkills.SUPERFUA: ('sfua',), AwokenSkills.SKILLCHARGE: ('rainbowhaste', 'skillcharge', 'hasteawo'), AwokenSkills.UNBINDABLE: ('unbindable', 'bindres'), @@ -176,7 +180,6 @@ class EvoTypes(Enum): class MiscModifiers(Enum): - CHIBI = 'Chibi' STORY = 'Story' FARMABLE = 'Farmable' TRADEABLE = 'Tradeable' @@ -207,7 +210,6 @@ class MiscModifiers(Enum): MISC_MAP = { - MiscModifiers.CHIBI: ('chibi', 'mini'), MiscModifiers.STORY: ('story',), MiscModifiers.FARMABLE: ('farmable',), MiscModifiers.TRADEABLE: ('tradeable', 'tradable'), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 4304ed00..65500423 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1175,10 +1175,10 @@ async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, quer f" Treenames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_treenames.items() if mon in ms))}\n" f" Nicknames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_cardnames.items() if mon in ms))}\n\n" f"[Modifier Tokens]\n" - f" Attribute: {' '.join(sorted(t for t in mods if t in dbcog.token_maps.COLOR_TOKENS))}\n" - f" Awakening: {' '.join(sorted(t for t in mods if t in dbcog.token_maps.AWAKENING_TOKENS))}\n" - f" Evo & Type: {' '.join(sorted(t for t in mods if t in EVOANDTYPE))}\n" - f" Other: {' '.join(sorted(t for t in mods if t not in dbcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" + f" Attribute: {' '.join(sorted(t for t in mods if t.split('-')[0] in dbcog.token_maps.COLOR_TOKENS))}\n" + f" Awakening: {' '.join(sorted(t for t in mods if t.split('-')[0] in dbcog.token_maps.AWAKENING_TOKENS))}\n" + f" Evo & Type: {' '.join(sorted(t for t in mods if t.split('-')[0] in EVOANDTYPE))}\n" + f" Other: {' '.join(sorted(t for t in mods if t.split('-')[0] not in dbcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" f"Manually Added: {' '.join(sorted(manual_modifiers))}\n") for page in pagify(ret): await ctx.send(box(page)) From da9d8a3a4b0848d9225a0a38cefe38a9bc1ed330 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Thu, 28 Apr 2022 20:17:08 -0500 Subject: [PATCH 1102/1220] Create padglobal rename (#1666) * Create padglobal rename * Remove str in parameters --- padglobal/padglobal.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/padglobal/padglobal.py b/padglobal/padglobal.py index 0a900ab1..fc0e48b3 100644 --- a/padglobal/padglobal.py +++ b/padglobal/padglobal.py @@ -345,6 +345,7 @@ async def _concatenate(self, ctx, command: str, operation: str, addition): @padglobal.command() async def eventswap(self, ctx): + """Shift contents of nextevent command to currentevent""" if 'currentevent' not in self.c_commands or 'nextevent' not in self.c_commands: await ctx.send(f"Please populate both {inline('currentevent')} and {inline('nextevent')} first." f" Cancelling.") @@ -364,6 +365,28 @@ async def eventswap(self, ctx): json.dump(self.c_commands, open(self.file_path, 'w+')) await ctx.send(f"Okay, I've updated {inline('lastevent')} and {inline('currentevent')} accordingly," f" and cleared {inline('nextevent')}.") + + @padglobal.command() + async def rename(self, ctx, old_name, new_name): + """Rename a PAD global command""" + if old_name not in self.c_commands: + await ctx.send(f"Please populate {inline(old_name)} first.") + return + if new_name in self.c_commands: + await ctx.send("A command already exists with that name.") + return + if not await get_user_confirmation(ctx, f"Are you sure you want to rename command {inline(old_name)} to {inline(new_name)}?"): + await ctx.send("The command was not renamed.") + return + aliases = await self._find_aliases(old_name) + for alias in aliases: + self.c_commands[alias] = new_name + + self.c_commands[new_name] = self.c_commands[old_name] + self.c_commands.pop(old_name) + + json.dump(self.c_commands, open(self.file_path, 'w+')) + await ctx.send(f"Okay, I've deleted {inline(old_name)} and moved its contents to {inline(new_name)}.") async def _find_aliases(self, command: str): aliases = [] From d25bf243836d971b2e65bc96a3fbe10456cfd301 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Thu, 28 Apr 2022 20:17:23 -0500 Subject: [PATCH 1103/1220] Create tradeable command (#1664) --- monidlistener/monidlistener.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 2ef27560..06f0fab5 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -108,3 +108,38 @@ async def is_command(self, msg): if msg.content.startswith(p): return True return False + + @commands.command(aliases=['tradable']) + async def tradeable(self, ctx, *, query: str): + """Look up if a card is tradeable""" + dbcog = self.bot.get_cog("DBCog") + monster = await dbcog.find_monster(query) + if monster is None: + await ctx.send("Sorry, your query `" + query + "` didn't match any results :(") + return + + if monster.sell_mp < 100: + ret = "\t\t✅ This card is tradable!" + else: + base_flag = False # Track other form tradability + gem_flag = False # Track evo gem tradability + + base_monster = dbcog.get_monster(monster.base_evo_id) + if base_monster.sell_mp < 100: + base_flag = True + + if monster.evo_gem_id is not None: + evo_gem: "MonsterModel" = dbcog.get_monster(monster.evo_gem_id) + if evo_gem.sell_mp < 100: + gem_flag = True + + ret = (f"\t\t{'⁉️' if base_flag or gem_flag else '❌'} This card is **not tradable**.") + if base_flag: + ret += "\n\t\t\t\t✅ Other forms of this card are tradable." + if gem_flag: + ret += "\n\t\t\t\t✅ Evo gem for this card is tradable." + elif monster.evo_gem_id is not None: + ret += "\n\t\t\t\t❌ Evo gem for this card is not tradable." + + await ctx.send("{}\n{}".format( + MonsterHeader.text_with_emoji(monster), ret)) From 4c1a1a8b3043e30164869aecd7e926efdbec6a36 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 5 May 2022 19:13:22 -0700 Subject: [PATCH 1104/1220] Add dungeon aliases to `^skyo` (#1669) * Add dungeon aliases * Use defaultdict --- dungeoncog/__init__.py | 1 + dungeoncog/dungeoncog.py | 88 ++++++++++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/dungeoncog/__init__.py b/dungeoncog/__init__.py index 1184bac8..22202afc 100644 --- a/dungeoncog/__init__.py +++ b/dungeoncog/__init__.py @@ -10,5 +10,6 @@ def setup(bot): n = DungeonCog(bot) bot.add_cog(n) + bot.loop.create_task(n.load_aliases()) bot.loop.create_task(n.register_menu()) bot.loop.create_task(n.load_emojis()) diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 7c46429c..eb3143f8 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -1,9 +1,14 @@ +import csv +import io import logging -from typing import TYPE_CHECKING +from collections import defaultdict +from typing import Any, TYPE_CHECKING +import aiohttp from discordmenu.emoji.emoji_cache import emoji_cache from redbot.core import commands -from redbot.core.utils.chat_formatting import pagify, box +from redbot.core.utils.chat_formatting import pagify +from tsutils.cogs.globaladmin import auth_check from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.query_settings.query_settings import QuerySettings from tsutils.user_interaction import send_cancellation_message @@ -13,7 +18,7 @@ from dungeoncog.menu.dungeon import DungeonMenu from dungeoncog.menu.menu_map import dungeon_menu_map from dungeoncog.view.dungeon import DungeonViewState -from dungeoncog.view.skyo_links import SkyoLinksViewProps, SkyoLinksView +from dungeoncog.view.skyo_links import SkyoLinksView, SkyoLinksViewProps if TYPE_CHECKING: from dbcog.database_manager import DBCogDatabase @@ -22,6 +27,10 @@ logger = logging.getLogger('red.padbot-cogs.dungeoncog') EMBED_NOT_GENERATED = -1 +DUNGEON_ALIASES = "https://docs.google.com/spreadsheets/d/e/" \ + "2PACX-1vQ3F4shS6w2na4FXA-vZyyhKcOQ0zRA1B3T7zaX0Bm4cEjW-1IVw91josPtLgc9Zh_TGh8GTD6zFmd0" \ + "/pub?gid=0&single=true&output=csv" + class DungeonCog(commands.Cog): """ @@ -34,6 +43,22 @@ def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot + self.aliases = defaultdict(set) + + gadmin: Any = self.bot.get_cog("GlobalAdmin") + if gadmin: + gadmin.register_perm("contentadmin") + + async def load_aliases(self): + self.aliases = defaultdict(set) + async with aiohttp.ClientSession() as session: + async with session.get(DUNGEON_ALIASES) as response: + reader = csv.reader(io.StringIO(await response.text()), delimiter=',') + next(reader) + for line in reader: + for a in line[2].replace(' ', '').split(', '): + self.aliases[a].add(line[0] or line[1]) + async def load_emojis(self): await self.bot.wait_until_ready() emoji_cache.set_guild_ids([g.id for g in self.bot.guilds]) @@ -163,33 +188,44 @@ async def skyo(self, ctx, *, search_text): query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, search_text) - formatted_text = f'%{search_text}%' - dgs = db.query_many( - 'SELECT dungeon_id, name_en FROM dungeons' - ' WHERE LOWER(name_en) LIKE ? OR LOWER(name_ja) LIKE ?' - ' ORDER BY dungeon_id LIMIT 20', (formatted_text, formatted_text)) - if not dgs: + if search_text.replace(' ', '') not in self.aliases: + formatted_text = f'%{search_text}%' + sds = db.query_many( + 'SELECT dungeons.dungeon_id, sub_dungeon_id, dungeons.name_en AS dg_name, sub_dungeons.name_en AS sd_name' + ' FROM sub_dungeons' + ' JOIN dungeons ON sub_dungeons.dungeon_id = dungeons.dungeon_id' + ' WHERE LOWER(dungeons.name_en) LIKE ? OR LOWER(dungeons.name_ja) LIKE ?' + ' OR LOWER(sub_dungeons.name_en) LIKE ? OR LOWER(sub_dungeons.name_ja) LIKE ?' + ' ORDER BY dungeons.dungeon_id LIMIT 20', (formatted_text,) * 4) + else: + formatted_text = ', '.join(self.aliases[search_text.replace(' ', '')]) + sds = db.query_many( + 'SELECT dungeons.dungeon_id, sub_dungeon_id, dungeons.name_en AS dg_name, sub_dungeons.name_en AS sd_name' + ' FROM sub_dungeons' + ' JOIN dungeons ON sub_dungeons.dungeon_id = dungeons.dungeon_id' + f' WHERE dungeons.dungeon_id IN (?) OR sub_dungeon_id IN (?)' + ' ORDER BY dungeons.dungeon_id LIMIT 20', (formatted_text,) * 2) + + if not sds: return await ctx.send(f"No dungeons found") - dungeons = [] - for dg in dgs: - subdgs = db.query_many("SELECT sub_dungeon_id, name_en FROM sub_dungeons" - " WHERE dungeon_id = ?" - " ORDER BY sub_dungeon_id", (dg['dungeon_id'],)) - dg_dict = { - 'name': dg['name_en'], - 'idx': dg['dungeon_id'], - 'subdungeons': [], - } - for subdg in subdgs: - dg_dict['subdungeons'].append({ - 'name': subdg['name_en'], - 'idx': subdg['sub_dungeon_id'], - }) - dungeons.append(dg_dict) + dungeons = defaultdict(lambda: {'subdungeons': []}) + for sd in sds: + dungeons[sd.dungeon_id]['name'] = sd.dg_name + dungeons[sd.dungeon_id]['idx'] = sd.dungeon_id + dungeons[sd.dungeon_id]['subdungeons'].append({ + 'name': sd.sd_name, + 'idx': sd.sub_dungeon_id}) menu = ClosableEmbedMenu.menu() - props = SkyoLinksViewProps(dungeons) + props = SkyoLinksViewProps(sorted(dungeons.values(), key=lambda d: d['idx'])) state = ClosableEmbedViewState(ctx.message.author.id, ClosableEmbedMenu.MENU_TYPE, search_text, query_settings, SkyoLinksView.VIEW_TYPE, props) return await menu.create(ctx, state) + + @commands.command(aliases=['firdg']) + @auth_check('contentadmin') + async def force_dungeon_index_reload(self, ctx): + async with ctx.typing(): + await self.load_aliases() + await ctx.send("Reloaded") From 727f4103208771a3480e630efa3b35fb0eb6a5fa Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 5 May 2022 19:36:33 -0700 Subject: [PATCH 1105/1220] Fix dungeon aliases (#1670) --- dungeoncog/dungeoncog.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index eb3143f8..fec56d2e 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -1,3 +1,4 @@ +import asyncio import csv import io import logging @@ -44,20 +45,23 @@ def __init__(self, bot, *args, **kwargs): self.bot = bot self.aliases = defaultdict(set) + self.aliases_loaded = asyncio.Event() gadmin: Any = self.bot.get_cog("GlobalAdmin") if gadmin: gadmin.register_perm("contentadmin") async def load_aliases(self): + self.aliases_loaded.clear() self.aliases = defaultdict(set) async with aiohttp.ClientSession() as session: async with session.get(DUNGEON_ALIASES) as response: reader = csv.reader(io.StringIO(await response.text()), delimiter=',') next(reader) for line in reader: - for a in line[2].replace(' ', '').split(', '): + for a in line[2].replace(' ', '').split(','): self.aliases[a].add(line[0] or line[1]) + self.aliases_loaded.set() async def load_emojis(self): await self.bot.wait_until_ready() @@ -188,6 +192,7 @@ async def skyo(self, ctx, *, search_text): query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, search_text) + await self.aliases_loaded.wait() if search_text.replace(' ', '') not in self.aliases: formatted_text = f'%{search_text}%' sds = db.query_many( @@ -198,13 +203,13 @@ async def skyo(self, ctx, *, search_text): ' OR LOWER(sub_dungeons.name_en) LIKE ? OR LOWER(sub_dungeons.name_ja) LIKE ?' ' ORDER BY dungeons.dungeon_id LIMIT 20', (formatted_text,) * 4) else: - formatted_text = ', '.join(self.aliases[search_text.replace(' ', '')]) + formatted_text = ', '.join(a for a in self.aliases[search_text.replace(' ', '')] if a.isnumeric()) sds = db.query_many( 'SELECT dungeons.dungeon_id, sub_dungeon_id, dungeons.name_en AS dg_name, sub_dungeons.name_en AS sd_name' ' FROM sub_dungeons' ' JOIN dungeons ON sub_dungeons.dungeon_id = dungeons.dungeon_id' - f' WHERE dungeons.dungeon_id IN (?) OR sub_dungeon_id IN (?)' - ' ORDER BY dungeons.dungeon_id LIMIT 20', (formatted_text,) * 2) + f' WHERE dungeons.dungeon_id IN ({formatted_text}) OR sub_dungeon_id IN ({formatted_text})' + ' ORDER BY dungeons.dungeon_id LIMIT 20') if not sds: return await ctx.send(f"No dungeons found") From af71716c47498ffbc005753571c6de78501c2807 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 7 May 2022 00:41:52 -0700 Subject: [PATCH 1106/1220] Improve dgid help text (#1671) --- dungeoncog/dungeoncog.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index fec56d2e..1f8d065d 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -119,20 +119,17 @@ async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", di dungeon = dungeons.pop() return dungeon - @commands.command(aliases=['dgid']) - async def dungeonid(self, ctx, name, difficulty=None, *bad: str): + @commands.command(aliases=['dgid'], ignore_extra=False) + async def dungeonid(self, ctx, dungeon_name, floor_name=None): + """Get encounter data for a dungeon. + + Quotes must be used around multi-word names + [p]dungeonid "blue bowl dragon" + [p]dungeonid "ultimate arena" "three hands of fate" + [p]dungeonid "castle of satan in the abyss" 3 """ - Name: Name of Dungeon - Difficulty: Difficulty level/name of floor (eg. for A1, "Bipolar Goddess") - """ - if bad: - await send_cancellation_message(ctx, "Too many arguments. Make sure to surround all" - " arguments with spaces in quotes.") - return - - # load dbcog cog for database access dbcog = await self.get_dbcog() - dungeon = await self.find_dungeon_from_name(ctx, name, dbcog.database.dungeon, difficulty) + dungeon = await self.find_dungeon_from_name(ctx, dungeon_name, dbcog.database.dungeon, floor_name) if dungeon is None: return await send_cancellation_message(ctx, "No dungeons found!") @@ -175,7 +172,7 @@ async def dungeonid(self, ctx, name, difficulty=None, *bad: str): menu = DungeonMenu.menu() original_author_id = ctx.message.author.id - view_state = DungeonViewState(original_author_id, 'DungeonMenu', name, pm_dungeon[0][0], + view_state = DungeonViewState(original_author_id, 'DungeonMenu', dungeon_name, pm_dungeon[0][0], dungeon.sub_dungeons[0].sub_dungeon_id, len(pm_dungeon), 1, len(pm_dungeon[0]), 0, int(dungeon.sub_dungeons[0].technical), dbcog.database, verbose=False) From d077b0f3be7496c8c3005b4ee4d86c338563df1c Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 7 May 2022 02:13:17 -0700 Subject: [PATCH 1107/1220] Choose transforms deterministically (#1672) * Choose previous transform deterministically where relevant * Fix forward transforms --- dbcog/monster_graph.py | 138 +++++++++++++++++++++-------------------- dbcog/monster_index.py | 2 +- 2 files changed, 71 insertions(+), 69 deletions(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 6d54dfe3..a1f02ce8 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -3,7 +3,7 @@ import re from collections import defaultdict from datetime import datetime -from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar +from typing import Any, Dict, List, Optional, Set, Tuple, TypeVar, cast from networkx import MultiDiGraph from tsutils.enums import Server @@ -50,8 +50,7 @@ COALESCE(series.name_ko, 'Unsorted') AS s_name_ko, series.series_type AS s_series_type, exchanges.target_monster_id AS evo_gem_id, - drops.drop_id, - transformations{0}.to_monster_id AS transform_to_id + drops.drop_id FROM monsters{0} LEFT OUTER JOIN leader_skills{0} ON monsters{0}.leader_skill_id = leader_skills{0}.leader_skill_id @@ -62,7 +61,6 @@ LEFT OUTER JOIN exchanges ON target_monsters.monster_id = exchanges.target_monster_id LEFT OUTER JOIN drops ON monsters{0}.monster_id = drops.monster_id LEFT OUTER JOIN monster_name_overrides ON monsters{0}.monster_id = monster_name_overrides.monster_id - LEFT OUTER JOIN transformations{0} ON monsters{0}.monster_id = transformations{0}.from_monster_id GROUP BY monsters{0}.monster_id""" @@ -133,6 +131,12 @@ INNER JOIN evolutions{0} ON evolutions{0}.evolution_id = latest_evolutions.evolution_id AND evolutions{0}.tstamp = latest_evolutions.tstamp""" +TRANSFORMS_QUERY = """SELECT + transformations{0}.* +FROM + transformations{0}""" + + AWAKENINGS_QUERY = """SELECT awakenings{0}.awakening_id, awakenings{0}.monster_id, @@ -189,6 +193,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: ms = self.database.query_many(MONSTER_QUERY.format(table_suffix)) es = self.database.query_many(EVOS_QUERY.format(table_suffix)) + tfs = self.database.query_many(TRANSFORMS_QUERY.format(table_suffix)) aws = self.database.query_many(AWAKENINGS_QUERY.format(table_suffix)) ems = self.database.query_many(EGG_QUERY.format(table_suffix) + where) exs = self.database.query_many(EXCHANGE_QUERY.format(table_suffix) + where) @@ -353,17 +358,16 @@ def build_graph(self, server: Server) -> MultiDiGraph: graph.add_node(m.monster_id, model=m_model) self.max_monster_id = max(self.max_monster_id, m.monster_id) - if m.transform_to_id: - if self.debug_monster_ids is None or m.transform_to_id in self.debug_monster_ids: - graph.add_edge(m.monster_id, m.transform_to_id, type='transformation') - graph.add_edge(m.transform_to_id, m.monster_id, type='back_transformation') - if m.evo_gem_id: if self.debug_monster_ids is None or m.evo_gem_id in self.debug_monster_ids: graph.add_edge(m.monster_id, m.evo_gem_id, type='evo_gem_from') graph.add_edge(m.evo_gem_id, m.monster_id, type='evo_gem_of') for e in es: + if self.debug_monster_ids is not None: + if e.from_id not in self.debug_monster_ids or e.to_id not in self.debug_monster_ids: + continue + evo_model = EvolutionModel( evolution_type=e.evolution_type, reversible=e.reversible, @@ -376,23 +380,28 @@ def build_graph(self, server: Server) -> MultiDiGraph: mat_5_id=self.debug_validate_id(e.mat_5_id), tstamp=e.tstamp, ) - if self.debug_monster_ids is not None: - if evo_model.from_id not in self.debug_monster_ids or evo_model.to_id not in self.debug_monster_ids: - continue - graph.add_edge( - evo_model.from_id, evo_model.to_id, type='evolution', model=evo_model) - graph.add_edge( - evo_model.to_id, evo_model.from_id, type='back_evolution', model=evo_model) + graph.add_edge(evo_model.from_id, evo_model.to_id, type='evolution', model=evo_model) + graph.add_edge(evo_model.to_id, evo_model.from_id, type='back_evolution', model=evo_model) # for material_of queries - already_used_in_this_evo = [] # don't add same mat more than once per evo + already_used_in_this_evo = set() # don't add same mat more than once per evo for mat in evo_model.mats: if mat in already_used_in_this_evo: continue graph.add_edge( mat, evo_model.to_id, type="material_of", model=evo_model) - already_used_in_this_evo.append(mat) + already_used_in_this_evo.add(mat) + + for tf in tfs: + if self.debug_monster_ids is not None: + if tf.from_monster_id not in self.debug_monster_ids or tf.to_monster_id not in self.debug_monster_ids: + continue + + # Make a model with percentages here. + + graph.add_edge(tf.from_monster_id, tf.to_monster_id, type='transformation') + graph.add_edge(tf.to_monster_id, tf.from_monster_id, type='back_transformation') exchanges = defaultdict(set) for ex in exs: @@ -430,28 +439,27 @@ def _get_edges(self, monster: MonsterModel, etype) -> Set[int]: return {mid for mid, atlas in self.graph_dict[monster.server_priority][monster.monster_id].items() for edge in atlas.values() if edge.get('type') == etype} - def _get_edge_or_none(self, monster: MonsterModel, etype) -> Optional[int]: + def _get_edge_or_none(self, monster: MonsterModel, etype: str) -> Optional[int]: edges = self._get_edges(monster, etype) if edges: return edges.pop() - def _get_edge_model(self, monster: MonsterModel, etype) -> Optional[BaseModel]: + def _get_edge_model_set(self, monster: MonsterModel, etype: str) -> Set[BaseModel]: possible_results = set() for atlas in self.graph_dict[monster.server_priority][monster.monster_id].values(): for edge in atlas.values(): if edge.get('type') == etype: - possible_results.add(edge['model']) - if len(possible_results) == 0: - return None - return sorted(possible_results, key=lambda x: x.tstamp)[-1] + possible_results.update(edge['models']) + return possible_results - def _get_edge_model_set(self, monster: MonsterModel, etype, model_type: Type[M] = BaseModel) -> Set[M]: + def _get_newest_edge_model(self, monster: MonsterModel, etype: str) -> Optional[BaseModel]: possible_results = set() for atlas in self.graph_dict[monster.server_priority][monster.monster_id].values(): for edge in atlas.values(): if edge.get('type') == etype: - possible_results.update(edge['models']) - return {model for model in possible_results} + possible_results.add(edge['model']) + if possible_results: + return min(possible_results, key=lambda x: x.tstamp) def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER, do_logging: bool = False) \ -> Optional[MonsterModel]: @@ -484,11 +492,8 @@ def get_transform_monsters(self, monster: MonsterModel) -> Set[MonsterModel]: mon = to_check.pop() if mon in mons: continue - if (next_transform := self.get_next_transform(mon)): - to_check.add(next_transform) - prev = self.get_prev_transform(mon) - if prev is not None: - to_check.add(prev) + to_check.update(self.get_next_transforms(mon)) + to_check.update(self.get_prev_transforms(mon)) mons.add(mon) return mons @@ -497,10 +502,10 @@ def process_alt_ids(self, monster: MonsterModel) -> List[int]: def process_alt_monsters_from_base(self, base_monster: MonsterModel) -> List[MonsterModel]: ids = [base_monster] - transform = self.get_next_transform(base_monster) - if transform and (transform.monster_id > base_monster.monster_id - or transform.monster_id == 5802): # I hate DMG very much - ids += self.process_alt_monsters_from_base(transform) + for transform in sorted(self.get_next_transforms(base_monster), key=self.get_id_order_key): + if transform and (transform.monster_id > base_monster.monster_id + or transform.monster_id == 5802): # I hate DMG very much + ids += [mid for mid in self.process_alt_monsters_from_base(transform) if mid not in ids] for evo in sorted(self.get_next_evolutions(base_monster), key=self.get_id_order_key): ids += self.process_alt_monsters_from_base(evo) return ids @@ -581,7 +586,7 @@ def get_numerical_sort_top_monster(self, monster: MonsterModel) -> Optional[Mons return sorted(alt_cards, key=lambda m: m.monster_id)[-1] def get_evolution(self, monster) -> Optional[EvolutionModel]: - return self._get_edge_model(monster, 'back_evolution') + return self._get_newest_edge_model(monster, 'back_evolution') def monster_is_reversible_evo(self, monster: MonsterModel) -> bool: prev_evo = self.get_evolution(monster) @@ -633,13 +638,36 @@ def true_evo_type(self, monster: MonsterModel) -> InternalEvoType: return InternalEvoType.Ultimate return InternalEvoType.Normal - def get_prev_evolution_id(self, monster: MonsterModel) -> Optional[int]: - return self._get_edge_or_none(monster, 'back_evolution') - def get_prev_evolution(self, monster: MonsterModel) -> Optional[MonsterModel]: - pe = self.get_prev_evolution_id(monster) + pe = self._get_edge_or_none(monster, 'back_evolution') return pe and self.get_monster(pe, server=monster.server_priority) + def get_next_evolutions(self, monster: MonsterModel) -> Set[MonsterModel]: + return {self.get_monster(mid, server=monster.server_priority) + for mid in self._get_edges(monster, 'evolution')} + + def get_prev_transforms(self, monster: MonsterModel) -> Set[MonsterModel]: + return {self.get_monster(mid, server=monster.server_priority) + for mid in self._get_edges(monster, 'back_transformation')} + + def get_prev_transform(self, monster: MonsterModel) -> Optional[MonsterModel]: + # This doesn't need a special function, so I'll remove it later + transforms = self.get_prev_transforms(monster) + if not transforms: + return None + return min(transforms, key=lambda m: m.monster_id) + + def get_next_transforms(self, monster: MonsterModel) -> Set[MonsterModel]: + return {self.get_monster(mid, server=monster.server_priority) + for mid in self._get_edges(monster, 'transformation')} + + def get_next_transform(self, monster: MonsterModel) -> Optional[MonsterModel]: + # Figure out how to get rid of this + transforms = self.get_next_transforms(monster) + if not transforms: + return None + return max(transforms, key=lambda m: m.monster_id) + def get_all_prev_evolutions(self, monster: MonsterModel, *, include_self: bool = True) -> List[MonsterModel]: ret = [] if include_self: @@ -680,32 +708,6 @@ def get_all_prev_transforms(self, monster: MonsterModel, *, include_self: bool = ret.pop(i) return ret - def get_next_evolution_ids(self, monster: MonsterModel) -> Set[int]: - return self._get_edges(monster, 'evolution') - - def get_next_evolutions(self, monster: MonsterModel) -> Set[MonsterModel]: - return {self.get_monster(mid, server=monster.server_priority) - for mid in self.get_next_evolution_ids(monster)} - - def get_prev_transform_id(self, monster: MonsterModel) -> Optional[int]: - transforms = list(self._get_edges(monster, 'back_transformation')) - if len(transforms) > 0: - return transforms[0] - return None - - def get_prev_transform(self, monster: MonsterModel) -> Optional[MonsterModel]: - prev = self.get_prev_transform_id(monster) - if prev is None: - return None - return self.get_monster(prev, server=monster.server_priority) - - def get_next_transform_id(self, monster: MonsterModel) -> Optional[int]: - return self._get_edge_or_none(monster, 'transformation') - - def get_next_transform(self, monster: MonsterModel) -> Optional[MonsterModel]: - nt = self.get_next_transform_id(monster) - return nt and self.get_monster(nt, server=monster.server_priority) - def get_monster_depth(self, monster: MonsterModel) -> float: if monster.monster_id == 5802: # DMG sucks! @@ -759,7 +761,7 @@ def get_monster_exchange_mat_ids(self, monster: MonsterModel) -> Set[int]: return self._get_edges(monster, 'exchange_from') def get_monster_exchange_models(self, monster: MonsterModel) -> Set[ExchangeModel]: - return self._get_edge_model_set(monster, 'exchange_from') + return cast(Set[ExchangeModel], self._get_edge_model_set(monster, 'exchange_from')) def get_monster_exchange_mats(self, monster: MonsterModel) -> Set[MonsterModel]: return {self.get_monster(mid, server=monster.server_priority) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 5a7cd153..edc70085 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -409,7 +409,7 @@ async def get_modifiers(self, monster: MonsterModel): lambda m: not self.graph.monster_is_transform_base(m)) self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.BASETRANS], lambda m: (self.graph.monster_is_transform_base(m) - and self.graph.get_next_transform(m))) + and self.graph.get_next_transforms(m))) # Awoken self.add_numbered_modifier(monster, modifiers, EVO_MAP[EvoTypes.AWOKEN], From 631ef66c3eb35929122d6554064f9c604e688d22 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 13 May 2022 22:01:18 -0500 Subject: [PATCH 1108/1220] Refactor ^tradeable command (#1674) --- monidlistener/monidlistener.py | 57 +++++++++++++++++----------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/monidlistener/monidlistener.py b/monidlistener/monidlistener.py index 06f0fab5..3b03fb13 100644 --- a/monidlistener/monidlistener.py +++ b/monidlistener/monidlistener.py @@ -4,7 +4,7 @@ from redbot.core import Config, checks, commands from tsutils.tsubaki.monster_header import MonsterHeader - +from tsutils.user_interaction import indented, confirmation_message, cancellation_message, send_cancellation_message if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -114,32 +114,33 @@ async def tradeable(self, ctx, *, query: str): """Look up if a card is tradeable""" dbcog = self.bot.get_cog("DBCog") monster = await dbcog.find_monster(query) - if monster is None: - await ctx.send("Sorry, your query `" + query + "` didn't match any results :(") - return + if monster is None: + return await send_cancellation_message( + ctx, f"Sorry, your query `{query}` didn't match any results :(") if monster.sell_mp < 100: - ret = "\t\t✅ This card is tradable!" - else: - base_flag = False # Track other form tradability - gem_flag = False # Track evo gem tradability - - base_monster = dbcog.get_monster(monster.base_evo_id) - if base_monster.sell_mp < 100: - base_flag = True - - if monster.evo_gem_id is not None: - evo_gem: "MonsterModel" = dbcog.get_monster(monster.evo_gem_id) - if evo_gem.sell_mp < 100: - gem_flag = True - - ret = (f"\t\t{'⁉️' if base_flag or gem_flag else '❌'} This card is **not tradable**.") - if base_flag: - ret += "\n\t\t\t\t✅ Other forms of this card are tradable." - if gem_flag: - ret += "\n\t\t\t\t✅ Evo gem for this card is tradable." - elif monster.evo_gem_id is not None: - ret += "\n\t\t\t\t❌ Evo gem for this card is not tradable." - - await ctx.send("{}\n{}".format( - MonsterHeader.text_with_emoji(monster), ret)) + return await self._send_tradeable_message( + ctx, monster, indented(confirmation_message("This card is tradable!"))) + + ret = [ + indented(cancellation_message("This card is **not tradable**.")) + ] + + base_monster = dbcog.get_monster(monster.base_evo_id) + if base_monster.sell_mp < 100: + ret.append(indented(confirmation_message( + "Other forms of this card are tradable."), level=2)) + + if monster.evo_gem_id is not None: + evo_gem: "MonsterModel" = dbcog.get_monster(monster.evo_gem_id) + if evo_gem.sell_mp < 100: + ret.append(indented(confirmation_message( + "Evo gem for this card is tradable."), level=2)) + else: + ret.append(indented(cancellation_message( + "Evo gem for this card is not tradable."), level=2)) + return await self._send_tradeable_message(ctx, monster, '\n'.join(ret)) + + @staticmethod + async def _send_tradeable_message(ctx, monster: "MonsterModel", msg: str): + await ctx.send("{}\n{}".format(MonsterHeader.text_with_emoji(monster), msg)) From 820ecf08f9569a3d7cf3264d0eb9567264b42edf Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 14 May 2022 19:54:41 -0700 Subject: [PATCH 1109/1220] Fix removing invalid flags that have args (#1675) --- dbcog/find_monster.py | 3 +-- dbcog/find_monster_tokens.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index ad25e6a3..7ba13f6f 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -57,7 +57,7 @@ def _process_settings(self, original_query: str) -> str: self.index = self.dbcog.indexes[query_settings.server] - return re.sub(r'\s*(--|—)\w+(:{.+?})?\s*', ' ', original_query) + return re.sub(r'\s*(--|—)\w+(:{.+?})?(?:\s+|$)', ' ', original_query) def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: result = [] @@ -310,7 +310,6 @@ async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: tokenized_query = self._process_settings(query).split() tokenized_query = [token.replace('\0', ' ') for token in tokenized_query] mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) - best_monster, matches_dict, valid_monsters = max( await self._find_monster_search(tokenized_query), await self._find_monster_search(mw_tokenized_query) diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index 0caeea58..3747b14b 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from fnmatch import fnmatch +from itertools import chain from typing import Iterable, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union import regex as re @@ -61,7 +62,7 @@ class TokenMatch(NamedTuple): def regexlist(tokens): - return '(?:' + '|'.join(re.escape(t) for t in tokens) + ")" + return '(?:' + '|'.join(re.escape(t) for t in chain(tokens, ['$^'])) + ")" class QueryToken(ABC): From 83a4f6a740622fe23311f6b2c5df555342983485 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 17 May 2022 10:59:19 -0700 Subject: [PATCH 1110/1220] Add groups to monsters (#1677) --- crud/crud.py | 14 +++++++++++- dbcog/find_monster.py | 8 +++---- dbcog/models/monster_model.py | 1 + dbcog/monster_graph.py | 40 ++++++++++++++++++++++------------- dbcog/monster_index.py | 3 +++ dbcog/token_mappings.py | 4 +++- padinfo/padinfo.py | 14 +++++++----- 7 files changed, 57 insertions(+), 27 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 96f0ebe3..678bbb5f 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -4,7 +4,7 @@ import re from datetime import datetime from io import BytesIO -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING import aiofiles import aiomysql @@ -13,6 +13,8 @@ from redbot.core import Config, checks, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils.cogs.globaladmin import auth_check +from tsutils.enums import Server +from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, send_cancellation_message if TYPE_CHECKING: @@ -544,6 +546,16 @@ async def rmchan(self, ctx): await self.config.chan.set(None) await ctx.tick() + @commands.command(aliases=['group#', 'ghgroup#']) + async def ghgroupn(self, ctx, *, query): + dbcog: Any = self.bot.get_cog("DBCog") + if dbcog is None: + return await ctx.send("DBCog not loaded.") + monster = await dbcog.find_monster(query, ctx.author.id) + if monster is None: + return await ctx.send("No matching monster found.") + await ctx.send(f"{MonsterHeader.text_with_emoji(monster)} is in GungHo Group #{monster.group_id}") + @crud.command() async def setmyemail(self, ctx, email=None): """Sets your email so GitHub commits can be properly attributed""" diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index 7ba13f6f..a98609c1 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -7,9 +7,7 @@ from tsutils.query_settings.query_settings import QuerySettings from dbcog.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, QueryToken, SpecialToken, \ - TOKEN_JW_DISTANCE, \ - TokenMatch, \ - calc_ratio_modifier, calc_ratio_name, string_to_token + TOKEN_JW_DISTANCE, TokenMatch, calc_ratio_modifier, calc_ratio_name, string_to_token from dbcog.models.monster_model import MonsterModel SERIES_TYPE_PRIORITY = { @@ -119,8 +117,8 @@ async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[QueryT if isinstance(token, SpecialToken) or token.value in self.index.all_modifiers \ or (any(self.calc_ratio_modifier(token, m, .1) > MODIFIER_JW_DISTANCE for m in longmods) - and token.value not in self.index.all_name_tokens - and len(token.value) >= 8): + and token.value not in self.index.all_name_tokens + and len(token.value) >= 8): lastmodpos = not token.negated modifiers.append(token) else: diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index 36b426e7..f2b08a0f 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -34,6 +34,7 @@ def __init__(self, **m): self.series = m['series'] self.series_id = m['series_id'] + self.group_id = m['group_id'] self.name_ja = m['name_ja'] self.name_ko = m['name_ko'] self.name_en = self.unoverridden_name_en = m['name_en'] diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index a1f02ce8..b22622ab 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -45,10 +45,6 @@ leader_skills{0}.extra_time, leader_skills{0}.tags, COALESCE(series.series_id, 0) AS s_series_id, - COALESCE(series.name_ja, 'Unsorted') AS s_name_ja, - COALESCE(series.name_en, 'Unsorted') AS s_name_en, - COALESCE(series.name_ko, 'Unsorted') AS s_name_ko, - series.series_type AS s_series_type, exchanges.target_monster_id AS evo_gem_id, drops.drop_id FROM @@ -136,7 +132,6 @@ FROM transformations{0}""" - AWAKENINGS_QUERY = """SELECT awakenings{0}.awakening_id, awakenings{0}.monster_id, @@ -158,13 +153,19 @@ """ EXCHANGE_QUERY = """SELECT - * + * FROM exchanges WHERE start_timestamp < strftime('%s', 'now') """ +SIMPLE_QUERY = """SELECT + * +FROM + {0} +""" + SERVER_ID_WHERE_CONDITION = " AND server_id = {}" @@ -266,6 +267,21 @@ def build_graph(self, server: Server) -> MultiDiGraph: idx = int(m[1:-1]) # Remove parentheses mtoegg[idx][e_type] = True + ss_query = self.database.query_many(SIMPLE_QUERY.format('series')) + series = {} + for row in ss_query: + series[row.series_id] = SeriesModel(series_id=row.series_id, + name_ja=row.name_ja, + name_en=row.name_en, + name_ko=row.name_ko, + series_type=row.series_type + ) + + mss_query = self.database.query_many(SIMPLE_QUERY.format('monster_series')) + monster_series = defaultdict(set) + for row in mss_query: + monster_series[row.monster_id].add(series[row.series_id]) + for m in ms: if self.debug_monster_ids is not None and m.monster_id not in self.debug_monster_ids: continue @@ -287,13 +303,6 @@ def build_graph(self, server: Server) -> MultiDiGraph: tags=m.tags, ) if m.leader_skill_id != 0 else None - s_model = SeriesModel(series_id=m.s_series_id, - name_ja=m.s_name_ja, - name_en=m.s_name_en, - name_ko=m.s_name_ko, - series_type=m.s_series_type - ) - m_model = MonsterModel(monster_id=m.monster_id, base_evo_id=m.base_id, monster_no_jp=m.monster_no_jp, @@ -303,8 +312,9 @@ def build_graph(self, server: Server) -> MultiDiGraph: awakenings=mtoawo[m.monster_id], leader_skill=ls_model, active_skill=acts.get(m.active_skill_id), - series=s_model, + series=series[m.s_series_id], series_id=m.s_series_id, + group_id=m.group_id, attribute_1_id=m.attribute_1_id, attribute_2_id=m.attribute_2_id, name_ja=m.name_ja, @@ -819,7 +829,7 @@ def monster_acquisition(self, monster: MonsterModel) -> str: elif self.monster_is_pem_evo(monster): return 'PEM Card' elif self.monster_is_vem_evo(monster): - return 'AdPem Card' + return 'AdPEM Card' def numeric_next_monster(self, monster: MonsterModel) -> Optional[MonsterModel]: next_monster = None diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index edc70085..a79dbfb1 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -376,6 +376,9 @@ async def get_modifiers(self, monster: MonsterModel): if monster.series_id in self.series_id_to_pantheon_nickname: modifiers.update(self.series_id_to_pantheon_nickname[monster.series_id]) + # Group + modifiers.add("_group" + str(monster.group_id)) + # Rarity modifiers.add(str(monster.rarity) + "*") modifiers.add(str(basemon.rarity) + "*b") diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index c482776e..b2068579 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -305,13 +305,15 @@ class MiscModifiers(Enum): } NUMERIC_MONSTER_ATTRIBUTE_ALIASES = { - (('monster_no_na',),): ('monsterid', 'monsterno'), + (('monster_no_na',),): ('monsterid', 'monsterno', 'monster#'), (('base_evo_id',),): ('baseid',), (('superawakening_count',),): ('sacount',), (('leader_skill', 'leader_skill_id'),): ('lsid',), (('active_skill', 'active_skill_id'),): ('asid',), (('series', 'series_id'),): ('sid', 'seriesid'), (('rarity',),): ('rarity', 'rare'), + (('series_id',),): ('seriesid', 'series#'), + (('rcv_min',),): ('minrcv',), (('buy_mp',),): ('buymp',), (('sell_mp',),): ('sellmp',), (('sell_gold',),): ('gold', 'coins'), diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 65500423..91efd02d 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -6,7 +6,7 @@ import re from enum import EnumMeta from io import BytesIO -from typing import List, Optional, TYPE_CHECKING, Type +from typing import Callable, List, Optional, TYPE_CHECKING, Type import discord from discord import Color @@ -1166,6 +1166,10 @@ async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, quer mods = dbcog.indexes[server].modifiers[mon] manual_modifiers = dbcog.indexes[server].manual_modifiers[mon.monster_id] EVOANDTYPE = dbcog.token_maps.EVO_TOKENS.union(dbcog.token_maps.TYPE_TOKENS) + + def mod_token_str(f: Callable[[str], bool]) -> str: + return ' '.join(sorted(t for t in mods if f(t) and not t.startswith('_'))) + ret = (f"[{mon.monster_id}] {mon.name_en}\n" f"Base: [{base_monster.monster_id}] {base_monster.name_en}\n" f"Series: {mon.series.name_en} ({mon.series_id}, {mon.series.series_type})\n\n" @@ -1175,10 +1179,10 @@ async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, quer f" Treenames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_treenames.items() if mon in ms))}\n" f" Nicknames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_cardnames.items() if mon in ms))}\n\n" f"[Modifier Tokens]\n" - f" Attribute: {' '.join(sorted(t for t in mods if t.split('-')[0] in dbcog.token_maps.COLOR_TOKENS))}\n" - f" Awakening: {' '.join(sorted(t for t in mods if t.split('-')[0] in dbcog.token_maps.AWAKENING_TOKENS))}\n" - f" Evo & Type: {' '.join(sorted(t for t in mods if t.split('-')[0] in EVOANDTYPE))}\n" - f" Other: {' '.join(sorted(t for t in mods if t.split('-')[0] not in dbcog.token_maps.OTHER_HIDDEN_TOKENS))}\n" + f" Attribute: {mod_token_str(lambda t: t.split('-')[0] in dbcog.token_maps.COLOR_TOKENS)}\n" + f" Awakening: {mod_token_str(lambda t: t.split('-')[0] in dbcog.token_maps.AWAKENING_TOKENS)}\n" + f" Evo & Type: {mod_token_str(lambda t: t.split('-')[0] in EVOANDTYPE)}\n" + f" Other: {mod_token_str(lambda t: t.split('-')[0] not in dbcog.token_maps.OTHER_HIDDEN_TOKENS)}\n" f"Manually Added: {' '.join(sorted(manual_modifiers))}\n") for page in pagify(ret): await ctx.send(box(page)) From 23dd886b6d8339beb86d33c4c5625aec8441d452 Mon Sep 17 00:00:00 2001 From: AA Date: Thu, 19 May 2022 06:41:04 -0700 Subject: [PATCH 1111/1220] Add secondary series to CRUD and graph (#1678) * Add editseries * Last changes * Remove newline * Fix affected rows --- crud/crud.py | 103 ++++--------- crud/editseries.py | 275 ++++++++++++++++++++++++++++++++++ dbcog/models/monster_model.py | 3 + dbcog/monster_graph.py | 9 ++ dbcog/monster_index.py | 7 +- 5 files changed, 319 insertions(+), 78 deletions(-) create mode 100644 crud/editseries.py diff --git a/crud/crud.py b/crud/crud.py index 678bbb5f..1a0fa4cf 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -17,6 +17,8 @@ from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, send_cancellation_message +from crud.editseries import EditSeries + if TYPE_CHECKING: from dbcog import DBCog @@ -54,7 +56,7 @@ async def check_crud_channel(ctx): return chan is None or chan == ctx.channel.id or ctx.author.id in ctx.bot.owner_ids -class Crud(commands.Cog): +class Crud(commands.Cog, EditSeries): """PadGuide CRUD""" def __init__(self, bot, *args, **kwargs): @@ -64,7 +66,7 @@ def __init__(self, bot, *args, **kwargs): self.config.register_global(config_file=None, pipeline_base=None, chan=None) self.config.register_user(email=None) - GADMIN_COG = self.bot.get_cog("GlobalAdmin") + GADMIN_COG: Any = self.bot.get_cog("GlobalAdmin") if GADMIN_COG: GADMIN_COG.register_perm("crud") else: @@ -89,7 +91,7 @@ async def get_cursor(self): return (await self.connect(json.loads(await db_config.read()))).cursor() async def get_dbcog(self) -> "DBCog": - dbcog = self.bot.get_cog("DBCog") + dbcog: "DBCog" = self.bot.get_cog("DBCog") if dbcog is None: raise ValueError("DBCog cog is not loaded") await dbcog.wait_until_ready() @@ -97,17 +99,17 @@ async def get_dbcog(self) -> "DBCog": def connect(self, db_config): return aiomysql.connect(host=db_config['host'], - user=db_config['user'], - password=db_config['password'], - db=db_config['db'], - charset=db_config['charset'], - cursorclass=aiomysql.cursors.DictCursor, - autocommit=True) + user=db_config['user'], + password=db_config['password'], + db=db_config['db'], + charset=db_config['charset'], + cursorclass=aiomysql.cursors.DictCursor, + autocommit=True) async def execute_write(self, ctx, sql, replacements): async with await self.get_cursor() as cursor: affected = await cursor.execute(sql, replacements) - await ctx.send(inline(cursor._executed)) + await ctx.send(inline(sql % replacements)) await ctx.send("{} row(s) affected.".format(affected)) async def execute_read(self, ctx, sql, replacements): @@ -115,7 +117,7 @@ async def execute_read(self, ctx, sql, replacements): await cursor.execute(sql, replacements) results = list(await cursor.fetchall()) msg = 'Results\n' + json.dumps(results, indent=2, ensure_ascii=False, sort_keys=True) - await ctx.send(inline(cursor._executed)) + await ctx.send(inline(sql % replacements)) for page in pagify(msg): await ctx.send(box(page)) @@ -164,7 +166,7 @@ async def git_verify(self, ctx, filepath): async def crud(self, ctx): """PadGuide database CRUD.""" - @crud.command() + @commands.command() async def editmonsname(self, ctx, monster_id: int, *, name): """Change a monster's name_en_override @@ -189,8 +191,7 @@ async def editmonsname(self, ctx, monster_id: int, *, name): old_val = await cursor.fetchall() if not old_val: - await ctx.send(f"There is no monster with id {monster_id}") - return + return await ctx.send(f"There is no monster with id {monster_id}") monster_name = old_val[0]['monster_name'] old_name = old_val[0]['old_override'] if not await get_user_confirmation(ctx, (f"Are you sure you want to change `{monster_name} ({monster_id})`'s" @@ -201,41 +202,6 @@ async def editmonsname(self, ctx, monster_id: int, *, name): f' VALUES (%s, %s, 1, UNIX_TIMESTAMP())') await self.execute_write(ctx, sql, (monster_id, name)) - @crud.command() - async def editmonsseries(self, ctx, monster_id: int, series_id: int): - """Change a monster's main series_id""" - async with await self.get_cursor() as cursor: - await cursor.execute("""SELECT - COALESCE(mno.name_en, monsters.name_en) AS monster_name, - series.name_en AS series_name - FROM monster_series - JOIN monsters ON monsters.monster_id = monster_series.monster_id - JOIN series ON series.series_id = monster_series.series_id - LEFT JOIN monster_name_overrides mno - ON mno.monster_id = monsters.monster_id AND is_translation = 1 - WHERE monsters.monster_id = %s AND priority = 1""", (monster_id,)) - rows = await cursor.fetchall() - if not rows: - await ctx.send("There is no monster with id: {}".format(monster_id)) - return - monster_name = rows[0]['monster_name'] - old_series = rows[0]['series_name'] - - await cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) - rows = await cursor.fetchall() - if not rows: - await ctx.send(f"There is no series with id {series_id}") - return - new_series = rows[0]['name_en'] - - if not await get_user_confirmation(ctx, (f"Are you sure you want to change `{monster_name} ({monster_id})`'s" - f" series id from `{old_series}` to `{new_series}`?")): - return - - sql = (f'UPDATE monster_series SET series_id = %s, tstamp = UNIX_TIMESTAMP()' - f' WHERE monster_id = %s AND priority = 1') - await self.execute_write(ctx, sql, (series_id, monster_id)) - @crud.command() async def searchdungeon(self, ctx, *, search_text): """Search for a dungeon via its jp or na name""" @@ -273,12 +239,10 @@ async def series_add(self, ctx, *elements): elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in SERIES_KEYS for x in elements)): - await ctx.send_help() - return + return await ctx.send_help() if "series_type" in elements and elements['series_type'] not in SERIES_TYPES: - await ctx.send("`series_type` must be one of: " + ", ".join(SERIES_TYPES)) - return + return await ctx.send("`series_type` must be one of: " + ", ".join(SERIES_TYPES)) EXTRAS = {} async with await self.get_cursor() as cursor: @@ -319,17 +283,14 @@ async def series_edit(self, ctx, series_id: int, *elements): [p]crud series edit 100 key1 "Value1" key2 "Value2" """ if len(elements) % 2 != 0: - await ctx.send_help() - return + return await ctx.send_help() elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in SERIES_KEYS for x in elements)): - await ctx.send_help() - return + return await ctx.send_help() if "series_type" in elements and elements['series_type'] not in SERIES_TYPES: - await ctx.send("`series_type` must be one of: " + ", ".join(SERIES_TYPES)) - return + return await ctx.send("`series_type` must be one of: " + ", ".join(SERIES_TYPES)) replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) sql = ('UPDATE series' @@ -398,17 +359,14 @@ async def awokenskill_add(self, ctx, *elements): [p]crud awokenskill add key1 "Value1" key2 "Value2" """ if len(elements) % 2 != 0: - await ctx.send_help() - return + return await ctx.send_help() elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in AWOKEN_SKILL_KEYS for x in elements)): - await ctx.send_help() - return + return await ctx.send_help() if "awoken_skill_id" not in elements or not elements["awoken_skill_id"].isdigit(): - await ctx.send("You must supply an numeric `awoken_skill_id` when adding a new awoken skill.") - return + return await ctx.send("You must supply an numeric `awoken_skill_id` when adding a new awoken skill.") elements["awoken_skill_id"] = int(elements["awoken_skill_id"]) EXTRAS = { @@ -467,23 +425,19 @@ async def awokenskill_edit(self, ctx, awoken_skill, *elements): elif awoken_skill.isdigit(): awoken_skill_id = int(awoken_skill) else: - await ctx.send("Invalid awoken skill.") - return + return await ctx.send("Invalid awoken skill.") awoken_skill_id = int(awoken_skill_id) if len(elements) % 2 != 0: - await ctx.send_help() - return + return await ctx.send_help() elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in AWOKEN_SKILL_KEYS for x in elements)): - await ctx.send_help() - return + return await ctx.send_help() if 'awoken_skill_id' in elements: - await ctx.send("`awoken_skill_id` is not a supported key for editing.") - return + return await ctx.send("`awoken_skill_id` is not a supported key for editing.") replacement_infix = ", ".join(["{} = %s".format(k) for k in elements.keys()]) sql = ('UPDATE awoken_skills' @@ -561,7 +515,6 @@ async def setmyemail(self, ctx, email=None): """Sets your email so GitHub commits can be properly attributed""" if email is not None and \ not re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email): - await ctx.send("Invalid email.") - return + return await ctx.send("Invalid email.") await self.config.user(ctx.author).email.set(email) await ctx.tick() diff --git a/crud/editseries.py b/crud/editseries.py new file mode 100644 index 00000000..658bdad9 --- /dev/null +++ b/crud/editseries.py @@ -0,0 +1,275 @@ +import logging +import re +from collections import defaultdict +from typing import Callable, Coroutine, Iterable, List, Literal, Sequence, Set, TYPE_CHECKING, Tuple, TypeVar + +from aiomysql import Cursor +from discord.ext.commands import BadArgument, Converter +from redbot.core import Config +from redbot.core.bot import Red +from redbot.core.commands import commands +from tsutils.enums import Server +from tsutils.errors import ClientInlineTextException +from tsutils.tsubaki.monster_header import MonsterHeader +from tsutils.user_interaction import get_user_confirmation + +if TYPE_CHECKING: + from dbcog import DBCog + from dbcog.models.monster_model import MonsterModel + +logger = logging.getLogger('red.padbot-cogs.crud.editseries') +T = TypeVar("T") + +if not TYPE_CHECKING: + class AliasedKeywordConverter(Converter): + def __init__(self, aliases): + self.aliases = aliases + + async def convert(self, ctx, argument) -> str: + for kw, aliases in self.aliases.items(): + if argument == kw or (aliases is not None and argument in aliases): + return kw + raise BadArgument(f"Bad keyword `{argument}`." + f" Valid keywords are `{'`, `'.join(self.aliases)}`.") + + + EditSeriesCommand = AliasedKeywordConverter({ + 'add': None, + 'remove': ('rm', 'delete', 'del'), + 'promote': None, + }) + EditSeriesTarget = AliasedKeywordConverter({ + 'monster': ('monsters'), + 'tree': None, + 'group': None, + }) +else: + EditSeriesCommand = Literal['add', 'remove', 'promote'] + EditSeriesTarget = Literal['monster', 'tree', 'group'] + + +def disjoint_sets(superset: Iterable["MonsterModel"], f: Callable[["MonsterModel"], bool])\ + -> Tuple[List["MonsterModel"], List["MonsterModel"]]: + a, b = set(), set() + for e in superset: + (a if f(e) else b).add(e) + return sorted(a, key=lambda m: m.monster_id), sorted(b, key=lambda m: m.monster_id) + + +class EditSeries: + config: Config + bot: Red + get_cursor: Callable[[], Coroutine[None, None, Cursor]] + get_dbcog: Callable[[], Coroutine[None, None, "DBCog"]] + + async def es_execute_write(self, ctx, sql: str, replacements: Sequence = ()) -> int: + async with await self.get_cursor() as cursor: + affected = 0 + for mon in re.split(r'\n\s*\n', sql): + changed = 0 + for query in mon.split(';'): + if query.strip(): + changed += await cursor.execute(query, replacements) + affected += bool(changed) + printable = re.sub(r'\n[ \t]+', r'\n', sql % replacements) + printable = re.sub(r'\n', r'\n\t', printable).strip() + logger.info(f"{ctx.author} executed the following query via {ctx.message.content}:" + f"\n{printable}" + f"\nwhich affected {affected} monster(s).") + await ctx.send("{} monster(s) affected.".format(affected)) + return affected + + @commands.command() + async def editseries(self, ctx, command: EditSeriesCommand, series_id: int, + target: EditSeriesTarget, *, target_str): + """Affect the series of a target. + + Commands: + add - Add a series. If this is the only series, add as a primary series + remove - Remove a series. If this is the primary series, randomly choose another to take its place + promote - Add a series as primary, possibly demoting an existing primary in the process + + Targets: + monster/monsters - One or more monsters specified by id + tree - A monster tree specified by a monster id of any monster in the tree + group - A GH group specified by a group id + + Examples: + [p]editseries add 91 monsters 1319, 1825 + [p]editseries rm 7 tree 1 + [p]editseries promote 28 group 112 + """ + + async def tryint(arg): + if not arg.isnumeric(): + await ctx.send_help() + raise ClientInlineTextException() + + monsters = None + if target == 'monster': + dbcog = await self.get_dbcog() + monsters = [dbcog.get_monster(int(mid)) for mid in re.split(r'\D+', target_str)] + elif target == 'tree': + dbcog = await self.get_dbcog() + monsters = dbcog.database.graph.get_alt_monsters(dbcog.get_monster(int(target_str))) + elif target == 'group': + dbcog = await self.get_dbcog() + monsters = dbcog.database.get_monsters_where(lambda m: m.group_id == int(target_str), + server=Server.COMBINED) + + if command == 'add': + await self.es_add(ctx, series_id, monsters) + elif command == 'remove': + await self.es_remove(ctx, series_id, monsters) + elif command == 'promote': + await self.es_promote(ctx, series_id, monsters) + + async def es_add(self, ctx, series_id: int, monsters: List["MonsterModel"]): + monsters_str = "(" + ",".join(str(m.monster_id) for m in monsters) + ")" + async with await self.get_cursor() as cursor: + await cursor.execute(f""" + SELECT + monster_id, series_id + FROM monster_series + WHERE monster_id IN {monsters_str}""", ()) + rows = await cursor.fetchall() + sids = defaultdict(set) + for row in rows: + sids[row['monster_id']].add(row['series_id']) + print(sids) + secondary, primary = disjoint_sets(monsters, lambda m: m.monster_id in sids) + seen, secondary = disjoint_sets(secondary, lambda m: series_id in sids[m.monster_id]) + await cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) + rows = await cursor.fetchall() + if not rows: + return await ctx.send(f"There is no series with id {series_id}") + new_series = next(iter(rows))['name_en'] + sql = "" + for monster in monsters: + if monster in seen: + continue + mid = monster.monster_id + sql += f""" + INSERT INTO monster_series + (monster_series_id, monster_id, series_id, priority, tstamp) + VALUES + ({series_id * 100000 + mid}, {mid}, {series_id}, {int(mid in sids)}, UNIX_TIMESTAMP()); + """ + confirmation = [f"You are adding series `{new_series}`."] + if primary: + confirmation.append(f"The following monsters will have the series added as a primary series:" + f" {', '.join(map(MonsterHeader.text_with_emoji, primary))}.") + if secondary: + confirmation.append(f"The following monsters will have the series added as a secondary series:" + f" {', '.join(map(MonsterHeader.text_with_emoji, secondary))}.") + if seen: + confirmation.append(f"The following monsters will not be changed as they already have the series:" + f" {', '.join(map(MonsterHeader.text_with_emoji, seen))}.") + if not await get_user_confirmation(ctx, '\n'.join(confirmation), + timeout=30, force_delete=False): + return + await self.es_execute_write(ctx, sql) + + async def es_remove(self, ctx, series_id: int, monsters: List["MonsterModel"]): + monsters_str = "(" + ",".join(str(m.monster_id) for m in monsters) + ")" + async with await self.get_cursor() as cursor: + await cursor.execute(f""" + SELECT + monster_id, series_id, priority + FROM monster_series + WHERE monster_id IN {monsters_str}""", ()) + rows = await cursor.fetchall() + sids = defaultdict(set) + primaries = {} + for row in rows: + sids[row['monster_id']].add(row['series_id']) + if row['priority']: + primaries[row['monster_id']] = row['series_id'] + await cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) + rows = await cursor.fetchall() + was_primary, wasnt_primary = disjoint_sets(monsters, lambda m: primaries[m.monster_id] == series_id) + unadded, wasnt_primary = disjoint_sets(wasnt_primary, lambda m: series_id not in sids[m.monster_id]) + if not rows: + return await ctx.send(f"There is no series with id {series_id}") + new_series = next(iter(rows))['name_en'] + sql = "" + for monster in monsters: + if monster in unadded: + continue + mid = monster.monster_id + sql += f""" + DELETE FROM monster_series + WHERE monster_id = {mid} AND series_id = {series_id}; + """ + if monster in was_primary: + sql += f"""UPDATE monster_series + SET priority = 1 + WHERE monster_id = {mid} + LIMIT 1; + """ + confirmation = [f"You are removing series `{new_series}`."] + if was_primary: + confirmation.append(f"The following monsters will have their primary series removed:" + f" {', '.join(map(MonsterHeader.text_with_emoji, was_primary))}.") + if wasnt_primary: + confirmation.append(f"The following monsters will have their secondary series removed:" + f" {', '.join(map(MonsterHeader.text_with_emoji, wasnt_primary))}.") + if unadded: + confirmation.append(f"The following monsters will continue to not have the series:" + f" {', '.join(map(MonsterHeader.text_with_emoji, unadded))}.") + if not await get_user_confirmation(ctx, '\n'.join(confirmation), + timeout=30, force_delete=False): + return + await self.es_execute_write(ctx, sql) + + async def es_promote(self, ctx, series_id: int, monsters: List["MonsterModel"]): + monsters_str = "(" + ",".join(str(m.monster_id) for m in monsters) + ")" + async with await self.get_cursor() as cursor: + await cursor.execute(f""" + SELECT + monster_id, series_id, priority + FROM monster_series + WHERE monster_id IN {monsters_str}""", ()) + rows = await cursor.fetchall() + await cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) + mids = [row['monster_id'] for row in rows] + primaries = {} + for row in rows: + if row['priority']: + primaries[row['monster_id']] = row['series_id'] + has_series, no_series = disjoint_sets(monsters, lambda m: m.monster_id in mids) + already_primary, has_series = disjoint_sets(has_series, lambda m: primaries[m.monster_id] == series_id) + rows = await cursor.fetchall() + if not rows: + return await ctx.send(f"There is no series with id {series_id}") + new_series = next(iter(rows))['name_en'] + sql = "" + for monster in monsters: + if monster in already_primary: + continue + mid = monster.monster_id + sql += f""" + DELETE FROM monster_series + WHERE monster_id = {mid} AND series_id = {series_id}; + UPDATE monster_series + SET priority = 0 + WHERE monster_id = {mid}; + INSERT INTO monster_series + (monster_series_id, monster_id, series_id, priority, tstamp) + VALUES + ({series_id * 100000 + mid}, {mid}, {series_id}, 1, UNIX_TIMESTAMP()); + """ + confirmation = [f"You are promoting series `{new_series}`."] + if has_series: + confirmation.append(f"The following monsters will have this primary series added:" + f" {', '.join(map(MonsterHeader.text_with_emoji, has_series))}.") + if no_series: + confirmation.append(f"The following monsters will no longer be unsorted:" + f" {', '.join(map(MonsterHeader.text_with_emoji, no_series))}.") + if already_primary: + confirmation.append(f"The following monsters will not be changed:" + f" {', '.join(map(MonsterHeader.text_with_emoji, already_primary))}.") + if not await get_user_confirmation(ctx, '\n'.join(confirmation), + timeout=30, force_delete=False): + return + await self.es_execute_write(ctx, sql) diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index f2b08a0f..e38293ca 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -1,5 +1,6 @@ import re from collections import defaultdict +from typing import Set import romkan from tsutils.enums import Server @@ -11,6 +12,7 @@ from .leader_skill_model import LeaderSkillModel from .monster.monster_difference import MonsterDifference from .monster_stats import monster_stats +from .series_model import SeriesModel class MonsterModel(BaseModel): @@ -33,6 +35,7 @@ def __init__(self, **m): self.active_skill_id = self.active_skill.active_skill_id if self.active_skill else None self.series = m['series'] + self.all_series: Set[SeriesModel] = m['all_series'] self.series_id = m['series_id'] self.group_id = m['group_id'] self.name_ja = m['name_ja'] diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index b22622ab..fae9f065 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -313,6 +313,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: leader_skill=ls_model, active_skill=acts.get(m.active_skill_id), series=series[m.s_series_id], + all_series=monster_series[m.monster_id], series_id=m.s_series_id, group_id=m.group_id, attribute_1_id=m.attribute_1_id, @@ -863,6 +864,14 @@ def monster_is_evo_gem(self, monster: MonsterModel) -> bool: # Evo gems can no longer be calculated via exchange as of Pixel Volcano Dragon return monster.name_en.endswith("'s Gem") or monster.name_ja.endswith("の希石") + def monster_is_gfe_exchange(self, monster: MonsterModel, stars: Optional[int] = None) -> bool: + monsters = {self.get_monster(mid, server=monster.server_priority) + for exc in self.get_monster_exchange_models(monster) + for mid in exc.required_monster_ids} + if stars is not None: + monsters = {m for m in monsters if m.rarity == stars} + return any(s.series_id == 34 for m in monsters for s in m.all_series) + def material_of_ids(self, monster: MonsterModel) -> List[int]: return sorted(self._get_edges(monster, 'material_of')) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index a79dbfb1..4bde65ec 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -372,9 +372,10 @@ async def get_modifiers(self, monster: MonsterModel): modifiers.update(TYPE_MAP[mt]) # Series - modifiers.add("series" + str(monster.series_id)) - if monster.series_id in self.series_id_to_pantheon_nickname: - modifiers.update(self.series_id_to_pantheon_nickname[monster.series_id]) + for series in monster.all_series: + modifiers.add("series" + str(series.series_id)) + if series.series_id in self.series_id_to_pantheon_nickname: + modifiers.update(self.series_id_to_pantheon_nickname[series.series_id]) # Group modifiers.add("_group" + str(monster.group_id)) From 201f0b0c4f7c4cab8043a1cdaf080c4a0cd3bc96 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 16:49:59 -0700 Subject: [PATCH 1112/1220] Add unsorted series to unsorted cards (#1681) --- dbcog/monster_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index fae9f065..dc6bf911 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -313,7 +313,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: leader_skill=ls_model, active_skill=acts.get(m.active_skill_id), series=series[m.s_series_id], - all_series=monster_series[m.monster_id], + all_series=monster_series[m.monster_id] or {series[0]}, series_id=m.s_series_id, group_id=m.group_id, attribute_1_id=m.attribute_1_id, From 29cb90e8cdd6fee13458abc68e67756a7eda0b13 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 17:00:38 -0700 Subject: [PATCH 1113/1220] Allow multiple trees and groups (#1682) * Allow multiple trees and groups * Shout on no matches --- crud/editseries.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/crud/editseries.py b/crud/editseries.py index 658bdad9..5606a32e 100644 --- a/crud/editseries.py +++ b/crud/editseries.py @@ -1,7 +1,7 @@ import logging import re from collections import defaultdict -from typing import Callable, Coroutine, Iterable, List, Literal, Sequence, Set, TYPE_CHECKING, Tuple, TypeVar +from typing import Callable, Coroutine, Iterable, List, Literal, Sequence, TYPE_CHECKING, Tuple, TypeVar from aiomysql import Cursor from discord.ext.commands import BadArgument, Converter @@ -40,15 +40,15 @@ async def convert(self, ctx, argument) -> str: }) EditSeriesTarget = AliasedKeywordConverter({ 'monster': ('monsters'), - 'tree': None, - 'group': None, + 'tree': ('trees'), + 'group': ('groups'), }) else: EditSeriesCommand = Literal['add', 'remove', 'promote'] EditSeriesTarget = Literal['monster', 'tree', 'group'] -def disjoint_sets(superset: Iterable["MonsterModel"], f: Callable[["MonsterModel"], bool])\ +def disjoint_sets(superset: Iterable["MonsterModel"], f: Callable[["MonsterModel"], bool]) \ -> Tuple[List["MonsterModel"], List["MonsterModel"]]: a, b = set(), set() for e in superset: @@ -71,10 +71,10 @@ async def es_execute_write(self, ctx, sql: str, replacements: Sequence = ()) -> if query.strip(): changed += await cursor.execute(query, replacements) affected += bool(changed) - printable = re.sub(r'\n[ \t]+', r'\n', sql % replacements) - printable = re.sub(r'\n', r'\n\t', printable).strip() + display = re.sub(r'\n[ \t]+', r'\n', sql % replacements) + display = re.sub(r'\n', r'\n\t', display).strip() logger.info(f"{ctx.author} executed the following query via {ctx.message.content}:" - f"\n{printable}" + f"\n{display}" f"\nwhich affected {affected} monster(s).") await ctx.send("{} monster(s) affected.".format(affected)) return affected @@ -90,9 +90,9 @@ async def editseries(self, ctx, command: EditSeriesCommand, series_id: int, promote - Add a series as primary, possibly demoting an existing primary in the process Targets: - monster/monsters - One or more monsters specified by id - tree - A monster tree specified by a monster id of any monster in the tree - group - A GH group specified by a group id + monster(s) - Monsters specified by id + tree(s) - Monster trees specified by monster ids of any monster in the tree + group(s) - GH groups specified by a group ids Examples: [p]editseries add 91 monsters 1319, 1825 @@ -105,17 +105,23 @@ async def tryint(arg): await ctx.send_help() raise ClientInlineTextException() + dbcog = await self.get_dbcog() + monsters = None if target == 'monster': - dbcog = await self.get_dbcog() - monsters = [dbcog.get_monster(int(mid)) for mid in re.split(r'\D+', target_str)] + monsters = [dbcog.get_monster(int(mid)) + for mid in re.split(r'\D+', target_str)] elif target == 'tree': - dbcog = await self.get_dbcog() - monsters = dbcog.database.graph.get_alt_monsters(dbcog.get_monster(int(target_str))) + monsters = [monster + for bmid in re.split(r'\D+', target_str) + for monster in dbcog.database.graph.get_alt_monsters(dbcog.get_monster(int(bmid)))] elif target == 'group': - dbcog = await self.get_dbcog() - monsters = dbcog.database.get_monsters_where(lambda m: m.group_id == int(target_str), - server=Server.COMBINED) + monsters = [monster + for gid in re.split(r'\D+', target_str) + for monster in dbcog.database.get_monsters_where(lambda m: m.group_id == int(gid), + server=Server.COMBINED)] + if not monsters: + return await ctx.send("No matching monsters found.") if command == 'add': await self.es_add(ctx, series_id, monsters) @@ -136,7 +142,6 @@ async def es_add(self, ctx, series_id: int, monsters: List["MonsterModel"]): sids = defaultdict(set) for row in rows: sids[row['monster_id']].add(row['series_id']) - print(sids) secondary, primary = disjoint_sets(monsters, lambda m: m.monster_id in sids) seen, secondary = disjoint_sets(secondary, lambda m: series_id in sids[m.monster_id]) await cursor.execute("SELECT name_en FROM series WHERE series_id = %s", (series_id,)) From e87a38c86e32f7817674279c7d456bdaac9d5e6c Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 17:08:03 -0700 Subject: [PATCH 1114/1220] [EditSeries] Display changes more clearly (#1683) --- crud/editseries.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crud/editseries.py b/crud/editseries.py index 5606a32e..57183524 100644 --- a/crud/editseries.py +++ b/crud/editseries.py @@ -19,6 +19,7 @@ logger = logging.getLogger('red.padbot-cogs.crud.editseries') T = TypeVar("T") +CR = '\n' if not TYPE_CHECKING: class AliasedKeywordConverter(Converter): @@ -163,13 +164,13 @@ async def es_add(self, ctx, series_id: int, monsters: List["MonsterModel"]): confirmation = [f"You are adding series `{new_series}`."] if primary: confirmation.append(f"The following monsters will have the series added as a primary series:" - f" {', '.join(map(MonsterHeader.text_with_emoji, primary))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, primary))}.") if secondary: confirmation.append(f"The following monsters will have the series added as a secondary series:" - f" {', '.join(map(MonsterHeader.text_with_emoji, secondary))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, secondary))}.") if seen: confirmation.append(f"The following monsters will not be changed as they already have the series:" - f" {', '.join(map(MonsterHeader.text_with_emoji, seen))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, seen))}.") if not await get_user_confirmation(ctx, '\n'.join(confirmation), timeout=30, force_delete=False): return @@ -215,13 +216,13 @@ async def es_remove(self, ctx, series_id: int, monsters: List["MonsterModel"]): confirmation = [f"You are removing series `{new_series}`."] if was_primary: confirmation.append(f"The following monsters will have their primary series removed:" - f" {', '.join(map(MonsterHeader.text_with_emoji, was_primary))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, was_primary))}.") if wasnt_primary: confirmation.append(f"The following monsters will have their secondary series removed:" - f" {', '.join(map(MonsterHeader.text_with_emoji, wasnt_primary))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, wasnt_primary))}.") if unadded: confirmation.append(f"The following monsters will continue to not have the series:" - f" {', '.join(map(MonsterHeader.text_with_emoji, unadded))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, unadded))}.") if not await get_user_confirmation(ctx, '\n'.join(confirmation), timeout=30, force_delete=False): return @@ -267,13 +268,13 @@ async def es_promote(self, ctx, series_id: int, monsters: List["MonsterModel"]): confirmation = [f"You are promoting series `{new_series}`."] if has_series: confirmation.append(f"The following monsters will have this primary series added:" - f" {', '.join(map(MonsterHeader.text_with_emoji, has_series))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, has_series))}.") if no_series: confirmation.append(f"The following monsters will no longer be unsorted:" - f" {', '.join(map(MonsterHeader.text_with_emoji, no_series))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, no_series))}.") if already_primary: confirmation.append(f"The following monsters will not be changed:" - f" {', '.join(map(MonsterHeader.text_with_emoji, already_primary))}.") + f" {CR.join(map(MonsterHeader.text_with_emoji, already_primary))}.") if not await get_user_confirmation(ctx, '\n'.join(confirmation), timeout=30, force_delete=False): return From 3f97cef93aad0fd0cc06fce164db3917f89b9cda Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 17:10:21 -0700 Subject: [PATCH 1115/1220] Display first on new line (#1684) --- crud/editseries.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crud/editseries.py b/crud/editseries.py index 57183524..ae250d46 100644 --- a/crud/editseries.py +++ b/crud/editseries.py @@ -163,13 +163,13 @@ async def es_add(self, ctx, series_id: int, monsters: List["MonsterModel"]): """ confirmation = [f"You are adding series `{new_series}`."] if primary: - confirmation.append(f"The following monsters will have the series added as a primary series:" + confirmation.append(f"The following monsters will have the series added as a primary series:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, primary))}.") if secondary: - confirmation.append(f"The following monsters will have the series added as a secondary series:" + confirmation.append(f"The following monsters will have the series added as a secondary series:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, secondary))}.") if seen: - confirmation.append(f"The following monsters will not be changed as they already have the series:" + confirmation.append(f"The following monsters will not be changed as they already have the series:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, seen))}.") if not await get_user_confirmation(ctx, '\n'.join(confirmation), timeout=30, force_delete=False): @@ -215,13 +215,13 @@ async def es_remove(self, ctx, series_id: int, monsters: List["MonsterModel"]): """ confirmation = [f"You are removing series `{new_series}`."] if was_primary: - confirmation.append(f"The following monsters will have their primary series removed:" + confirmation.append(f"The following monsters will have their primary series removed:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, was_primary))}.") if wasnt_primary: - confirmation.append(f"The following monsters will have their secondary series removed:" + confirmation.append(f"The following monsters will have their secondary series removed:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, wasnt_primary))}.") if unadded: - confirmation.append(f"The following monsters will continue to not have the series:" + confirmation.append(f"The following monsters will continue to not have the series:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, unadded))}.") if not await get_user_confirmation(ctx, '\n'.join(confirmation), timeout=30, force_delete=False): @@ -267,13 +267,13 @@ async def es_promote(self, ctx, series_id: int, monsters: List["MonsterModel"]): """ confirmation = [f"You are promoting series `{new_series}`."] if has_series: - confirmation.append(f"The following monsters will have this primary series added:" + confirmation.append(f"The following monsters will have this primary series added:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, has_series))}.") if no_series: - confirmation.append(f"The following monsters will no longer be unsorted:" + confirmation.append(f"The following monsters will no longer be unsorted:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, no_series))}.") if already_primary: - confirmation.append(f"The following monsters will not be changed:" + confirmation.append(f"The following monsters will not be changed:\n" f" {CR.join(map(MonsterHeader.text_with_emoji, already_primary))}.") if not await get_user_confirmation(ctx, '\n'.join(confirmation), timeout=30, force_delete=False): From 65d9548f5b119bb8de50393202c5bf7ba35cab35 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 17:18:12 -0700 Subject: [PATCH 1116/1220] Add clarity to docstring and remove unnecessary whitespace (#1685) --- crud/editseries.py | 50 +++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/crud/editseries.py b/crud/editseries.py index ae250d46..72204aaf 100644 --- a/crud/editseries.py +++ b/crud/editseries.py @@ -91,12 +91,12 @@ async def editseries(self, ctx, command: EditSeriesCommand, series_id: int, promote - Add a series as primary, possibly demoting an existing primary in the process Targets: - monster(s) - Monsters specified by id - tree(s) - Monster trees specified by monster ids of any monster in the tree - group(s) - GH groups specified by a group ids + monster - Monsters specified by id + tree - Monster trees specified by monster ids of any monster in the tree + group - GH groups specified by a group ids Examples: - [p]editseries add 91 monsters 1319, 1825 + [p]editseries add 91 monster 1319, 1825 [p]editseries rm 7 tree 1 [p]editseries promote 28 group 112 """ @@ -163,15 +163,15 @@ async def es_add(self, ctx, series_id: int, monsters: List["MonsterModel"]): """ confirmation = [f"You are adding series `{new_series}`."] if primary: - confirmation.append(f"The following monsters will have the series added as a primary series:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, primary))}.") + confirmation.append("The following monsters will have the series added as a primary series:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, primary))) if secondary: - confirmation.append(f"The following monsters will have the series added as a secondary series:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, secondary))}.") + confirmation.append("The following monsters will have the series added as a secondary series:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, secondary))) if seen: - confirmation.append(f"The following monsters will not be changed as they already have the series:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, seen))}.") - if not await get_user_confirmation(ctx, '\n'.join(confirmation), + confirmation.append("The following monsters will not be changed as they already have the series:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, seen))) + if not await get_user_confirmation(ctx, '\n\n'.join(confirmation), timeout=30, force_delete=False): return await self.es_execute_write(ctx, sql) @@ -215,15 +215,15 @@ async def es_remove(self, ctx, series_id: int, monsters: List["MonsterModel"]): """ confirmation = [f"You are removing series `{new_series}`."] if was_primary: - confirmation.append(f"The following monsters will have their primary series removed:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, was_primary))}.") + confirmation.append("The following monsters will have their primary series removed:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, was_primary))) if wasnt_primary: - confirmation.append(f"The following monsters will have their secondary series removed:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, wasnt_primary))}.") + confirmation.append("The following monsters will have their secondary series removed:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, wasnt_primary))) if unadded: - confirmation.append(f"The following monsters will continue to not have the series:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, unadded))}.") - if not await get_user_confirmation(ctx, '\n'.join(confirmation), + confirmation.append("The following monsters will continue to not have the series:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, unadded))) + if not await get_user_confirmation(ctx, '\n\n'.join(confirmation), timeout=30, force_delete=False): return await self.es_execute_write(ctx, sql) @@ -267,15 +267,15 @@ async def es_promote(self, ctx, series_id: int, monsters: List["MonsterModel"]): """ confirmation = [f"You are promoting series `{new_series}`."] if has_series: - confirmation.append(f"The following monsters will have this primary series added:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, has_series))}.") + confirmation.append("The following monsters will have this primary series added:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, has_series))) if no_series: - confirmation.append(f"The following monsters will no longer be unsorted:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, no_series))}.") + confirmation.append("The following monsters will no longer be unsorted:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, no_series))) if already_primary: - confirmation.append(f"The following monsters will not be changed:\n" - f" {CR.join(map(MonsterHeader.text_with_emoji, already_primary))}.") - if not await get_user_confirmation(ctx, '\n'.join(confirmation), + confirmation.append("The following monsters will not be changed:\n" + + '\n'.join(map(MonsterHeader.text_with_emoji, already_primary))) + if not await get_user_confirmation(ctx, '\n\n'.join(confirmation), timeout=30, force_delete=False): return await self.es_execute_write(ctx, sql) From 6a337cc2e4bdb6bff2b5907b5abc0861ea80ea6a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 17:59:19 -0700 Subject: [PATCH 1117/1220] Rename PadGuideDB to PipelineUI (#1686) --- README.md | 4 +- {padguidedb => pipelineui}/__init__.py | 4 +- {padguidedb => pipelineui}/info.json | 0 .../padguidedb.py => pipelineui/pipelineui.py | 46 +++++++++---------- 4 files changed, 26 insertions(+), 28 deletions(-) rename {padguidedb => pipelineui}/__init__.py (63%) rename {padguidedb => pipelineui}/info.json (100%) rename padguidedb/padguidedb.py => pipelineui/pipelineui.py (96%) diff --git a/README.md b/README.md index 096f13d0..7886ae83 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ Most cogs here relate to the mobile game 'Puzzle & Dragons'. | Cog | Purpose | | --- | --- | -| azurelane | **DISCONTINUED** Azur Lane card lookup | | crud | Database editor. Not useful for bots other than prod Tsubaki | | dbcog | Central cog to host all PAD data | | damagecalc | Simple attack damage calculator | @@ -89,12 +88,11 @@ Most cogs here relate to the mobile game 'Puzzle & Dragons'. | padbuildimg | Creates images of PAD teams via a special query language | | padbuilds | See user-made builds for dungeons i think??? (nobody uses this) | | padevents | A scheduler cog to help players see upcoming daily GH dungeons. | -| padguidedb | Database editor. Not offered to Tsubaki clones. | | padglobal | Global PAD info commands | | padinfo | Monster lookup and info display | | padmonitor | Keeps an eye out for PAD updates | | padsearch | Search for monsters with specific attributes | -| padrem | Rare Egg Machine simulation. I don't think this ever existed | +| pipelineui | Database editor. Not offered to Tsubaki clones. | | pricecheck | Lumon's thing. Something about stamina trade equivalence | | profile | Global user PAD profile storage and lookup | | schoolidol | **DISCONTINUED** Love Live! School Idol Festival card lookup | diff --git a/padguidedb/__init__.py b/pipelineui/__init__.py similarity index 63% rename from padguidedb/__init__.py rename to pipelineui/__init__.py index 402038c5..3aebfb03 100644 --- a/padguidedb/__init__.py +++ b/pipelineui/__init__.py @@ -1,8 +1,8 @@ -from .padguidedb import PadGuideDb +from .pipelineui import PipelineUI __red_end_user_data_statement__ = "No personal data is stored." def setup(bot): - pdb = PadGuideDb(bot) + pdb = PipelineUI(bot) bot.add_cog(pdb) diff --git a/padguidedb/info.json b/pipelineui/info.json similarity index 100% rename from padguidedb/info.json rename to pipelineui/info.json diff --git a/padguidedb/padguidedb.py b/pipelineui/pipelineui.py similarity index 96% rename from padguidedb/padguidedb.py rename to pipelineui/pipelineui.py index 20dac899..65ca48da 100644 --- a/padguidedb/padguidedb.py +++ b/pipelineui/pipelineui.py @@ -10,20 +10,20 @@ from tsutils.cog_settings import CogSettings from tsutils.user_interaction import send_repeated_consecutive_messages -logger = logging.getLogger('red.padbot-cogs.padguidedb') +logger = logging.getLogger('red.padbot-cogs.pipelineui') def is_padguidedb_admin_check(ctx): is_owner = ctx.author.id in ctx.bot.owner_ids - return is_owner or ctx.bot.get_cog("PadGuideDb").settings.checkAdmin(ctx.author.id) + return is_owner or ctx.bot.get_cog("PipelineUI").settings.checkAdmin(ctx.author.id) def is_padguidedb_admin(): return commands.check(is_padguidedb_admin_check) -class PadGuideDb(commands.Cog): - """PadGuide Database manipulator""" +class PipelineUI(commands.Cog): + """UI to the Tsubaki database""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -61,19 +61,19 @@ def connect(self, db_config): cursorclass=pymysql.cursors.DictCursor, autocommit=True) - @commands.group(aliases=['pdb']) + @commands.group(aliases=['pdb', 'pui']) @is_padguidedb_admin() - async def padguidedb(self, context): + async def pipelineui(self, context): """PadGuide database manipulation.""" - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def addadmin(self, ctx, user: discord.Member): """Adds a user to the padguide db admin""" self.settings.addAdmin(user.id) await ctx.tick() - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def rmadmin(self, ctx, user): """Removes a user from the padguide db admin""" @@ -89,14 +89,14 @@ async def rmadmin(self, ctx, user): return await ctx.tick() - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def setconfigfile(self, ctx, *, config_file): """Set the database config file.""" self.settings.setConfigFile(config_file) await ctx.tick() - @padguidedb.command() + @pipelineui.command() async def searchdungeon(self, ctx, *, search_text): """Search for a dungeon via its jp or na name""" search_text = '%{}%'.format(search_text) @@ -111,7 +111,7 @@ async def searchdungeon(self, ctx, *, search_text): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.command() + @pipelineui.command() async def subdungeonlist(self, ctx, dungeon_id): """Show the subdungeon ids of a dungeon""" with self.get_cursor() as cursor: @@ -130,7 +130,7 @@ async def subdungeonlist(self, ctx, dungeon_id): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.command() + @pipelineui.command() async def searchsubdungeons(self, ctx, *, search_text): """Show the subdungeon ids of all matching dungeons""" search_text = f"%{search_text}%" @@ -153,7 +153,7 @@ async def searchsubdungeons(self, ctx, *, search_text): for row in results)) await ctx.send_interactive(map(box, pagify('\n\n'.join(msg), delims=['\n\n']))) - @padguidedb.command() + @pipelineui.command() @is_padguidedb_admin() async def loaddungeon(self, ctx, server: str, dungeon_id: int, dungeon_floor_id: int, queues: int = 1): if queues > 5: @@ -204,7 +204,7 @@ async def do_dungeon_load(self, ctx, server, dungeon_id, dungeon_floor_id): 'Load for {} {} {} finished'.format(server, dungeon_id, dungeon_floor_id))) self.queue_size -= 1 - @padguidedb.command() + @pipelineui.command() @is_padguidedb_admin() async def olddungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): with self.get_cursor() as cursor: @@ -221,7 +221,7 @@ async def olddungeondrops(self, ctx, dungeon_id: int, dungeon_floor_id: int): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.command() + @pipelineui.command() @is_padguidedb_admin() async def dungeondrops(self, ctx, dungeon_id: int): with self.get_cursor() as cursor: @@ -251,7 +251,7 @@ async def dungeondrops(self, ctx, dungeon_id: int): for page in pagify(msg): await ctx.send(box(page)) - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def cleardungeon(self, ctx, dungeon_id: int): with self.get_cursor() as cursor: @@ -259,7 +259,7 @@ async def cleardungeon(self, ctx, dungeon_id: int): cursor.execute(sql) await ctx.tick() - @padguidedb.group() + @pipelineui.group() @is_padguidedb_admin() async def pipeline(self, context): """PadGuide pipeline utilities.""" @@ -346,42 +346,42 @@ async def extractimages(self, ctx): return await ctx.send(inline('Image extract finished')) - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def setdungeonscriptfile(self, ctx, *, dungeon_script_file): """Set the dungeon script file.""" self.settings.setDungeonScriptFile(dungeon_script_file) await ctx.tick() - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def setfulletlfile(self, ctx, *, full_etl_file): """Set the full ETL file.""" self.settings.setFullETLFile(full_etl_file) await ctx.tick() - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def setdungeonprocessorfile(self, ctx, *, full_etl_file): """Set the full ETL file.""" self.settings.setDungeonProcessorFile(full_etl_file) await ctx.tick() - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def setimageupdatefile(self, ctx, *, image_update_file): """Set the image update file.""" self.settings.setImageUpdateFile(image_update_file) await ctx.tick() - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def setpythonexecutable(self, ctx, *, python_executable): """Set the python executable file.""" self.settings.setPythonExecutable(python_executable) await ctx.tick() - @padguidedb.command() + @pipelineui.command() @checks.is_owner() async def setuserinfo(self, ctx, server: str, user_uuid: str, user_intid: str): """Set the dungeon script.""" From a55b7cce193e266ea986dc323ecd96d33019d5c0 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 18:23:42 -0700 Subject: [PATCH 1118/1220] Update pad-cogs README (#1687) --- README.md | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7886ae83..bce918b5 100644 --- a/README.md +++ b/README.md @@ -78,21 +78,25 @@ Once the bot is launched, set it to use your repo directory as a cog path. Type Most cogs here relate to the mobile game 'Puzzle & Dragons'. -| Cog | Purpose | -| --- | --- | -| crud | Database editor. Not useful for bots other than prod Tsubaki | -| dbcog | Central cog to host all PAD data | -| damagecalc | Simple attack damage calculator | -| feedback | A specialized feedback cog for Tsubaki specifically | -| padboard | Converts board images to dawnglare board/solved board links | -| padbuildimg | Creates images of PAD teams via a special query language | -| padbuilds | See user-made builds for dungeons i think??? (nobody uses this) | -| padevents | A scheduler cog to help players see upcoming daily GH dungeons. | -| padglobal | Global PAD info commands | -| padinfo | Monster lookup and info display | -| padmonitor | Keeps an eye out for PAD updates | -| padsearch | Search for monsters with specific attributes | -| pipelineui | Database editor. Not offered to Tsubaki clones. | -| pricecheck | Lumon's thing. Something about stamina trade equivalence | -| profile | Global user PAD profile storage and lookup | -| schoolidol | **DISCONTINUED** Love Live! School Idol Festival card lookup | +| Cog | Purpose | +| --- | --- | +| azurelane | **DISCONTINUED** Azur Lane card lookup | +| crowddata | Crowdsourced data collection | +| crud | Database editor. Not useful for bots other than prod Tsubaki | +| dbcog | Central cog to host all PAD data | +| dungeoncog | A cog to search and get info for dungeons | +| damagecalc | Simple attack damage calculator | +| feedback | A specialized feedback cog for Tsubaki specifically | +| monidlistener | A listener to give info for monster ids | +| padboard | Converts board images to dawnglare board/solved board links | +| padbuildimg | Creates images of PAD teams via a special query language | +| padbuilds | See user-made builds for dungeons i think??? (nobody uses this) | +| padevents | A scheduler cog to help players see upcoming daily GH dungeons. | +| padglobal | Global PAD info commands | +| padinfo | Monster lookup and info display | +| padmonitor | Keeps an eye out for PAD updates | +| padsearch | Search for monsters with specific attributes | +| pipelineui | Interface for running the pipeline (Tsubaki-only) | +| pricecheck | Lumon's thing. Something about stamina trade equivalence | +| profile | Global user PAD profile storage and lookup | +| schoolidol | **DISCONTINUED** Love Live! School Idol Festival card lookup | From a467c81a95fa2cdbe95e86e2b247ecf929e26669 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 20 May 2022 18:29:36 -0700 Subject: [PATCH 1119/1220] Add better help messages (#1688) --- crud/crud.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 1a0fa4cf..0dfb4cb8 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -91,7 +91,7 @@ async def get_cursor(self): return (await self.connect(json.loads(await db_config.read()))).cursor() async def get_dbcog(self) -> "DBCog": - dbcog: "DBCog" = self.bot.get_cog("DBCog") + dbcog: Any = self.bot.get_cog("DBCog") if dbcog is None: raise ValueError("DBCog cog is not loaded") await dbcog.wait_until_ready() @@ -234,15 +234,15 @@ async def series_add(self, ctx, *elements): [p]crud series add key1 "Value1" key2 "Value2" """ if len(elements) % 2 != 0: - await ctx.send_help() - return + return await send_cancellation_message(ctx, "Imbalanced key-value pairs. Make sure" + " multi-word values are in quotes") elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in SERIES_KEYS for x in elements)): - return await ctx.send_help() + return await send_cancellation_message(ctx, f"Valid keys are {', '.join(map(inline, SERIES_KEYS))}") if "series_type" in elements and elements['series_type'] not in SERIES_TYPES: - return await ctx.send("`series_type` must be one of: " + ", ".join(SERIES_TYPES)) + return await send_cancellation_message(ctx, "`series_type` must be one of: " + ", ".join(SERIES_TYPES)) EXTRAS = {} async with await self.get_cursor() as cursor: From 33af21bc93a1f68d47c1a6e7620169d11e63aa24 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 22 May 2022 18:27:51 -0700 Subject: [PATCH 1120/1220] Fix editseries add (#1690) --- crud/editseries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crud/editseries.py b/crud/editseries.py index 72204aaf..19269419 100644 --- a/crud/editseries.py +++ b/crud/editseries.py @@ -159,7 +159,7 @@ async def es_add(self, ctx, series_id: int, monsters: List["MonsterModel"]): INSERT INTO monster_series (monster_series_id, monster_id, series_id, priority, tstamp) VALUES - ({series_id * 100000 + mid}, {mid}, {series_id}, {int(mid in sids)}, UNIX_TIMESTAMP()); + ({series_id * 100000 + mid}, {mid}, {series_id}, {int(mid not in sids)}, UNIX_TIMESTAMP()); """ confirmation = [f"You are adding series `{new_series}`."] if primary: From ce1e9fdae12c13b1fb8f5069796955060a304408 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 23 May 2022 00:54:48 -0700 Subject: [PATCH 1121/1220] Sort monsters in padmonitor (#1689) --- padmonitor/padmonitor.py | 72 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 79d7464d..69c0961b 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -7,14 +7,16 @@ from redbot.core import checks, commands from redbot.core.utils.chat_formatting import box, pagify -from typing import TYPE_CHECKING +from typing import Dict, List, Optional, TYPE_CHECKING from tsutils.cog_settings import CogSettings from tsutils.formatting import contains_ja -logger = logging.getLogger('red.padbot-cogs.padmonitor') if TYPE_CHECKING: from dbcog.database_context import DbContext + from dbcog.models.monster_model import MonsterModel + +logger = logging.getLogger('red.padbot-cogs.padmonitor') class PadMonitor(commands.Cog): @@ -48,47 +50,45 @@ async def check_seen_loop(self): async def check_seen(self): """Refresh the monster indexes.""" - DGCOG = self.bot.get_cog('DBCog') - await DGCOG.wait_until_ready() - db_context: "DbContext" = DGCOG.database - all_monsters = [*db_context.get_all_monsters()] + dbcog = self.bot.get_cog('DBCog') + await dbcog.wait_until_ready() + all_monsters = [*dbcog.database.get_all_monsters()] jp_monster_map = {m.monster_id: m for m in all_monsters if m.on_jp} na_monster_map = {m.monster_id: m for m in all_monsters if m.on_na} - def process(existing, new_map, name): + def process(existing: List[int], new_map: Dict[int, "MonsterModel"], name: str)\ + -> Optional[str]: if not existing: + # If everything is new, assume nothing is and log all current monsters as seen logger.info('preloading %i', len(new_map)) existing.extend(new_map.keys()) self.settings.save_settings() return None - existing_set = set(existing) - new_set = set(new_map.keys()) - delta_set = new_set - existing_set + delta_set = set(new_map.keys()).difference(existing) - if delta_set: - existing.extend(delta_set) - self.settings.save_settings() - - msg = 'New monsters added to {}:'.format(name) - for m in [new_map[x] for x in delta_set]: - msg += '\n\t[{}] {}'.format(m.monster_id, m.unoverridden_name_en) - if contains_ja(m.name_en) and m.name_en_override is not None: - msg += ' ({})'.format(m.name_en_override) - return msg - else: + if not delta_set: + # There are no new monsters return None - jp_results = process(self.settings.jp_seen(), jp_monster_map, 'JP') - na_results = process(self.settings.na_seen(), na_monster_map, 'NA') - - for msg in [jp_results, na_results]: - if not msg: - continue + existing.extend(delta_set) + self.settings.save_settings() + msg = 'New monsters added to {}:'.format(name) + for mid in sorted(delta_set): + m = new_map[mid] + msg += f'\n\t[{m.monster_id}] {m.unoverridden_name_en}' + if contains_ja(m.name_en) and m.name_en_override is not None: + msg += f' ({m.name_en_override})' + return msg + + if (jp_results := process(self.settings.jp_seen(), jp_monster_map, 'JP')): + for channel_id in self.settings.new_monster_channels(): + await self.announce(channel_id, jp_results) + if (na_results := process(self.settings.na_seen(), na_monster_map, 'NA')): for channel_id in self.settings.new_monster_channels(): - await self.announce(channel_id, msg) + await self.announce(channel_id, na_results) - async def announce(self, channel_id, message): + async def announce(self, channel_id: int, message: str): try: channel = self.bot.get_channel(int(channel_id)) for page in pagify(message): @@ -139,34 +139,34 @@ def make_default_settings(self): } return config - def jp_seen(self): + def jp_seen(self) -> List[int]: return self.bot_settings['jp_seen_ids'] - def na_seen(self): + def na_seen(self) -> List[int]: return self.bot_settings['na_seen_ids'] - def add_jp_seen(self, monster_id: int): + def add_jp_seen(self, monster_id: int) -> None: ids = self.jp_seen() if monster_id not in ids: ids.append(monster_id) self.save_settings() - def add_na_seen(self, monster_id: int): + def add_na_seen(self, monster_id: int) -> None: ids = self.na_seen() if monster_id not in ids: ids.append(monster_id) self.save_settings() - def new_monster_channels(self): + def new_monster_channels(self) -> List[int]: return self.bot_settings['new_monster_channels'] - def add_new_monster_channel(self, channel_id): + def add_new_monster_channel(self, channel_id: int) -> None: channels = self.new_monster_channels() if channel_id not in channels: channels.append(channel_id) self.save_settings() - def rm_new_monster_channel(self, channel_id): + def rm_new_monster_channel(self, channel_id: int) -> None: channels = self.new_monster_channels() if channel_id in channels: channels.remove(channel_id) From 48fd6407c8dabbd7eba106723272f98afae0a5e0 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 23 May 2022 20:34:38 -0700 Subject: [PATCH 1122/1220] Add ja searching to ^attr (#1691) --- dbcog/dbcog.py | 24 +++++++++++++++++------- dbcog/token_mappings.py | 25 ++++++++++++++++++------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index f1fe01a3..630068e0 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -11,7 +11,6 @@ import os import shutil import time -from functools import reduce from io import BytesIO from typing import Any, Dict, List, Optional, Tuple, Union @@ -342,10 +341,24 @@ def get_aliased_attribute(monster: MonsterModel, alias: str) -> Union[Dict[str, raise ValueError(f"Invalid alias {alias}") ret = {} - keys = [ks for ks in zip(*MONSTER_ATTR_ALIAS_TO_ATTR_MAP[alias]) - if len(ks) == 1 or not reduce(lambda x, y: x == y, ks)].pop() + keys = MONSTER_ATTR_ALIAS_TO_ATTR_MAP[alias] + while all(len(key) > 1 for key in keys) \ + and len({key[0] for key in keys}) == 1: + keys = [key[1:] for key in keys] + while all(len(key) > 1 for key in keys) \ + and len({key[-1] for key in keys}) == 1: + keys = [key[:-1] for key in keys] + keys = [' - '.join(key) for key in keys] + for string, replacement in { + '_en': ' (english)', + '_ja': ' (japanese)', + '_ko': ' (korean)', + '_': ' ', + }.items(): + keys = [key.replace(string, replacement) for key in keys] + for key, attrs in zip(keys, MONSTER_ATTR_ALIAS_TO_ATTR_MAP[alias]): - value: Any = monster + value = monster for attr in attrs: value = getattr(value, attr) ret[key] = value @@ -367,9 +380,6 @@ async def attr(self, ctx, attr, *, query): return await ctx.send("Sorry, {} doesn't have this attribute".format(monster.name_en)) if not isinstance(data, dict): return await ctx.send(data) - for k, v in data.items(): - if k.endswith('_en'): - return await ctx.send(v) return await ctx.send('\n'.join([f"**{k}**: {v}" for k, v in data.items()])) @commands.command() diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index b2068579..2020ae23 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -332,13 +332,24 @@ class MiscModifiers(Enum): NUMERIC_MONSTER_ATTRIBUTE_NAMES = {*sum(NUMERIC_MONSTER_ATTRIBUTE_ALIASES.values(), ())} STRING_MONSTER_ATTRIBUTE_ALIASES = { - (('name_en',), ('name_ja',)): ('monstername', 'cardname'), - (('leader_skill', 'name_en'), ('active_skill', 'name_en'),): ('skillname',), - (('leader_skill', 'desc_en'), ('active_skill', 'desc_en'),): ('skilltext',), - (('leader_skill', 'name_en'),): ('lsname',), - (('leader_skill', 'desc_en'),): ('lsdesc',), - (('active_skill', 'name_en'),): ('asname',), - (('active_skill', 'desc_en'),): ('asdesc',), + (('name_en',), + ('name_ja',)): ('monstername', 'cardname'), + (('leader_skill', 'name_en'), + ('leader_skill', 'name_ja'), + ('active_skill', 'name_en'), + ('active_skill', 'name_ja')): ('skillname',), + (('leader_skill', 'desc_en'), + ('leader_skill', 'desc_ja'), + ('active_skill', 'desc_en'), + ('active_skill', 'desc_ja')): ('skilltext',), + (('leader_skill', 'name_en'), + ('leader_skill', 'name_ja')): ('lsname',), + (('leader_skill', 'desc_en'), + ('leader_skill', 'desc_ja')): ('lsdesc',), + (('active_skill', 'name_en'), + ('active_skill', 'name_ja')): ('asname',), + (('active_skill', 'desc_en'), + ('active_skill', 'desc_ja')): ('asdesc',), (('series', 'name_en'),): ('seriesname',), (('history_us',),): ('regdate', 'dateadded'), } From 7a9f4f303e6913b1aa3fbfe970cbda93b680bfd1 Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 23 May 2022 20:44:09 -0700 Subject: [PATCH 1123/1220] Add gfeshop modifier (#1692) * Remove new revos * Add gfeshop modifier --- dbcog/monster_graph.py | 2 +- dbcog/monster_index.py | 9 +++++++++ dbcog/token_mappings.py | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index dc6bf911..ee94f856 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -870,7 +870,7 @@ def monster_is_gfe_exchange(self, monster: MonsterModel, stars: Optional[int] = for mid in exc.required_monster_ids} if stars is not None: monsters = {m for m in monsters if m.rarity == stars} - return any(s.series_id == 34 for m in monsters for s in m.all_series) + return len({m for m in monsters for s in m.all_series if s.series_id == 34}) > 1 def material_of_ids(self, monster: MonsterModel) -> List[int]: return sorted(self._get_edges(monster, 'material_of')) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 4bde65ec..96700b12 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -482,6 +482,15 @@ def is_story(m, do_transform=True): if is_story(monster): modifiers.update(MISC_MAP[MiscModifiers.STORY]) + # GFE Shop + if monster.in_rem: + if self.graph.monster_is_gfe_exchange(monster): + modifiers.update(MISC_MAP[MiscModifiers.GFESHOP]) + if self.graph.monster_is_gfe_exchange(monster, 6): + modifiers.update(MISC_MAP[MiscModifiers.GFESHOP6S]) + if self.graph.monster_is_gfe_exchange(monster, 7): + modifiers.update(MISC_MAP[MiscModifiers.GFESHOP7S]) + # New if self.graph.monster_is_new(monster): modifiers.update(MISC_MAP[MiscModifiers.NEW]) diff --git a/dbcog/token_mappings.py b/dbcog/token_mappings.py index 2020ae23..084c5d40 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/token_mappings.py @@ -207,6 +207,9 @@ class MiscModifiers(Enum): PERMANENT_EXCHANGE = 'Permanently exchangable' TEMP_EXCHANGE = 'Temporarily exchangable at some point in time' HAS_GEM = 'Has an evo gem' + GFESHOP = 'GFE exchangable' + GFESHOP6S = '6* GFE exchangable' + GFESHOP7S = '7* GFE exchangable' MISC_MAP = { @@ -237,6 +240,9 @@ class MiscModifiers(Enum): MiscModifiers.PERMANENT_EXCHANGE: ('permshop', 'shopperm'), MiscModifiers.TEMP_EXCHANGE: ('tempshop', 'shoptemp'), MiscModifiers.HAS_GEM: ('hasgem',), + MiscModifiers.GFESHOP: ('gfeshop',), + MiscModifiers.GFESHOP6S: ('6*gfeshop', 'gfeshop6*'), + MiscModifiers.GFESHOP7S: ('7*gfeshop', 'gfeshop7*'), } MULTI_WORD_TOKENS = {tuple(ts.split()) for ts in { From f9de32121c1730bf2bc238e456866392dc36864d Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 24 May 2022 02:38:19 -0700 Subject: [PATCH 1124/1220] Fix azurlane cog (#1695) --- {azurelane => azurlane}/__init__.py | 4 +- .../azurelane.py => azurlane/azurlane.py | 71 +++++++++---------- {azurelane => azurlane}/info.json | 4 +- 3 files changed, 36 insertions(+), 43 deletions(-) rename {azurelane => azurlane}/__init__.py (71%) rename azurelane/azurelane.py => azurlane/azurlane.py (68%) rename {azurelane => azurlane}/info.json (81%) diff --git a/azurelane/__init__.py b/azurlane/__init__.py similarity index 71% rename from azurelane/__init__.py rename to azurlane/__init__.py index f3650fcb..b6f4faaa 100644 --- a/azurelane/__init__.py +++ b/azurlane/__init__.py @@ -1,9 +1,9 @@ -from .azurelane import AzureLane +from .azurlane import AzurLane __red_end_user_data_statement__ = "No personal data is stored." def setup(bot): - n = AzureLane(bot) + n = AzurLane(bot) bot.add_cog(n) bot.loop.create_task(n.reload_al()) diff --git a/azurelane/azurelane.py b/azurlane/azurlane.py similarity index 68% rename from azurelane/azurelane.py rename to azurlane/azurlane.py index 2b617451..ca3eaa64 100644 --- a/azurelane/azurelane.py +++ b/azurlane/azurlane.py @@ -1,23 +1,25 @@ -import aiohttp import difflib -import discord import json import logging -from io import BytesIO from collections import OrderedDict +from io import BytesIO + +import aiohttp +import discord from redbot.core import checks, commands -from redbot.core.utils.chat_formatting import inline from tsutils.emoji import char_to_emoji from tsutils.old_menu import EmojiUpdater, Menu +from tsutils.tsubaki.links import CLOUDFRONT_URL +from tsutils.user_interaction import send_cancellation_message -logger = logging.getLogger('red.padbot-cogs.azurelane') +logger = logging.getLogger('red.padbot-cogs.azurlane') -BASE_URL = 'https://storage.googleapis.com/mirubot/alimages/raw' -DATA_URL = '{}/azure_lane.json'.format(BASE_URL) +BASE_URL = f'{CLOUDFRONT_URL}/azurlane/{{}}' +DATA_URL = BASE_URL.format('data.json') -class AzureLane(commands.Cog): - """AzureLane.""" +class AzurLane(commands.Cog): + """AzurLane.""" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) @@ -47,16 +49,10 @@ async def reload_al(self): async with session.get(DATA_URL) as resp: raw_resp = await resp.text() self.card_data = json.loads(raw_resp)['items'] - logger.info('done retrieving cards: {}'.format(len(self.card_data))) + logger.info(f'Done retrieving cards: {len(self.card_data)}') self.id_to_card = {c['id']: c for c in self.card_data} - name_to_card = {'{}'.format(c['name_en']).lower(): c for c in self.card_data} - # collection_name_to_card = {'{} {}'.format( - # i['title'], c['name_en']).lower(): c for i in c['images'] for c in self.card_data} - self.names_to_card = { - **name_to_card, - # **collection_name_to_card, - } + self.names_to_card = {'{}'.format(c['name_en']).lower(): c for c in self.card_data} @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -77,13 +73,13 @@ async def alid(self, ctx, *, query: str): if c: await self.do_menu(ctx, c) else: - await ctx.send(inline('no matches')) + await send_cancellation_message(ctx, f'No matches for query `{query}`.') async def do_menu(self, ctx, c): emoji_to_embed = OrderedDict() for idx, image_info in enumerate(c['images']): emoji = char_to_emoji(str(idx)) - emoji_to_embed[emoji] = make_card_embed(c, idx) + emoji_to_embed[emoji] = self.make_card_embed(c, idx) starting_menu_emoji = list(emoji_to_embed.keys())[0] return await self._do_menu(ctx, starting_menu_emoji, emoji_to_embed) @@ -102,23 +98,20 @@ async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): except Exception as ex: logger.exception('Menu failure') - -def make_card_embed(c, image_idx): - cid = c['id'] - name = c['name_en'] - info_url = c['url'] - image = c['images'][image_idx] - image_title = image['title'] - - url = image['url'] - file_name = url[url.rfind("/") + 1:] - - header = '[{}] {} - {}'.format(cid, name, image_title) - image_url = 'https://storage.googleapis.com/mirubot/alimages/raw/{}'.format(file_name) - - embed = discord.Embed() - embed.title = header - embed.url = info_url - embed.set_image(url=image_url) - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed + @classmethod + def make_card_embed(cls, c, image_idx): + cid = c['id'] + name = c['name_en'] + info_url = c['url'] + image = c['images'][image_idx] + image_title = image['title'] + + url = image['url'] + file_name = url[url.rfind("/") + 1:] + + embed = discord.Embed() + embed.title = f'[{cid}] {name} - {image_title}' + embed.url = BASE_URL.format(file_name) + embed.set_image(url=BASE_URL.format(file_name)) + embed.set_footer(text='Requester may click the reactions below to switch tabs') + return embed diff --git a/azurelane/info.json b/azurlane/info.json similarity index 81% rename from azurelane/info.json rename to azurlane/info.json index edb26126..9690c9f2 100644 --- a/azurelane/info.json +++ b/azurlane/info.json @@ -4,7 +4,7 @@ ], "description": "Displays data about Azur Lane.", "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]alid to get started. NOTE: This cog is no longer supported.", + "install_msg": "Use [p]alid to get started.", "short": "Shows Azur Lane data", "min_bot_version": "3.4.0", "max_bot_version": "0.0.0", @@ -13,7 +13,7 @@ 8, 1 ], - "hidden": true, + "hidden": false, "disabled": false, "required_cogs": {}, "requirements": [ From 6d8febbbf7d1461fa4454359c432298174bdec46 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 25 May 2022 18:52:49 -0500 Subject: [PATCH 1125/1220] Remove schoolidol cog, it's unused (#1696) --- schoolidol/__init__.py | 9 --- schoolidol/info.json | 26 ------- schoolidol/schoolidol.py | 154 --------------------------------------- 3 files changed, 189 deletions(-) delete mode 100644 schoolidol/__init__.py delete mode 100644 schoolidol/info.json delete mode 100644 schoolidol/schoolidol.py diff --git a/schoolidol/__init__.py b/schoolidol/__init__.py deleted file mode 100644 index 8b204c43..00000000 --- a/schoolidol/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .schoolidol import SchoolIdol - -__red_end_user_data_statement__ = "No personal data is stored." - - -def setup(bot): - n = SchoolIdol(bot) - bot.add_cog(n) - bot.loop.create_task(n.reload_sif()) diff --git a/schoolidol/info.json b/schoolidol/info.json deleted file mode 100644 index 669bccfb..00000000 --- a/schoolidol/info.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Displays data about School Idol.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]sifid to get started.", - "short": "Shows School Idol data", - "min_bot_version": "3.4.0", - "max_bot_version": "0.0.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "required_cogs": {}, - "requirements": [ - "tsutils", - "aiohttp" - ], - "tags": [ - "schoolidol" - ] -} diff --git a/schoolidol/schoolidol.py b/schoolidol/schoolidol.py deleted file mode 100644 index c9e063cd..00000000 --- a/schoolidol/schoolidol.py +++ /dev/null @@ -1,154 +0,0 @@ -import aiohttp -import difflib -import discord -import json -import logging -from io import BytesIO -from collections import OrderedDict -from redbot.core import checks, commands -from redbot.core.utils.chat_formatting import inline -from tsutils.emoji import char_to_emoji -from tsutils.old_menu import EmojiUpdater, Menu - -logger = logging.getLogger('red.padbot-cogs.schoolidol') - -FIRST_REQ = 'https://schoolido.lu/api/cards/?page_size=100' - - -class SchoolIdol(commands.Cog): - """SchoolIdol.""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - self.card_data = [] - self.menu = Menu(bot) - self.regular_emoji = char_to_emoji('r') - self.idol_emoji = char_to_emoji('i') - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - async def reload_sif(self): - await self.bot.wait_until_ready() - - next_req = FIRST_REQ - async with aiohttp.ClientSession() as session: - while next_req: - async with session.get(next_req) as resp: - raw_resp = await resp.text() - js_resp = json.loads(raw_resp) - next_req = js_resp['next'] - self.card_data.extend(js_resp['results']) - logger.info('done retrieving cards: {}'.format(len(self.card_data))) - - self.id_to_card = {c['id']: c for c in self.card_data} - name_to_card = {'{}'.format(c['idol']['name']).lower(): c for c in self.card_data} - firstname_to_card = {c['idol']['name'].lower().split(' ')[-1]: c for c in self.card_data} - collection_name_to_card = {'{} {}'.format( - c['translated_collection'], c['idol']['name']).lower(): c for c in self.card_data} - collection_firstname_to_card = {'{} {}'.format( - c['translated_collection'], c['idol']['name'].split(' ')[-1]).lower(): c for c in self.card_data} - self.names_to_card = { - **name_to_card, - **firstname_to_card, - **collection_name_to_card, - **collection_firstname_to_card, - } - - @commands.command() - @checks.bot_has_permissions(embed_links=True) - async def sifid(self, ctx, *, query: str): - """SIF query.""" - query = query.lower().strip() - c = None - if query.isdigit(): - c = self.id_to_card.get(int(query), None) - else: - c = self.names_to_card.get(query, None) - if c is None: - matches = difflib.get_close_matches( - query, self.names_to_card.keys(), n=1, cutoff=.6) - if len(matches): - c = self.names_to_card[matches[0]] - - if c: - await self.do_menu(ctx, c) - else: - await ctx.send(inline('no matches')) - - async def do_menu(self, ctx, c): - emoji_to_embed = OrderedDict() - regular_embed = make_card_embed(c, IMAGE_FIELD) - if regular_embed: - emoji_to_embed[self.regular_emoji] = regular_embed - starting_menu_emoji = self.regular_emoji - - idol_embed = make_card_embed(c, IDOL_IMAGE_FIELD) - if idol_embed: - emoji_to_embed[self.idol_emoji] = idol_embed - starting_menu_emoji = self.idol_emoji - - if starting_menu_emoji is None: - await ctx.send(inline('no images found')) - return - - return await self._do_menu(ctx, starting_menu_emoji, emoji_to_embed) - - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): - remove_emoji = self.menu.emoji['no'] - emoji_to_embed[remove_emoji] = self.menu.reaction_delete_message - - try: - result_msg, result_embed = await self.menu.custom_menu(ctx, - EmojiUpdater(emoji_to_embed), starting_menu_emoji, - timeout=20) - if result_msg and result_embed: - # Message is finished but not deleted, clear the footer - result_embed.set_footer(text=discord.Embed.Empty) - await self.bot.edit_message(result_msg, embed=result_embed) - except Exception as ex: - logger.exception('Menu failure') - - -def make_card_embed(c, url_field): - cid = c['id'] - image_url = c[url_field] - if not image_url: - return None - - base_url = 'https://storage.googleapis.com/mirubot/sifimages/processed/{}_{}.png' - - embed = discord.Embed() - embed.title = toHeader(c) - embed.url = get_info_url(c) - embed.set_image(url=base_url.format(cid, url_field)) - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed - - -IMAGE_FIELD = 'transparent_image' -IDOL_IMAGE_FIELD = 'transparent_idolized_image' - - -def toHeader(c): - cid = c['id'] - collection = c['translated_collection'] - name = c['idol']['name'] - if collection: - return '[{}] {} {}'.format(cid, collection, name) - else: - return '[{}] {}'.format(cid, name) - - -def get_info_url(c): - return 'https://schoolido.lu/cards/{}/'.format(c['id']) From 9baae6e19ab30913cebbd6a54065cfb4f3c97291 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 25 May 2022 18:22:59 -0700 Subject: [PATCH 1126/1220] Fallback to Jaro if substring fails in monster attibutes in id (#1697) --- dbcog/find_monster_tokens.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster_tokens.py index 3747b14b..5db64896 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster_tokens.py @@ -4,7 +4,7 @@ from typing import Iterable, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union import regex as re -from Levenshtein import jaro_winkler +from Levenshtein import jaro, jaro_winkler from tsutils.helper_classes import DummyObject from tsutils.tsubaki.monster_header import MonsterHeader @@ -205,6 +205,7 @@ def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) async def special_matches(self, monster): + max_score = 0 for class_attrs in self.monster_class_attributes: val: MonsterModel = monster for class_attr in class_attrs: @@ -220,6 +221,9 @@ async def special_matches(self, monster): return True, MatchData() elif self.string in val: return True, MatchData() + max_score = max(max_score, jaro(self.string, val)) + if max_score >= TOKEN_JW_DISTANCE: + return max_score, MatchData() return False, MatchData() @@ -320,7 +324,7 @@ async def special_matches(self, monster): if monster.attr1 == self.attr: return True, MatchData() if monster.attr2 == self.attr: - return .999, MatchData(subattr_match=True) + return 1 - EPSILON, MatchData(subattr_match=True) return False, MatchData() From 8a464595031554459de076f340fd72b8c64a4160 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Wed, 25 May 2022 20:30:51 -0500 Subject: [PATCH 1127/1220] Migrate Azurlane to menu2 (#1698) --- azurlane/__init__.py | 1 + azurlane/azurlane.py | 94 +++++++++++--------------- azurlane/azurlane_view.py | 84 ++++++++++++++++++++++++ azurlane/menu.py | 134 ++++++++++++++++++++++++++++++++++++++ azurlane/menu_map.py | 5 ++ azurlane/resources.py | 4 ++ 6 files changed, 267 insertions(+), 55 deletions(-) create mode 100644 azurlane/azurlane_view.py create mode 100644 azurlane/menu.py create mode 100644 azurlane/menu_map.py create mode 100644 azurlane/resources.py diff --git a/azurlane/__init__.py b/azurlane/__init__.py index b6f4faaa..55de70c1 100644 --- a/azurlane/__init__.py +++ b/azurlane/__init__.py @@ -6,4 +6,5 @@ def setup(bot): n = AzurLane(bot) bot.add_cog(n) + bot.loop.create_task(n.register_menu()) bot.loop.create_task(n.reload_al()) diff --git a/azurlane/azurlane.py b/azurlane/azurlane.py index ca3eaa64..095c710d 100644 --- a/azurlane/azurlane.py +++ b/azurlane/azurlane.py @@ -1,31 +1,34 @@ import difflib import json import logging -from collections import OrderedDict from io import BytesIO +from typing import Any import aiohttp -import discord -from redbot.core import checks, commands -from tsutils.emoji import char_to_emoji -from tsutils.old_menu import EmojiUpdater, Menu -from tsutils.tsubaki.links import CLOUDFRONT_URL +from redbot.core import checks, commands, Config +from tsutils.menu.components.config import BotConfig +from tsutils.query_settings.query_settings import QuerySettings from tsutils.user_interaction import send_cancellation_message -logger = logging.getLogger('red.padbot-cogs.azurlane') +from azurlane.azurlane_view import AzurlaneViewState +from azurlane.menu import AzurlaneMenu, AzurlaneMenuPanes +from azurlane.menu_map import azurlane_menu_map +from azurlane.resources import DATA_URL -BASE_URL = f'{CLOUDFRONT_URL}/azurlane/{{}}' -DATA_URL = BASE_URL.format('data.json') +logger = logging.getLogger('red.padbot-cogs.azurlane') class AzurLane(commands.Cog): """AzurLane.""" + menu_map = azurlane_menu_map + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.card_data = [] - self.menu = Menu(bot) + + self.config = Config.get_conf(self, identifier=9401770) self.id_to_card = None self.names_to_card = None @@ -42,6 +45,21 @@ async def red_delete_data_for_user(self, *, requester, user_id): """ return + async def register_menu(self): + await self.bot.wait_until_ready() + menulistener = self.bot.get_cog("MenuListener") + if menulistener is None: + logger.warning("MenuListener is not loaded.") + return + await menulistener.register(self) + + async def get_menu_default_data(self, ims): + data = { + 'alcog': self, + 'user_config': await BotConfig.get_user(self.config, ims['original_author_id']), + } + return data + async def reload_al(self): await self.bot.wait_until_ready() @@ -70,48 +88,14 @@ async def alid(self, ctx, *, query: str): if len(matches): c = self.names_to_card[matches[0]] - if c: - await self.do_menu(ctx, c) - else: - await send_cancellation_message(ctx, f'No matches for query `{query}`.') - - async def do_menu(self, ctx, c): - emoji_to_embed = OrderedDict() - for idx, image_info in enumerate(c['images']): - emoji = char_to_emoji(str(idx)) - emoji_to_embed[emoji] = self.make_card_embed(c, idx) - starting_menu_emoji = list(emoji_to_embed.keys())[0] - return await self._do_menu(ctx, starting_menu_emoji, emoji_to_embed) - - async def _do_menu(self, ctx, starting_menu_emoji, emoji_to_embed): - remove_emoji = self.menu.emoji['no'] - emoji_to_embed[remove_emoji] = self.menu.reaction_delete_message - - try: - result_msg, result_embed = await self.menu.custom_menu(ctx, - EmojiUpdater(emoji_to_embed), starting_menu_emoji, - timeout=20) - if result_msg and result_embed: - # Message is finished but not deleted, clear the footer - result_embed.set_footer(text=discord.Embed.Empty) - await result_msg.edit(embed=result_embed) - except Exception as ex: - logger.exception('Menu failure') - - @classmethod - def make_card_embed(cls, c, image_idx): - cid = c['id'] - name = c['name_en'] - info_url = c['url'] - image = c['images'][image_idx] - image_title = image['title'] - - url = image['url'] - file_name = url[url.rfind("/") + 1:] - - embed = discord.Embed() - embed.title = f'[{cid}] {name} - {image_title}' - embed.url = BASE_URL.format(file_name) - embed.set_image(url=BASE_URL.format(file_name)) - embed.set_footer(text='Requester may click the reactions below to switch tabs') - return embed + if not c: + return await send_cancellation_message(ctx, f'No matches for query `{query}`.') + reaction_list = AzurlaneMenuPanes.get_initial_reaction_list(len(c['images'])) + menu = AzurlaneMenu.menu() + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) + state = AzurlaneViewState(ctx.message.author.id, AzurlaneMenu.MENU_TYPE, + query_settings, + c, 0, + reaction_list=reaction_list) + + await menu.create(ctx, state) diff --git a/azurlane/azurlane_view.py b/azurlane/azurlane_view.py new file mode 100644 index 00000000..b296c7dc --- /dev/null +++ b/azurlane/azurlane_view.py @@ -0,0 +1,84 @@ +from typing import Collection, TYPE_CHECKING + +from discordmenu.embed.components import EmbedMain, EmbedBodyImage +from discordmenu.embed.view import EmbedView +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase +from tsutils.query_settings.query_settings import QuerySettings + +from azurlane.resources import BASE_URL + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + +ORDINAL_WORDS = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth'] + + +class AzurlaneViewState(ViewStateBase): + def __init__(self, original_author_id, menu_type, query_settings: QuerySettings, + c, image_idx, + extra_state=None, reaction_list=None + ): + super().__init__(original_author_id, menu_type, '', extra_state=extra_state, reaction_list=reaction_list) + self.query_settings = query_settings + self.menu_type = menu_type + self.image_idx = image_idx + self.c = c + + def serialize(self): + ret = super().serialize() + ret.update({ + 'idx': self.c['id'], + 'query_settings': self.query_settings.serialize(), + 'current_index': self.image_idx, + }) + return ret + + @classmethod + async def deserialize(cls, alcog, _user_config: UserConfig, ims: dict): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + query_settings = QuerySettings.deserialize(ims.get('query_settings')) + reaction_list = ims['reaction_list'] + image_idx = ims['current_index'] + + card = alcog.id_to_card[ims['idx']] + return AzurlaneViewState(original_author_id, menu_type, query_settings, card, image_idx, + reaction_list=reaction_list) + + +class AzurlaneView: + VIEW_TYPE = 'Azurlane' + + @staticmethod + def get_count(arr: Collection["MonsterModel"], *elements: "MonsterModel") -> str: + if not arr: + return "N/A" + count = sum(1 for m in arr if m in elements) + if count == 0: + return "0" + return f"{round(100 * count / len(arr), 3)}%" + + @classmethod + def embed(cls, state: AzurlaneViewState): + c = state.c + + cid = c['id'] + name = c['name_en'] + image = c['images'][state.image_idx] + image_title = image['title'] + + url = image['url'] + file_name = url[url.rfind("/") + 1:] + full_url = BASE_URL.format(file_name) + + return EmbedView( + EmbedMain( + color=state.query_settings.embedcolor, + title=f'[{cid}] {name} - {image_title}', + url=full_url + ), + embed_body_image=EmbedBodyImage(full_url), + embed_footer=embed_footer_with_state(state) + ) diff --git a/azurlane/menu.py b/azurlane/menu.py new file mode 100644 index 00000000..ccd168c3 --- /dev/null +++ b/azurlane/menu.py @@ -0,0 +1,134 @@ +from typing import Optional + +from discord import Message +from discordmenu.embed.control import EmbedControl +from discordmenu.embed.menu import EmbedMenu +from tsutils.emoji import char_to_emoji +from tsutils.menu.components.panes import MenuPanes + +from azurlane.azurlane_view import AzurlaneViewState, AzurlaneView + + +class AzurlaneEmoji: + zero = char_to_emoji('0') + one = char_to_emoji('1') + two = char_to_emoji('2') + three = char_to_emoji('3') + four = char_to_emoji('4') + five = char_to_emoji('5') + six = char_to_emoji('6') + seven = char_to_emoji('7') + eight = char_to_emoji('8') + nine = char_to_emoji('9') + ten = char_to_emoji('10') + + +class AzurlaneMenu: + MENU_TYPE = AzurlaneView.VIEW_TYPE + + @staticmethod + def menu(initial_control=None): + if initial_control is None: + initial_control = AzurlaneMenu.pane_control + embed = EmbedMenu(AzurlaneMenuPanes.transitions(), initial_control) + return embed + + @staticmethod + async def respond_with_n(message: Optional[Message], ims, n, **data): + ims['current_index'] = n + return await AzurlaneMenu.respond_with_pane(message, ims, **data) + + @staticmethod + async def respond_with_0(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 0, **data) + + @staticmethod + async def respond_with_1(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 1, **data) + + @staticmethod + async def respond_with_2(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 2, **data) + + @staticmethod + async def respond_with_3(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 3, **data) + + @staticmethod + async def respond_with_4(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 4, **data) + + @staticmethod + async def respond_with_5(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 5, **data) + + @staticmethod + async def respond_with_6(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 6, **data) + + @staticmethod + async def respond_with_7(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 7, **data) + + @staticmethod + async def respond_with_8(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 8, **data) + + @staticmethod + async def respond_with_9(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 9, **data) + + @staticmethod + async def respond_with_10(message: Optional[Message], ims, **data): + return await AzurlaneMenu.respond_with_n(message, ims, 10, **data) + + @classmethod + async def respond_with_pane(cls, message: Optional[Message], ims, **data): + alcog = data['alcog'] + user_config = data['user_config'] + view_state = await AzurlaneViewState.deserialize(alcog, user_config, ims) + return AzurlaneMenu.pane_control(view_state) + + @classmethod + def pane_control(cls, state: AzurlaneViewState): + if state is None: + return None + reaction_list = state.reaction_list + return EmbedControl( + [AzurlaneView.embed(state)], + reaction_list + ) + + +class AzurlaneMenuPanes(MenuPanes): + + NON_MONSTER_EMOJI_COUNT = 0 + + DATA = { + AzurlaneEmoji.zero: ( + AzurlaneMenu.respond_with_0, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.one: ( + AzurlaneMenu.respond_with_1, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.two: ( + AzurlaneMenu.respond_with_2, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.three: ( + AzurlaneMenu.respond_with_3, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.four: ( + AzurlaneMenu.respond_with_4, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.five: ( + AzurlaneMenu.respond_with_5, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.six: ( + AzurlaneMenu.respond_with_6, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.seven: ( + AzurlaneMenu.respond_with_7, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.eight: ( + AzurlaneMenu.respond_with_8, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.nine: ( + AzurlaneMenu.respond_with_9, AzurlaneView.VIEW_TYPE), + AzurlaneEmoji.ten: ( + AzurlaneMenu.respond_with_10, AzurlaneView.VIEW_TYPE), + } + + @classmethod + def get_initial_reaction_list(cls, number_of_skins: int): + return cls.emoji_names()[:number_of_skins + cls.NON_MONSTER_EMOJI_COUNT] diff --git a/azurlane/menu_map.py b/azurlane/menu_map.py new file mode 100644 index 00000000..47989b04 --- /dev/null +++ b/azurlane/menu_map.py @@ -0,0 +1,5 @@ +from azurlane.menu import AzurlaneMenu, AzurlaneMenuPanes + +azurlane_menu_map = { + AzurlaneMenu.MENU_TYPE: (AzurlaneMenu, AzurlaneMenuPanes), +} diff --git a/azurlane/resources.py b/azurlane/resources.py new file mode 100644 index 00000000..bbf5529f --- /dev/null +++ b/azurlane/resources.py @@ -0,0 +1,4 @@ +from tsutils.tsubaki.links import CLOUDFRONT_URL + +BASE_URL = f'{CLOUDFRONT_URL}/azurlane/{{}}' +DATA_URL = BASE_URL.format('data.json') From 946f8f1bb549850b52f89558377a58ec8295a3b1 Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 27 May 2022 01:00:18 -0700 Subject: [PATCH 1128/1220] Set ready flag per-index (#1699) --- dbcog/dbcog.py | 15 +++++++++------ dbcog/find_monster.py | 9 ++++++--- dbcog/idtest_mixin.py | 24 ++++++++++++++---------- dbcog/monster_index.py | 32 ++++++++++++++++++++++++++++++++ padinfo/core/leader_skills.py | 2 +- padinfo/padinfo.py | 34 ++++++++++++++++++---------------- 6 files changed, 80 insertions(+), 36 deletions(-) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 630068e0..2e7bbfd7 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -154,8 +154,7 @@ async def forceindexreload3(self, ctx): async def create_index(self): """Exported function that allows a client cog to create an id3 monster index""" for server in SERVERS: - self.indexes[server] = MonsterIndex(server) - await self.indexes[server].setup(self.database.graph) + await self.indexes[server].reset(self.database.graph) self.mon_finder = FindMonster(self, self.fm_flags_default) asyncio.create_task(self.check_index()) @@ -192,6 +191,10 @@ async def check_index(self): for channel in channels: await channel.send(box(page)) + async def get_index(self, server: Server = DEFAULT_SERVER) -> MonsterIndex: + await self.indexes[server].is_ready.wait() + return self.indexes[server] + def get_monster(self, monster_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[MonsterModel]: """Exported function that allows a client cog to get a full MonsterModel by monster_id""" return self.database.graph.get_monster(monster_id, server=server) @@ -216,8 +219,7 @@ async def reload_data_task(self): try: async with StatusManager(self.bot): await self.download_and_refresh_nicknames() - logger.info('Done refreshing DBCog, triggering ready') - self._is_ready.set() + logger.info('Done refreshing DBCog') except Exception as ex: short_wait = True logger.exception("DBCog data download/refresh failed: %s", ex) @@ -241,7 +243,8 @@ async def download_and_refresh_nicknames(self): logger.info('Loading database') self.database = load_database(self.database, await self.get_debug_monsters()) - logger.info('Building monster index') + logger.info('Building monster index, triggering ready') + self._is_ready.set() await self.create_index() logger.info('Done refreshing database') @@ -406,7 +409,7 @@ async def exportmodifiers(self, ctx, server: LiteralConverter["COMBINED", "NA"] atable = [(awakenings[k.value].name_en, ", ".join(map(inline, v))) for k, v in maps.AWOKEN_SKILL_MAP.items()] ret += "\n\n### Awakenings\n\n" + tabulate(atable, headers=["Meaning", "Tokens"], tablefmt="github") stable = [(series[k].name_en, ", ".join(map(inline, v))) - for k, v in self.indexes[server].series_id_to_pantheon_nickname.items()] + for k, v in (await self.get_index(server)).series_id_to_pantheon_nickname.items()] ret += "\n\n### Series\n\n" + tabulate(stable, headers=["Meaning", "Tokens"], tablefmt="github") ctable = [(k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in maps.COLOR_MAP.items()] ctable += [("Sub " + k.name.replace("Nil", "None"), ", ".join(map(inline, v))) for k, v in diff --git a/dbcog/find_monster.py b/dbcog/find_monster.py index a98609c1..81e1e0d5 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster.py @@ -45,15 +45,18 @@ class ExtraInfo(NamedTuple): class FindMonster: + TOKEN_JW_DISTANCE = TOKEN_JW_DISTANCE + MODIFIER_JW_DISTANCE = MODIFIER_JW_DISTANCE + def __init__(self, dbcog, flags: Dict[str, Any]): self.dbcog = dbcog self.flags = flags self.index = self.dbcog.indexes[Server(flags['server'])] - def _process_settings(self, original_query: str) -> str: + async def _process_settings(self, original_query: str) -> str: query_settings = QuerySettings.extract(self.flags, original_query) - self.index = self.dbcog.indexes[query_settings.server] + self.index = await self.dbcog.get_index(query_settings.server) return re.sub(r'\s*(--|—)\w+(:{.+?})?(?:\s+|$)', ' ', original_query) @@ -305,7 +308,7 @@ async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: query = re.sub(r':=?r?("[^"]+"|\'[^\']+\')', lambda m: m.group(0).replace(' ', '\0'), query) query = re.sub(r'\[[^[]+]', lambda m: m.group(0).replace(' ', '\0'), query) - tokenized_query = self._process_settings(query).split() + tokenized_query = (await self._process_settings(query)).split() tokenized_query = [token.replace('\0', ' ') for token in tokenized_query] mw_tokenized_query = self._merge_multi_word_tokens(tokenized_query) best_monster, matches_dict, valid_monsters = max( diff --git a/dbcog/idtest_mixin.py b/dbcog/idtest_mixin.py index 3e9439ce..1d7802ec 100644 --- a/dbcog/idtest_mixin.py +++ b/dbcog/idtest_mixin.py @@ -1,6 +1,6 @@ import re from datetime import datetime -from typing import Callable, Coroutine, Dict, List, Optional +from typing import Callable, Coroutine, List, Optional from redbot.core import Config, checks, commands from redbot.core.bot import Red @@ -17,7 +17,7 @@ class IdTest: bot: Red config: Config - indexes: Dict[Server, MonsterIndex] + get_index: Callable[[Server], Coroutine[None, None, MonsterIndex]] find_monster: Callable[[int], Coroutine[None, None, MonsterModel]] wait_until_ready: Callable[[], Coroutine[None, None, None]] @@ -128,8 +128,8 @@ async def norf_add(self, ctx, mid: int, token, server, reason, fluffy): suite.sort(key=lambda v: (v['id'], v['token'], v['fluff'])) await self.wait_until_ready() - fluff = mid in [m.monster_id for m in self.indexes[server].fluff_tokens[token]] - name = mid in [m.monster_id for m in self.indexes[server].name_tokens[token]] + fluff = mid in [m.monster_id for m in (await self.get_index(server)).fluff_tokens[token]] + name = mid in [m.monster_id for m in (await self.get_index(server)).name_tokens[token]] passing = (fluffy and fluff) or (not fluffy and name) if await get_user_reaction(ctx, f"Added {'passing' if passing else 'failing'}" @@ -447,9 +447,9 @@ async def norf_run(self, ctx): v['fluff'], v['server'])))): fluff = case['id'] in [m.monster_id for m in - self.indexes[Server(case['server'])].fluff_tokens[case['token']]] + (await self.get_index(Server(case['server']))).fluff_tokens[case['token']]] name = case['id'] in [m.monster_id for m in - self.indexes[Server(case['server'])].name_tokens[case['token']]] + (await self.get_index(Server(case['server']))).name_tokens[case['token']]] if (case['fluff'] and not fluff) or (not case['fluff'] and not name): q = '"{}"'.format(case['token']) @@ -495,8 +495,10 @@ async def idt_runall(self, ctx): qc += 1 async for c, v in AsyncIter(enumerate(fsuite)): - fluff = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m + in (await self.get_index(Server(v['server']))).fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m + in (await self.get_index(Server(v['server']))).name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) @@ -542,8 +544,10 @@ async def run_tests(self) -> List[str]: fsuite = await self.config.fluff_suite() async for c, v in AsyncIter(enumerate(fsuite)): - fluff = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].fluff_tokens[v['token']]] - name = v['id'] in [m.monster_id for m in self.indexes[Server(v['server'])].name_tokens[v['token']]] + fluff = v['id'] in [m.monster_id for m + in (await self.get_index(Server(v['server']))).fluff_tokens[v['token']]] + name = v['id'] in [m.monster_id for m + in (await self.get_index(Server(v['server']))).name_tokens[v['token']]] if (v['fluff'] and not fluff) or (not v['fluff'] and not name): q = '"{}"'.format(v['token']) diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 96700b12..570a0fc0 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -65,6 +65,38 @@ def __init__(self, server: Server = DEFAULT_SERVER): self.graph: Optional[MonsterGraph] = None + async def reset(self, graph: MonsterGraph): + self.is_ready.clear() + self.issues = [] + + self.monster_id_to_cardname = defaultdict(set) + self.monster_id_to_treename = defaultdict(set) + self.treename_overrides = set() + self.monster_id_to_name = defaultdict(set) + self.monster_id_to_forcedfluff = defaultdict(set) + self.series_id_to_pantheon_nickname = defaultdict(set) + self.content_token_aliases = defaultdict(set) + self.manual_modifiers = defaultdict(set) + self.manual_removed_modifiers = defaultdict(set) + + self.all_name_tokens = {} + self.manual = {} + self.manual_cardnames = defaultdict(set) + self.manual_treenames = defaultdict(set) + self.name_tokens = defaultdict(set) + self.fluff_tokens = defaultdict(set) + + self.all_modifiers = set() + self._known_mods = set() + self.modifiers = defaultdict(set) + self.suffixes = set() + + self.multi_word_tokens = {} + self.mwtoken_creators = defaultdict(set) + self.mwt_to_len = defaultdict(lambda: 1) + + await self.setup(graph) + async def setup(self, graph: MonsterGraph): self.graph = graph monsters = graph.get_all_monsters(self.server) diff --git a/padinfo/core/leader_skills.py b/padinfo/core/leader_skills.py index a52ab3de..a779e332 100644 --- a/padinfo/core/leader_skills.py +++ b/padinfo/core/leader_skills.py @@ -89,7 +89,7 @@ async def leaderskill_query(dbcog, raw_query, author_id): if sep == ' ': # Handle a very specific failure case, user typing something like "uuvo ragdra" m = await dbcog.find_monster(query, author_id) - if m and left_query in dbcog.indexes[m.server_priority].modifiers[m]: + if m and left_query in (await dbcog.get_index(m.server_priority)).modifiers[m]: left_query = query right_query = None break diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 91efd02d..f976e75b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1158,13 +1158,14 @@ async def send_id_failure_message(ctx, query: str): async def debugid(self, ctx, server: Optional[Server] = Server.COMBINED, *, query): """Get helpful id information about a monster""" dbcog = await self.get_dbcog() + index = await dbcog.get_index(server) mon = await dbcog.find_monster(query, ctx.author.id) if mon is None: await ctx.send(box("Your query didn't match any monsters.")) return base_monster = dbcog.database.graph.get_base_monster(mon) - mods = dbcog.indexes[server].modifiers[mon] - manual_modifiers = dbcog.indexes[server].manual_modifiers[mon.monster_id] + mods = index.modifiers[mon] + manual_modifiers = index.manual_modifiers[mon.monster_id] EVOANDTYPE = dbcog.token_maps.EVO_TOKENS.union(dbcog.token_maps.TYPE_TOKENS) def mod_token_str(f: Callable[[str], bool]) -> str: @@ -1173,11 +1174,11 @@ def mod_token_str(f: Callable[[str], bool]) -> str: ret = (f"[{mon.monster_id}] {mon.name_en}\n" f"Base: [{base_monster.monster_id}] {base_monster.name_en}\n" f"Series: {mon.series.name_en} ({mon.series_id}, {mon.series.series_type})\n\n" - f"[Name Tokens] {' '.join(sorted(t for t, ms in dbcog.indexes[server].name_tokens.items() if mon in ms))}\n" - f"[Fluff Tokens] {' '.join(sorted(t for t, ms in dbcog.indexes[server].fluff_tokens.items() if mon in ms))}\n\n" + f"[Name Tokens] {' '.join(sorted(t for t, ms in index.name_tokens.items() if mon in ms))}\n" + f"[Fluff Tokens] {' '.join(sorted(t for t, ms in index.fluff_tokens.items() if mon in ms))}\n\n" f"[Manual Tokens]\n" - f" Treenames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_treenames.items() if mon in ms))}\n" - f" Nicknames: {' '.join(sorted(t for t, ms in dbcog.indexes[server].manual_cardnames.items() if mon in ms))}\n\n" + f" Treenames: {' '.join(sorted(t for t, ms in index.manual_treenames.items() if mon in ms))}\n" + f" Nicknames: {' '.join(sorted(t for t, ms in index.manual_cardnames.items() if mon in ms))}\n\n" f"[Modifier Tokens]\n" f" Attribute: {mod_token_str(lambda t: t.split('-')[0] in dbcog.token_maps.COLOR_TOKENS)}\n" f" Awakening: {mod_token_str(lambda t: t.split('-')[0] in dbcog.token_maps.AWAKENING_TOKENS)}\n" @@ -1224,6 +1225,7 @@ async def idmeaning(self, ctx, token, server: Optional[Server] = Server.COMBINED """Get all the meanings of a token (bold signifies base of a tree)""" token = token.replace(" ", "") DGCOG = await self.get_dbcog() + index = await DGCOG.get_index(server) tms = DGCOG.token_maps awokengroup = "(" + "|".join(re.escape(aw) for aws in tms.AWOKEN_SKILL_MAP.values() for aw in aws) + ")" @@ -1239,7 +1241,7 @@ def f(m, s): token_ret = "" so = [] for mon in sorted(token_dict[token], key=lambda m: m.monster_id): - if (mon in DGCOG.indexes[server].mwtoken_creators[token]) == is_multiword: + if (mon in index.mwtoken_creators[token]) == is_multiword: so.append(mon) if len(so) > 5: token_ret += f"\n\n{token_type}\n" + ", ".join(f(m, str(m.monster_id)) for m in so[:10]) @@ -1249,23 +1251,23 @@ def f(m, s): f(m, f"{str(m.monster_id).rjust(4)}. {m.name_en}") for m in so) return token_ret - ret += write_name_token(DGCOG.indexes[server].manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) - ret += write_name_token(DGCOG.indexes[server].manual, "[Manual Tokens]") - ret += write_name_token(DGCOG.indexes[server].name_tokens, "[Name Tokens]") - ret += write_name_token(DGCOG.indexes[server].fluff_tokens, "[Fluff Tokens]") + ret += write_name_token(index.manual, "\N{LARGE PURPLE CIRCLE} [Multi-Word Tokens]", True) + ret += write_name_token(index.manual, "[Manual Tokens]") + ret += write_name_token(index.name_tokens, "[Name Tokens]") + ret += write_name_token(index.fluff_tokens, "[Fluff Tokens]") - submwtokens = [t for t in DGCOG.indexes[server].multi_word_tokens if token in t] + submwtokens = [t for t in index.multi_word_tokens if token in t] if submwtokens: ret += "\n\n[Multi-word Super-tokens]\n" for t in submwtokens: - if not DGCOG.indexes[server].all_name_tokens[''.join(t)]: + if not index.all_name_tokens[''.join(t)]: continue - creators = sorted(DGCOG.indexes[server].mwtoken_creators["".join(t)], key=lambda m: m.monster_id) + creators = sorted(index.mwtoken_creators["".join(t)], key=lambda m: m.monster_id) ret += f"{' '.join(t).title()}" ret += f" ({', '.join(f'{m.monster_id}' for m in creators)})" if creators else '' ret += (" ( \u2014> " + str(DGCOG.mon_finder.get_most_eligable_monster( - DGCOG.indexes[server].all_name_tokens[''.join(t)]).monster_id) + index.all_name_tokens[''.join(t)]).monster_id) + ")\n") def additmods(ms, om): @@ -1292,7 +1294,7 @@ def additmods(ms, om): '/' + k[1].name.replace("Nil", "None") + additmods(v, token) for k, v in tms.DUAL_COLOR_MAP.items() if token in v], *["Series: " + series[k].name_en + additmods(v, token) - for k, v in DGCOG.indexes[server].series_id_to_pantheon_nickname.items() if token in v], + for k, v in index.series_id_to_pantheon_nickname.items() if token in v], *["Rarity: " + m for m in re.findall(r"^(\d+)\*$", token)], *["Base rarity: " + m for m in re.findall(r"^(\d+)\*b$", token)], From 7463e99b14bdff846ad37a336d64412f28bfb5cb Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 27 May 2022 17:56:26 -0700 Subject: [PATCH 1129/1220] Strip angle brackets from github names (#1700) --- crud/crud.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 0dfb4cb8..1ddc35f0 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -13,7 +13,6 @@ from redbot.core import Config, checks, commands, errors from redbot.core.utils.chat_formatting import box, inline, pagify from tsutils.cogs.globaladmin import auth_check -from tsutils.enums import Server from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, send_cancellation_message @@ -149,7 +148,8 @@ async def git_verify(self, ctx, filepath): index.write() tree = index.write_tree() email = await self.config.user(ctx.author).email() - author = pygit2.Signature(str(ctx.author), email or "famiel@tsubakibot.com") + author = pygit2.Signature(re.sub(r'[<>]', '', str(ctx.author)), + email or "famiel@tsubakibot.com") commiter = pygit2.Signature("Famiel", "famiel@tsubakibot.com") parent, ref = repo.resolve_refish(refish=repo.head.name) repo.create_commit(ref.name, author, commiter, "Updating JSON", tree, [parent.oid]) @@ -337,16 +337,16 @@ async def awokenskill_search(self, ctx, *, search_text): """Search for a awoken skill via its jp or na name""" dbcog = await self.get_dbcog() if search_text in dbcog.KNOWN_AWOKEN_SKILL_TOKENS: - awoken_skill_id = dbcog.KNOWN_AWOKEN_SKILL_TOKENS[search_text].value - where = f'awoken_skill_id = {awoken_skill_id}' + where = f'awoken_skill_id = %s' + replacements = (dbcog.KNOWN_AWOKEN_SKILL_TOKENS[search_text].value,) else: - search_text = '%{}%'.format(search_text).lower() - where = f"lower(name_en) LIKE {search_text} OR lower(name_ja) LIKE {search_text}" - sql = ('SELECT awoken_skill_id, name_en, name_ja, name_ko, desc_en,' - ' desc_ja, desc_ko FROM awoken_skills' - ' WHERE %s' - ' ORDER BY awoken_skill_id DESC LIMIT 20') - await self.execute_read(ctx, sql, [where]) + where = f"lower(name_en) LIKE %s OR lower(name_ja) LIKE %s" + replacements = ('%{}%'.format(search_text).lower(),) * 2 + sql = (f'SELECT awoken_skill_id, name_en, name_ja, name_ko, desc_en,' + f' desc_ja, desc_ko FROM awoken_skills' + f' WHERE {where}' + f' ORDER BY awoken_skill_id DESC LIMIT 20') + await self.execute_read(ctx, sql, replacements) @awokenskill.command(name="add") @checks.is_owner() From cbf7afdfc84238a7630036438a3eabd3e96878ed Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 29 May 2022 07:21:23 -0700 Subject: [PATCH 1130/1220] Add selfhasmat (#1704) This also adds gem matching to hasmat --- dbcog/dbcog.py | 16 ++++++-- dbcog/find_monster/__init__.py | 0 dbcog/{ => find_monster}/find_monster.py | 2 +- .../{ => find_monster}/find_monster_tokens.py | 37 +++++++++++++++---- dbcog/{ => find_monster}/idtest_mixin.py | 0 dbcog/{ => find_monster}/token_mappings.py | 2 +- dbcog/monster_graph.py | 13 +++++++ dbcog/monster_index.py | 2 +- profile/profile.py | 2 +- 9 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 dbcog/find_monster/__init__.py rename dbcog/{ => find_monster}/find_monster.py (99%) rename dbcog/{ => find_monster}/find_monster_tokens.py (94%) rename dbcog/{ => find_monster}/idtest_mixin.py (100%) rename dbcog/{ => find_monster}/token_mappings.py (99%) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 2e7bbfd7..2af16ae1 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -25,16 +25,16 @@ from tsutils.tsubaki.links import CLOUDFRONT_URL from tsutils.user_interaction import StatusManager, send_confirmation_message -from . import token_mappings +from .find_monster import token_mappings from .database_context import DbContext from .database_loader import load_database -from .find_monster import ExtraInfo, FindMonster, MonsterInfo -from .idtest_mixin import IdTest +from dbcog.find_monster.find_monster import ExtraInfo, FindMonster, MonsterInfo +from dbcog.find_monster.idtest_mixin import IdTest from .models.enum_types import DEFAULT_SERVER, SERVERS from .models.monster_model import MonsterModel from .models.monster_stats import MonsterStatModifierInput, monster_stats from .monster_index import MonsterIndex -from .token_mappings import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES, AWOKEN_SKILL_MAP, \ +from dbcog.find_monster.token_mappings import MONSTER_ATTR_ALIAS_TO_ATTR_MAP, MONSTER_CLASS_ATTRIBUTES, AWOKEN_SKILL_MAP, \ KNOWN_AWOKEN_SKILL_TOKENS logger = logging.getLogger('red.padbot-cogs.dbcog') @@ -159,6 +159,14 @@ async def create_index(self): self.mon_finder = FindMonster(self, self.fm_flags_default) asyncio.create_task(self.check_index()) + async def load_old_index(self): + data = b'\x80\x04}\x94.' + for server in SERVERS: + self.indexes[server] = MonsterIndex.deserialize(data, server, self.database.graph) + + self.mon_finder = FindMonster(self, self.fm_flags_default) + asyncio.create_task(self.check_index()) + async def check_index(self): issues = [] issues.extend(self.database.graph.issues) diff --git a/dbcog/find_monster/__init__.py b/dbcog/find_monster/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dbcog/find_monster.py b/dbcog/find_monster/find_monster.py similarity index 99% rename from dbcog/find_monster.py rename to dbcog/find_monster/find_monster.py index 81e1e0d5..e4a3a3c8 100644 --- a/dbcog/find_monster.py +++ b/dbcog/find_monster/find_monster.py @@ -6,7 +6,7 @@ from tsutils.formatting import rmdiacritics from tsutils.query_settings.query_settings import QuerySettings -from dbcog.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, QueryToken, SpecialToken, \ +from dbcog.find_monster.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, QueryToken, SpecialToken, \ TOKEN_JW_DISTANCE, TokenMatch, calc_ratio_modifier, calc_ratio_name, string_to_token from dbcog.models.monster_model import MonsterModel diff --git a/dbcog/find_monster_tokens.py b/dbcog/find_monster/find_monster_tokens.py similarity index 94% rename from dbcog/find_monster_tokens.py rename to dbcog/find_monster/find_monster_tokens.py index 5db64896..7a0c62ce 100644 --- a/dbcog/find_monster_tokens.py +++ b/dbcog/find_monster/find_monster_tokens.py @@ -8,14 +8,14 @@ from tsutils.helper_classes import DummyObject from tsutils.tsubaki.monster_header import MonsterHeader -from dbcog.models.enum_types import Attribute, AwokenSkills -from dbcog.models.monster_model import MonsterModel -from dbcog.monster_index import MonsterIndex -from dbcog.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, BOOL_MONSTER_ATTRIBUTE_ALIASES, \ +from dbcog.find_monster.token_mappings import AWAKENING_TOKENS, AWOKEN_SKILL_MAP, BOOL_MONSTER_ATTRIBUTE_ALIASES, \ BOOL_MONSTER_ATTRIBUTE_NAMES, \ NUMERIC_MONSTER_ATTRIBUTE_ALIASES, \ NUMERIC_MONSTER_ATTRIBUTE_NAMES, \ PLUS_AWOKENSKILL_MAP, STRING_MONSTER_ATTRIBUTE_ALIASES, STRING_MONSTER_ATTRIBUTE_NAMES +from dbcog.models.enum_types import Attribute, AwokenSkills +from dbcog.models.monster_model import MonsterModel +from dbcog.monster_index import MonsterIndex if TYPE_CHECKING: from dbcog import DBCog @@ -277,15 +277,37 @@ async def special_matches(self, monster): return score / self.max_score, MatchData(subquery_result=matched) -class HasMaterial(SubqueryToken): - RE_MATCH = r"ha[sz]mat:([\"']?)(.+)\1" +SQT = TypeVar("SQT", bound=Type[SubqueryToken]) + + +def make_tree_based(subquery_token: SQT, name: str, regex: str) -> SQT: + class _TreeToken(subquery_token): + RE_MATCH = regex + + def get_matching_monsters(self, monster): + return chain(*(super(_TreeToken, self).get_matching_monsters(nevo) + for nevo in self.dbcog.database.graph.get_all_prev_evolutions(monster))) + + _TreeToken.__name__ = name + return _TreeToken + + +class SelfHasMaterial(SubqueryToken): + RE_MATCH = (rf"(?:selfha[sz]mat|ha[sz]matself):([\"']?)(.+)\1") def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): _, subquery = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() super().__init__(fullvalue, subquery, negated=negated, exact=exact, dbcog=dbcog) def get_matching_monsters(self, monster): - return self.dbcog.database.graph.evo_mats(monster) + return self.dbcog.database.graph.evo_mats(self.dbcog.database.graph.evo_gem_monster(monster) or monster) + + +HasMaterial = make_tree_based( + SelfHasMaterial, + "HasMaterial", + rf"ha[sz]mat:([\"']?)(.+)\1" +) class SeriesOf(SubqueryToken): @@ -410,6 +432,7 @@ async def string_to_token(string: str, dbcog: "DBCog") -> QueryToken: MonsterAttributeNumeric, MonsterAttributeString, MonsterAttributeBool, + SelfHasMaterial, HasMaterial, SeriesOf, AttributeToken, diff --git a/dbcog/idtest_mixin.py b/dbcog/find_monster/idtest_mixin.py similarity index 100% rename from dbcog/idtest_mixin.py rename to dbcog/find_monster/idtest_mixin.py diff --git a/dbcog/token_mappings.py b/dbcog/find_monster/token_mappings.py similarity index 99% rename from dbcog/token_mappings.py rename to dbcog/find_monster/token_mappings.py index 084c5d40..8c8948cc 100644 --- a/dbcog/token_mappings.py +++ b/dbcog/find_monster/token_mappings.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Mapping, Tuple, TypeVar -from .models.enum_types import Attribute, AwokenSkills, MonsterType +from dbcog.models.enum_types import Attribute, AwokenSkills, MonsterType K = TypeVar('K') V = TypeVar('V') diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index ee94f856..af2c568c 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -719,6 +719,19 @@ def get_all_prev_transforms(self, monster: MonsterModel, *, include_self: bool = ret.pop(i) return ret + def get_all_next_evolutions(self, monster: MonsterModel, include_self: bool = True) -> Set[MonsterModel]: + to_parse = {monster} + ret = set() + while to_parse: + mon = to_parse.pop() + if mon in ret: + continue + ret.add(mon) + to_parse.update(self.get_next_evolutions(mon)) + if not include_self: + ret.remove(monster) + return ret + def get_monster_depth(self, monster: MonsterModel) -> float: if monster.monster_id == 5802: # DMG sucks! diff --git a/dbcog/monster_index.py b/dbcog/monster_index.py index 570a0fc0..4db8a71d 100644 --- a/dbcog/monster_index.py +++ b/dbcog/monster_index.py @@ -15,7 +15,7 @@ from .models.enum_types import AwokenSkills, DEFAULT_SERVER from .models.monster_model import MonsterModel from .monster_graph import MonsterGraph -from .token_mappings import ALL_TOKEN_DICTS, AWOKEN_SKILL_MAP, COLOR_MAP, DUAL_COLOR_MAP, EVO_MAP, EvoTypes, \ +from dbcog.find_monster.token_mappings import ALL_TOKEN_DICTS, AWOKEN_SKILL_MAP, COLOR_MAP, DUAL_COLOR_MAP, EVO_MAP, EvoTypes, \ HAZARDOUS_IN_NAME_MODS, KNOWN_MODIFIERS, LEGAL_END_TOKENS, MISC_MAP, MULTI_WORD_TOKENS, MiscModifiers, \ PROBLEMATIC_SERIES_TOKENS, SUB_COLOR_MAP, TYPE_MAP diff --git a/profile/profile.py b/profile/profile.py index 7ae3bac2..9eb40a69 100644 --- a/profile/profile.py +++ b/profile/profile.py @@ -10,7 +10,7 @@ logger = logging.getLogger('red.padbot-cogs.profile') -SUPPORTED_SERVERS = ["NA", "KR", "JP", "EU"] +SUPPORTED_SERVERS = ["NA", "KR", "JP"] def validate_and_clean_id(pad_id): From 22820521b00d09620b53c08b9931074e535860d8 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sun, 29 May 2022 20:00:44 -0500 Subject: [PATCH 1131/1220] Make debugiddist case insensitive (#1706) --- padinfo/padinfo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index f976e75b..9157f475 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -1192,11 +1192,11 @@ def mod_token_str(f: Callable[[str], bool]) -> str: async def debugiddist(self, ctx, s1, s2): """Find the distance between two queries. - For name tokens, the full word goes second as name token matching is not commutitive + For name tokens, the full word goes second as name token matching is not commutitive. Inputs will be converted to lowercase. """ - dbcog = await self.get_dbcog() - + s1 = s1.lower() + s2 = s2.lower() dist = dbcog.mon_finder.calc_ratio_modifier(s1, s2) dist2 = dbcog.mon_finder.calc_ratio_name(s1, s2) yes = '\N{WHITE HEAVY CHECK MARK}' From ff3e9c6934482bab95e281e8ffb45b08a0f99a78 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 31 May 2022 00:32:23 -0700 Subject: [PATCH 1132/1220] Add new awoken skills (#1708) --- dbcog/find_monster/token_mappings.py | 13 +++++++++++++ dbcog/models/enum_types.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/dbcog/find_monster/token_mappings.py b/dbcog/find_monster/token_mappings.py index 8c8948cc..a66d9eaa 100644 --- a/dbcog/find_monster/token_mappings.py +++ b/dbcog/find_monster/token_mappings.py @@ -176,6 +176,19 @@ class EvoTypes(Enum): AwokenSkills.ATTR4BOOST: ('attr4', '4attr'), AwokenSkills.ATTR5BOOST: ('attr5', '5attr'), AwokenSkills.BLOBBOOST: ('blobboost', 'blob', 'brickboost', 'brick'), + AwokenSkills.ADDTYPEDRAGON: (), + AwokenSkills.ADDTYPEGOD: (), + AwokenSkills.ADDTYPEDEVIL: (), + AwokenSkills.ADDTYPEMACHINE: (), + AwokenSkills.ADDTYPEBALANCED: (), + AwokenSkills.ADDTYPEATTACKER: (), + AwokenSkills.ADDTYPEPHYSICAL: (), + AwokenSkills.ADDTYPEHEALER: (), + AwokenSkills.SUBATTRRED: (), + AwokenSkills.SUBATTRBLUE: (), + AwokenSkills.SUBATTRGREEN: (), + AwokenSkills.SUBATTRLIGHT: (), + AwokenSkills.SUBATTRDARK: (), } diff --git a/dbcog/models/enum_types.py b/dbcog/models/enum_types.py index fe7bf21d..b20221d4 100644 --- a/dbcog/models/enum_types.py +++ b/dbcog/models/enum_types.py @@ -141,3 +141,16 @@ class AwokenSkills(Enum): ATTR4BOOST = 80 ATTR5BOOST = 81 BLOBBOOST = 82 + ADDTYPEDRAGON = 83 + ADDTYPEGOD = 84 + ADDTYPEDEVIL = 85 + ADDTYPEMACHINE = 86 + ADDTYPEBALANCED = 87 + ADDTYPEATTACKER = 88 + ADDTYPEPHYSICAL = 89 + ADDTYPEHEALER = 90 + SUBATTRRED = 91 + SUBATTRBLUE = 92 + SUBATTRGREEN = 93 + SUBATTRLIGHT = 94 + SUBATTRDARK = 95 From 9817bc1566232fbc8106c0a5a3eeb6b017469ca1 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 1 Jun 2022 00:41:52 -0700 Subject: [PATCH 1133/1220] Add subquery_data to ExtraInfo (#1714) --- dbcog/find_monster/find_monster.py | 45 ++++++++++++++- dbcog/find_monster/find_monster_tokens.py | 69 +++++++++++++++-------- 2 files changed, 86 insertions(+), 28 deletions(-) diff --git a/dbcog/find_monster/find_monster.py b/dbcog/find_monster/find_monster.py index e4a3a3c8..675f1e73 100644 --- a/dbcog/find_monster/find_monster.py +++ b/dbcog/find_monster/find_monster.py @@ -2,6 +2,7 @@ from collections import defaultdict from typing import Any, DefaultDict, Dict, Iterable, List, Mapping, NamedTuple, Optional, Set, Tuple, Union +from frozendict import frozendict from tsutils.enums import Server from tsutils.formatting import rmdiacritics from tsutils.query_settings.query_settings import QuerySettings @@ -23,8 +24,13 @@ class MonsterMatch: def __init__(self): + # The total score of the monster self.score: float = 0 + + # Match data for all name tokens self.name: Set[TokenMatch] = set() + + # Match data for all modifier tokens self.mod: Set[TokenMatch] = set() def __repr__(self): @@ -35,12 +41,32 @@ def __repr__(self): class MonsterInfo(NamedTuple): + # The best matching monster matched_monster: Optional[MonsterModel] + + # How the user tokens matched each monster monster_matches: MatchMap + + # All monsters that were not rejected by the query valid_monsters: Set[MonsterModel] +class SubqueryData(NamedTuple): + # Subquery token type matched + label: str + + # Subquery given + subquery: str + + # Immutable Parent-to-Child monster mapping + map: Mapping[MonsterModel, MonsterModel] + + class ExtraInfo(NamedTuple): + # Subquery Result + subquery_data: Set[SubqueryData] + + # Unused return_code: int @@ -177,7 +203,7 @@ async def do_matching(monsters: Set[MonsterModel], value: float, name_type: str) for matched_monster in monsters: if matched_monster not in valid_monsters and await token.matches(matched_monster, self.index): matches[matched_monster].name.add( - TokenMatch(token.value, match, MatchData(name_type=name_type))) + TokenMatch(token.value, match, MatchData(token, name_type=name_type))) matches[matched_monster].score += value valid_monsters.add(matched_monster) @@ -253,7 +279,18 @@ def get_most_eligable_monster(self, monsters: Iterable[MonsterModel], tokenized_ return max(monsters, key=lambda m: self.get_priority_tuple(m, tokenized_query, matches)) - async def _find_monster_search(self, tokenized_query: List[str]) -> \ + def build_extra_info(self, monster_matches: MatchMap) -> ExtraInfo: + subquery_data = defaultdict(dict) + for monster, match in monster_matches.items(): + for mod_token in match.mod: + if mod_token.match_data.subquery_result: + subquery_data[mod_token.match_data.token][monster] = mod_token.match_data.subquery_result + subquery_data = {SubqueryData(token.label, token.subquery, frozendict(m)) + for token, m in subquery_data.items()} + + return ExtraInfo(subquery_data, 0) + + async def _find_monCOLOR_MAPster_search(self, tokenized_query: List[str]) -> \ Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel]]: mod_tokens, name_query_tokens = await self._interpret_query(tokenized_query) @@ -319,7 +356,9 @@ async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: key=lambda t: t[1].get(t[0], MonsterMatch()).score ) - return MonsterInfo(best_monster, matches_dict, valid_monsters), ExtraInfo(0) + monster_info = MonsterInfo(best_monster, matches_dict, valid_monsters) + extra_info = self.build_extra_info(matches_dict) + return monster_info, extra_info async def find_monster(self, query: str) -> Tuple[Optional[MonsterModel], ExtraInfo]: """Get the best matching monster for a query. Returns None if no eligable monsters exist.""" diff --git a/dbcog/find_monster/find_monster_tokens.py b/dbcog/find_monster/find_monster_tokens.py index 7a0c62ce..987d6006 100644 --- a/dbcog/find_monster/find_monster_tokens.py +++ b/dbcog/find_monster/find_monster_tokens.py @@ -27,9 +27,19 @@ class MatchData(NamedTuple): + # The actual matching token + token: "QueryToken" + + # Type of name token name_type: Optional[str] = None + + # Subquery result subquery_result: Optional[MonsterModel] = None + + # Whether a attribute token matched the subattribute subattr_match: Optional[bool] = None + + # Which or result was chosen or_index: Optional[Tuple[int, str]] = None def __repr__(self): @@ -56,8 +66,13 @@ def __or__(self, other) -> "MatchData": class TokenMatch(NamedTuple): + # User token string token: str + + # Monster token string matched: str + + # Extra match data match_data: MatchData @@ -100,7 +115,7 @@ async def matches(self, monster, index): if matched_token not in index.modifiers[monster] or ratio <= MODIFIER_JW_DISTANCE: return 0.0, None - return ratio, TokenMatch(self.value, matched_token, MatchData()) + return ratio, TokenMatch(self.value, matched_token, MatchData(self)) class SpecialToken(QueryToken): @@ -111,13 +126,13 @@ def __init__(self, value, *, negated=False, exact=False, dbcog: "DBCog"): super().__init__(value, negated=negated, exact=exact) async def special_matches(self, monster: MonsterModel) -> Tuple[Union[bool, float], MatchData]: - return True, MatchData() + return True, MatchData(self) async def matches(self, monster, index): mult, data = await self.special_matches(monster) if self.value in index.modifiers[monster] and mult <= 1: # If the full value is actually a modifier - return 1.0, TokenMatch(self.value, self.value, MatchData()) + return 1.0, TokenMatch(self.value, self.value, MatchData(self)) return mult, TokenMatch(self.value, '', data) async def prepare(self): @@ -138,7 +153,7 @@ async def special_matches(self, monster): monster_total_awakenings_matching_token = 0 for awakening in monster.awakenings: if awakening.is_super and not self.allows_super_awakenings: - return False, MatchData() + return False, MatchData(self) # Keep track of whether we matched this cycle for SA check at the end matched = True @@ -156,13 +171,13 @@ async def special_matches(self, monster): matched = False if monster_total_awakenings_matching_token >= self.minimum_count: - return True, MatchData() + return True, MatchData(self) # If we already matched an SA and didn't return True, fail immediately. # We only allow one SA to count towards the total for each MultipleAwakeningToken if awakening.is_super and matched: - return False, MatchData() - return False, MatchData() + return False, MatchData(self) + return False, MatchData(self) class MonsterAttributeNumeric(SpecialToken): @@ -185,12 +200,12 @@ async def special_matches(self, monster): continue # Test each character of the equality operator because I don't want to use the eval function here if "<" in self.operator and val < self.rhs: - return True, MatchData() + return True, MatchData(self) if ">" in self.operator and val > self.rhs: - return True, MatchData() + return True, MatchData(self) if "=" in self.operator and val == self.rhs: - return True, MatchData() - return False, MatchData() + return True, MatchData(self) + return False, MatchData(self) class MonsterAttributeString(SpecialToken): @@ -214,17 +229,17 @@ async def special_matches(self, monster): continue val: str = val.lower() if self.match == "=" and val == self.string: # Exact match - return True, MatchData() + return True, MatchData(self) elif self.match == "r" and bool(re.search(self.string, val)): # Regex match - return True, MatchData() + return True, MatchData(self) elif self.match == "g" and fnmatch(val, '*' + self.string + '*'): # Glob match - return True, MatchData() + return True, MatchData(self) elif self.string in val: - return True, MatchData() + return True, MatchData(self) max_score = max(max_score, jaro(self.string, val)) if max_score >= TOKEN_JW_DISTANCE: - return max_score, MatchData() - return False, MatchData() + return max_score, MatchData(self) + return False, MatchData(self) class MonsterAttributeBool(SpecialToken): @@ -245,11 +260,13 @@ async def special_matches(self, monster): if val is None: continue if val == self.bool_value: - return True, MatchData() - return False, MatchData() + return True, MatchData(self) + return False, MatchData(self) class SubqueryToken(SpecialToken): + label: str + def __init__(self, fullvalue, subquery, *, negated=False, exact=False, dbcog): self.subquery = subquery self.sub_matches = None @@ -271,10 +288,10 @@ def get_matching_monsters(self, monster: MonsterModel) -> Iterable[MonsterModel] async def special_matches(self, monster): mons = self.valid_monsters.intersection(self.get_matching_monsters(monster)) if not mons: - return False, MatchData() + return False, MatchData(self) matched, score = max(((mon, self.sub_matches.get(mon, DummyObject(score=0)).score) for mon in mons), key=lambda x: x[1]) - return score / self.max_score, MatchData(subquery_result=matched) + return score / self.max_score, MatchData(self, subquery_result=matched) SQT = TypeVar("SQT", bound=Type[SubqueryToken]) @@ -293,6 +310,7 @@ def get_matching_monsters(self, monster): class SelfHasMaterial(SubqueryToken): + label = 'hasmat' RE_MATCH = (rf"(?:selfha[sz]mat|ha[sz]matself):([\"']?)(.+)\1") def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): @@ -311,6 +329,7 @@ def get_matching_monsters(self, monster): class SeriesOf(SubqueryToken): + label = 'seriesof' RE_MATCH = r"seriesof:([\"']?)(.+)\1" def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): @@ -344,10 +363,10 @@ def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): async def special_matches(self, monster): if monster.attr1 == self.attr: - return True, MatchData() + return True, MatchData(self) if monster.attr2 == self.attr: - return 1 - EPSILON, MatchData(subattr_match=True) - return False, MatchData() + return 1 - EPSILON, MatchData(self, subattr_match=True) + return False, MatchData(self) class OrToken(SpecialToken): @@ -372,7 +391,7 @@ async def matches(self, monster, index): # print(token, score, data, monster) if score > best_score: best_score = score - best_data = TokenMatch(self.value, '', data.match_data | MatchData(or_index=(idx, token.value))) + best_data = TokenMatch(self.value, '', data.match_data | MatchData(self, or_index=(idx, token.value))) return best_score, best_data From c9396557e356796b293c54d8c41958717dd1db31 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 1 Jun 2022 17:12:13 -0700 Subject: [PATCH 1134/1220] Remove committed typo --- dbcog/find_monster/find_monster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbcog/find_monster/find_monster.py b/dbcog/find_monster/find_monster.py index 675f1e73..b4a37d57 100644 --- a/dbcog/find_monster/find_monster.py +++ b/dbcog/find_monster/find_monster.py @@ -290,7 +290,7 @@ def build_extra_info(self, monster_matches: MatchMap) -> ExtraInfo: return ExtraInfo(subquery_data, 0) - async def _find_monCOLOR_MAPster_search(self, tokenized_query: List[str]) -> \ + async def _find_monster_search(self, tokenized_query: List[str]) -> \ Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel]]: mod_tokens, name_query_tokens = await self._interpret_query(tokenized_query) From 2df8bbfcfa9b43249466c659550d94a216ab77d4 Mon Sep 17 00:00:00 2001 From: Blastose Date: Sat, 4 Jun 2022 10:07:04 -0700 Subject: [PATCH 1135/1220] Remove repeated assignment to defense (#1718) --- dungeoncog/dungeon_monster.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dungeoncog/dungeon_monster.py b/dungeoncog/dungeon_monster.py index 20ddb87f..32a901bb 100644 --- a/dungeoncog/dungeon_monster.py +++ b/dungeoncog/dungeon_monster.py @@ -12,7 +12,6 @@ def __init__(self, hp, atk, defense, turns, level, monster, error=None): self.hp = hp self.atk = atk self.defense = defense - self.defense = hp self.turns = turns self.level = level self.groups: List[GroupedSkills] = [] From fd015c76bd38252ebebd2c0884a5794ab112a92e Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 6 Jun 2022 02:52:53 -0700 Subject: [PATCH 1136/1220] Use azurlane URL rather than cloudfront (#1720) --- azurlane/azurlane_view.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/azurlane/azurlane_view.py b/azurlane/azurlane_view.py index b296c7dc..b7de8488 100644 --- a/azurlane/azurlane_view.py +++ b/azurlane/azurlane_view.py @@ -1,14 +1,12 @@ from typing import Collection, TYPE_CHECKING -from discordmenu.embed.components import EmbedMain, EmbedBodyImage +from discordmenu.embed.components import EmbedBodyImage, EmbedMain from discordmenu.embed.view import EmbedView from tsutils.menu.components.config import UserConfig from tsutils.menu.components.footers import embed_footer_with_state from tsutils.menu.view.view_state_base import ViewStateBase from tsutils.query_settings.query_settings import QuerySettings -from azurlane.resources import BASE_URL - if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -70,15 +68,13 @@ def embed(cls, state: AzurlaneViewState): image_title = image['title'] url = image['url'] - file_name = url[url.rfind("/") + 1:] - full_url = BASE_URL.format(file_name) return EmbedView( EmbedMain( color=state.query_settings.embedcolor, title=f'[{cid}] {name} - {image_title}', - url=full_url + url=url ), - embed_body_image=EmbedBodyImage(full_url), + embed_body_image=EmbedBodyImage(url), embed_footer=embed_footer_with_state(state) ) From a97c25e540ac3d68a811423a65f4016762def415 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 11 Jun 2022 21:25:00 -0700 Subject: [PATCH 1137/1220] Add IDRangeToken & use subqueries in OrToken (#1722) --- dbcog/find_monster/find_monster.py | 2 +- dbcog/find_monster/tokens/__init__.py | 0 .../{ => tokens}/find_monster_tokens.py | 53 ++++++++++++++----- 3 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 dbcog/find_monster/tokens/__init__.py rename dbcog/find_monster/{ => tokens}/find_monster_tokens.py (89%) diff --git a/dbcog/find_monster/find_monster.py b/dbcog/find_monster/find_monster.py index b4a37d57..dd537635 100644 --- a/dbcog/find_monster/find_monster.py +++ b/dbcog/find_monster/find_monster.py @@ -7,7 +7,7 @@ from tsutils.formatting import rmdiacritics from tsutils.query_settings.query_settings import QuerySettings -from dbcog.find_monster.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, QueryToken, SpecialToken, \ +from dbcog.find_monster.tokens.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, QueryToken, SpecialToken, \ TOKEN_JW_DISTANCE, TokenMatch, calc_ratio_modifier, calc_ratio_name, string_to_token from dbcog.models.monster_model import MonsterModel diff --git a/dbcog/find_monster/tokens/__init__.py b/dbcog/find_monster/tokens/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dbcog/find_monster/find_monster_tokens.py b/dbcog/find_monster/tokens/find_monster_tokens.py similarity index 89% rename from dbcog/find_monster/find_monster_tokens.py rename to dbcog/find_monster/tokens/find_monster_tokens.py index 987d6006..91315b07 100644 --- a/dbcog/find_monster/find_monster_tokens.py +++ b/dbcog/find_monster/tokens/find_monster_tokens.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from collections import namedtuple from fnmatch import fnmatch from itertools import chain from typing import Iterable, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union @@ -373,28 +374,55 @@ class OrToken(SpecialToken): RE_MATCH = r"\[.+ .+\]" def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): - self._strings = re.sub(r'"[^"]+"|\'[^\']+\'', - lambda m: m.group(0).replace(' ', ''), - fullvalue[1:-1]).split() + self.subqueries = [s.replace('\0', ' ') + for s in re.sub(r'"[^"]+"|\'[^\']+\'', + lambda m: m.group(0).replace(' ', '\0')[1:-1], + fullvalue[1:-1]).split()] + print(self.subqueries) self.tokens: Optional[List[QueryToken]] = None + self.results = [] super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) async def prepare(self): - self.tokens = [await string_to_token(s, self.dbcog) for s in self._strings] + Subquery = namedtuple("Subquery", 'sub_matches valid_monsters max_score') + for subquery in self.subqueries: + m_info, _ = await self.dbcog.find_monster_debug(subquery) + self.results.append(Subquery( + m_info.monster_matches, + m_info.valid_monsters, + max((m_info.monster_matches[mon].score for mon in m_info.valid_monsters), default=0) + )) - async def matches(self, monster, index): - best_score, best_data = 0.0, None - for idx, token in enumerate(self.tokens): - score, data = await token.matches(monster, index) - # print(token, score, data, monster) - if score > best_score: - best_score = score - best_data = TokenMatch(self.value, '', data.match_data | MatchData(self, or_index=(idx, token.value))) + async def special_matches(self, monster): + best_score = 0 + best_data = MatchData(self) + for c, subquery in enumerate(self.results): + mons = subquery.valid_monsters + if monster not in subquery.valid_monsters: + continue + score = subquery.sub_matches.get(monster, DummyObject(score=0)).score + if best_score < score / subquery.max_score: + best_score = score / subquery.max_score + best_data = MatchData(self, or_index=(c, self.subqueries[c])) return best_score, best_data +class IDRangeToken(SpecialToken): + RE_MATCH = r"(\d+?)-(\d*)" + + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): + lbound, rbound = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() + self.lower_bound = int(lbound) + self.upper_bound = int(rbound or 9e9) + + super().__init__(fullvalue, negated=negated, exact=exact, dbcog=dbcog) + + async def special_matches(self, monster): + return self.lower_bound <= monster.monster_id <= self.upper_bound, MatchData(self) + + def calc_ratio_modifier(s1: Union[QueryToken, str], s2: str, prefix_weight: float = .05) -> float: """Calculate the modifier distance between two tokens""" if isinstance(s1, QueryToken): @@ -456,4 +484,5 @@ async def string_to_token(string: str, dbcog: "DBCog") -> QueryToken: SeriesOf, AttributeToken, OrToken, + IDRangeToken, } From eba9edd3ebe1deeef791c6f6dbde88457ae69aa1 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Mon, 13 Jun 2022 09:49:32 +0800 Subject: [PATCH 1138/1220] Freeze requirements.txt for performance from fresh start (#1726) --- requirements.txt | 74 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/requirements.txt b/requirements.txt index c48882c1..c4616eed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,60 @@ -aiofiles -aiohttp -aioodbc -discord +aiofiles==0.8.0 +aiohttp==3.7.4.post0 +aiohttp-json-rpc==0.13.3 +aiomysql==0.0.22 +aiosqlite==0.17.0 +appdirs==1.4.4 +apsw-wheels==3.36.0.post1 +async-timeout==3.0.1 +attrs==21.2.0 +Babel==2.9.1 +backoff==1.10.0 +cffi==1.14.6 +chardet==4.0.0 +click==8.0.1 +colorama==0.4.4 +commonmark==0.9.1 +contextlib2==21.6.0 +decorator==5.1.1 +discord==1.0.1 discord-menu -git+git://github.com/TsubakiBotPad/python-romkan -jinja2 -networkx -opencv-python -Pillow -ply -prettytable -pymysql -python-levenshtein -pytz -Red-DiscordBot[postgres] -regex +discord.py +frozendict==2.3.2 +fuzzywuzzy==0.18.0 +git+https://github.com/TsubakiBotPad/python-romkan.git +idna==3.2 +Jinja2==3.0.1 +Markdown==3.3.4 +MarkupSafe==2.1.1 +multidict==5.1.0 +networkx==2.5 +numpy==1.22.4 +opencv-python==4.6.0.66 +Pillow==8.0.1 +ply==3.11 +prettytable==1.0.1 +protobuf==3.17.3 +psutil==5.8.0 +pycparser==2.20 +Pygments==2.10.0 +PyMySQL==0.9.3 +PyNaCl==1.4.0 +python-dateutil==2.8.2 +python-Levenshtein-wheels==0.13.2 +pytz==2021.1 +PyYAML==5.4.1 +Red-DiscordBot +Red-Lavalink==0.9.0 +regex==2021.8.28 +rich==10.9.0 +romkan==0.2.1 +schema==0.7.4 setuptools -tabulate +six==1.16.0 +tabulate==0.8.7 tsutils +typing-extensions==3.10.0.2 +uvloop==0.16.0 +wcwidth==0.2.5 wheel -protobuf \ No newline at end of file +yarl==1.6.3 \ No newline at end of file From 80a0c89e528ede0b854e24c0c7d290d8ce415a78 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Mon, 13 Jun 2022 11:43:30 +0800 Subject: [PATCH 1139/1220] Fix extra parameter in nadiff (#1728) --- padinfo/view/id.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/padinfo/view/id.py b/padinfo/view/id.py index ae7fbe3b..149d1833 100644 --- a/padinfo/view/id.py +++ b/padinfo/view/id.py @@ -136,11 +136,11 @@ def set_na_diff_invalid_message(self, ims: dict) -> bool: def get_na_diff_invalid_message(self) -> Optional[str]: monster: "MonsterModel" = self.monster if monster.on_na and not monster.on_jp: - return invalid_monster_text(self.query, monster, self.nadiff_na_only_text, link=True) + return invalid_monster_text(self.query, monster, self.nadiff_na_only_text) if monster.on_jp and not monster.on_na: - return invalid_monster_text(self.query, monster, self.nadiff_jp_only_text, link=True) + return invalid_monster_text(self.query, monster, self.nadiff_jp_only_text) if not self.is_jp_buffed: - return invalid_monster_text(self.query, monster, self.nadiff_identical_text, link=True) + return invalid_monster_text(self.query, monster, self.nadiff_identical_text) return None From 76fc9b924a4c556e9d37019211845dda2a597520 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Mon, 13 Jun 2022 18:06:59 +0800 Subject: [PATCH 1140/1220] Update README setup instructions (#1727) --- README.md | 169 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 102 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index bce918b5..ba1cfef4 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,137 @@ -# padbot-cogs +# pad-cogs **Important: You must use Python 3.8 for the setup!!** If you use an earlier or later version, things WILL break. It's not just convenient to be on the same version as us, it's mandatory. -# Setting up your own Tsubaki instance for contributing code +# Setting up your own Tsubaki instance -## Installing Tools +## Prerequisites / Tools -* Install Git (see the Red install link below if you need help doing this, or read any tutorial) -* Install Python 3.8 (see the Red install link below if you need help doing this, or read any tutorial) -* Familiarize yourself with how to use a command prompt and Git Bash (if you can't do this you will have a bad time) +- Install Git +- Install Python 3.8 +- Familiarize yourself with how to use a command prompt and Git Bash (if you can't do this you will have a bad time) +[Red's Installation Guide](https://docs.discord.red/en/stable/install_guides/index.html) is a useful resource if you're a first timer. + +I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) for development. Change into the +`PycharmProjects` directory and clone your fork of the repo: ## Check out the repo -Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/padbot-cogs). +Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/pad-cogs). -> Note: If you are developing a cog that is in the [misc-cogs repo](https://github.com/TsubakiBotPad/misc-cogs), you may need to complete some steps for both pad-cogs AND misc-cogs. +> Note: If you are developing a cog that is in the [misc-cogs repo](https://github.com/TsubakiBotPad/misc-cogs), you may need to complete some steps for both pad-cogs AND misc-cogs. You will need a GitHub account to do this. Click the button that says "Fork" in the upper-right-hand corner of the page. -I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) for development. Change into the -`PycharmProjects` directory and clone your fork of the repo: +Git clone your forked repository. -```bash -# Clone your repo, not this one!!!!!!!! You will want to load your own changes!!!! -git clone https://github.com/TsubakiBotPad/padbot-cogs.git -``` +## Create a Discord Bot -## Set up your bot -The installation instructions in Red's official documentation are pretty good. You will need to be prepared to do the following: -* Create a venv -* Install the Red library -* Create a Discord API key, make a bot, and associate it to your installation +If you don't have one already, follow the instructions to create a bot in Red's official documentation: -> You may want to perform the next step, "Installing dependencies," partway through the RED installation. The ideal time is immediately after you have created your venv, but before doing anything else. +[Creating a bot account](https://docs.discord.red/en/stable/bot_application_guide.html#creating-a-bot-account) -Installation instruction links: -* [Windows install instructions](https://docs.discord.red/en/stable/install_windows.html) -* [Linux/Mac install instructions](https://docs.discord.red/en/stable/install_linux_mac.html) +Keep the bot `token` that you get from Discord at the end of the instructions handy - you will need it to set up the bot later. -If you are running on a cloud server for the first time and aren't sure what Linux distro to pick, please, please, please, please, please pick Ubuntu. Their docs aren't well-tested on other distros. +## Installation -## Installing dependencies -First download `requirements.txt` so that you can run it from your Red venv. In Linux this is done by running the following from the directory holding the folder `name-of-bot` that you created during the Red setup process: -```bash -wget https://raw.githubusercontent.com/TsubakiBotPad/padbot-cogs/master/requirements.txt -``` -Then run: -```bash -source name-of-bot/bin/activate -pip install -r requirements.txt -deactivate -``` -The rest of the guide takes place from inside Discord. Replace `^` with your prefix. +1. Clone this repo (fork if you're contributing) +1. Create a python 3 venv. `virtualenv -p python3 ` +1. Activate the venv. +1. `pip install -r requirements.txt` + +The above steps install Red automatically. You can now follow the Red instructions to startup the bot: + +[Setting Up and Running Red (macOS)](https://docs.discord.red/en/stable/install_guides/mac.html#setting-up-and-running-red) + +In command line: + +- `redbot-setup` +- `redbot ` + +## Configure Your Bot + +The rest of the guide takes place from inside Discord. Replace `^` with your prefix to talk to your bot. + +### Tell your bot where to find the code -## Telling your bot where to find your cogs Once the bot is launched, set it to use your repo directory as a cog path. Type this in Discord where the bot is: ``` ^addpath path/to/repo/root ``` +e.g + +``` +^addpath /Users/Tsubaki/src/pad-cogs +``` + > Note: you may have to do this for `pad-cogs`, `core-cogs`, and `misc-cogs`. -## Loading cogs +### Load cogs -* Some cogs will have cross dependencies on each other. Use `^traceback 1` if you encounter any errors to see your traceback. -* Common dependencies include: - * dbcog - * padinfo +Load relevant cogs using the `^load` command. + +``` +Syntax: ^load ... + +e.g +^load dbcog padinfo +``` + +You can see which cogs you have loaded using the `^cogs` command. + +### Test a simple command + +You should now be able to run basic commands like `^id` + +``` +^id tsubaki +``` + +Should return something like: +image + +Happy developing! + +## Appendix + +### Debugging + +- Use `^traceback 1` if you encounter any errors to display them in discord. ### Emoji -* If you are doing in-depth development of the `^id` command, you may need your bot invited to the emoji servers. River has access to these, talk to her. -* You will use `^padinfo emojiserver add` with the IDs she gives you. -## Cog list +You may have noticed that emojis are replaced with placeholders in some commands. This is because the bot does not have access to the specific emojis. + +- If you are doing in-depth development of the `^id` command, your bot will need to be invited to the emoji servers. Talk to River to get access to these servers. + +- Once you and your bot have joined the servers, use `^padinfo emojiserver add ... ` with the IDs she gives you. + +### Cog list Most cogs here relate to the mobile game 'Puzzle & Dragons'. -| Cog | Purpose | -| --- | --- | -| azurelane | **DISCONTINUED** Azur Lane card lookup | -| crowddata | Crowdsourced data collection | -| crud | Database editor. Not useful for bots other than prod Tsubaki | -| dbcog | Central cog to host all PAD data | -| dungeoncog | A cog to search and get info for dungeons | -| damagecalc | Simple attack damage calculator | -| feedback | A specialized feedback cog for Tsubaki specifically | -| monidlistener | A listener to give info for monster ids | -| padboard | Converts board images to dawnglare board/solved board links | -| padbuildimg | Creates images of PAD teams via a special query language | -| padbuilds | See user-made builds for dungeons i think??? (nobody uses this) | -| padevents | A scheduler cog to help players see upcoming daily GH dungeons. | -| padglobal | Global PAD info commands | -| padinfo | Monster lookup and info display | -| padmonitor | Keeps an eye out for PAD updates | -| padsearch | Search for monsters with specific attributes | -| pipelineui | Interface for running the pipeline (Tsubaki-only) | -| pricecheck | Lumon's thing. Something about stamina trade equivalence | -| profile | Global user PAD profile storage and lookup | -| schoolidol | **DISCONTINUED** Love Live! School Idol Festival card lookup | +| Cog | Purpose | +| ------------- | --------------------------------------------------------------- | +| azurelane | **DISCONTINUED** Azure Lane card lookup | +| crowddata | Crowdsourced data collection | +| crud | Database editor. Not useful for bots other than prod Tsubaki | +| dbcog | Central cog to host all PAD data | +| dungeoncog | A cog to search and get info for dungeons | +| damagecalc | Simple attack damage calculator | +| feedback | A specialized feedback cog for Tsubaki specifically | +| monidlistener | A listener to give info for monster ids | +| padboard | Converts board images to dawnglare board/solved board links | +| padbuildimg | Creates images of PAD teams via a special query language | +| padbuilds | See user-made builds for dungeons i think??? (nobody uses this) | +| padevents | A scheduler cog to help players see upcoming daily GH dungeons. | +| padglobal | Global PAD info commands | +| padinfo | Monster lookup and info display | +| padmonitor | Keeps an eye out for PAD updates | +| padsearch | Search for monsters with specific attributes | +| pipelineui | Interface for running the pipeline (Tsubaki-only) | +| pricecheck | Lumon's thing. Something about stamina trade equivalence | +| profile | Global user PAD profile storage and lookup | +| schoolidol | **DISCONTINUED** Love Live! School Idol Festival card lookup | From 8e61433b1427e31360fe484021344d85b4e7c58a Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:51:48 -0400 Subject: [PATCH 1141/1220] PADle (#1715) There are a couple fast follows still after this, but we want to be able to merge menu2 code --- __init__.py | 0 padle/__init__.py | 9 + padle/info.json | 21 + padle/menu/__init__.py | 0 padle/menu/closable_embed.py | 9 + padle/menu/menu_map.py | 9 + padle/menu/padle_scroll.py | 79 ++++ padle/monsterdiff.py | 144 ++++++ padle/padle.py | 786 ++++++++++++++++++++++++++++++++ padle/view/__init__.py | 0 padle/view/confirmation.py | 24 + padle/view/padle_scroll_view.py | 98 ++++ 12 files changed, 1179 insertions(+) create mode 100644 __init__.py create mode 100644 padle/__init__.py create mode 100644 padle/info.json create mode 100644 padle/menu/__init__.py create mode 100644 padle/menu/closable_embed.py create mode 100644 padle/menu/menu_map.py create mode 100644 padle/menu/padle_scroll.py create mode 100644 padle/monsterdiff.py create mode 100644 padle/padle.py create mode 100644 padle/view/__init__.py create mode 100644 padle/view/confirmation.py create mode 100644 padle/view/padle_scroll_view.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padle/__init__.py b/padle/__init__.py new file mode 100644 index 00000000..9e0cf0f2 --- /dev/null +++ b/padle/__init__.py @@ -0,0 +1,9 @@ +from .padle import PADle + +__red_end_user_data_statement__ = "Guess data for PADles played is saved." + + +async def setup(bot): + n = PADle(bot) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) + bot.loop.create_task(n.register_menu()) diff --git a/padle/info.json b/padle/info.json new file mode 100644 index 00000000..ce2d7e7e --- /dev/null +++ b/padle/info.json @@ -0,0 +1,21 @@ +{ + "author": [ + "fluff#2368" + ], + "description": "PADle, a Wordle game for PAD.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "Use [p]padle.", + "min_bot_version": "3.4.0", + "max_bot_version": "0.0.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "required_cogs": {}, + "requirements": ["tsutils>=4.0.0"], + "tags": [] + } + \ No newline at end of file diff --git a/padle/menu/__init__.py b/padle/menu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padle/menu/closable_embed.py b/padle/menu/closable_embed.py new file mode 100644 index 00000000..393c1624 --- /dev/null +++ b/padle/menu/closable_embed.py @@ -0,0 +1,9 @@ +from tsutils.menu.closable_embed_base import ClosableEmbedMenuBase +from padle.view.confirmation import PADleMonsterConfirmationView + + +class ClosableEmbedMenu(ClosableEmbedMenuBase): + MENU_TYPE = PADleMonsterConfirmationView.VIEW_TYPE + view_types = { + PADleMonsterConfirmationView.VIEW_TYPE: PADleMonsterConfirmationView + } diff --git a/padle/menu/menu_map.py b/padle/menu/menu_map.py new file mode 100644 index 00000000..7a21a2cc --- /dev/null +++ b/padle/menu/menu_map.py @@ -0,0 +1,9 @@ +from tsutils.menu.closable_embed_base import ClosableEmbedMenuPanes + +from padle.menu.closable_embed import ClosableEmbedMenu +from padle.menu.padle_scroll import PADleScrollMenu, PADleMenuPanes + +padle_menu_map = { + ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), + PADleScrollMenu.MENU_TYPE: (PADleScrollMenu, PADleMenuPanes) +} diff --git a/padle/menu/padle_scroll.py b/padle/menu/padle_scroll.py new file mode 100644 index 00000000..465952ae --- /dev/null +++ b/padle/menu/padle_scroll.py @@ -0,0 +1,79 @@ +from math import ceil +from typing import Optional + +from discord import Message +from discordmenu.embed.menu import EmbedControl, EmbedMenu +from tsutils.menu.components.panes import MenuPanes + +from padle.view.padle_scroll_view import PADleScrollView, PADleScrollViewState + + +class ScrollEmojis: + prev_page = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + next_page = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + + +class PADleScrollMenu: + MENU_TYPE = 'PADleScrollMenu' + + view_types = { + PADleScrollViewState.VIEW_STATE_TYPE: PADleScrollView + } + + @staticmethod + def menu(initial_control=None): + if initial_control is None: + initial_control = PADleScrollMenu.control + embed = EmbedMenu(PADleMenuPanes.transitions(), initial_control) + return embed + + @classmethod + def get_num_pages_ims(cls, ims): + return ims['num_pages'] + + @classmethod + async def respond_with_left(cls, message: Optional[Message], ims, **data): + # PADleScrollMenu.decrement_page(ims) + if ims['current_page'] < PADleScrollMenu.get_num_pages_ims(ims) - 1: + ims['current_page'] = ims['current_page'] + 1 + else: + ims['current_page'] = 0 + return await PADleScrollMenu.respond_with_pane(message, ims, **data) + + @classmethod + async def respond_with_right(cls, message: Optional[Message], ims, **data): + if ims['current_page'] > 0: + ims['current_page'] = ims['current_page'] - 1 + else: + ims['current_page'] = PADleScrollMenu.get_num_pages_ims(ims) - 1 + return await PADleScrollMenu.respond_with_pane(message, ims, **data) + + @classmethod + async def respond_with_pane(cls, message: Optional[Message], ims, **data) -> PADleScrollViewState: + dbcog = data['dbcog'] + user_config = data['user_config'] + today_guesses = data['today_guesses'] + view_state = await PADleScrollViewState.deserialize(dbcog, user_config, today_guesses, ims) + return PADleScrollMenu.control(view_state) + + @staticmethod + def control(state: PADleScrollViewState): + if state is None: + return None + reaction_list = PADleMenuPanes.get_initial_reaction_list() + return EmbedControl( + [PADleScrollView.embed(state)], + reaction_list + ) + + +class PADleMenuPanes(MenuPanes): + INITIAL_EMOJI = ScrollEmojis.prev_page + DATA = { + ScrollEmojis.next_page: (PADleScrollMenu.respond_with_left, PADleScrollView.VIEW_TYPE), + ScrollEmojis.prev_page: (PADleScrollMenu.respond_with_right, PADleScrollView.VIEW_TYPE), + } + + @classmethod + def get_initial_reaction_list(cls): + return [ScrollEmojis.prev_page, ScrollEmojis.next_page] diff --git a/padle/monsterdiff.py b/padle/monsterdiff.py new file mode 100644 index 00000000..b22c3777 --- /dev/null +++ b/padle/monsterdiff.py @@ -0,0 +1,144 @@ +from tsutils.tsubaki.custom_emoji import get_awakening_emoji, get_rarity_emoji, get_type_emoji, \ + get_attribute_emoji_by_enum +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from dbcog.dbcog import MonsterModel + +class MonsterDiff: + + def __init__(self, monster: "MonsterModel", guess_monster: "MonsterModel"): + self.monster = monster + self.guess_monster = guess_monster + self.awakenings_diff = self.get_awakenings_diff(monster, guess_monster) + self.rarity_diff = self.get_rarity_diff(monster, guess_monster) + self.attr_diff = self.get_attr_diff(monster, guess_monster) + self.mp_diff = self.get_mp_diff(monster, guess_monster) + self.type_diff = self.get_type_diff(monster, guess_monster) + + def get_diff_score(self): + score = 0 + for v in self.awakenings_diff: + score += v + for v in self.attr_diff: + score += v + for v in self.type_diff: + score += v + score += 1 if self.rarity_diff == 0 else 0 + score += 1 if self.mp_diff == 0 else 0 + return score + + def get_attr_diff(self, monster, guess_monster): + attr1 = guess_monster.attr1.name.lower() + attr2 = guess_monster.attr2.name.lower() + attr_feedback = [1 if attr1 == monster.attr1.name.lower() else 0, + 1 if attr2 == monster.attr2.name.lower() else 0] + if (attr1 == monster.attr2.name.lower() and + attr2 != monster.attr2.name.lower() and attr1 != monster.attr1.name.lower()): + attr_feedback[0] = 0.5 + if (attr2 == monster.attr1.name.lower() and + attr2 != monster.attr2.name.lower() and attr1 != monster.attr1.name.lower()): + attr_feedback[1] = 0.5 + return attr_feedback + + def get_awakenings_diff(self, monster, guess_monster): + guess_awo_count = len(guess_monster.awakenings) - guess_monster.superawakening_count + monster_awo_count = len(monster.awakenings) - guess_monster.superawakening_count + unused = monster.awakenings[:monster_awo_count] + feedback = [] + for index, guess_awakening in enumerate(guess_monster.awakenings[:guess_awo_count]): + if (index < monster_awo_count and + monster.awakenings[index].awoken_skill_id == guess_awakening.awoken_skill_id): + feedback.append(1) + unused.remove(guess_awakening) + else: + feedback.append(0) + for index, guess_awakening in enumerate(guess_monster.awakenings[:guess_awo_count]): + if guess_awakening in unused and feedback[index] != 1: + feedback[index] = 0.5 + unused.remove(guess_awakening) + return feedback + + def get_rarity_diff(self, monster, guess_monster): + if monster.rarity == guess_monster.rarity: + return 0 + elif guess_monster.rarity > monster.rarity: + return -1 + else: + return 1 + + def get_mp_diff(self, monster, guess_monster): + if monster.sell_mp == guess_monster.sell_mp: + return 0 + elif guess_monster.sell_mp > monster.sell_mp: + return -1 + else: + return 1 + + def get_type_diff(self, monster, guess_monster): + diff = [] + for type in guess_monster.types: + if type in monster.types: + diff.append(1) + else: + diff.append(0) + return diff + + def get_name_line_feedback_text(self) -> str: + line = [] + attr1 = self.guess_monster.attr1.name.lower() + attr2 = self.guess_monster.attr2.name.lower() + attr_feedback = [] + for num in self.attr_diff: + if num == 0: + attr_feedback.append("\N{CROSS MARK}") + elif num == 0.5: + attr_feedback.append("\N{LARGE YELLOW SQUARE}") + else: + attr_feedback.append("\N{WHITE HEAVY CHECK MARK}") + line.append(get_attribute_emoji_by_enum(self.guess_monster.attr1)) + line.append(attr_feedback[0] + " / ") + line.append(get_attribute_emoji_by_enum(self.guess_monster.attr2)) + line.append(attr_feedback[1] + " ") + line.append("[" + str(self.guess_monster.monster_id) + "] ") + line.append(self.guess_monster.name_en) + return "".join(line) + + def get_other_info_feedback_text(self) -> str: + line = [get_rarity_emoji(self.guess_monster.rarity)] + if self.rarity_diff == 0: + line.append("\N{WHITE HEAVY CHECK MARK} | ") + elif self.rarity_diff == -1: + line.append("\N{DOWNWARDS BLACK ARROW}\N{VARIATION SELECTOR-16} | ") + else: + line.append("\N{UPWARDS BLACK ARROW}\N{VARIATION SELECTOR-16} | ") + for type in self.guess_monster.types: + line.append(get_type_emoji(type)) + if type in self.monster.types: + line.append("\N{WHITE HEAVY CHECK MARK} ") + else: + line.append("\N{CROSS MARK} ") + line.append(" | Sell MP: ") + line.append('{:,}'.format(self.guess_monster.sell_mp)) + if self.mp_diff == 0: + line.append("\N{WHITE HEAVY CHECK MARK}") + elif self.mp_diff == -1: + line.append("\N{DOWNWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}") + else: + line.append("\N{UPWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}") + + return "".join(line) + + def get_awakenings_feedback_text(self) -> str: + awakes = [] + feedback = [] + guess_awo_count = len(self.guess_monster.awakenings) - self.guess_monster.superawakening_count + for index, guess_awake in enumerate(self.guess_monster.awakenings[:guess_awo_count]): + awakes.append(get_awakening_emoji(guess_awake.awoken_skill_id, guess_awake.name)) + val = self.awakenings_diff[index] + if val == 0: + feedback.append("\N{CROSS MARK}") + elif val == 0.5: + feedback.append("\N{LARGE YELLOW SQUARE}") + else: + feedback.append("\N{WHITE HEAVY CHECK MARK}") + return "\n".join(["".join(awakes), "".join(feedback)]) diff --git a/padle/padle.py b/padle/padle.py new file mode 100644 index 00000000..7c5f1e4b --- /dev/null +++ b/padle/padle.py @@ -0,0 +1,786 @@ +import asyncio +import datetime +import discord +import json +import logging +import random +import re +from contextlib import suppress +from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedFooter +from discordmenu.embed.view import EmbedView +from io import BytesIO +from math import ceil +from padle.menu.closable_embed import ClosableEmbedMenu +from padle.menu.menu_map import padle_menu_map +from padle.menu.padle_scroll import PADleScrollMenu, PADleScrollViewState +from padle.monsterdiff import MonsterDiff +from padle.view.confirmation import PADleMonsterConfirmationView, PADleMonsterConfirmationViewProps +from redbot.core import Config, commands +from redbot.core.utils.chat_formatting import pagify +from tsutils.cogs.globaladmin import auth_check +from tsutils.emoji import NO_EMOJI, YES_EMOJI +from tsutils.helper_functions import conditional_iterator +from tsutils.menu.components.config import BotConfig +from tsutils.menu.components.footers import TSUBAKI_FLOWER_ICON_URL +from tsutils.menu.view.closable_embed import ClosableEmbedViewState +from tsutils.query_settings.query_settings import QuerySettings +from tsutils.tsubaki.links import MonsterImage, MonsterLink +from tsutils.tsubaki.monster_header import MonsterHeader +from tsutils.user_interaction import get_user_confirmation, send_confirmation_message, send_cancellation_message, \ + get_user_reaction +from typing import TYPE_CHECKING, Any, List, Optional, Union + +if TYPE_CHECKING: + from dbcog.dbcog import DBCog + # from dbcog.monster_graph import MonsterGraph # used for creating the filter + +logger = logging.getLogger('red.padbot-cogs.padle') + + +class PADle(commands.Cog): + """A Wordle game for PAD""" + + menu_map = padle_menu_map + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + self.config = Config.get_conf(self, identifier=94073) + self.config.register_user(todays_guesses=[], start=False, done=False, score=[], + edit_id=0, channel_id=0, all_guesses={}) + self.config.register_global(padle_today=3260, stored_day=0, num_days=1, + subs=[], all_scores=[], save_daily_scores=[], + monsters_list=[], tmrw_padle=0) + self.config.register_guild(allow=False) + self._daily_padle_loop = bot.loop.create_task(self.generate_padle()) + GACOG: Any = self.bot.get_cog("GlobalAdmin") + if GACOG: + GACOG.register_perm("padleadmin") + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + all_guesses = await self.config.user_from_id(user_id).all_guesses() + if all_guesses: + data = f"You have {len(all_guesses)} days of guess data stored." + f"Here are your guesses:\n{json.dumps(all_guesses)}" + else: + data = f"No data is stored for user with ID {user_id}." + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data.""" + await self.config.user_from_id(user_id).clear() + + async def register_menu(self): + await self.bot.wait_until_ready() + menulistener: Any = self.bot.get_cog("MenuListener") + if menulistener is None: + logger.warning("MenuListener is not loaded.") + return + await menulistener.register(self) + + async def get_dbcog(self) -> "DBCog": + dbcog: Any = self.bot.get_cog("DBCog") + if dbcog is None: + raise ValueError("DBCog cog is not loaded") + await dbcog.wait_until_ready() + return dbcog + + def cog_unload(self): + self._daily_padle_loop.cancel() + + async def get_menu_default_data(self, ims): + user = self.bot.get_user(ims['original_author_id']) + data = { + 'dbcog': await self.get_dbcog(), + 'user_config': await BotConfig.get_user(self.config, ims['original_author_id']), + 'today_guesses': await self.get_today_guesses(user, ims.get('current_day')) + } + return data + + async def get_today_guesses(self, user: discord.User, current_day: Union[str, int]) -> Optional[List[int]]: + if current_day is None: + return None + if user is None: + return {} + async with self.config.user(user).all_guesses() as all_guesses: + return all_guesses.get(str(current_day)) + + @commands.group() + async def padle(self, ctx): + """Commands pertaining to PADle""" + + @commands.group(aliases=['padleconfig']) + @auth_check('padleadmin') + async def padleadmin(self, ctx): + """Commands pertaining to PADle setup""" + + @padle.command() + async def help(self, ctx): + """Instructions for PADle""" + prefix = ctx.prefix + await ctx.send("- PADle is similar to Wordle, except for PAD cards.\n" + "- You have infinite tries to guess the hidden PAD card (chosen from a list " + "of more well-known monsters). Everyone is trying to guess the same PAD card. " + "With each guess, you are given feedback as to how similar the two cards are, " + "including comparing the awakenings, rarity, typings, attributes, and monster " + "point sell value.\n- A new PADle is available every day. " + "Use `{}padle start` to begin!".format(prefix)) + + @padle.command(aliases=["sub"]) + async def subscribe(self, ctx, sub_arg: bool = True): + """Subscribe to daily notifications of new PADles""" + prefix = ctx.prefix + async with self.config.subs() as subbed_users: + if not sub_arg: + if ctx.author.id in subbed_users: + return await self.unsubscribe(ctx) + return await send_cancellation_message(ctx, "You are not subscribed.") + if ctx.author.id not in subbed_users: + subbed_users.append(ctx.author.id) + return await send_confirmation_message(ctx, "You will now receive notifications of new PADles. " + "You can unsubscribe with `{}padle subscribe 0`.".format( + prefix)) + + confirmation = await get_user_confirmation(ctx, "You are already subscribed. Did you mean to unsubscribe?") + if confirmation: + await self.unsubscribe(ctx) + elif confirmation is None: + await send_cancellation_message(ctx, "Confirmation timeout") + else: + await ctx.send("No changes were made, you will still receive notifications of new PADles.") + + async def unsubscribe(self, ctx): + async with self.config.subs() as subs: + subs.remove(ctx.author.id) + return await send_confirmation_message(ctx, "You will no longer receive notifications of new PADles.") + + async def can_play_in_guild(self, ctx): + return (ctx.guild is not None and ctx.author.guild_permissions.administrator and ctx.guild.member_count < 10 and + await self.config.guild(ctx.guild).allow()) + + @padle.command() + async def start(self, ctx): + """Start a game of PADle""" + if await self.config.user(ctx.author).done(): + return await ctx.send("You have already finished today's PADle!") + if await self.config.user(ctx.author).start(): + return await ctx.send("You have already started a game!") + if (ctx.guild is not None and ctx.author.guild_permissions.administrator and ctx.guild.member_count < 10 and + not await self.config.guild(ctx.guild).allow()): + guild_confirmation = await get_user_confirmation(ctx, "It looks like you're in a private server. " + "Would you like to play PADle in this server" + " rather than in DMs?", timeout=30, + force_delete=False, show_feedback=True) + if guild_confirmation is None: + return await send_cancellation_message(ctx, "Confirmation timeout.") + if await self.config.guild(ctx.guild).allow(): + return await ctx.send("You have already enabled PADle in this server!") + if not guild_confirmation: + await send_confirmation_message(ctx, "PADles will continue being played in DMs.") + else: + await send_confirmation_message(ctx, "PADles can now be played in this server.") + await self.config.guild(ctx.guild).allow.set(True) + confirmation = await get_user_confirmation(ctx, "Start today's (#{}) PADle game?".format( + await self.config.num_days()), timeout=30, force_delete=False, show_feedback=True) + if confirmation is None: + return await send_cancellation_message(ctx, "Confirmation timeout.") + if await self.config.user(ctx.author).start(): + return await send_cancellation_message(ctx, "You cannot start multiple games!") + if not confirmation: + return await send_cancellation_message(ctx, "The PADle game was not started.") + + prefix = ctx.prefix + em = discord.Embed(title="PADle #{}".format(await self.config.num_days()), type="rich", + description=f"Guess a card with `{prefix}padle guess `!\nIf " + f"you give up, use `{prefix}padle giveup`.\n" + "If you accidentally deleted the guess list message," + f" use `{prefix}padle resend`.", + color=discord.Color.teal()) + if await self.can_play_in_guild(ctx): + await ctx.send(embed=em) + await self.config.user(ctx.author).start.set(True) + return + # else + try: + await ctx.author.send(embed=em) + except discord.HTTPException: + return await send_cancellation_message(ctx, + "Looks like I can't DM you. Try checking your Privacy Settings.") + if ctx.guild is not None: + await send_confirmation_message(ctx, "Check your DMs!") + await self.config.user(ctx.author).start.set(True) + + @padle.command() + async def globalstats(self, ctx, day: int = 0): + """Global stats for the PADle (optionally on a specified day)""" + num_days = await self.config.num_days() + if day <= 0 or day > num_days: + day = await self.config.num_days() + if day == num_days: + stats = await self.config.all_scores() + giveups = 0 + completes = 0 + average = 0 + for item in stats: + if item == "X": + giveups += 1 + else: + completes += 1 + average += int(item) + embed = discord.Embed(title="PADle #{} Stats".format(await self.config.num_days()), type="rich") + if completes == 0: + embed.description = (f"**Total Wins**: {completes}\n**Total Losses**: {giveups}\n**Win Rate**: 0%\n" + "**Average Guess Count**: 0") + else: + embed.description = ("**Total Wins**: {}\n**Total Losses**: {}\n**Win Rate**: {:.2%}\n" + "**Average Guess Count**: {:.2f}").format(completes, giveups, + completes / (completes + giveups), + (average / completes)) + embed.set_footer(text=f"Try {ctx.prefix}padle globalstats [day] for other day's stats!", + icon_url=TSUBAKI_FLOWER_ICON_URL) + else: + embed = await self.get_past_padle_embed(ctx, day) + await ctx.send(embed=embed) + + @padle.command() + async def stats(self, ctx): + """Personal PADle stats""" + if await self.config.user(ctx.author).start() and not await self.config.user(ctx.author).done(): + return await send_cancellation_message(ctx, "Please finish today's PADle before checking your stats!") + all_guesses = await self.config.user(ctx.author).all_guesses() + played = len(all_guesses) + save_daily = await self.config.save_daily_scores() + if played == 0: + return await send_cancellation_message(ctx, "You haven't played PADle yet!") + cur_streak = 0 + max_streak = 0 + wins = 0 + for day in range(1, await self.config.num_days() + 1): + if day == await self.config.num_days(): + correct_id = await self.config.padle_today() + else: + correct_id = save_daily[day - 1][0] + if str(day) in all_guesses and len(all_guesses[str(day)]) != 0 and all_guesses[str(day)][-1] == correct_id: + cur_streak += 1 + wins += 1 + else: + if cur_streak > max_streak: + max_streak = cur_streak + if day != await self.config.num_days(): + cur_streak = 0 + + if cur_streak > max_streak: + max_streak = cur_streak + all_monsters_guessed = [] + for key, value in all_guesses.items(): + all_monsters_guessed.extend(value) + mode = max(set(all_monsters_guessed), key=all_monsters_guessed.count) + dbcog = await self.get_dbcog() + m = dbcog.get_monster(mode) + m_embed = EmbedView( + EmbedMain( + title=f"{ctx.author.name}'s PADle Stats", + description=(f"**Games Played**: {played}\n" + f"**Win Rate**: {wins / played:.2%}\n" + f"**Current Streak**: {cur_streak}\n" + f"**Max Streak**: {max_streak}\n" + f"**Favorite Guessed Monster**: {MonsterHeader.menu_title(m).to_markdown()}")), + embed_thumbnail=EmbedThumbnail( + MonsterImage.icon(m.monster_id))).to_embed() + await ctx.send(embed=m_embed) + + async def get_past_padle_embed(self, ctx, day: int): + dbcog = await self.get_dbcog() + daily_scores = await self.config.save_daily_scores() + info = daily_scores[day - 1] + stats = info[1] + giveups = 0 + completes = 0 + average = 0 + for item in stats: + if item == "X": + giveups += 1 + else: + completes += 1 + average += int(item) + guess_monster = dbcog.get_monster(int(info[0])) + embed = EmbedView( + EmbedMain( + title="PADle #{} Stats".format(day)), + embed_thumbnail=EmbedThumbnail( + MonsterImage.icon(guess_monster.monster_id))).to_embed() + if completes == 0: + embed.description = ("**" + MonsterHeader.menu_title(guess_monster).to_markdown() + "**\n" + + "**Total Wins**: {}\n**Total Losses**: {}\n**Win Rate**: 0%\n" + "**Average Guess Count**: 0").format(completes, giveups) + else: + embed.description = ("**" + MonsterHeader.menu_title(guess_monster).to_markdown() + "**\n" + + "**Total Wins**: {}\n**Total Losses**: {}\n**Win Rate**: {:.2%}\n" + "**Average Guess Count**: {:.2f}").format(completes, giveups, + completes / (completes + giveups), + (average / completes)) + return embed + + async def do_quit_early(self, ctx): + prefix = ctx.prefix + if ctx.guild is not None and not await self.can_play_in_guild(ctx): + await ctx.send("You can only play PADle in DMs!") + return True + if not await self.config.user(ctx.author).start(): + await ctx.send(f"You have not started the game of PADle yet, try `{prefix}padle start`!") + return True + if await self.config.user(ctx.author).done(): + await ctx.send("You have already played today's PADle!") + return True + return False + + @padle.command() + async def guess(self, ctx, *, guess): + """Guess a card for the daily PADle""" + if await self.do_quit_early(ctx): + return + dbcog = await self.get_dbcog() + + guess_monster = await dbcog.find_monster(guess, ctx.author.id) + if guess_monster is None: + close_menu = ClosableEmbedMenu.menu() + props = PADleMonsterConfirmationViewProps("Monster not found, please try again.") + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, guess) + state = ClosableEmbedViewState(ctx.author.id, ClosableEmbedMenu.MENU_TYPE, guess, + query_settings, PADleMonsterConfirmationView.VIEW_TYPE, props) + await close_menu.create(ctx, state) + return + + m_embed = EmbedView( + EmbedMain( + title=MonsterHeader.menu_title(guess_monster).to_markdown(), + description="Did you mean this monster?"), + embed_thumbnail=EmbedThumbnail( + MonsterImage.icon(guess_monster.monster_id))).to_embed() + confirmation = await get_user_reaction(ctx, m_embed, YES_EMOJI, NO_EMOJI, timeout=20) + if confirmation is None: + return await send_cancellation_message(ctx, "Confirmation timeout.") + if confirmation == NO_EMOJI: + return await send_cancellation_message(ctx, "Please guess again.") + if not await self.config.user(ctx.author).start(): + return await send_cancellation_message(ctx, + "You have not started a game yet! The PADle may have just expired.") + if await self.config.user(ctx.author).done(): + return await send_cancellation_message(ctx, "You have already finished today's PADle!") + async with self.config.user(ctx.author).todays_guesses() as todays_guesses: + todays_guesses.append(guess_monster.monster_id) + monster = dbcog.get_monster(int(await self.config.padle_today())) + cur_day = await self.config.num_days() + todays_guesses = await self.config.user(ctx.author).todays_guesses() + async with self.config.user(ctx.author).all_guesses() as all_guesses: + all_guesses[str(cur_day)] = todays_guesses + + channel = self.bot.get_channel(await self.config.user(ctx.author).channel_id()) + if channel is not None: + with suppress(discord.HTTPException): + del_msg = await channel.fetch_message(await self.config.user(ctx.author).edit_id()) + await del_msg.delete() + + guess_monster_diff = MonsterDiff(monster, guess_monster) + points = guess_monster_diff.get_diff_score() + + padle_menu = PADleScrollMenu.menu() + # query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, guess) + cur_page = ceil(len(todays_guesses) / 5) - 1 + page_guesses = await PADleScrollViewState.do_queries(dbcog, + todays_guesses[((cur_page) * 5):((cur_page + 1) * 5)]) + state = PADleScrollViewState(ctx.author.id, PADleScrollMenu.MENU_TYPE, guess, monster=monster, + cur_day_page_guesses=page_guesses, current_day=cur_day, + current_page=cur_page, num_pages=cur_page + 1) + message = await padle_menu.create(ctx, state) + await self.config.user(ctx.author).edit_id.set(message.id) + await self.config.user(ctx.author).channel_id.set(ctx.channel.id) + + score = "\N{LARGE YELLOW SQUARE}" + if guess_monster.monster_id == monster.monster_id: + await ctx.send("You got the PADle in {} guesses! Use `{}padle score` to share your score.".format( + len(await self.config.user(ctx.author).todays_guesses()), ctx.prefix)) + await self.config.user(ctx.author).done.set(True) + async with self.config.all_scores() as all_scores: + all_scores.append(str(len(await self.config.user(ctx.author).todays_guesses()))) + score = "\N{LARGE GREEN SQUARE}" + else: + if points < 9: + score = "\N{LARGE ORANGE SQUARE}" + if points < 5: + score = "\N{LARGE RED SQUARE}" + async with self.config.user(ctx.author).score() as scores: + scores.append(score) + + @padle.command() + async def resend(self, ctx): + """Resends the guess list message""" + if await self.do_quit_early(ctx): + return + dbcog = await self.get_dbcog() + monster = dbcog.get_monster(int(await self.config.padle_today())) + cur_day = await self.config.num_days() + todays_guesses = await self.config.user(ctx.author).todays_guesses() + if len(todays_guesses) == 0: + return await send_cancellation_message(ctx, "You haven't guessed yet!") + padle_menu = PADleScrollMenu.menu() + cur_page = ceil(len(todays_guesses) / 5) - 1 + page_guesses = await PADleScrollViewState.do_queries(dbcog, + todays_guesses[((cur_page) * 5):((cur_page + 1) * 5)]) + state = PADleScrollViewState(ctx.author.id, PADleScrollMenu.MENU_TYPE, monster=monster, + cur_day_page_guesses=page_guesses, current_day=cur_day, + current_page=cur_page, num_pages=cur_page + 1) + message = await padle_menu.create(ctx, state) + await self.config.user(ctx.author).edit_id.set(message.id) + await self.config.user(ctx.author).channel_id.set(ctx.channel.id) + + @padle.command() + async def giveup(self, ctx): + """Give up on today's PADle""" + if await self.do_quit_early(ctx): + return + confirmation = await get_user_confirmation(ctx, "Are you sure you would like to give up?", timeout=20) + dbcog = await self.get_dbcog() + if not confirmation: + if confirmation is None: + await send_cancellation_message(ctx, "Confirmation timeout.") + return + monster = dbcog.get_monster(int(await self.config.padle_today())) + await self.config.user(ctx.author).done.set(True) + m_embed = EmbedView(EmbedMain(title=MonsterHeader.menu_title(monster).to_markdown(), + description="PADle #{}".format(await self.config.num_days()), + url=MonsterLink.ilmina(monster), color=discord.Color.red()), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(monster.monster_id))).to_embed() + await ctx.send(embed=m_embed) + await ctx.send(f"Use `{ctx.prefix}padle score` to share your score!") + async with self.config.user(ctx.author).score() as scores: + scores.append("\N{CROSS MARK}") + async with self.config.all_scores() as all_scores: + all_scores.append("X") + cur_day = await self.config.num_days() + todays_guesses = await self.config.user(ctx.author).todays_guesses() + async with self.config.user(ctx.author).all_guesses() as all_guesses: + all_guesses[str(cur_day)] = todays_guesses + + @padle.command(aliases=["share"]) + async def score(self, ctx): + """Share your PADle score""" + prefix = ctx.prefix + if not await self.config.user(ctx.author).done(): + await ctx.send("You have not done today's PADle yet!") + return + score = await self.config.user(ctx.author).score() + value = "X" if "\N{CROSS MARK}" in score else str(len(await self.config.user(ctx.author).todays_guesses())) + if ctx.guild is None: + msg = "PADle #{}: {}".format(await self.config.num_days(), value) + "\n" + "".join(score) + await ctx.send(msg) + await ctx.send(f"*Hint: You can use the command `{prefix}padle score` anywhere " + f"and have {ctx.me.name} automatically share your score for you!*") + else: + await ctx.message.delete() + msg = ctx.author.mention + "'s PADle #{}: {}".format(await self.config.num_days(), value) + "\n" + "".join( + score) + await ctx.send(msg) + + async def generate_padle(self): + async def is_day_change(): + cur_day = datetime.datetime.now().day + old_day = await self.config.stored_day() + if cur_day != old_day: + await self.config.stored_day.set(cur_day) + return True + + await self.bot.wait_until_ready() + async for _ in conditional_iterator(is_day_change, poll_interval=10): + try: + async with self.config.save_daily_scores() as save_daily: + save_daily.append([await self.config.padle_today(), await self.config.all_scores()]) + # async with aopen("./pad-cogs/padle/monsters.txt", "r") as f: + # monsters = (await f.readline()).split(",") + tmrw_padle = await self.config.tmrw_padle() + if tmrw_padle == 0: + MONSTERS_LIST = await self.config.monsters_list() + await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) + else: + await self.config.padle_today.set(tmrw_padle) + await self.config.tmrw_padle.set(0) + num = await self.config.num_days() + await self.config.num_days.set(num + 1) + all_users = await self.config.all_users() + await self.config.all_scores.set([]) + for userid in all_users: + user = self.bot.get_user(userid) + if user is None: + continue + # save past guesses only if started + if await self.config.user(user).start(): + async with self.config.user(user).all_guesses() as all_guesses: + all_guesses[str(num)] = await self.config.user(user).todays_guesses() + await self.config.user(user).todays_guesses.set([]) + # need to send message if a user is mid-game + if await self.config.user(user).start() and not await self.config.user(user).done(): + await user.send("The PADle expired; a new one is available.") + await self.config.user(user).start.set(False) + await self.config.user(user).done.set(False) + await self.config.user(user).score.set([]) + await self.config.user(user).edit_id.set(0) + await self.config.user(user).channel_id.set(0) + subbed_users = await self.config.subs() + for userid in subbed_users: + user = self.bot.get_user(userid) + if user is not None: + await user.send("PADle #{} is now available!".format(await self.config.num_days())) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Error in loop:") + + @padleadmin.command() + async def fullreset(self, ctx): + """Resets all stats and information""" + confirmation = await get_user_confirmation(ctx, + "Fully reset all stats and data in PADle? You cannot undo this!") + if confirmation is None: + return await send_cancellation_message(ctx, "Confirmation timeout.") + if not confirmation: + return await send_cancellation_message(ctx, "Nothing was reset.") + + await self.config.padle_today.set(3260) + + await self.config.tmrw_padle.set(0) + await self.config.num_days.set(1) + await self.config.subs.set([]) + await self.config.all_scores.set([]) + await self.config.save_daily_scores.set([]) + all_users = await self.config.all_users() + for userid in all_users: + user = self.bot.get_user(userid) + if user is None: + continue + await self.config.user(user).todays_guesses.set([]) + # need to send message if a user is mid-game + if await self.config.user(user).start() and not await self.config.user(user).done(): + try: + await user.send("A full reset occured, the PADle expired.") + except: + pass + await self.config.user(user).start.set(False) + await self.config.user(user).done.set(False) + await self.config.user(user).score.set([]) + await self.config.user(user).edit_id.set(0) + await self.config.user(user).channel_id.set(0) + await self.config.user(user).all_guesses.set({}) + await ctx.tick() + + @padleadmin.command() + async def add(self, ctx, *ids: int): + """Adds space-separated list of monster IDs to the list of possible PADles""" + dbcog = await self.get_dbcog() + result = [] + valids = [] + monsters_list = await self.config.monsters_list() + for id in ids: + m = dbcog.get_monster(id) + if m is None: + result.append(f"{str(id)} is not a valid monster ID.") + continue + if not m.on_na or m.name_en is None: + result.append(f"{str(id)} is not a monster on the NA server.") + continue + if int(id) in monsters_list: + result.append( + f"{MonsterHeader.menu_title(m, use_emoji=True).to_markdown()} is already a possible PADle.") + continue + valids.append(int(id)) + result.append(f"{MonsterHeader.menu_title(m, use_emoji=True).to_markdown()} was added.") + async with self.config.monsters_list() as m_list: + m_list.extend(valids) + for page in pagify("\n".join(result)): + await ctx.send(page) + + @padleadmin.command() + async def remove(self, ctx, *ids: int): + """Removes space-separated list of monster IDs to the list of possible PADles""" + dbcog = await self.get_dbcog() + result = [] + valids = [] + monsters_list = await self.config.monsters_list() + for id in ids: + m = dbcog.get_monster(id) + if m is None: + result.append(f"{str(id)} is not a valid monster ID.") + continue + if not m.on_na or m.name_en is None: + result.append(f"{str(id)} is not a monster on the NA server.") + continue + if int(id) not in monsters_list: + result.append(f"{MonsterHeader.menu_title(m, use_emoji=True).to_markdown()} is not a possible PADle.") + continue + valids.append(int(id)) + result.append(f"{MonsterHeader.menu_title(m, use_emoji=True).to_markdown()} was removed.") + await self.config.monsters_list.set([m for m in monsters_list if m not in valids]) + for page in pagify("\n".join(result)): + await ctx.send(page) + + @padleadmin.command() + async def set(self, ctx): + """Set possible PADles to attached CSV/text file""" + if ctx.message.attachments: + try: + data = [int(x) for x in + re.sub("[^0-9,]", "", (await ctx.message.attachments[0].read()).decode("utf-8")).split(",")] + dbcog = await self.get_dbcog() + for id in data: + m = dbcog.get_monster(int(id)) + if m is None or not m.on_na or m.name_en is None: + return await send_cancellation_message(ctx, f"{id} was invalid. Please remove " + "this ID and try again.") + await self.config.monsters_list.set(data) + await ctx.tick() + except Exception as e: + await send_cancellation_message(ctx, "Something went wrong.") + logger.exception(str(e)) + else: + return await send_cancellation_message(ctx, "Looks like no file was attached!") + + @padleadmin.command() + async def list(self, ctx): + """Lists all possible PADles""" + dbcog = await self.get_dbcog() + monsters_list = await self.config.monsters_list() + monsters_list.sort() + result = [] + for id in monsters_list: + m = dbcog.get_monster(int(id)) + if m is None or not m.on_na or m.name_en is None: + # this should never happen but just in case + result.append(f"**Error**: {id} is a valid PADle but not a monster.") + continue + result.append(MonsterHeader.menu_title(m, use_emoji=True).to_markdown()) + for page in pagify("\n".join(result)): + await ctx.send(page) + await ctx.send(f"There are **{len(result)}** valid PADles!") + + @padleadmin.command(aliases=["settmrw", "settmrwpadle"]) + async def settomorrowpadle(self, ctx, id: int): + """Sets tomorrow's PADle to a specified monster ID""" + dbcog = await self.get_dbcog() + monsters_list = await self.config.monsters_list() + if id == 0: + confirmation = await get_user_confirmation(ctx, "Would you like to make tomorrow's PADle random?") + if confirmation is None: + return await send_cancellation_message(ctx, "Confirmation timeout") + if not confirmation: + return await ctx.send("No changes were made.") + await self.config.tmrw_padle.set(0) + return await send_confirmation_message(ctx, "Tomorrow's PADle will be random.") + if id not in monsters_list: + # for the most part, avoid abuse and ensure validness + return await ctx.send("This monster is not a valid PADle. If you would like to set it as tomorrow's" + " PADle, please add it to the list of PADles first.") + m = dbcog.get_monster(id) + title = MonsterHeader.menu_title(m, use_emoji=True).to_markdown() + confirmation = await get_user_confirmation(ctx, "Are you sure you would like to change tomorrow's PADle to " + f"{title}?") + if confirmation is None: + return await send_cancellation_message(ctx, "Confirmation timeout") + if not confirmation: + return await ctx.send("No change to tomorrow's PADle was made.") + await self.config.tmrw_padle.set(id) + prefix = ctx.prefix + await send_confirmation_message(ctx, f"Tomorrow's PADle will be {title}! " + f"Use `{prefix}padle settomorrowpadle 0` to undo this.") + + @padleadmin.command(aliases=["isin"]) + async def contains(self, ctx, *, query): + """Checks if a monster is in the list of possible PADles""" + monsters_list = await self.config.monsters_list() + dbcog = await self.get_dbcog() + monster = await dbcog.find_monster(query, ctx.author.id) + if monster is None: + return + title = MonsterHeader.menu_title(monster, use_emoji=True).to_markdown() + if monster.monster_id in monsters_list: + return await send_confirmation_message(ctx, f"{title} is a possible PADle.") + await send_cancellation_message(ctx, f"{title} is not a possible PADle.") + + @padleadmin.command() + async def advance(self, ctx): + """Advance day""" + confirmation = await get_user_confirmation(ctx, "Advance to the next day? You cannot undo this!") + if confirmation is None: + return await send_cancellation_message(ctx, "Confirmation timeout.") + if not confirmation: + return await send_cancellation_message(ctx, "The PADle was unchanged.") + try: + await self.config.stored_day.set(datetime.datetime.now().day) + async with self.config.save_daily_scores() as save_daily: + save_daily.append([await self.config.padle_today(), await self.config.all_scores()]) + # async with aopen("./pad-cogs/padle/monsters.txt", "r") as f: + # monsters = (await f.readline()).split(",") + tmrw_padle = await self.config.tmrw_padle() + if tmrw_padle == 0: + MONSTERS_LIST = await self.config.monsters_list() + await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) + else: + await self.config.padle_today.set(tmrw_padle) + await self.config.tmrw_padle.set(0) + num = await self.config.num_days() + await self.config.num_days.set(num + 1) + all_users = await self.config.all_users() + await self.config.all_scores.set([]) + for userid in all_users: + user = self.bot.get_user(userid) + if user is None: + continue + # save past guesses only if started + if await self.config.user(user).start(): + async with self.config.user(user).all_guesses() as all_guesses: + all_guesses[str(num)] = await self.config.user(user).todays_guesses() + await self.config.user(user).todays_guesses.set([]) + # need to send message if a user is mid-game + if await self.config.user(user).start() and not await self.config.user(user).done(): + await user.send("The PADle expired; a new one is available.") + await self.config.user(user).start.set(False) + await self.config.user(user).done.set(False) + await self.config.user(user).score.set([]) + await self.config.user(user).edit_id.set(0) + await self.config.user(user).channel_id.set(0) + subbed_users = await self.config.subs() + for userid in subbed_users: + user = self.bot.get_user(userid) + if user is not None: + await user.send("PADle #{} is now available!".format(await self.config.num_days())) + await ctx.tick() + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Error in loop:") + + @padleadmin.command() + async def filter(self, ctx): + """Re-creates the list of monsters""" + confirmation = await get_user_confirmation(ctx, "This command will take a long time to execute. Run anyways?") + if not confirmation: + return + dbcog = await self.get_dbcog() + mgraph = dbcog.database.graph + final = [] + # await ctx.send(mgraph.max_monster_id) + for i in range(1, 8800): # hard coded bc max_monster_id as calculated above is 15k??? + monster = await dbcog.find_monster(str(i), ctx.author) + with suppress(Exception): + nextTrans = mgraph.get_next_transform(monster) + prevTrans = mgraph.get_prev_transform(monster) + if(monster is not None and monster.name_en is not None and monster.on_na and + monster.series.series_type != 'collab' and + ((monster.sell_mp >= 50000 and (monster.superawakening_count > 1 or prevTrans is not None)) or + "Super Reincarnated" in monster.name_en) and not monster.is_equip and + nextTrans is None and monster.level >= 99): + final.append(str(i)) + with open("result.txt", "w") as file: + file.write(",".join(final)) + with open("result.txt", "rb") as file: + await ctx.send("List of monsters:", file=discord.File(file, "result.txt")) + await ctx.tick() diff --git a/padle/view/__init__.py b/padle/view/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/padle/view/confirmation.py b/padle/view/confirmation.py new file mode 100644 index 00000000..44a754a7 --- /dev/null +++ b/padle/view/confirmation.py @@ -0,0 +1,24 @@ +from tsutils.menu.view.closable_embed import ClosableEmbedViewState +from tsutils.menu.components.footers import embed_footer_with_state +from discordmenu.embed.view import EmbedView +from discordmenu.embed.components import EmbedMain +from discord import Color + + +class PADleMonsterConfirmationViewProps: + def __init__(self, title: str): + self.title = title + + +class PADleMonsterConfirmationView: + VIEW_TYPE = 'PADleConfirmation' + + @classmethod + def embed(cls, state: ClosableEmbedViewState, props: PADleMonsterConfirmationViewProps): + return EmbedView( + EmbedMain( + color=Color.red(), + title=props.title, + ), + embed_footer=embed_footer_with_state(state, text="Click the X to close.") + ) diff --git a/padle/view/padle_scroll_view.py b/padle/view/padle_scroll_view.py new file mode 100644 index 00000000..f87070c1 --- /dev/null +++ b/padle/view/padle_scroll_view.py @@ -0,0 +1,98 @@ +from typing import TYPE_CHECKING + +from discordmenu.embed.components import EmbedMain, EmbedField +from discordmenu.embed.base import Box +from discordmenu.embed.view import EmbedView +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.components.config import UserConfig +from tsutils.menu.view.view_state_base import ViewStateBase +from padle.monsterdiff import MonsterDiff + +from tsutils.menu.components.footers import TSUBAKI_FLOWER_ICON_URL + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + + +class PADleScrollViewState(ViewStateBase): + VIEW_STATE_TYPE: str = "PADleScrollView" + + def __init__(self, original_author_id, menu_type, raw_query="", monster=None, cur_day_page_guesses=[], + current_page=0, extra_state=None, reaction_list=None, current_day: int = 0, num_pages=0): + super().__init__(original_author_id, menu_type, raw_query, + extra_state=extra_state) + self.cur_day_page_guesses = cur_day_page_guesses + self.current_page = current_page + self.reaction_list = reaction_list + self.current_day = current_day + self.monster = monster + self.num_pages = num_pages + + def get_cur_fields(self): + fields = [] + for index, item in enumerate(self.cur_day_page_guesses): + guessed_mon = MonsterDiff(self.monster, item) + fields.append(EmbedField( + title=f"**Guess #{5 * (self.current_page) + index + 1}**:", + body=Box("\n".join( + [guessed_mon.get_name_line_feedback_text(), guessed_mon.get_other_info_feedback_text(), + guessed_mon.get_awakenings_feedback_text()])), + )) + return fields + + @classmethod + async def do_queries(cls, dbcog, guess_ids): + monster_list = [] + for id in guess_ids: + monster_list.append(dbcog.get_monster(int(id))) + return monster_list + + def get_pages_footer(self): + return "Page " + str(self.current_page + 1) + "/" + str(self.num_pages) + + @classmethod + def get_monster_ids_list(cls, monster_list): + ids = [] + for monster in monster_list: + ids.append(monster.monster_id) + return ids + + def serialize(self): + ret = super().serialize() + ret.update({ + 'current_page': self.current_page, + 'reaction_list': self.reaction_list, + 'current_day': self.current_day, + 'cur_monster': self.monster.monster_id, + 'num_pages': self.num_pages + }) + return ret + + @classmethod + async def deserialize(cls, dbcog, _user_config: UserConfig, todays_guesses, ims: dict): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + reaction_list = ims['reaction_list'] + cur_page = ims['current_page'] + current_day = ims['current_day'] + cur_monster = ims['cur_monster'] + num_pages = ims['num_pages'] + monster = dbcog.get_monster(int(cur_monster)) + cur_day_page_guesses = await cls.do_queries(dbcog, todays_guesses[((cur_page) * 5):((cur_page + 1) * 5)]) + return PADleScrollViewState(original_author_id, menu_type, "", current_page=cur_page, + current_day=current_day, reaction_list=reaction_list, + cur_day_page_guesses=cur_day_page_guesses, monster=monster, num_pages=num_pages) + + +class PADleScrollView: + VIEW_TYPE = 'PADleScroll' + + @staticmethod + def embed(state: PADleScrollViewState): + fields = state.get_cur_fields() + return EmbedView( + EmbedMain( + title=f"**PADle #{state.current_day}**", + ), + embed_footer=embed_footer_with_state(state, text=state.get_pages_footer()), + embed_fields=fields) From dda1b2620d1151394746d3dfe74f077c91686fc4 Mon Sep 17 00:00:00 2001 From: TsubakiBotPad <69992611+TsubakiBotPad@users.noreply.github.com> Date: Tue, 14 Jun 2022 14:11:22 +0800 Subject: [PATCH 1142/1220] Upgrade Discord menu to 0.16.15 (#1729) * Save - embedcontrol -> Wrapper * Save EmbedMenu interface change 1 * padle --- azurlane/menu.py | 6 +++--- dungeoncog/menu/dungeon.py | 11 ++++++---- dungeoncog/menu/simple.py | 11 ++++++---- padinfo/menu/awakening_list.py | 6 +++--- padinfo/menu/button_info.py | 6 +++--- padinfo/menu/id.py | 32 ++++++++++++++++------------- padinfo/menu/leader_skill.py | 11 +++++----- padinfo/menu/leader_skill_single.py | 11 +++++----- padinfo/menu/monster_list.py | 7 ++++--- padinfo/menu/na_diff.py | 15 ++++++++------ padinfo/menu/series_scroll.py | 7 ++++--- padinfo/menu/transforminfo.py | 12 ++++++----- padle/menu/padle_scroll.py | 9 ++++---- 13 files changed, 82 insertions(+), 62 deletions(-) diff --git a/azurlane/menu.py b/azurlane/menu.py index ccd168c3..88e879e8 100644 --- a/azurlane/menu.py +++ b/azurlane/menu.py @@ -1,7 +1,7 @@ from typing import Optional from discord import Message -from discordmenu.embed.control import EmbedControl +from discordmenu.embed.wrapper import EmbedWrapper from discordmenu.embed.menu import EmbedMenu from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes @@ -94,8 +94,8 @@ def pane_control(cls, state: AzurlaneViewState): if state is None: return None reaction_list = state.reaction_list - return EmbedControl( - [AzurlaneView.embed(state)], + return EmbedWrapper( + AzurlaneView.embed(state), reaction_list ) diff --git a/dungeoncog/menu/dungeon.py b/dungeoncog/menu/dungeon.py index c51eedc3..326092ce 100644 --- a/dungeoncog/menu/dungeon.py +++ b/dungeoncog/menu/dungeon.py @@ -1,7 +1,9 @@ from typing import Optional from discord import Message -from discordmenu.embed.control import EmbedControl +from discordmenu.embed.emoji import DELETE_MESSAGE_EMOJI +from discordmenu.embed.transitions import EmbedTransition, EmbedMenuDefaultTransitions +from discordmenu.embed.wrapper import EmbedWrapper from discordmenu.embed.menu import EmbedMenu from tsutils.menu.components.panes import MenuPanes, emoji_buttons @@ -19,7 +21,8 @@ class DungeonMenu: @staticmethod def menu(): embed = EmbedMenu(DungeonMenuPanes.transitions(), DungeonMenu.message_control, - delete_func=DungeonMenu.respond_with_delete) + EmbedMenuDefaultTransitions( + delete_message=EmbedTransition(DELETE_MESSAGE_EMOJI, DungeonMenu.respond_with_delete))) return embed @staticmethod @@ -84,8 +87,8 @@ async def respond_with_next_page(message: Optional[Message], ims, **data): def message_control(state: DungeonViewState): if state is None: return None - return EmbedControl( - [DungeonView.embed(state)], + return EmbedWrapper( + DungeonView.embed(state), DungeonMenuPanes.emoji_names() ) diff --git a/dungeoncog/menu/simple.py b/dungeoncog/menu/simple.py index b72fb7bd..12799347 100644 --- a/dungeoncog/menu/simple.py +++ b/dungeoncog/menu/simple.py @@ -1,7 +1,9 @@ from typing import Optional from discord import Message -from discordmenu.embed.control import EmbedControl +from discordmenu.embed.emoji import DELETE_MESSAGE_EMOJI +from discordmenu.embed.transitions import EmbedMenuDefaultTransitions, EmbedTransition +from discordmenu.embed.wrapper import EmbedWrapper from discordmenu.embed.menu import EmbedMenu from tsutils.menu.components.panes import MenuPanes, emoji_buttons @@ -19,7 +21,8 @@ class SimpleMenu: @staticmethod def menu(): embed = EmbedMenu(SimpleMenuPanes.transitions(), SimpleMenu.message_control, - delete_func=SimpleMenu.respond_with_delete) + EmbedMenuDefaultTransitions( + delete_message=EmbedTransition(DELETE_MESSAGE_EMOJI, SimpleMenu.respond_with_delete))) return embed @staticmethod @@ -45,8 +48,8 @@ def message_control(state: SimpleViewState): if state is None: return None reaction_list = state.reaction_list - return EmbedControl( - [SimpleView.embed(state)], + return EmbedWrapper( + SimpleView.embed(state), reaction_list ) diff --git a/padinfo/menu/awakening_list.py b/padinfo/menu/awakening_list.py index 2297b40e..f8727d57 100644 --- a/padinfo/menu/awakening_list.py +++ b/padinfo/menu/awakening_list.py @@ -1,7 +1,7 @@ from typing import Optional from discord import Message -from discordmenu.embed.control import EmbedControl +from discordmenu.embed.wrapper import EmbedWrapper from discordmenu.embed.menu import EmbedMenu from tsutils.menu.components.panes import MenuPanes, emoji_buttons from tsutils.query_settings.enums import ChildMenuType @@ -69,8 +69,8 @@ def awakening_list_control(state: AwakeningListViewState): if state is None: return None reaction_list = AwakeningListMenuPanes.get_user_reaction_list(state.sort_type) - return EmbedControl( - [AwakeningListView.embed(state)], + return EmbedWrapper( + AwakeningListView.embed(state), reaction_list ) diff --git a/padinfo/menu/button_info.py b/padinfo/menu/button_info.py index db32aed1..9219505b 100644 --- a/padinfo/menu/button_info.py +++ b/padinfo/menu/button_info.py @@ -1,7 +1,7 @@ from typing import Optional from discord import Message -from discordmenu.embed.control import EmbedControl +from discordmenu.embed.wrapper import EmbedWrapper from discordmenu.embed.menu import EmbedMenu from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes, emoji_buttons @@ -90,8 +90,8 @@ def button_info_control(state: ButtonInfoViewState): if state is None: return None reaction_list = ButtonInfoMenuPanes.get_user_reaction_list(state.display_options) - return EmbedControl( - [ButtonInfoView.embed(state)], + return EmbedWrapper( + ButtonInfoView.embed(state), reaction_list ) diff --git a/padinfo/menu/id.py b/padinfo/menu/id.py index e1f75096..d1bf46de 100644 --- a/padinfo/menu/id.py +++ b/padinfo/menu/id.py @@ -1,7 +1,10 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedControl, EmbedMenu +from discordmenu.embed.emoji import DELETE_MESSAGE_EMOJI +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.transitions import EmbedMenuDefaultTransitions, EmbedTransition +from discordmenu.embed.wrapper import EmbedWrapper from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes from tsutils.menu.simple_text import SimpleTextMenu @@ -32,7 +35,8 @@ def menu(initial_control=None): initial_control = IdMenu.id_control embed = EmbedMenu(IdMenuPanes.transitions(), initial_control, - delete_func=IdMenu.respond_with_delete) + EmbedMenuDefaultTransitions( + delete_message=EmbedTransition(DELETE_MESSAGE_EMOJI, IdMenu.respond_with_delete))) return embed @staticmethod @@ -112,8 +116,8 @@ async def respond_with_otherinfo(message: Optional[Message], ims, **data): def id_control(state: IdViewState): if state is None: return None - return EmbedControl( - [IdView.embed(state)], + return EmbedWrapper( + IdView.embed(state), state.reaction_list or IdMenuPanes.emoji_names() ) @@ -121,8 +125,8 @@ def id_control(state: IdViewState): def evos_control(state: Optional[EvosViewState]): if state is None: return None - return EmbedControl( - [EvosView.embed(state)], + return EmbedWrapper( + EvosView.embed(state), state.reaction_list or IdMenuPanes.emoji_names() ) @@ -130,8 +134,8 @@ def evos_control(state: Optional[EvosViewState]): def mats_control(state: Optional[MaterialsViewState]): if state is None: return None - return EmbedControl( - [MaterialsView.embed(state)], + return EmbedWrapper( + MaterialsView.embed(state), state.reaction_list or IdMenuPanes.emoji_names() ) @@ -139,8 +143,8 @@ def mats_control(state: Optional[MaterialsViewState]): def pic_control(state: PicViewState): if state is None: return None - return EmbedControl( - [PicView.embed(state)], + return EmbedWrapper( + PicView.embed(state), state.reaction_list or IdMenuPanes.emoji_names() ) @@ -148,8 +152,8 @@ def pic_control(state: PicViewState): def pantheon_control(state: Optional[PantheonViewState]): if state is None: return None - return EmbedControl( - [PantheonView.embed(state)], + return EmbedWrapper( + PantheonView.embed(state), state.reaction_list or IdMenuPanes.emoji_names() ) @@ -157,8 +161,8 @@ def pantheon_control(state: Optional[PantheonViewState]): def otherinfo_control(state: OtherInfoViewState): if state is None: return None - return EmbedControl( - [OtherInfoView.embed(state)], + return EmbedWrapper( + OtherInfoView.embed(state), state.reaction_list or IdMenuPanes.emoji_names() ) diff --git a/padinfo/menu/leader_skill.py b/padinfo/menu/leader_skill.py index ad88ed26..b14488de 100644 --- a/padinfo/menu/leader_skill.py +++ b/padinfo/menu/leader_skill.py @@ -1,7 +1,8 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedControl, EmbedMenu +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.wrapper import EmbedWrapper from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes, emoji_buttons @@ -52,15 +53,15 @@ async def respond_with_house(message: Optional[Message], ims, **data): @staticmethod def ls_control(state: LeaderSkillViewState): - return EmbedControl( - [LeaderSkillView.embed(state)], + return EmbedWrapper( + LeaderSkillView.embed(state), LeaderSkillMenuPanes.emoji_names() ) @staticmethod def id_control(state: IdViewState): - return EmbedControl( - [IdView.embed(state)], + return EmbedWrapper( + IdView.embed(state), LeaderSkillMenuPanes.emoji_names() ) diff --git a/padinfo/menu/leader_skill_single.py b/padinfo/menu/leader_skill_single.py index ac39c34e..7997a114 100644 --- a/padinfo/menu/leader_skill_single.py +++ b/padinfo/menu/leader_skill_single.py @@ -1,7 +1,8 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedControl, EmbedMenu +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.wrapper import EmbedWrapper from tsutils.menu.components.panes import MenuPanes, emoji_buttons from padinfo.view.id import IdView, IdViewState @@ -30,15 +31,15 @@ async def respond_with_id(message: Optional[Message], ims, *, dbcog, user_config @staticmethod def ls_control(state: LeaderSkillSingleViewState): - return EmbedControl( - [LeaderSkillSingleView.embed(state)], + return EmbedWrapper( + LeaderSkillSingleView.embed(state), LeaderSkillSingleMenuPanes.emoji_names() ) @staticmethod def id_control(state: IdViewState): - return EmbedControl( - [IdView.embed(state)], + return EmbedWrapper( + IdView.embed(state), LeaderSkillSingleMenuPanes.emoji_names() ) diff --git a/padinfo/menu/monster_list.py b/padinfo/menu/monster_list.py index bc3a605b..c4636dea 100644 --- a/padinfo/menu/monster_list.py +++ b/padinfo/menu/monster_list.py @@ -1,7 +1,8 @@ from typing import List, Optional, Type from discord import Message -from discordmenu.embed.menu import EmbedControl, EmbedMenu +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.wrapper import EmbedWrapper from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes, emoji_buttons @@ -174,8 +175,8 @@ def monster_list_control(cls, state: MonsterListViewState): return None reaction_list = state.reaction_list view_type = cls._get_view(state) - return EmbedControl( - [view_type.embed(state)], + return EmbedWrapper( + view_type.embed(state), reaction_list ) diff --git a/padinfo/menu/na_diff.py b/padinfo/menu/na_diff.py index 3648cb60..4907a3ac 100644 --- a/padinfo/menu/na_diff.py +++ b/padinfo/menu/na_diff.py @@ -1,7 +1,9 @@ from typing import Optional from discord import Message -from discordmenu.embed.control import EmbedControl +from discordmenu.embed.emoji import DELETE_MESSAGE_EMOJI +from discordmenu.embed.transitions import EmbedTransition, EmbedMenuDefaultTransitions +from discordmenu.embed.wrapper import EmbedWrapper from discordmenu.embed.menu import EmbedMenu from tsutils.enums import Server from tsutils.menu.components.panes import MenuPanes @@ -26,7 +28,8 @@ def menu(initial_control=None): if initial_control is None: initial_control = NaDiffMenu.id_control embed = EmbedMenu(NaDiffMenuPanes.transitions(), initial_control, - delete_func=NaDiffMenu.respond_with_delete) + EmbedMenuDefaultTransitions( + delete_message=EmbedTransition(DELETE_MESSAGE_EMOJI, NaDiffMenu.respond_with_delete))) return embed @staticmethod @@ -71,8 +74,8 @@ async def respond_with_delete(message: Optional[Message], ims, **data): def id_control(state: IdViewState): if state is None: return None - return EmbedControl( - [IdView.embed(state)], + return EmbedWrapper( + IdView.embed(state), state.reaction_list or NaDiffMenuPanes.emoji_names() ) @@ -80,8 +83,8 @@ def id_control(state: IdViewState): def message_control(state: SimpleTextViewState): if state is None: return None - return EmbedControl( - [SimpleTextView.embed(state)], + return EmbedWrapper( + SimpleTextView.embed(state), state.reaction_list ) diff --git a/padinfo/menu/series_scroll.py b/padinfo/menu/series_scroll.py index 79a30721..b480f6b8 100644 --- a/padinfo/menu/series_scroll.py +++ b/padinfo/menu/series_scroll.py @@ -1,7 +1,8 @@ from typing import List, Optional from discord import Message -from discordmenu.embed.menu import EmbedControl, EmbedMenu +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.wrapper import EmbedWrapper from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes, emoji_buttons @@ -156,8 +157,8 @@ async def respond_with_10(message: Optional[Message], ims, **data): @staticmethod def monster_list_control(state: SeriesScrollViewState): - return EmbedControl( - [SeriesScrollView.embed(state)], + return EmbedWrapper( + SeriesScrollView.embed(state), SeriesScrollMenuPanes.get_initial_reaction_list(state.max_len_so_far) ) diff --git a/padinfo/menu/transforminfo.py b/padinfo/menu/transforminfo.py index ad735456..9404a807 100644 --- a/padinfo/menu/transforminfo.py +++ b/padinfo/menu/transforminfo.py @@ -1,7 +1,9 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedControl, EmbedMenu +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.wrapper import EmbedWrapper + from tsutils.emoji import char_to_emoji from tsutils.menu.components.panes import MenuPanes, emoji_buttons @@ -66,15 +68,15 @@ async def respond_with_overview(message: Optional[Message], ims, **data): @staticmethod def tf_control(state: TransformInfoViewState): reaction_list = state.reaction_list - return EmbedControl( - [TransformInfoView.embed(state)], + return EmbedWrapper( + TransformInfoView.embed(state), reaction_list ) @staticmethod def id_control(state: IdViewState, reaction_list): - return EmbedControl( - [IdView.embed(state)], + return EmbedWrapper( + IdView.embed(state), reaction_list ) diff --git a/padle/menu/padle_scroll.py b/padle/menu/padle_scroll.py index 465952ae..31b2b206 100644 --- a/padle/menu/padle_scroll.py +++ b/padle/menu/padle_scroll.py @@ -2,7 +2,8 @@ from typing import Optional from discord import Message -from discordmenu.embed.menu import EmbedControl, EmbedMenu +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.wrapper import EmbedWrapper from tsutils.menu.components.panes import MenuPanes from padle.view.padle_scroll_view import PADleScrollView, PADleScrollViewState @@ -49,7 +50,7 @@ async def respond_with_right(cls, message: Optional[Message], ims, **data): return await PADleScrollMenu.respond_with_pane(message, ims, **data) @classmethod - async def respond_with_pane(cls, message: Optional[Message], ims, **data) -> PADleScrollViewState: + async def respond_with_pane(cls, message: Optional[Message], ims, **data) -> EmbedWrapper: dbcog = data['dbcog'] user_config = data['user_config'] today_guesses = data['today_guesses'] @@ -61,8 +62,8 @@ def control(state: PADleScrollViewState): if state is None: return None reaction_list = PADleMenuPanes.get_initial_reaction_list() - return EmbedControl( - [PADleScrollView.embed(state)], + return EmbedWrapper( + PADleScrollView.embed(state), reaction_list ) From b3de2ccee50091a16eeb133f6a600f8288f91622 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Tue, 14 Jun 2022 16:28:40 -0400 Subject: [PATCH 1143/1220] Set default PADle to Tsubaki & use csv (#1732) --- padle/padle.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/padle/padle.py b/padle/padle.py index 7c5f1e4b..3083f497 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -5,6 +5,7 @@ import logging import random import re +import csv from contextlib import suppress from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedFooter from discordmenu.embed.view import EmbedView @@ -39,7 +40,9 @@ class PADle(commands.Cog): """A Wordle game for PAD""" - + + # Used if no monster list is set + FALLBACK_PADLE_MONSTER = 3260 menu_map = padle_menu_map def __init__(self, bot, *args, **kwargs): @@ -48,8 +51,8 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=94073) self.config.register_user(todays_guesses=[], start=False, done=False, score=[], edit_id=0, channel_id=0, all_guesses={}) - self.config.register_global(padle_today=3260, stored_day=0, num_days=1, - subs=[], all_scores=[], save_daily_scores=[], + self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, stored_day=0, + num_days=1, subs=[], all_scores=[], save_daily_scores=[], monsters_list=[], tmrw_padle=0) self.config.register_guild(allow=False) self._daily_padle_loop = bot.loop.create_task(self.generate_padle()) @@ -496,12 +499,13 @@ async def is_day_change(): try: async with self.config.save_daily_scores() as save_daily: save_daily.append([await self.config.padle_today(), await self.config.all_scores()]) - # async with aopen("./pad-cogs/padle/monsters.txt", "r") as f: - # monsters = (await f.readline()).split(",") tmrw_padle = await self.config.tmrw_padle() if tmrw_padle == 0: MONSTERS_LIST = await self.config.monsters_list() - await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) + if len(MONSTERS_LIST) == 0: + await self.config.padle_today.set(self.FALLBACK_PADLE_MONSTER) + else: + await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) else: await self.config.padle_today.set(tmrw_padle) await self.config.tmrw_padle.set(0) @@ -546,7 +550,7 @@ async def fullreset(self, ctx): if not confirmation: return await send_cancellation_message(ctx, "Nothing was reset.") - await self.config.padle_today.set(3260) + await self.config.padle_today.set(self.FALLBACK_PADLE_MONSTER) await self.config.tmrw_padle.set(0) await self.config.num_days.set(1) @@ -717,12 +721,13 @@ async def advance(self, ctx): await self.config.stored_day.set(datetime.datetime.now().day) async with self.config.save_daily_scores() as save_daily: save_daily.append([await self.config.padle_today(), await self.config.all_scores()]) - # async with aopen("./pad-cogs/padle/monsters.txt", "r") as f: - # monsters = (await f.readline()).split(",") tmrw_padle = await self.config.tmrw_padle() if tmrw_padle == 0: MONSTERS_LIST = await self.config.monsters_list() - await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) + if len(MONSTERS_LIST) == 0: + await self.config.padle_today.set(self.FALLBACK_PADLE_MONSTER) + else: + await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) else: await self.config.padle_today.set(tmrw_padle) await self.config.tmrw_padle.set(0) From e31862fd2d9da8a73e37fb35c3e1be808257e004 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Tue, 14 Jun 2022 18:17:09 -0400 Subject: [PATCH 1144/1220] CSV reader for padle set (#1733) --- padle/padle.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/padle/padle.py b/padle/padle.py index 3083f497..bce22522 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -1,15 +1,15 @@ import asyncio import datetime +import re import discord import json import logging import random -import re import csv from contextlib import suppress from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedFooter from discordmenu.embed.view import EmbedView -from io import BytesIO +from io import BytesIO, StringIO from math import ceil from padle.menu.closable_embed import ClosableEmbedMenu from padle.menu.menu_map import padle_menu_map @@ -632,16 +632,27 @@ async def set(self, ctx): """Set possible PADles to attached CSV/text file""" if ctx.message.attachments: try: - data = [int(x) for x in - re.sub("[^0-9,]", "", (await ctx.message.attachments[0].read()).decode("utf-8")).split(",")] + input = StringIO((await ctx.message.attachments[0].read()).decode("utf-8")) + reader = csv.reader(input, delimiter=",") + valid = [] + invalid = [] dbcog = await self.get_dbcog() - for id in data: - m = dbcog.get_monster(int(id)) - if m is None or not m.on_na or m.name_en is None: - return await send_cancellation_message(ctx, f"{id} was invalid. Please remove " - "this ID and try again.") - await self.config.monsters_list.set(data) - await ctx.tick() + for row in reader: + for text_id in row: + id = re.sub(" ", "", text_id) + if not id.isnumeric(): + invalid.append(id) + continue + m = dbcog.get_monster(int(id)) + if m is None or not m.on_na or m.name_en is None: + invalid.append(id) + continue + valid.append(int(id)) + if len(invalid) == 0: + await self.config.monsters_list.set(valid) + return await ctx.tick() + await send_cancellation_message(ctx, "The following IDs were invalid: " + f"{', '.join(invalid)}. Please remove them and try again.") except Exception as e: await send_cancellation_message(ctx, "Something went wrong.") logger.exception(str(e)) From 73d512da3f416e54a23469c47aaef0432b5df321 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Tue, 14 Jun 2022 18:46:08 -0400 Subject: [PATCH 1145/1220] Fix first PADle day (#1734) --- padle/padle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/padle/padle.py b/padle/padle.py index bce22522..a3ba930c 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -7,7 +7,7 @@ import random import csv from contextlib import suppress -from discordmenu.embed.components import EmbedThumbnail, EmbedMain, EmbedFooter +from discordmenu.embed.components import EmbedThumbnail, EmbedMain from discordmenu.embed.view import EmbedView from io import BytesIO, StringIO from math import ceil @@ -51,7 +51,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=94073) self.config.register_user(todays_guesses=[], start=False, done=False, score=[], edit_id=0, channel_id=0, all_guesses={}) - self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, stored_day=0, + self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, stored_day=datetime.datetime.now().day, num_days=1, subs=[], all_scores=[], save_daily_scores=[], monsters_list=[], tmrw_padle=0) self.config.register_guild(allow=False) @@ -551,7 +551,7 @@ async def fullreset(self, ctx): return await send_cancellation_message(ctx, "Nothing was reset.") await self.config.padle_today.set(self.FALLBACK_PADLE_MONSTER) - + await self.config.stored_day.set(datetime.datetime.now().day) await self.config.tmrw_padle.set(0) await self.config.num_days.set(1) await self.config.subs.set([]) From 085a1c219758c0a0bbda00f9377cef87c56ce6c9 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Wed, 15 Jun 2022 22:32:07 -0400 Subject: [PATCH 1146/1220] Menu2 for PADle globalstats (#1741) --- padle/menu/globalstats.py | 76 ++++++++++++++++++++ padle/menu/menu_map.py | 4 +- padle/menu/padle_scroll.py | 5 +- padle/padle.py | 88 ++++++----------------- padle/view/globalstats_view.py | 122 ++++++++++++++++++++++++++++++++ padle/view/padle_scroll_view.py | 6 +- 6 files changed, 229 insertions(+), 72 deletions(-) create mode 100644 padle/menu/globalstats.py create mode 100644 padle/view/globalstats_view.py diff --git a/padle/menu/globalstats.py b/padle/menu/globalstats.py new file mode 100644 index 00000000..d187b5ab --- /dev/null +++ b/padle/menu/globalstats.py @@ -0,0 +1,76 @@ +from discord import Message +from discordmenu.embed.menu import EmbedMenu +from discordmenu.embed.wrapper import EmbedWrapper +from padle.view.globalstats_view import GlobalStatsView, GlobalStatsViewState +from tsutils.menu.components.panes import MenuPanes +from typing import Optional + + +class ScrollEmojis: + prev_page = '\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}' + next_page = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' + home = '\N{HOUSE BUILDING}' + +class GlobalStatsMenu: + MENU_TYPE = 'PADleGlobalStatsMenu' + + view_types = { + GlobalStatsViewState.VIEW_STATE_TYPE: GlobalStatsView + } + + @staticmethod + def menu(initial_control=None): + if initial_control is None: + initial_control = GlobalStatsMenu.control + embed = EmbedMenu(GlobalStatsMenuPanes.transitions(), initial_control) + return embed + + @classmethod + async def respond_with_left(cls, message: Optional[Message], ims, **data): + GlobalStatsViewState.decrement_page(ims) + state = await cls._get_view_state(ims, **data) + return GlobalStatsMenu.control(state) + + @classmethod + async def respond_with_right(cls, message: Optional[Message], ims, **data): + GlobalStatsViewState.increment_page(ims) + state = await cls._get_view_state(ims, **data) + return GlobalStatsMenu.control(state) + + @classmethod + async def _get_view_state(cls, ims: dict, **data) -> GlobalStatsViewState: + dbcog = data['dbcog'] + user_config = data['user_config'] + padle_cog = data['padle_cog'] + return await GlobalStatsViewState.deserialize(dbcog, user_config, padle_cog, ims) + + @classmethod + async def respond_with_pane(cls, message: Optional[Message], ims, **data) -> EmbedWrapper: + view_state = await cls._get_view_state(ims, **data) + return GlobalStatsMenu.control(view_state) + + @staticmethod + def control(state: GlobalStatsViewState): + if state is None: + return None + reaction_list = GlobalStatsMenuPanes.get_initial_reaction_list() + return EmbedWrapper( + GlobalStatsView.embed(state), + reaction_list + ) + + +class GlobalStatsMenuPanes(MenuPanes): + INITIAL_EMOJI = ScrollEmojis.home + + DATA = { + ScrollEmojis.next_page: (GlobalStatsMenu.respond_with_right, GlobalStatsView.VIEW_TYPE), + ScrollEmojis.prev_page: (GlobalStatsMenu.respond_with_left, GlobalStatsView.VIEW_TYPE), + ScrollEmojis.home: (GlobalStatsMenu.respond_with_pane, GlobalStatsView.VIEW_TYPE), + } + + HIDDEN_EMOJIS = [ScrollEmojis.home] + + @classmethod + def get_initial_reaction_list(cls): + return [ScrollEmojis.prev_page, ScrollEmojis.next_page] diff --git a/padle/menu/menu_map.py b/padle/menu/menu_map.py index 7a21a2cc..c2320f26 100644 --- a/padle/menu/menu_map.py +++ b/padle/menu/menu_map.py @@ -2,8 +2,10 @@ from padle.menu.closable_embed import ClosableEmbedMenu from padle.menu.padle_scroll import PADleScrollMenu, PADleMenuPanes +from padle.menu.globalstats import GlobalStatsMenu, GlobalStatsMenuPanes padle_menu_map = { ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), - PADleScrollMenu.MENU_TYPE: (PADleScrollMenu, PADleMenuPanes) + PADleScrollMenu.MENU_TYPE: (PADleScrollMenu, PADleMenuPanes), + GlobalStatsMenu.MENU_TYPE: (GlobalStatsMenu, GlobalStatsMenuPanes), } diff --git a/padle/menu/padle_scroll.py b/padle/menu/padle_scroll.py index 31b2b206..6e1ca96b 100644 --- a/padle/menu/padle_scroll.py +++ b/padle/menu/padle_scroll.py @@ -34,7 +34,6 @@ def get_num_pages_ims(cls, ims): @classmethod async def respond_with_left(cls, message: Optional[Message], ims, **data): - # PADleScrollMenu.decrement_page(ims) if ims['current_page'] < PADleScrollMenu.get_num_pages_ims(ims) - 1: ims['current_page'] = ims['current_page'] + 1 else: @@ -53,8 +52,8 @@ async def respond_with_right(cls, message: Optional[Message], ims, **data): async def respond_with_pane(cls, message: Optional[Message], ims, **data) -> EmbedWrapper: dbcog = data['dbcog'] user_config = data['user_config'] - today_guesses = data['today_guesses'] - view_state = await PADleScrollViewState.deserialize(dbcog, user_config, today_guesses, ims) + padle_cog = data['padle_cog'] + view_state = await PADleScrollViewState.deserialize(dbcog, padle_cog, user_config, ims) return PADleScrollMenu.control(view_state) @staticmethod diff --git a/padle/padle.py b/padle/padle.py index a3ba930c..db21483b 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -14,6 +14,7 @@ from padle.menu.closable_embed import ClosableEmbedMenu from padle.menu.menu_map import padle_menu_map from padle.menu.padle_scroll import PADleScrollMenu, PADleScrollViewState +from padle.menu.globalstats import GlobalStatsMenu, GlobalStatsViewState from padle.monsterdiff import MonsterDiff from padle.view.confirmation import PADleMonsterConfirmationView, PADleMonsterConfirmationViewProps from redbot.core import Config, commands @@ -22,7 +23,6 @@ from tsutils.emoji import NO_EMOJI, YES_EMOJI from tsutils.helper_functions import conditional_iterator from tsutils.menu.components.config import BotConfig -from tsutils.menu.components.footers import TSUBAKI_FLOWER_ICON_URL from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.query_settings.query_settings import QuerySettings from tsutils.tsubaki.links import MonsterImage, MonsterLink @@ -40,7 +40,7 @@ class PADle(commands.Cog): """A Wordle game for PAD""" - + # Used if no monster list is set FALLBACK_PADLE_MONSTER = 3260 menu_map = padle_menu_map @@ -51,7 +51,7 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=94073) self.config.register_user(todays_guesses=[], start=False, done=False, score=[], edit_id=0, channel_id=0, all_guesses={}) - self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, stored_day=datetime.datetime.now().day, + self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, stored_day=datetime.datetime.now().day, num_days=1, subs=[], all_scores=[], save_daily_scores=[], monsters_list=[], tmrw_padle=0) self.config.register_guild(allow=False) @@ -93,11 +93,10 @@ def cog_unload(self): self._daily_padle_loop.cancel() async def get_menu_default_data(self, ims): - user = self.bot.get_user(ims['original_author_id']) data = { 'dbcog': await self.get_dbcog(), 'user_config': await BotConfig.get_user(self.config, ims['original_author_id']), - 'today_guesses': await self.get_today_guesses(user, ims.get('current_day')) + 'padle_cog': self, } return data @@ -220,31 +219,20 @@ async def globalstats(self, ctx, day: int = 0): num_days = await self.config.num_days() if day <= 0 or day > num_days: day = await self.config.num_days() + if day == num_days: stats = await self.config.all_scores() - giveups = 0 - completes = 0 - average = 0 - for item in stats: - if item == "X": - giveups += 1 - else: - completes += 1 - average += int(item) - embed = discord.Embed(title="PADle #{} Stats".format(await self.config.num_days()), type="rich") - if completes == 0: - embed.description = (f"**Total Wins**: {completes}\n**Total Losses**: {giveups}\n**Win Rate**: 0%\n" - "**Average Guess Count**: 0") - else: - embed.description = ("**Total Wins**: {}\n**Total Losses**: {}\n**Win Rate**: {:.2%}\n" - "**Average Guess Count**: {:.2f}").format(completes, giveups, - completes / (completes + giveups), - (average / completes)) - embed.set_footer(text=f"Try {ctx.prefix}padle globalstats [day] for other day's stats!", - icon_url=TSUBAKI_FLOWER_ICON_URL) + monster = None else: - embed = await self.get_past_padle_embed(ctx, day) - await ctx.send(embed=embed) + dbcog = await self.get_dbcog() + all = await self.config.save_daily_scores() + stats = all[day - 1][1] + monster = dbcog.get_monster(all[day - 1][0]) + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, "") + global_stats_menu = GlobalStatsMenu.menu() + state = GlobalStatsViewState(ctx.author.id, GlobalStatsMenu.MENU_TYPE, query_settings, "", + current_day=day, num_days=num_days, monster=monster, stats=stats) + await global_stats_menu.create(ctx, state) @padle.command() async def stats(self, ctx): @@ -293,38 +281,6 @@ async def stats(self, ctx): MonsterImage.icon(m.monster_id))).to_embed() await ctx.send(embed=m_embed) - async def get_past_padle_embed(self, ctx, day: int): - dbcog = await self.get_dbcog() - daily_scores = await self.config.save_daily_scores() - info = daily_scores[day - 1] - stats = info[1] - giveups = 0 - completes = 0 - average = 0 - for item in stats: - if item == "X": - giveups += 1 - else: - completes += 1 - average += int(item) - guess_monster = dbcog.get_monster(int(info[0])) - embed = EmbedView( - EmbedMain( - title="PADle #{} Stats".format(day)), - embed_thumbnail=EmbedThumbnail( - MonsterImage.icon(guess_monster.monster_id))).to_embed() - if completes == 0: - embed.description = ("**" + MonsterHeader.menu_title(guess_monster).to_markdown() + "**\n" + - "**Total Wins**: {}\n**Total Losses**: {}\n**Win Rate**: 0%\n" - "**Average Guess Count**: 0").format(completes, giveups) - else: - embed.description = ("**" + MonsterHeader.menu_title(guess_monster).to_markdown() + "**\n" + - "**Total Wins**: {}\n**Total Losses**: {}\n**Win Rate**: {:.2%}\n" - "**Average Guess Count**: {:.2f}").format(completes, giveups, - completes / (completes + giveups), - (average / completes)) - return embed - async def do_quit_early(self, ctx): prefix = ctx.prefix if ctx.guild is not None and not await self.can_play_in_guild(ctx): @@ -652,7 +608,7 @@ async def set(self, ctx): await self.config.monsters_list.set(valid) return await ctx.tick() await send_cancellation_message(ctx, "The following IDs were invalid: " - f"{', '.join(invalid)}. Please remove them and try again.") + f"{', '.join(invalid)}. Please remove them and try again.") except Exception as e: await send_cancellation_message(ctx, "Something went wrong.") logger.exception(str(e)) @@ -773,7 +729,7 @@ async def advance(self, ctx): raise except Exception: logger.exception("Error in loop:") - + @padleadmin.command() async def filter(self, ctx): """Re-creates the list of monsters""" @@ -784,15 +740,15 @@ async def filter(self, ctx): mgraph = dbcog.database.graph final = [] # await ctx.send(mgraph.max_monster_id) - for i in range(1, 8800): # hard coded bc max_monster_id as calculated above is 15k??? + for i in range(1, 8800): # hard coded bc max_monster_id as calculated above is 15k??? monster = await dbcog.find_monster(str(i), ctx.author) with suppress(Exception): nextTrans = mgraph.get_next_transform(monster) prevTrans = mgraph.get_prev_transform(monster) - if(monster is not None and monster.name_en is not None and monster.on_na and - monster.series.series_type != 'collab' and - ((monster.sell_mp >= 50000 and (monster.superawakening_count > 1 or prevTrans is not None)) or - "Super Reincarnated" in monster.name_en) and not monster.is_equip and + if (monster is not None and monster.name_en is not None and monster.on_na and + monster.series.series_type != 'collab' and + ((monster.sell_mp >= 50000 and (monster.superawakening_count > 1 or prevTrans is not None)) or + "Super Reincarnated" in monster.name_en) and not monster.is_equip and nextTrans is None and monster.level >= 99): final.append(str(i)) with open("result.txt", "w") as file: diff --git a/padle/view/globalstats_view.py b/padle/view/globalstats_view.py new file mode 100644 index 00000000..1463a3b9 --- /dev/null +++ b/padle/view/globalstats_view.py @@ -0,0 +1,122 @@ +from discordmenu.embed.components import EmbedMain, EmbedThumbnail, EmbedField +from discordmenu.embed.view import EmbedView +from discordmenu.embed.base import Box +from discordmenu.embed.text import BoldText +from tsutils.menu.components.config import UserConfig +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.view_state_base import ViewStateBase +from tsutils.tsubaki.links import MonsterImage +from tsutils.tsubaki.monster_header import MonsterHeader +from tsutils.query_settings.query_settings import QuerySettings + + + +class GlobalStatsViewState(ViewStateBase): + VIEW_STATE_TYPE: str = "PADleGlobalStatsView" + + def __init__(self, original_author_id, menu_type, query_settings: QuerySettings, raw_query="", + current_day=0, num_days=0, extra_state=None, reaction_list=None, monster=None, stats=[]): + super().__init__(original_author_id, menu_type, raw_query, + extra_state=extra_state) + self.query_settings = query_settings + self.reaction_list = reaction_list + self.current_day = current_day + self.num_days = num_days + self.monster = monster + self.stats = stats + + def serialize(self): + ret = super().serialize() + ret.update({ + 'current_day': self.current_day, + 'reaction_list': self.reaction_list, + 'num_days': self.num_days, + 'query_settings': self.query_settings.serialize() + }) + return ret + + @staticmethod + def increment_page(ims): + if ims['current_day'] < ims['num_days']: + ims['current_day'] = ims['current_day'] + 1 + else: + ims['current_day'] = 1 + + @staticmethod + def decrement_page(ims): + if ims['current_day'] > 1: + ims['current_day'] = ims['current_day'] - 1 + else: + ims['current_day'] = ims['num_days'] + + @classmethod + async def deserialize(cls, dbcog, _user_config: UserConfig, padle_cog, ims: dict): + original_author_id = ims['original_author_id'] + menu_type = ims['menu_type'] + reaction_list = ims['reaction_list'] + current_day = ims['current_day'] + num_days = ims['num_days'] + query_settings = QuerySettings.deserialize(ims.get('query_settings')) + daily_scores_list = await padle_cog.config.save_daily_scores() + cur_day_scores = await padle_cog.config.all_scores() + if num_days == current_day: + return GlobalStatsViewState(original_author_id, menu_type, query_settings, "", current_day=current_day, + num_days=num_days, reaction_list=reaction_list, monster=None, + stats=cur_day_scores) + else: + info = daily_scores_list[current_day - 1] + m = dbcog.get_monster(int(info[0])) + return GlobalStatsViewState(original_author_id, menu_type, query_settings, "", current_day=current_day, + num_days=num_days, reaction_list=reaction_list, monster=m, + stats=info[1]) + + +class GlobalStatsView: + VIEW_TYPE = 'PADleGlobalStats' + + @classmethod + def get_pages_footer(cls, state): + return "Day " + str(state.current_day) + "/" + str(state.num_days) + + @staticmethod + def embed(state: GlobalStatsViewState): + description = GlobalStatsView.get_description(state) + if state.num_days == state.current_day: + return EmbedView( + EmbedMain( + title=BoldText(f"PADle #{state.current_day}"), + color=state.query_settings.embedcolor, + ), + embed_fields=[EmbedField("Stats", Box(description))], + embed_footer=embed_footer_with_state(state, text=GlobalStatsView.get_pages_footer(state))) + else: + return EmbedView( + EmbedMain( + title=BoldText(f"PADle #{state.current_day}"), + description=Box(BoldText(MonsterHeader.menu_title(state.monster).to_markdown())), + color=state.query_settings.embedcolor, + ), + embed_fields=[EmbedField("Stats", Box(description))], + embed_footer=embed_footer_with_state(state, text=GlobalStatsView.get_pages_footer(state)), + embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id))) + + @classmethod + def get_description(cls, state): + giveups = 0 + completes = 0 + average = 0 + for item in state.stats: + if item == "X": + giveups += 1 + else: + completes += 1 + average += int(item) + if completes == 0: + description = (f"**Total Wins**: {completes}\n**Total Losses**: {giveups}\n**Win Rate**: 0%\n" + "**Average Guess Count**: 0") + else: + description = ("**Total Wins**: {}\n**Total Losses**: {}\n**Win Rate**: {:.2%}\n" + "**Average Guess Count**: {:.2f}").format(completes, giveups, + completes / (completes + giveups), + (average / completes)) + return description diff --git a/padle/view/padle_scroll_view.py b/padle/view/padle_scroll_view.py index f87070c1..fa08a646 100644 --- a/padle/view/padle_scroll_view.py +++ b/padle/view/padle_scroll_view.py @@ -69,7 +69,7 @@ def serialize(self): return ret @classmethod - async def deserialize(cls, dbcog, _user_config: UserConfig, todays_guesses, ims: dict): + async def deserialize(cls, dbcog, padle_cog, _user_config: UserConfig, ims: dict): original_author_id = ims['original_author_id'] menu_type = ims['menu_type'] reaction_list = ims['reaction_list'] @@ -78,7 +78,9 @@ async def deserialize(cls, dbcog, _user_config: UserConfig, todays_guesses, ims: cur_monster = ims['cur_monster'] num_pages = ims['num_pages'] monster = dbcog.get_monster(int(cur_monster)) - cur_day_page_guesses = await cls.do_queries(dbcog, todays_guesses[((cur_page) * 5):((cur_page + 1) * 5)]) + user = padle_cog.bot.get_user(original_author_id) + today_guesses = await padle_cog.get_today_guesses(user, ims.get('current_day')) + cur_day_page_guesses = await cls.do_queries(dbcog, today_guesses[((cur_page) * 5):((cur_page + 1) * 5)]) return PADleScrollViewState(original_author_id, menu_type, "", current_page=cur_page, current_day=current_day, reaction_list=reaction_list, cur_day_page_guesses=cur_day_page_guesses, monster=monster, num_pages=num_pages) From 21cf30143e2b4fbf2b81fd39b6a38314db0a81e8 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Wed, 15 Jun 2022 23:12:42 -0400 Subject: [PATCH 1147/1220] Misc. PADle fixes (#1738) --- padle/padle.py | 91 ++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/padle/padle.py b/padle/padle.py index db21483b..ab5d3c3d 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -1,5 +1,4 @@ import asyncio -import datetime import re import discord import json @@ -7,6 +6,7 @@ import random import csv from contextlib import suppress +from datetime import datetime, timezone, timedelta from discordmenu.embed.components import EmbedThumbnail, EmbedMain from discordmenu.embed.view import EmbedView from io import BytesIO, StringIO @@ -43,6 +43,8 @@ class PADle(commands.Cog): # Used if no monster list is set FALLBACK_PADLE_MONSTER = 3260 + # Set hours to preferred timezone offset from UTC for day change calculations + tzinfo = timezone(timedelta(hours = -8.0)) menu_map = padle_menu_map def __init__(self, bot, *args, **kwargs): @@ -51,9 +53,9 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=94073) self.config.register_user(todays_guesses=[], start=False, done=False, score=[], edit_id=0, channel_id=0, all_guesses={}) - self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, stored_day=datetime.datetime.now().day, - num_days=1, subs=[], all_scores=[], save_daily_scores=[], - monsters_list=[], tmrw_padle=0) + self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, + stored_day=datetime.now(self.tzinfo).day, num_days=1, subs=[], + all_scores=[], save_daily_scores=[], monsters_list=[], tmrw_padle=0) self.config.register_guild(allow=False) self._daily_padle_loop = bot.loop.create_task(self.generate_padle()) GACOG: Any = self.bot.get_cog("GlobalAdmin") @@ -64,8 +66,8 @@ async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" all_guesses = await self.config.user_from_id(user_id).all_guesses() if all_guesses: - data = f"You have {len(all_guesses)} days of guess data stored." - f"Here are your guesses:\n{json.dumps(all_guesses)}" + data = (f"You have {len(all_guesses)} days of guess data stored." + f"Here are your guesses:\n{json.dumps(all_guesses)}") else: data = f"No data is stored for user with ID {user_id}." return {"user_data.txt": BytesIO(data.encode())} @@ -105,8 +107,7 @@ async def get_today_guesses(self, user: discord.User, current_day: Union[str, in return None if user is None: return {} - async with self.config.user(user).all_guesses() as all_guesses: - return all_guesses.get(str(current_day)) + return (await self.config.user(user).all_guesses()).get(str(current_day)) @commands.group() async def padle(self, ctx): @@ -210,7 +211,7 @@ async def start(self, ctx): return await send_cancellation_message(ctx, "Looks like I can't DM you. Try checking your Privacy Settings.") if ctx.guild is not None: - await send_confirmation_message(ctx, "Check your DMs!") + await send_confirmation_message(ctx, f"{ctx.author.name}, check your DMs!") await self.config.user(ctx.author).start.set(True) @padle.command() @@ -252,20 +253,18 @@ async def stats(self, ctx): correct_id = await self.config.padle_today() else: correct_id = save_daily[day - 1][0] - if str(day) in all_guesses and len(all_guesses[str(day)]) != 0 and all_guesses[str(day)][-1] == correct_id: + if str(day) in all_guesses and correct_id in all_guesses[str(day)]: cur_streak += 1 wins += 1 - else: - if cur_streak > max_streak: - max_streak = cur_streak - if day != await self.config.num_days(): - cur_streak = 0 - - if cur_streak > max_streak: - max_streak = cur_streak + elif day != await self.config.num_days() or await self.config.user(ctx.author).done(): + cur_streak = 0 + if cur_streak > max_streak: + max_streak = cur_streak all_monsters_guessed = [] for key, value in all_guesses.items(): all_monsters_guessed.extend(value) + if not all_monsters_guessed: + all_monsters_guessed.append(self.FALLBACK_PADLE_MONSTER) mode = max(set(all_monsters_guessed), key=all_monsters_guessed.count) dbcog = await self.get_dbcog() m = dbcog.get_monster(mode) @@ -444,7 +443,7 @@ async def score(self, ctx): async def generate_padle(self): async def is_day_change(): - cur_day = datetime.datetime.now().day + cur_day = datetime.now(self.tzinfo).day old_day = await self.config.stored_day() if cur_day != old_day: await self.config.stored_day.set(cur_day) @@ -506,8 +505,12 @@ async def fullreset(self, ctx): if not confirmation: return await send_cancellation_message(ctx, "Nothing was reset.") - await self.config.padle_today.set(self.FALLBACK_PADLE_MONSTER) - await self.config.stored_day.set(datetime.datetime.now().day) + MONSTERS_LIST = await self.config.monsters_list() + if len(MONSTERS_LIST) == 0: + await self.config.padle_today.set(self.FALLBACK_PADLE_MONSTER) + else: + await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) + await self.config.stored_day.set(datetime.now(self.tzinfo).day) await self.config.tmrw_padle.set(0) await self.config.num_days.set(1) await self.config.subs.set([]) @@ -540,19 +543,19 @@ async def add(self, ctx, *ids: int): result = [] valids = [] monsters_list = await self.config.monsters_list() - for id in ids: - m = dbcog.get_monster(id) + for monster_id in ids: + m = dbcog.get_monster(monster_id) if m is None: - result.append(f"{str(id)} is not a valid monster ID.") + result.append(f"{monster_id} is not a valid monster ID.") continue if not m.on_na or m.name_en is None: - result.append(f"{str(id)} is not a monster on the NA server.") + result.append(f"{monster_id} is not a monster on the NA server.") continue - if int(id) in monsters_list: + if monster_id in monsters_list: result.append( f"{MonsterHeader.menu_title(m, use_emoji=True).to_markdown()} is already a possible PADle.") continue - valids.append(int(id)) + valids.append(monster_id) result.append(f"{MonsterHeader.menu_title(m, use_emoji=True).to_markdown()} was added.") async with self.config.monsters_list() as m_list: m_list.extend(valids) @@ -611,7 +614,7 @@ async def set(self, ctx): f"{', '.join(invalid)}. Please remove them and try again.") except Exception as e: await send_cancellation_message(ctx, "Something went wrong.") - logger.exception(str(e)) + logger.exception("Error when setting CSV of PADles.") else: return await send_cancellation_message(ctx, "Looks like no file was attached!") @@ -685,7 +688,7 @@ async def advance(self, ctx): if not confirmation: return await send_cancellation_message(ctx, "The PADle was unchanged.") try: - await self.config.stored_day.set(datetime.datetime.now().day) + await self.config.stored_day.set(datetime.now(self.tzinfo).day) async with self.config.save_daily_scores() as save_daily: save_daily.append([await self.config.padle_today(), await self.config.all_scores()]) tmrw_padle = await self.config.tmrw_padle() @@ -733,26 +736,26 @@ async def advance(self, ctx): @padleadmin.command() async def filter(self, ctx): """Re-creates the list of monsters""" - confirmation = await get_user_confirmation(ctx, "This command will take a long time to execute. Run anyways?") - if not confirmation: - return dbcog = await self.get_dbcog() mgraph = dbcog.database.graph final = [] - # await ctx.send(mgraph.max_monster_id) - for i in range(1, 8800): # hard coded bc max_monster_id as calculated above is 15k??? - monster = await dbcog.find_monster(str(i), ctx.author) - with suppress(Exception): - nextTrans = mgraph.get_next_transform(monster) - prevTrans = mgraph.get_prev_transform(monster) - if (monster is not None and monster.name_en is not None and monster.on_na and - monster.series.series_type != 'collab' and - ((monster.sell_mp >= 50000 and (monster.superawakening_count > 1 or prevTrans is not None)) or - "Super Reincarnated" in monster.name_en) and not monster.is_equip and - nextTrans is None and monster.level >= 99): - final.append(str(i)) + for i in range(1, mgraph.max_monster_id): + monster = dbcog.get_monster(i) + if monster is None: + continue + if(self.is_valid_padle(monster, mgraph)): + final.append(str(i)) with open("result.txt", "w") as file: file.write(",".join(final)) with open("result.txt", "rb") as file: await ctx.send("List of monsters:", file=discord.File(file, "result.txt")) await ctx.tick() + + def is_valid_padle(monster, mgraph): + nextTrans = mgraph.get_next_transform(monster) + prevTrans = mgraph.get_prev_transform(monster) + return (monster.name_en is not None and monster.on_na and + monster.series.series_type != 'collab' and + ((monster.sell_mp >= 50000 and (monster.superawakening_count > 1 or prevTrans is not None)) or + "Super Reincarnated" in monster.name_en) and not monster.is_equip and + nextTrans is None and monster.level >= 99) From 924aa757169f6b82d91207bf2984fc8bfff48fec Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Wed, 15 Jun 2022 23:02:56 -0500 Subject: [PATCH 1148/1220] Add Chu to buttoninfo (#1745) --- padinfo/core/button_info.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/padinfo/core/button_info.py b/padinfo/core/button_info.py index c1968bcc..f4602e5d 100644 --- a/padinfo/core/button_info.py +++ b/padinfo/core/button_info.py @@ -35,7 +35,8 @@ # account for attributes when spacing MonsterStat("Nergi Hunter{}", 4172, 40, [2, 4]), MonsterStat("Oversoul{} ", 5273, 50, [0, 1, 2, 3, 4]), - MonsterStat("Ryuno Ume{} ", 5252, 80, [2, 4]) + MonsterStat("Ryuno Ume{} ", 5252, 80, [2, 4]), + MonsterStat("Chu{} ", 4316, 60, [1, 2]) ] CARD_BUTTONS = [ MonsterStat("Assassin{} ", 5021, 200, []), From 60d083adf4625d21b0c7d2d1242229ec5473ae92 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Thu, 16 Jun 2022 00:27:55 -0500 Subject: [PATCH 1149/1220] Print name of modified file in CRUD commits (#1743) --- crud/crud.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 1ddc35f0..796587ad 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -58,6 +58,8 @@ async def check_crud_channel(ctx): class Crud(commands.Cog, EditSeries): """PadGuide CRUD""" + json_folder = 'etl/pad/storage_processor/' + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot @@ -120,7 +122,7 @@ async def execute_read(self, ctx, sql, replacements): for page in pagify(msg): await ctx.send(box(page)) - async def git_verify(self, ctx, filepath): + async def git_verify(self, ctx, folder, filename): try: repo = pygit2.Repository(await self.config.pipeline_base()) except pygit2.GitError: @@ -143,6 +145,7 @@ async def git_verify(self, ctx, filepath): f" github username token `") try: + filepath = folder + filename index = repo.index index.add(filepath) index.write() @@ -152,7 +155,7 @@ async def git_verify(self, ctx, filepath): email or "famiel@tsubakibot.com") commiter = pygit2.Signature("Famiel", "famiel@tsubakibot.com") parent, ref = repo.resolve_refish(refish=repo.head.name) - repo.create_commit(ref.name, author, commiter, "Updating JSON", tree, [parent.oid]) + repo.create_commit(ref.name, author, commiter, f"Updating {filename}", tree, [parent.oid]) upcred = pygit2.UserPass(keys['username'], keys['token']) remote = discord.utils.get(repo.remotes, name="origin") remote.push(['refs/heads/master'], callbacks=pygit2.RemoteCallbacks(credentials=upcred)) @@ -259,7 +262,7 @@ async def series_add(self, ctx, *elements): await self.execute_write(ctx, sql, (*elements.values(),)) - fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/series.json') + fn = os.path.join(await self.config.pipeline_base(), self.json_folder, 'series.json') async with aiofiles.open(fn, 'r') as f: j = json.loads(await f.read()) j.append({ @@ -271,7 +274,7 @@ async def series_add(self, ctx, *elements): }) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - await self.git_verify(ctx, 'etl/pad/storage_processor/series.json') + await self.git_verify(ctx, self.json_folder, 'series.json') @series.command(name="edit") async def series_edit(self, ctx, series_id: int, *elements): @@ -299,7 +302,7 @@ async def series_edit(self, ctx, series_id: int, *elements): await self.execute_write(ctx, sql, (*elements.values(), series_id)) - fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/series.json') + fn = os.path.join(await self.config.pipeline_base(), self.json_folder, 'series.json') async with aiofiles.open(fn, 'r') as f: j = json.loads(await f.read()) for e in j: @@ -307,7 +310,7 @@ async def series_edit(self, ctx, series_id: int, *elements): e.update(elements) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - await self.git_verify(ctx, 'etl/pad/storage_processor/series.json') + await self.git_verify(ctx, self.json_folder, 'series.json') @series.command(name="delete") @checks.is_owner() @@ -318,7 +321,7 @@ async def series_delete(self, ctx, series_id: int): await self.execute_write(ctx, sql, series_id) - fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/series.json') + fn = os.path.join(await self.config.pipeline_base(), self.json_folder, 'series.json') async with aiofiles.open(fn, 'r') as f: j = json.loads(await f.read()) for e in j[:]: @@ -326,7 +329,7 @@ async def series_delete(self, ctx, series_id: int): j.remove(e) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - await self.git_verify(ctx, 'etl/pad/storage_processor/series.json') + await self.git_verify(ctx, self.json_folder, 'series.json') @crud.group(aliases=["awos"]) async def awokenskill(self, ctx): @@ -384,7 +387,7 @@ async def awokenskill_add(self, ctx, *elements): await self.execute_write(ctx, sql, (*elements.values(),)) - fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/awoken_skill.json') + fn = os.path.join(await self.config.pipeline_base(), self.json_folder, 'awoken_skill.json') async with aiofiles.open(fn, 'r') as f: j = json.loads(await f.read()) j.append({ @@ -407,7 +410,7 @@ async def awokenskill_add(self, ctx, *elements): }) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - await self.git_verify(ctx, 'etl/pad/storage_processor/awoken_skill.json') + await self.git_verify(ctx, self.json_folder, 'awoken_skill.json') @awokenskill.command(name="edit") async def awokenskill_edit(self, ctx, awoken_skill, *elements): @@ -446,7 +449,7 @@ async def awokenskill_edit(self, ctx, awoken_skill, *elements): await self.execute_write(ctx, sql, (*elements.values(), awoken_skill_id)) - fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/awoken_skill.json') + fn = os.path.join(await self.config.pipeline_base(), self.json_folder, 'awoken_skill.json') async with aiofiles.open(fn, 'r') as f: j = json.loads(await f.read()) @@ -455,7 +458,7 @@ async def awokenskill_edit(self, ctx, awoken_skill, *elements): e.update(elements) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - await self.git_verify(ctx, 'etl/pad/storage_processor/awoken_skill.json') + await self.git_verify(ctx, self.json_folder, 'awoken_skill.json') @awokenskill.command(name="delete") @checks.is_owner() @@ -466,7 +469,7 @@ async def awokenskill_delete(self, ctx, awoken_skill_id: int): await self.execute_write(ctx, sql, awoken_skill_id) - fn = os.path.join(await self.config.pipeline_base(), 'etl/pad/storage_processor/awoken_skill.json') + fn = os.path.join(await self.config.pipeline_base(), self.json_folder, 'awoken_skill.json') async with aiofiles.open(fn, 'r') as f: j = json.loads(await f.read()) for e in j[:]: @@ -474,7 +477,7 @@ async def awokenskill_delete(self, ctx, awoken_skill_id: int): j.remove(e) async with aiofiles.open(fn, 'w') as f: await f.write(json.dumps(j, indent=2, ensure_ascii=False, sort_keys=True)) - await self.git_verify(ctx, 'etl/pad/storage_processor/awoken_skill.json') + await self.git_verify(ctx, self.json_folder, 'awoken_skill.json') @crud.command() @checks.is_owner() From 393d11c277222f2504b04604029ad58128287947 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Thu, 16 Jun 2022 17:06:27 -0400 Subject: [PATCH 1150/1220] Misc. PADle changes 2 (#1746) - changed filter (and cleaned up the check) - padle help message (will send on first game played) resolves #1742 - validrules message - menu2 for stats resolves #1744 --- padle/help_texts.py | 26 +++++++ padle/info.json | 40 +++++----- padle/menu/globalstats.py | 5 +- padle/menu/menu_map.py | 7 +- padle/menu/personal_stats.py | 9 +++ padle/monsterdiff.py | 2 + padle/padle.py | 117 ++++++++++++++++++------------ padle/view/globalstats_view.py | 7 +- padle/view/personal_stats_view.py | 42 +++++++++++ 9 files changed, 179 insertions(+), 76 deletions(-) create mode 100644 padle/help_texts.py create mode 100644 padle/menu/personal_stats.py create mode 100644 padle/view/personal_stats_view.py diff --git a/padle/help_texts.py b/padle/help_texts.py new file mode 100644 index 00000000..86b585b1 --- /dev/null +++ b/padle/help_texts.py @@ -0,0 +1,26 @@ +HELP_TEXT = """PADle is a Wordle-inspired guessing game, but with some differences! +{db} You are not guessing words! Instead, you are guessing monsters. +{db} The difference is based on awakenings, rarity, monster points, and a few other attributes. \ +Your current score screen will tell you if your guesses are correct, wrong, or misplaced (for awakenings). +{db} Because this can be much harder than guessing words, you have infinite guesses! +{db} Like Wordle, you can guess any monster, but only certain monsters can be the final PADle. \ +See `{p}padle validrules` to see what monsters count as "valid" final PADles. +{db} Share your number of guesses with `{p}padle score`! + +To see the main menu at any time, type `{p}padle`. To see this specific greeting again, type `{p}padle help`. + +Happy puzzling! {tsubaki}""" + +RULES_TEXT = """Valid monsters include the following: +{db} Super Reincarnated evolutions of pantheons +{db} 50k+ MP AND [have a Super Awakening OR are max transformed] +{db} 15k MP AND are non-event AND [have a Super Awakening OR are max transformed] + +The following monsters are exceptions to the above and will NOT be included: +{rd} Collab monsters (note this does not exclude Event monsters such as from Heroine or Relic Saga) +{rd} An equip evolution +{rd} Monsters not on the NA server +{rd} Monsters that are not max-transformed + +*NOTE: The Super Awakening check ensures monsters chosen are max-evo, for the most part.* +""" diff --git a/padle/info.json b/padle/info.json index ce2d7e7e..09c742e7 100644 --- a/padle/info.json +++ b/padle/info.json @@ -1,21 +1,23 @@ { - "author": [ - "fluff#2368" - ], - "description": "PADle, a Wordle game for PAD.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]padle.", - "min_bot_version": "3.4.0", - "max_bot_version": "0.0.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "required_cogs": {}, - "requirements": ["tsutils>=4.0.0"], - "tags": [] - } + "author": [ + "fluff#2368" + ], + "description": "PADle, a Wordle game for PAD.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "Use [p]padle.", + "min_bot_version": "3.4.0", + "max_bot_version": "0.0.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": false, + "disabled": false, + "required_cogs": {}, + "requirements": [ + "tsutils>=4.0.0" + ], + "tags": [] +} \ No newline at end of file diff --git a/padle/menu/globalstats.py b/padle/menu/globalstats.py index d187b5ab..981adbfe 100644 --- a/padle/menu/globalstats.py +++ b/padle/menu/globalstats.py @@ -11,6 +11,7 @@ class ScrollEmojis: next_page = '\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}' home = '\N{HOUSE BUILDING}' + class GlobalStatsMenu: MENU_TYPE = 'PADleGlobalStatsMenu' @@ -62,13 +63,13 @@ def control(state: GlobalStatsViewState): class GlobalStatsMenuPanes(MenuPanes): INITIAL_EMOJI = ScrollEmojis.home - + DATA = { ScrollEmojis.next_page: (GlobalStatsMenu.respond_with_right, GlobalStatsView.VIEW_TYPE), ScrollEmojis.prev_page: (GlobalStatsMenu.respond_with_left, GlobalStatsView.VIEW_TYPE), ScrollEmojis.home: (GlobalStatsMenu.respond_with_pane, GlobalStatsView.VIEW_TYPE), } - + HIDDEN_EMOJIS = [ScrollEmojis.home] @classmethod diff --git a/padle/menu/menu_map.py b/padle/menu/menu_map.py index c2320f26..65f1e04b 100644 --- a/padle/menu/menu_map.py +++ b/padle/menu/menu_map.py @@ -1,11 +1,12 @@ -from tsutils.menu.closable_embed_base import ClosableEmbedMenuPanes - from padle.menu.closable_embed import ClosableEmbedMenu -from padle.menu.padle_scroll import PADleScrollMenu, PADleMenuPanes from padle.menu.globalstats import GlobalStatsMenu, GlobalStatsMenuPanes +from padle.menu.padle_scroll import PADleScrollMenu, PADleMenuPanes +from padle.menu.personal_stats import PersonalStatsMenu +from tsutils.menu.closable_embed_base import ClosableEmbedMenuPanes padle_menu_map = { ClosableEmbedMenu.MENU_TYPE: (ClosableEmbedMenu, ClosableEmbedMenuPanes), + PersonalStatsMenu.MENU_TYPE: (PersonalStatsMenu, ClosableEmbedMenuPanes), PADleScrollMenu.MENU_TYPE: (PADleScrollMenu, PADleMenuPanes), GlobalStatsMenu.MENU_TYPE: (GlobalStatsMenu, GlobalStatsMenuPanes), } diff --git a/padle/menu/personal_stats.py b/padle/menu/personal_stats.py new file mode 100644 index 00000000..3b29a7f1 --- /dev/null +++ b/padle/menu/personal_stats.py @@ -0,0 +1,9 @@ +from padle.view.personal_stats_view import PersonalStatsView +from tsutils.menu.closable_embed_base import ClosableEmbedMenuBase + + +class PersonalStatsMenu(ClosableEmbedMenuBase): + MENU_TYPE = PersonalStatsView.VIEW_TYPE + view_types = { + PersonalStatsView.VIEW_TYPE: PersonalStatsView + } diff --git a/padle/monsterdiff.py b/padle/monsterdiff.py index b22c3777..337c4345 100644 --- a/padle/monsterdiff.py +++ b/padle/monsterdiff.py @@ -1,9 +1,11 @@ from tsutils.tsubaki.custom_emoji import get_awakening_emoji, get_rarity_emoji, get_type_emoji, \ get_attribute_emoji_by_enum from typing import TYPE_CHECKING + if TYPE_CHECKING: from dbcog.dbcog import MonsterModel + class MonsterDiff: def __init__(self, monster: "MonsterModel", guess_monster: "MonsterModel"): diff --git a/padle/padle.py b/padle/padle.py index ab5d3c3d..cf67eec7 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -1,22 +1,26 @@ import asyncio -import re +import csv import discord +import itertools import json import logging import random -import csv +import re from contextlib import suppress -from datetime import datetime, timezone, timedelta +from datetime import datetime from discordmenu.embed.components import EmbedThumbnail, EmbedMain from discordmenu.embed.view import EmbedView from io import BytesIO, StringIO from math import ceil +from padle.help_texts import HELP_TEXT, RULES_TEXT from padle.menu.closable_embed import ClosableEmbedMenu +from padle.menu.globalstats import GlobalStatsMenu, GlobalStatsViewState from padle.menu.menu_map import padle_menu_map from padle.menu.padle_scroll import PADleScrollMenu, PADleScrollViewState -from padle.menu.globalstats import GlobalStatsMenu, GlobalStatsViewState +from padle.menu.personal_stats import PersonalStatsMenu from padle.monsterdiff import MonsterDiff from padle.view.confirmation import PADleMonsterConfirmationView, PADleMonsterConfirmationViewProps +from padle.view.personal_stats_view import PersonalStatsView, PersonalStatsViewProps from redbot.core import Config, commands from redbot.core.utils.chat_formatting import pagify from tsutils.cogs.globaladmin import auth_check @@ -25,6 +29,8 @@ from tsutils.menu.components.config import BotConfig from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.query_settings.query_settings import QuerySettings +from tsutils.time import NA_TIMEZONE +from tsutils.tsubaki.custom_emoji import get_emoji from tsutils.tsubaki.links import MonsterImage, MonsterLink from tsutils.tsubaki.monster_header import MonsterHeader from tsutils.user_interaction import get_user_confirmation, send_confirmation_message, send_cancellation_message, \ @@ -43,8 +49,6 @@ class PADle(commands.Cog): # Used if no monster list is set FALLBACK_PADLE_MONSTER = 3260 - # Set hours to preferred timezone offset from UTC for day change calculations - tzinfo = timezone(timedelta(hours = -8.0)) menu_map = padle_menu_map def __init__(self, bot, *args, **kwargs): @@ -53,8 +57,8 @@ def __init__(self, bot, *args, **kwargs): self.config = Config.get_conf(self, identifier=94073) self.config.register_user(todays_guesses=[], start=False, done=False, score=[], edit_id=0, channel_id=0, all_guesses={}) - self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, - stored_day=datetime.now(self.tzinfo).day, num_days=1, subs=[], + self.config.register_global(padle_today=self.FALLBACK_PADLE_MONSTER, + stored_day=self._day_today(), num_days=1, subs=[], all_scores=[], save_daily_scores=[], monsters_list=[], tmrw_padle=0) self.config.register_guild(allow=False) self._daily_padle_loop = bot.loop.create_task(self.generate_padle()) @@ -66,7 +70,7 @@ async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" all_guesses = await self.config.user_from_id(user_id).all_guesses() if all_guesses: - data = (f"You have {len(all_guesses)} days of guess data stored." + data = (f"You have {len(all_guesses)} days of guess data stored. " f"Here are your guesses:\n{json.dumps(all_guesses)}") else: data = f"No data is stored for user with ID {user_id}." @@ -109,6 +113,9 @@ async def get_today_guesses(self, user: discord.User, current_day: Union[str, in return {} return (await self.config.user(user).all_guesses()).get(str(current_day)) + def _day_today(self): + return datetime.now(NA_TIMEZONE).day + @commands.group() async def padle(self, ctx): """Commands pertaining to PADle""" @@ -121,14 +128,16 @@ async def padleadmin(self, ctx): @padle.command() async def help(self, ctx): """Instructions for PADle""" - prefix = ctx.prefix - await ctx.send("- PADle is similar to Wordle, except for PAD cards.\n" - "- You have infinite tries to guess the hidden PAD card (chosen from a list " - "of more well-known monsters). Everyone is trying to guess the same PAD card. " - "With each guess, you are given feedback as to how similar the two cards are, " - "including comparing the awakenings, rarity, typings, attributes, and monster " - "point sell value.\n- A new PADle is available every day. " - "Use `{}padle start` to begin!".format(prefix)) + await ctx.send(self._get_help_text(ctx)) + + def _get_help_text(self, ctx): + args = {"db": get_emoji("db"), "p": ctx.prefix, "tsubaki": get_emoji("tsubaki")} + return HELP_TEXT.format(**args) + + @padle.command() + async def validrules(self, ctx): + args = {"db": get_emoji("db"), "rd": get_emoji("rd")} + await ctx.send(RULES_TEXT.format(**args)) @padle.command(aliases=["sub"]) async def subscribe(self, ctx, sub_arg: bool = True): @@ -201,11 +210,17 @@ async def start(self, ctx): f" use `{prefix}padle resend`.", color=discord.Color.teal()) if await self.can_play_in_guild(ctx): + if len(await self.config.user(ctx.author).all_guesses()) == 0: + await ctx.send("Hello! It looks like this is your first PADle game.") + await ctx.send(self._get_help_text(ctx)) await ctx.send(embed=em) await self.config.user(ctx.author).start.set(True) return # else try: + if len(await self.config.user(ctx.author).all_guesses()) == 0: + await ctx.author.send("Hello! It looks like this is your first PADle game.") + await ctx.author.send(self._get_help_text(ctx)) await ctx.author.send(embed=em) except discord.HTTPException: return await send_cancellation_message(ctx, @@ -231,7 +246,7 @@ async def globalstats(self, ctx, day: int = 0): monster = dbcog.get_monster(all[day - 1][0]) query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, "") global_stats_menu = GlobalStatsMenu.menu() - state = GlobalStatsViewState(ctx.author.id, GlobalStatsMenu.MENU_TYPE, query_settings, "", + state = GlobalStatsViewState(ctx.author.id, GlobalStatsMenu.MENU_TYPE, query_settings, "", current_day=day, num_days=num_days, monster=monster, stats=stats) await global_stats_menu.create(ctx, state) @@ -258,27 +273,21 @@ async def stats(self, ctx): wins += 1 elif day != await self.config.num_days() or await self.config.user(ctx.author).done(): cur_streak = 0 - if cur_streak > max_streak: - max_streak = cur_streak - all_monsters_guessed = [] - for key, value in all_guesses.items(): - all_monsters_guessed.extend(value) + max_streak = max(max_streak, cur_streak) + flatten = itertools.chain.from_iterable + all_monsters_guessed = list(flatten(all_guesses.values())) if not all_monsters_guessed: all_monsters_guessed.append(self.FALLBACK_PADLE_MONSTER) mode = max(set(all_monsters_guessed), key=all_monsters_guessed.count) dbcog = await self.get_dbcog() m = dbcog.get_monster(mode) - m_embed = EmbedView( - EmbedMain( - title=f"{ctx.author.name}'s PADle Stats", - description=(f"**Games Played**: {played}\n" - f"**Win Rate**: {wins / played:.2%}\n" - f"**Current Streak**: {cur_streak}\n" - f"**Max Streak**: {max_streak}\n" - f"**Favorite Guessed Monster**: {MonsterHeader.menu_title(m).to_markdown()}")), - embed_thumbnail=EmbedThumbnail( - MonsterImage.icon(m.monster_id))).to_embed() - await ctx.send(embed=m_embed) + menu = PersonalStatsMenu.menu() + query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, "") + props = PersonalStatsViewProps(query_settings, ctx.author.name, played, wins / played, + cur_streak, max_streak, m) + state = ClosableEmbedViewState(ctx.author.id, PersonalStatsMenu.MENU_TYPE, "", query_settings, + PersonalStatsView.VIEW_TYPE, props) + await menu.create(ctx, state) async def do_quit_early(self, ctx): prefix = ctx.prefix @@ -443,7 +452,7 @@ async def score(self, ctx): async def generate_padle(self): async def is_day_change(): - cur_day = datetime.now(self.tzinfo).day + cur_day = self._day_today() old_day = await self.config.stored_day() if cur_day != old_day: await self.config.stored_day.set(cur_day) @@ -510,7 +519,7 @@ async def fullreset(self, ctx): await self.config.padle_today.set(self.FALLBACK_PADLE_MONSTER) else: await self.config.padle_today.set(int(random.choice(MONSTERS_LIST))) - await self.config.stored_day.set(datetime.now(self.tzinfo).day) + await self.config.stored_day.set(self._day_today()) await self.config.tmrw_padle.set(0) await self.config.num_days.set(1) await self.config.subs.set([]) @@ -688,7 +697,7 @@ async def advance(self, ctx): if not confirmation: return await send_cancellation_message(ctx, "The PADle was unchanged.") try: - await self.config.stored_day.set(datetime.now(self.tzinfo).day) + await self.config.stored_day.set(self._day_today()) async with self.config.save_daily_scores() as save_daily: save_daily.append([await self.config.padle_today(), await self.config.all_scores()]) tmrw_padle = await self.config.tmrw_padle() @@ -737,25 +746,37 @@ async def advance(self, ctx): async def filter(self, ctx): """Re-creates the list of monsters""" dbcog = await self.get_dbcog() - mgraph = dbcog.database.graph + graph = dbcog.database.graph final = [] - for i in range(1, mgraph.max_monster_id): + for i in range(1, graph.max_monster_id + 1): monster = dbcog.get_monster(i) if monster is None: continue - if(self.is_valid_padle(monster, mgraph)): + if self.is_valid_padle(monster, graph): final.append(str(i)) with open("result.txt", "w") as file: file.write(",".join(final)) with open("result.txt", "rb") as file: await ctx.send("List of monsters:", file=discord.File(file, "result.txt")) await ctx.tick() - - def is_valid_padle(monster, mgraph): - nextTrans = mgraph.get_next_transform(monster) - prevTrans = mgraph.get_prev_transform(monster) - return (monster.name_en is not None and monster.on_na and + + def is_valid_padle(self, monster, graph): + next_trans = graph.get_next_transform(monster) + prev_trans = graph.get_prev_transform(monster) + return ( # Monster is in NA + monster.name_en is not None and monster.on_na and + # No collab monsters monster.series.series_type != 'collab' and - ((monster.sell_mp >= 50000 and (monster.superawakening_count > 1 or prevTrans is not None)) or - "Super Reincarnated" in monster.name_en) and not monster.is_equip and - nextTrans is None and monster.level >= 99) + # No equips + not monster.is_equip and + # No monsters with next transforms + next_trans is None and + monster.level >= 99 and + # A GFE (max transformed or has SA) + ((monster.sell_mp >= 50000 and (monster.superawakening_count > 1 or prev_trans is not None)) or + # OR a Super Reincarnated evo of a pantheon + ("Super Reincarnated" in monster.name_en and monster.sell_mp == 5000) or + # OR 15k non-collab, non-event monsters with SA / max transformed + (monster.sell_mp == 15000 and (monster.superawakening_count > 1 or prev_trans is not None) and + monster.series.series_type == 'regular')) + ) diff --git a/padle/view/globalstats_view.py b/padle/view/globalstats_view.py index 1463a3b9..04612b15 100644 --- a/padle/view/globalstats_view.py +++ b/padle/view/globalstats_view.py @@ -10,11 +10,10 @@ from tsutils.query_settings.query_settings import QuerySettings - class GlobalStatsViewState(ViewStateBase): VIEW_STATE_TYPE: str = "PADleGlobalStatsView" - def __init__(self, original_author_id, menu_type, query_settings: QuerySettings, raw_query="", + def __init__(self, original_author_id, menu_type, query_settings: QuerySettings, raw_query="", current_day=0, num_days=0, extra_state=None, reaction_list=None, monster=None, stats=[]): super().__init__(original_author_id, menu_type, raw_query, extra_state=extra_state) @@ -77,7 +76,7 @@ class GlobalStatsView: @classmethod def get_pages_footer(cls, state): return "Day " + str(state.current_day) + "/" + str(state.num_days) - + @staticmethod def embed(state: GlobalStatsViewState): description = GlobalStatsView.get_description(state) @@ -99,7 +98,7 @@ def embed(state: GlobalStatsViewState): embed_fields=[EmbedField("Stats", Box(description))], embed_footer=embed_footer_with_state(state, text=GlobalStatsView.get_pages_footer(state)), embed_thumbnail=EmbedThumbnail(MonsterImage.icon(state.monster.monster_id))) - + @classmethod def get_description(cls, state): giveups = 0 diff --git a/padle/view/personal_stats_view.py b/padle/view/personal_stats_view.py new file mode 100644 index 00000000..282961a9 --- /dev/null +++ b/padle/view/personal_stats_view.py @@ -0,0 +1,42 @@ +from discordmenu.embed.components import EmbedMain, EmbedThumbnail +from discordmenu.embed.view import EmbedView +from tsutils.menu.components.footers import embed_footer_with_state +from tsutils.menu.view.closable_embed import ClosableEmbedViewState +from tsutils.query_settings.query_settings import QuerySettings +from tsutils.tsubaki.links import MonsterImage +from tsutils.tsubaki.monster_header import MonsterHeader +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dbcog.models.monster_model import MonsterModel + + +class PersonalStatsViewProps: + def __init__(self, query_settings: QuerySettings, username: str, played: int, win_rate: float, + cur_streak: int, max_streak: int, favorite_monster: "MonsterModel"): + self.query_settings = query_settings + self.username = username + self.played = played + self.win_rate = win_rate + self.cur_streak = cur_streak + self.max_streak = max_streak + self.monster = favorite_monster + + +class PersonalStatsView: + VIEW_TYPE = 'PADlePersonalStats' + + @classmethod + def embed(cls, state: ClosableEmbedViewState, props: PersonalStatsViewProps): + return EmbedView( + EmbedMain( + title=f"{props.username}'s PADle Stats", + description=(f"**Games Played**: {props.played}\n" + f"**Win Rate**: {props.win_rate:.2%}\n" + f"**Current Streak**: {props.cur_streak}\n" + f"**Max Streak**: {props.max_streak}\n" + f"**Favorite Guessed Monster**: {MonsterHeader.menu_title(props.monster).to_markdown()}"), + color=props.query_settings.embedcolor), + embed_thumbnail=EmbedThumbnail( + MonsterImage.icon(props.monster.monster_id)), + embed_footer=embed_footer_with_state(state, text="Click the X to close.")) From 5428621bfa1d51f6cd3a03197c36bc9fa20290ea Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Fri, 17 Jun 2022 17:15:56 -0400 Subject: [PATCH 1151/1220] Validrules docstring and username to start message (#1748) --- padle/help_texts.py | 6 ++++-- padle/padle.py | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/padle/help_texts.py b/padle/help_texts.py index 86b585b1..e253499a 100644 --- a/padle/help_texts.py +++ b/padle/help_texts.py @@ -11,11 +11,13 @@ Happy puzzling! {tsubaki}""" -RULES_TEXT = """Valid monsters include the following: +RULES_TEXT = """You are able to guess any monster, but the final PADle answer is chosen from a limited list of monsters. + +Valid monsters include the following: {db} Super Reincarnated evolutions of pantheons {db} 50k+ MP AND [have a Super Awakening OR are max transformed] {db} 15k MP AND are non-event AND [have a Super Awakening OR are max transformed] - + The following monsters are exceptions to the above and will NOT be included: {rd} Collab monsters (note this does not exclude Event monsters such as from Heroine or Relic Saga) {rd} An equip evolution diff --git a/padle/padle.py b/padle/padle.py index cf67eec7..ece1c8e0 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -136,6 +136,7 @@ def _get_help_text(self, ctx): @padle.command() async def validrules(self, ctx): + """Show rules for the valid-answer monster list""" args = {"db": get_emoji("db"), "rd": get_emoji("rd")} await ctx.send(RULES_TEXT.format(**args)) @@ -193,8 +194,11 @@ async def start(self, ctx): else: await send_confirmation_message(ctx, "PADles can now be played in this server.") await self.config.guild(ctx.guild).allow.set(True) - confirmation = await get_user_confirmation(ctx, "Start today's (#{}) PADle game?".format( - await self.config.num_days()), timeout=30, force_delete=False, show_feedback=True) + if ctx.guild is None: + message = "Start today's (#{}) PADle game?".format(await self.config.num_days()) + else: + message = "{}, start today's (#{}) PADle game?".format(ctx.author.name, await self.config.num_days()) + confirmation = await get_user_confirmation(ctx, message, timeout=30, force_delete=False, show_feedback=True) if confirmation is None: return await send_cancellation_message(ctx, "Confirmation timeout.") if await self.config.user(ctx.author).start(): From 23b055bb61d427377d31bdc22e030aa086fe8b18 Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Fri, 17 Jun 2022 17:38:42 -0400 Subject: [PATCH 1152/1220] [PADle] Fix filter command (#1749) --- padle/padle.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/padle/padle.py b/padle/padle.py index ece1c8e0..a46f9cd1 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -758,10 +758,7 @@ async def filter(self, ctx): continue if self.is_valid_padle(monster, graph): final.append(str(i)) - with open("result.txt", "w") as file: - file.write(",".join(final)) - with open("result.txt", "rb") as file: - await ctx.send("List of monsters:", file=discord.File(file, "result.txt")) + await ctx.send("List of monsters:", file=discord.File(BytesIO(",".join(final).encode('utf-8')), "result.txt")) await ctx.tick() def is_valid_padle(self, monster, graph): From 42e6716d0baf02201bbe94eb65bf3b887bbe7a6d Mon Sep 17 00:00:00 2001 From: Ethan <68720253+ethannp@users.noreply.github.com> Date: Fri, 17 Jun 2022 18:32:36 -0400 Subject: [PATCH 1153/1220] [PADle] Fix unsubscribe (#1750) --- padle/padle.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/padle/padle.py b/padle/padle.py index a46f9cd1..b287d7ec 100644 --- a/padle/padle.py +++ b/padle/padle.py @@ -144,29 +144,29 @@ async def validrules(self, ctx): async def subscribe(self, ctx, sub_arg: bool = True): """Subscribe to daily notifications of new PADles""" prefix = ctx.prefix - async with self.config.subs() as subbed_users: - if not sub_arg: - if ctx.author.id in subbed_users: - return await self.unsubscribe(ctx) - return await send_cancellation_message(ctx, "You are not subscribed.") - if ctx.author.id not in subbed_users: - subbed_users.append(ctx.author.id) - return await send_confirmation_message(ctx, "You will now receive notifications of new PADles. " - "You can unsubscribe with `{}padle subscribe 0`.".format( - prefix)) - - confirmation = await get_user_confirmation(ctx, "You are already subscribed. Did you mean to unsubscribe?") - if confirmation: - await self.unsubscribe(ctx) - elif confirmation is None: - await send_cancellation_message(ctx, "Confirmation timeout") - else: - await ctx.send("No changes were made, you will still receive notifications of new PADles.") - - async def unsubscribe(self, ctx): - async with self.config.subs() as subs: - subs.remove(ctx.author.id) - return await send_confirmation_message(ctx, "You will no longer receive notifications of new PADles.") + subbed_users = await self.config.subs() + if not sub_arg: + if ctx.author.id in subbed_users: + async with self.config.subs() as subs: + subs.remove(ctx.author.id) + return await send_confirmation_message(ctx, "You will no longer receive notifications of new PADles.") + return await send_cancellation_message(ctx, "You are not subscribed.") + if ctx.author.id not in subbed_users: + async with self.config.subs() as subs: + subs.append(ctx.author.id) + return await send_confirmation_message(ctx, "You will now receive notifications of new PADles. " + "You can unsubscribe with `{}padle subscribe 0`.".format( + prefix)) + + confirmation = await get_user_confirmation(ctx, "You are already subscribed. Did you mean to unsubscribe?") + if confirmation: + async with self.config.subs() as subs: + subs.remove(ctx.author.id) + return await send_confirmation_message(ctx, "You will no longer receive notifications of new PADles.") + elif confirmation is None: + await send_cancellation_message(ctx, "Confirmation timeout") + else: + await ctx.send("No changes were made, you will still receive notifications of new PADles.") async def can_play_in_guild(self, ctx): return (ctx.guild is not None and ctx.author.guild_permissions.administrator and ctx.guild.member_count < 10 and From d195e70bf2aee5c4ee584d927057e6f5dd389885 Mon Sep 17 00:00:00 2001 From: kary5678 <60301055+kary5678@users.noreply.github.com> Date: Tue, 21 Jun 2022 13:10:43 -0500 Subject: [PATCH 1154/1220] Fix PADle awakenings count (#1751) --- padle/monsterdiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padle/monsterdiff.py b/padle/monsterdiff.py index 337c4345..b6d56ecb 100644 --- a/padle/monsterdiff.py +++ b/padle/monsterdiff.py @@ -44,7 +44,7 @@ def get_attr_diff(self, monster, guess_monster): def get_awakenings_diff(self, monster, guess_monster): guess_awo_count = len(guess_monster.awakenings) - guess_monster.superawakening_count - monster_awo_count = len(monster.awakenings) - guess_monster.superawakening_count + monster_awo_count = len(monster.awakenings) - monster.superawakening_count unused = monster.awakenings[:monster_awo_count] feedback = [] for index, guess_awakening in enumerate(guess_monster.awakenings[:guess_awo_count]): From c654d21dd40a5feb55c664c9b69644836fbd2759 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 22 Jun 2022 18:57:11 -0700 Subject: [PATCH 1155/1220] Fix CRUD repo commit (#1752) --- crud/crud.py | 30 ++++++++++++++++++++++++++---- crud/editseries.py | 21 ++++++++++++++------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/crud/crud.py b/crud/crud.py index 796587ad..3798fa2b 100644 --- a/crud/crud.py +++ b/crud/crud.py @@ -123,6 +123,8 @@ async def execute_read(self, ctx, sql, replacements): await ctx.send(box(page)) async def git_verify(self, ctx, folder, filename): + filepath = folder + filename + try: repo = pygit2.Repository(await self.config.pipeline_base()) except pygit2.GitError: @@ -145,7 +147,6 @@ async def git_verify(self, ctx, folder, filename): f" github username token `") try: - filepath = folder + filename index = repo.index index.add(filepath) index.write() @@ -236,6 +237,8 @@ async def series_add(self, ctx, *elements): Example Usage: [p]crud series add key1 "Value1" key2 "Value2" """ + if not elements: + return await ctx.send_help() if len(elements) % 2 != 0: return await send_cancellation_message(ctx, "Imbalanced key-value pairs. Make sure" " multi-word values are in quotes") @@ -285,8 +288,11 @@ async def series_edit(self, ctx, series_id: int, *elements): Example Usage: [p]crud series edit 100 key1 "Value1" key2 "Value2" """ - if len(elements) % 2 != 0: + if not elements: return await ctx.send_help() + if len(elements) % 2 != 0: + return await send_cancellation_message(ctx, "Imbalanced key-value pairs. Make sure" + " multi-word values are in quotes") elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in SERIES_KEYS for x in elements)): @@ -361,8 +367,11 @@ async def awokenskill_add(self, ctx, *elements): Example Usage: [p]crud awokenskill add key1 "Value1" key2 "Value2" """ - if len(elements) % 2 != 0: + if not elements: return await ctx.send_help() + if len(elements) % 2 != 0: + return await send_cancellation_message(ctx, "Imbalanced key-value pairs. Make sure" + " multi-word values are in quotes") elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in AWOKEN_SKILL_KEYS for x in elements)): @@ -432,8 +441,11 @@ async def awokenskill_edit(self, ctx, awoken_skill, *elements): awoken_skill_id = int(awoken_skill_id) - if len(elements) % 2 != 0: + if not elements: return await ctx.send_help() + if len(elements) % 2 != 0: + return await send_cancellation_message(ctx, "Imbalanced key-value pairs. Make sure" + " multi-word values are in quotes") elements = {elements[i]: elements[i + 1] for i in range(0, len(elements), 2)} if not (elements and all(x in AWOKEN_SKILL_KEYS for x in elements)): @@ -513,6 +525,16 @@ async def ghgroupn(self, ctx, *, query): return await ctx.send("No matching monster found.") await ctx.send(f"{MonsterHeader.text_with_emoji(monster)} is in GungHo Group #{monster.group_id}") + @commands.command(aliases=['collab#', 'ghcollab#']) + async def ghcollabn(self, ctx, *, query): + dbcog: Any = self.bot.get_cog("DBCog") + if dbcog is None: + return await ctx.send("DBCog not loaded.") + monster = await dbcog.find_monster(query, ctx.author.id) + if monster is None: + return await ctx.send("No matching monster found.") + await ctx.send(f"{MonsterHeader.text_with_emoji(monster)} is in GungHo Collab #{monster.collab_id}") + @crud.command() async def setmyemail(self, ctx, email=None): """Sets your email so GitHub commits can be properly attributed""" diff --git a/crud/editseries.py b/crud/editseries.py index 19269419..0062c47b 100644 --- a/crud/editseries.py +++ b/crud/editseries.py @@ -43,10 +43,11 @@ async def convert(self, ctx, argument) -> str: 'monster': ('monsters'), 'tree': ('trees'), 'group': ('groups'), + 'collab': ('collabs'), }) else: EditSeriesCommand = Literal['add', 'remove', 'promote'] - EditSeriesTarget = Literal['monster', 'tree', 'group'] + EditSeriesTarget = Literal['monster', 'tree', 'group', 'collab'] def disjoint_sets(superset: Iterable["MonsterModel"], f: Callable[["MonsterModel"], bool]) \ @@ -86,14 +87,15 @@ async def editseries(self, ctx, command: EditSeriesCommand, series_id: int, """Affect the series of a target. Commands: - add - Add a series. If this is the only series, add as a primary series - remove - Remove a series. If this is the primary series, randomly choose another to take its place - promote - Add a series as primary, possibly demoting an existing primary in the process + `add` - Add a series. If this is the only series, add as a primary series + `remove` - Remove a series. If this is the primary series, randomly choose another to take its place + `promote` - Add a series as primary, possibly demoting an existing primary in the process Targets: - monster - Monsters specified by id - tree - Monster trees specified by monster ids of any monster in the tree - group - GH groups specified by a group ids + `monster` - Monsters specified by id + `tree` - Monster trees specified by monster ids of any monster in the tree + `group` - GH groups specified by group id + `collab` - GH collabs specified by collab id Examples: [p]editseries add 91 monster 1319, 1825 @@ -121,6 +123,11 @@ async def tryint(arg): for gid in re.split(r'\D+', target_str) for monster in dbcog.database.get_monsters_where(lambda m: m.group_id == int(gid), server=Server.COMBINED)] + elif target == 'collab': + monsters = [monster + for gid in re.split(r'\D+', target_str) + for monster in dbcog.database.get_monsters_where(lambda m: m.collab_id == int(gid), + server=Server.COMBINED)] if not monsters: return await ctx.send("No matching monsters found.") From f0bba136facfd6ee29801d702fe23a967419f32f Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 24 Jun 2022 01:27:51 -0700 Subject: [PATCH 1156/1220] [PADle] replace built-in emojis with custom (#1753) Using built-in emoji causes problems on iOS because the built-in emojis render at different widths from the custom emojis. So, we uploaded custom emojis to replace the built-in ones as needed. --- padle/monsterdiff.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/padle/monsterdiff.py b/padle/monsterdiff.py index b6d56ecb..b98a00a8 100644 --- a/padle/monsterdiff.py +++ b/padle/monsterdiff.py @@ -1,7 +1,8 @@ -from tsutils.tsubaki.custom_emoji import get_awakening_emoji, get_rarity_emoji, get_type_emoji, \ - get_attribute_emoji_by_enum from typing import TYPE_CHECKING +from tsutils.tsubaki.custom_emoji import get_attribute_emoji_by_enum, get_awakening_emoji, get_emoji, get_rarity_emoji, \ + get_type_emoji + if TYPE_CHECKING: from dbcog.dbcog import MonsterModel @@ -92,11 +93,11 @@ def get_name_line_feedback_text(self) -> str: attr_feedback = [] for num in self.attr_diff: if num == 0: - attr_feedback.append("\N{CROSS MARK}") + attr_feedback.append(get_emoji('red_cross_custom')) elif num == 0.5: - attr_feedback.append("\N{LARGE YELLOW SQUARE}") + attr_feedback.append(get_emoji('yellow_square_custom')) else: - attr_feedback.append("\N{WHITE HEAVY CHECK MARK}") + attr_feedback.append(get_emoji('green_check_custom')) line.append(get_attribute_emoji_by_enum(self.guess_monster.attr1)) line.append(attr_feedback[0] + " / ") line.append(get_attribute_emoji_by_enum(self.guess_monster.attr2)) @@ -108,7 +109,7 @@ def get_name_line_feedback_text(self) -> str: def get_other_info_feedback_text(self) -> str: line = [get_rarity_emoji(self.guess_monster.rarity)] if self.rarity_diff == 0: - line.append("\N{WHITE HEAVY CHECK MARK} | ") + line.append(get_emoji('green_check_custom') + " | ") elif self.rarity_diff == -1: line.append("\N{DOWNWARDS BLACK ARROW}\N{VARIATION SELECTOR-16} | ") else: @@ -116,13 +117,13 @@ def get_other_info_feedback_text(self) -> str: for type in self.guess_monster.types: line.append(get_type_emoji(type)) if type in self.monster.types: - line.append("\N{WHITE HEAVY CHECK MARK} ") + line.append(get_emoji('green_check_custom') + " ") else: - line.append("\N{CROSS MARK} ") + line.append(get_emoji('red_cross_custom') + " ") line.append(" | Sell MP: ") line.append('{:,}'.format(self.guess_monster.sell_mp)) if self.mp_diff == 0: - line.append("\N{WHITE HEAVY CHECK MARK}") + line.append(get_emoji('green_check_custom')) elif self.mp_diff == -1: line.append("\N{DOWNWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}") else: @@ -138,9 +139,9 @@ def get_awakenings_feedback_text(self) -> str: awakes.append(get_awakening_emoji(guess_awake.awoken_skill_id, guess_awake.name)) val = self.awakenings_diff[index] if val == 0: - feedback.append("\N{CROSS MARK}") + feedback.append(get_emoji('red_cross_custom')) elif val == 0.5: - feedback.append("\N{LARGE YELLOW SQUARE}") + feedback.append(get_emoji('yellow_square_custom')) else: - feedback.append("\N{WHITE HEAVY CHECK MARK}") + feedback.append(get_emoji('green_check_custom')) return "\n".join(["".join(awakes), "".join(feedback)]) From b6609e8caaf23a92b39b1a6b1d82fac51a90564c Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 24 Jun 2022 09:53:51 -0700 Subject: [PATCH 1157/1220] Add dungeons to PadMonitor (#1754) --- dbcog/database_context.py | 35 +++-- dbcog/dungeon_context.py | 262 +++++++++++++++++++++++++++---------- dungeoncog/dungeoncog.py | 11 +- dungeoncog/processors.py | 169 ++++++++++++------------ dungeoncog/view/dungeon.py | 3 +- padmonitor/padmonitor.py | 205 +++++++++++++++++------------ 6 files changed, 424 insertions(+), 261 deletions(-) diff --git a/dbcog/database_context.py b/dbcog/database_context.py index 0f0c9677..6d71859a 100644 --- a/dbcog/database_context.py +++ b/dbcog/database_context.py @@ -1,4 +1,5 @@ -from typing import Any, Callable, Hashable, Iterable, List, Optional, Union +from functools import lru_cache +from typing import Callable, Hashable, Iterable, List, Optional from tsutils.enums import Server @@ -19,22 +20,23 @@ dungeons.name_ko AS d_name_ko, dungeons.dungeon_type AS dungeon_type FROM - schedule LEFT OUTER JOIN dungeons ON schedule.dungeon_id = dungeons.dungeon_id""" + schedule + LEFT JOIN dungeons ON schedule.dungeon_id = dungeons.dungeon_id +""" -class DbContext(object): +class DbContext: def __init__(self, database: DBCogDatabase, graph: MonsterGraph, dungeon: DungeonContext, debug_monster_ids: Optional[List[int]] = None): self.database = database self.graph = graph self.dungeon = dungeon - self.cached_monster_ids = None - self.cached_monsters = {} self.cached_filters = {} self.debug_monster_ids = debug_monster_ids self.awoken_skill_map = {awsk.awoken_skill_id: awsk for awsk in self.get_all_awoken_skills()} + self.series_map = {series.series_id: series for series in self.get_all_series()} def get_monsters_where(self, f: Callable[[MonsterModel], bool], *, server: Server, cache_key: Hashable = None) \ -> List[MonsterModel]: @@ -47,15 +49,19 @@ def get_monsters_where(self, f: Callable[[MonsterModel], bool], *, server: Serve self.cached_filters[(cache_key, server)] = self.get_monsters_where(f, server=server) return self.cached_filters[(cache_key, server)] + @lru_cache(maxsize=None) def get_monsters_by_series(self, series_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.series_id == series_id, server=server) + @lru_cache(maxsize=None) def get_monsters_by_active(self, active_skill_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id, server=server) + @lru_cache(maxsize=None) def get_monsters_by_leader(self, leader_skill_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.leader_skill_id == leader_skill_id, server=server) + @lru_cache(maxsize=None) def get_all_monster_ids(self, server: Server) -> Iterable[int]: # We don't need to query if we're in debug mode. We already know exactly which monsters we're working with if self.debug_monster_ids is not None: @@ -65,27 +71,16 @@ def get_all_monster_ids(self, server: Server) -> Iterable[int]: query = self.database.query_many(f"SELECT monster_id FROM monsters{suffix}", as_generator=True) return (m.monster_id for m in query) + @lru_cache(maxsize=None) def get_all_monsters(self, server: Server = DEFAULT_SERVER) -> List[MonsterModel]: - if server in self.cached_monsters: - return self.cached_monsters[server] - - self.cached_monsters[server] = [self.graph.get_monster(mid, server=server) - for mid in self.get_all_monster_ids(server)] - return self.cached_monsters[server] - - def get_all_events(self) -> Iterable[ScheduledEventModel]: - result = self.database.query_many(SCHEDULED_EVENT_QUERY) - for se in result: - se['dungeon_model'] = DungeonModel(name_ja=se['d_name_ja'], - name_en=se['d_name_en'], - name_ko=se['d_name_ko'], - **se) - yield ScheduledEventModel(**se) + return [self.graph.get_monster(mid, server=server) for mid in self.get_all_monster_ids(server)] + @lru_cache(maxsize=None) def get_all_awoken_skills(self) -> List[AwokenSkillModel]: result = self.database.query_many("SELECT * FROM awoken_skills") return [AwokenSkillModel(**r) for r in result] + @lru_cache(maxsize=None) def get_all_series(self) -> List[SeriesModel]: result = self.database.query_many("SELECT * FROM series") return [SeriesModel(**r) for r in result] diff --git a/dbcog/dungeon_context.py b/dbcog/dungeon_context.py index e23d1847..00a96fff 100644 --- a/dbcog/dungeon_context.py +++ b/dbcog/dungeon_context.py @@ -1,15 +1,29 @@ -from typing import Optional, List +from collections import defaultdict +from functools import lru_cache +from typing import Dict, List, Mapping, Optional, Set + +from tsutils.enums import Server from dbcog.database_manager import DBCogDatabase from dbcog.models.dungeon_model import DungeonModel from dbcog.models.encounter_model import EncounterModel from dbcog.models.enemy_data_model import EnemyDataModel from dbcog.models.enemy_skill_model import EnemySkillModel +from dbcog.models.enum_types import DEFAULT_SERVER from dbcog.models.sub_dungeon_model import SubDungeonModel + +def format_with_suffix(text: str, server: Server) -> str: + suffix_map = { + Server.COMBINED: '', + Server.NA: '_na' + } + return text.format(suffix_map[server]) + + NICKNAME_QUERY = ''' SELECT - enemy_data.behavior, + enemy_data{0}.behavior, encounters.encounter_id, encounters.dungeon_id, encounters.sub_dungeon_id, @@ -22,20 +36,20 @@ encounters.hp, encounters.atk, encounters.defense, - sub_dungeons.name_ja AS sub_name_ja, - sub_dungeons.name_en AS sub_name_en, - sub_dungeons.name_ko AS sub_name_ko, - sub_dungeons.technical, - dungeons.name_ja, - dungeons.name_en, - dungeons.name_ko, - dungeons.dungeon_type + sub_dungeons{0}.name_ja AS sub_name_ja, + sub_dungeons{0}.name_en AS sub_name_en, + sub_dungeons{0}.name_ko AS sub_name_ko, + sub_dungeons{0}.technical, + dungeons{0}.name_ja, + dungeons{0}.name_en, + dungeons{0}.name_ko, + dungeons{0}.dungeon_type FROM encounters - LEFT OUTER JOIN dungeons on encounters.dungeon_id = dungeons.dungeon_id - LEFT OUTER JOIN enemy_data on encounters.enemy_id = enemy_data.enemy_id - LEFT OUTER JOIN monsters on encounters.monster_id = monsters.monster_id - LEFT OUTER JOIN sub_dungeons on sub_dungeons.sub_dungeon_id = encounters.sub_dungeon_id + LEFT OUTER JOIN dungeons on encounters.dungeon_id = dungeons{0}.dungeon_id + LEFT OUTER JOIN enemy_data{0} on encounters.enemy_id = enemy_data{0}.enemy_id + LEFT OUTER JOIN monsters{0} on encounters.monster_id = monsters{0}.monster_id + LEFT OUTER JOIN sub_dungeons on sub_dungeons{0}.sub_dungeon_id = encounters.sub_dungeon_id WHERE encounters.sub_dungeon_id = ? ORDER BY @@ -45,67 +59,67 @@ SUB_DUNGEON_QUERY = ''' SELECT - sub_dungeons.sub_dungeon_id, - sub_dungeons.dungeon_id, - sub_dungeons.name_ja, - sub_dungeons.name_en, - sub_dungeons.name_ko, - sub_dungeons.technical + sub_dungeons{0}.sub_dungeon_id, + sub_dungeons{0}.dungeon_id, + sub_dungeons{0}.name_ja, + sub_dungeons{0}.name_en, + sub_dungeons{0}.name_ko, + sub_dungeons{0}.technical FROM - sub_dungeons + sub_dungeons{0} WHERE - sub_dungeons.dungeon_id = ? + sub_dungeons{0}.dungeon_id = ? ''' DUNGEON_QUERY = ''' SELECT - dungeons.dungeon_id, - dungeons.name_ja, - dungeons.name_en, - dungeons.name_ko, - dungeons.dungeon_type + dungeons{0}.dungeon_id, + dungeons{0}.name_ja, + dungeons{0}.name_en, + dungeons{0}.name_ko, + dungeons{0}.dungeon_type FROM - dungeons + dungeons{0} WHERE - dungeons.name_en LIKE ? + dungeons{0}.name_en LIKE ? ''' ES_QUERY = ''' SELECT - enemy_skills.enemy_skill_id, - enemy_skills.name_en, - enemy_skills.desc_en, - enemy_skills.desc_en_emoji, - enemy_skills.min_hits, - enemy_skills.max_hits, - enemy_skills.atk_mult + enemy_skills{0}.enemy_skill_id, + enemy_skills{0}.name_en, + enemy_skills{0}.desc_en, + enemy_skills{0}.desc_en_emoji, + enemy_skills{0}.min_hits, + enemy_skills{0}.max_hits, + enemy_skills{0}.atk_mult FROM - enemy_skills + enemy_skills{0} WHERE - enemy_skill_id = ? + enemy_skills{0}.enemy_skill_id = ? ''' SUB_DUNGEONS_QUERY_BY_NAME = ''' SELECT * FROM - sub_dungeons + sub_dungeons{0} WHERE - sub_dungeons.dungeon_id = ? AND - sub_dungeons.name_en LIKE ? + sub_dungeons{0}.dungeon_id = ? AND + sub_dungeons{0}.name_en LIKE ? ORDER BY - sub_dungeons.sub_dungeon_id + sub_dungeons{0}.sub_dungeon_id ''' SUB_DUNGEON_QUERY_BY_INDEX = ''' SELECT * FROM - sub_dungeons + sub_dungeons{0} WHERE - sub_dungeons.sub_dungeon_id = ? + sub_dungeons{0}.sub_dungeon_id = ? ORDER BY - sub_dungeons.sub_dungeon_id + sub_dungeons{0}.sub_dungeon_id ''' ENCOUNTER_QUERY = ''' @@ -152,14 +166,39 @@ ENEMY_DATA_QUERY = ''' SELECT - enemy_data.enemy_id, - enemy_data.behavior + enemy_data{0}.enemy_id, + enemy_data{0}.behavior FROM - enemy_data + enemy_data{0} +WHERE + enemy_data{0}.enemy_id = ? +''' + +MONSTER_DROP_QUERY = ''' +SELECT + dungeons{0}.dungeon_id, + sub_dungeons{0}.sub_dungeon_id +FROM + drops + JOIN monsters{0} ON drops.monster_id = monsters{0}.monster_id + JOIN encounters ON drops.encounter_id = encounters.encounter_id + JOIN sub_dungeons{0} ON encounters.sub_dungeon_id = sub_dungeons{0}.sub_dungeon_id + JOIN dungeons{0} ON sub_dungeons{0}.dungeon_id = dungeons{0}.dungeon_id WHERE - enemy_data.enemy_id = ? + monsters{0}.monster_id = ? ''' +SCHEDULED_EVENT_QUERY = """SELECT + schedule.*, + dungeons.name_ja AS d_name_ja, + dungeons.name_en AS d_name_en, + dungeons.name_ko AS d_name_ko, + dungeons.dungeon_type AS dungeon_type +FROM + schedule + LEFT JOIN dungeons ON schedule.dungeon_id = dungeons.dungeon_id +""" + # TODO: Move to gdoc DUNGEON_NICKNAMES = { 'a1': 1022001, @@ -196,18 +235,26 @@ class DungeonContext(object): def __init__(self, database: DBCogDatabase): + self._enemy_data_map: Dict[Server, Dict[int, EnemyDataModel]] = {} + self._encounter_map: Dict[Server, Dict[int, EncounterModel]] = {} + self._encounters_by_sub_dungeon_id: Dict[Server, Mapping[int, Set[EncounterModel]]] = {} + self._sub_dungeon_map: Dict[Server, Dict[int, SubDungeonModel]] = {} + self._sub_dungeons_by_dungeon_id: Dict[Server, Mapping[int, Set[SubDungeonModel]]] = {} + self._dungeon_map: Dict[Server, Dict[int, DungeonModel]] = {} + self.database = database - def get_dungeons_from_name(self, name: str) -> List[DungeonModel]: - dungeons_result = self.database.query_many(DUNGEON_QUERY, (name+"%",)) + def get_dungeons_from_name(self, name: str, *, server: Server) -> List[DungeonModel]: + dungeons_result = self.database.query_many(format_with_suffix(DUNGEON_QUERY, server), (name + "%",)) dungeons = [] for d in dungeons_result: dungeons.append(DungeonModel([], **d)) for dm in dungeons: - subs = self.database.query_many(SUB_DUNGEON_QUERY, (dm.dungeon_id,)) + subs = self.database.query_many(format_with_suffix(SUB_DUNGEON_QUERY, server), (dm.dungeon_id,)) for s in subs: - encounters = self.database.query_many(ENCOUNTER_QUERY, (s['sub_dungeon_id'],)) + encounters = self.database.query_many(format_with_suffix(ENCOUNTER_QUERY, server), + (s['sub_dungeon_id'],)) ems = [] for e in encounters: data = self.database.query_one(ENEMY_DATA_QUERY, (e["enemy_id"],)) @@ -219,14 +266,14 @@ def get_dungeons_from_name(self, name: str) -> List[DungeonModel]: dm.sub_dungeons.append(SubDungeonModel(ems, **s)) return dungeons - def get_dungeons_from_nickname(self, name: str) -> List[DungeonModel]: + def get_dungeons_from_nickname(self, name: str, *, server: Server) -> List[DungeonModel]: if name not in DUNGEON_NICKNAMES: return [] sub_id = DUNGEON_NICKNAMES.get(name) - mega = self.database.query_many(NICKNAME_QUERY, (sub_id,)) + mega = self.database.query_many(format_with_suffix(NICKNAME_QUERY, server), (sub_id,)) ems = [] for enc in mega: - data = self.database.query_one(ENEMY_DATA_QUERY, (enc["enemy_id"],)) + data = self.database.query_one(format_with_suffix(ENEMY_DATA_QUERY, server), (enc["enemy_id"],)) if data is not None: edm = EnemyDataModel(**data) else: @@ -241,13 +288,13 @@ def get_dungeons_from_nickname(self, name: str) -> List[DungeonModel]: technical=mega[0]['technical']) return [DungeonModel([sm], **mega[0])] - def get_floor_from_sub_dungeon(self, sub_id: int, floor: int) -> List[EncounterModel]: - floor_query = self.database.query_many(SPECIFIC_FLOOR_QUERY, (sub_id, floor)) - invade_query = self.database.query_many(SPECIFIC_FLOOR_QUERY, (sub_id, -1)) + def get_floor_from_sub_dungeon(self, sub_id: int, floor: int, *, server: Server) -> List[EncounterModel]: + floor_query = self.database.query_many(format_with_suffix(SPECIFIC_FLOOR_QUERY, server), (sub_id, floor)) + invade_query = self.database.query_many(format_with_suffix(SPECIFIC_FLOOR_QUERY, server), (sub_id, -1)) encounter_models = [] floor_query.extend(invade_query) for f in floor_query: - data = self.database.query_one(ENEMY_DATA_QUERY, (f['enemy_id'],)) + data = self.database.query_one(format_with_suffix(ENEMY_DATA_QUERY, server), (f['enemy_id'],)) if data is not None: edm = EnemyDataModel(**data) else: @@ -255,21 +302,24 @@ def get_floor_from_sub_dungeon(self, sub_id: int, floor: int) -> List[EncounterM encounter_models.append(EncounterModel(edm, **f)) return encounter_models - def get_enemy_skill(self, enemy_skill_id: int) -> EnemySkillModel: - enemy_skill_query = self.database.query_one(ES_QUERY, (enemy_skill_id,)) + def get_enemy_skill(self, enemy_skill_id: int, *, server: Server) -> EnemySkillModel: + enemy_skill_query = self.database.query_one(format_with_suffix(ES_QUERY, server), (enemy_skill_id,)) return EnemySkillModel(**enemy_skill_query) - def get_sub_dungeon_id_from_name(self, dungeon_id: int, sub_name: str) -> Optional[int]: - if sub_name is None: - sub_name = "" - if sub_name.isdigit(): - sub_dungeons = self.database.query_many(SUB_DUNGEON_QUERY_BY_INDEX, (dungeon_id * 1000 + int(sub_name),)) + def get_sub_dungeon_id_from_name(self, dungeon_id: int, sub_dungeon_name: Optional[str], *, server: Server) \ + -> Optional[int]: + if sub_dungeon_name is None: + sub_dungeon_name = "" + if sub_dungeon_name.isdigit(): + sub_dungeons = self.database.query_many(format_with_suffix(SUB_DUNGEON_QUERY_BY_INDEX, server), + (dungeon_id * 1000 + int(sub_dungeon_name),)) else: - sub_dungeons = self.database.query_many(SUB_DUNGEONS_QUERY_BY_NAME, (dungeon_id, f"%{sub_name}%")) + sub_dungeons = self.database.query_many(format_with_suffix(SUB_DUNGEONS_QUERY_BY_NAME, server), + (dungeon_id, f"%{sub_dungeon_name}%")) if len(sub_dungeons) == 0: return None elif len(sub_dungeons) > 1: - if 'plus' in sub_name.lower(): + if 'plus' in sub_dungeon_name.lower(): for sd in sub_dungeons: if 'plus' in sd['name_en'].lower(): return sd['sub_dungeon_id'] @@ -280,3 +330,77 @@ def get_sub_dungeon_id_from_name(self, dungeon_id: int, sub_name: str) -> Option elif len(sub_dungeons) > 2: return 1 return sub_dungeons[0]['sub_dungeon_id'] + + @lru_cache(maxsize=None) + def get_all_enemy_data(self, *, server: Server = DEFAULT_SERVER) -> List[EnemyDataModel]: + suffix = "" + if server == Server.NA: + pass + # TODO: There's currently no servers for enemy data. + # suffix = '_na' + + result = self.database.query_many(f"SELECT * FROM enemy_data{suffix}") + return [EnemyDataModel(**r) for r in result] + + def get_enemy_data(self, enemy_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[EnemyDataModel]: + if server not in self._enemy_data_map: + self._enemy_data_map[server] = {ed.enemy_id: ed for ed in self.get_all_enemy_data(server=server)} + return self._enemy_data_map[server].get(enemy_id) + + @lru_cache(maxsize=None) + def get_all_encounters(self, server: Server = DEFAULT_SERVER) -> List[EncounterModel]: + suffix = "" + if server == Server.NA: + pass + # TODO: There's currently no servers for encounters. + # suffix = '_na' + + result = self.database.query_many(f"SELECT * FROM encounters{suffix}") + return [EncounterModel(self.get_enemy_data(r.enemy_id), **r) for r in result] + + def get_encounter(self, encounter_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[EncounterModel]: + if server not in self._encounter_map: + self._encounter_map[server] = {e.encounter_id: e for e in self.get_all_encounters(server=server)} + return self._encounter_map[server].get(encounter_id) + + @lru_cache(maxsize=None) + def get_all_sub_dungeons(self, server: Server = DEFAULT_SERVER) -> List[SubDungeonModel]: + if server not in self._encounters_by_sub_dungeon_id: + self._encounters_by_sub_dungeon_id[server] = defaultdict(set) + for encounter in self.get_all_encounters(server=server): + self._encounters_by_sub_dungeon_id[server][encounter.sub_dungeon_id].add(encounter) + + suffix = "" + if server == Server.NA: + suffix = '_na' + + result = self.database.query_many(f"SELECT * FROM sub_dungeons{suffix}") + return [SubDungeonModel(sorted(self._encounters_by_sub_dungeon_id[server][r.sub_dungeon_id], + key=lambda e: e.encounter_id), + **r) for r in result] + + def get_sub_dungeon(self, sub_dungeon_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[SubDungeonModel]: + if server not in self._sub_dungeon_map: + self._sub_dungeon_map[server] = {e.sub_dungeon_id: e for e in self.get_all_sub_dungeons(server=server)} + return self._sub_dungeon_map[server].get(sub_dungeon_id) + + @lru_cache(maxsize=None) + def get_all_dungeons(self, server: Server = DEFAULT_SERVER) -> List[DungeonModel]: + if server not in self._sub_dungeons_by_dungeon_id: + self._sub_dungeons_by_dungeon_id[server] = defaultdict(set) + for sub_dungeon in self.get_all_sub_dungeons(server=server): + self._sub_dungeons_by_dungeon_id[server][sub_dungeon.dungeon_id].add(sub_dungeon) + + suffix = "" + if server == Server.NA: + suffix = '_na' + + result = self.database.query_many(f"SELECT * FROM dungeons{suffix}") + return [DungeonModel(sorted(self._sub_dungeons_by_dungeon_id[server][r.dungeon_id], + key=lambda sd: sd.sub_dungeon_id), + **r) for r in result] + + def get_dungeon(self, dungeon_id: int, *, server: Server = DEFAULT_SERVER) -> Optional[DungeonModel]: + if server not in self._dungeon_map: + self._dungeon_map[server] = {e.dungeon_id: e for e in self.get_all_dungeons(server=server)} + return self._dungeon_map[server].get(dungeon_id) diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 1f8d065d..00971647 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -10,10 +10,12 @@ from redbot.core import commands from redbot.core.utils.chat_formatting import pagify from tsutils.cogs.globaladmin import auth_check +from tsutils.enums import Server from tsutils.menu.view.closable_embed import ClosableEmbedViewState from tsutils.query_settings.query_settings import QuerySettings from tsutils.user_interaction import send_cancellation_message +from dbcog.models.enum_types import DEFAULT_SERVER from dungeoncog.enemy_skills_pb2 import MonsterBehavior from dungeoncog.menu.closable_embed import ClosableEmbedMenu from dungeoncog.menu.dungeon import DungeonMenu @@ -89,19 +91,20 @@ async def get_dbcog(self): await dbcog.wait_until_ready() return dbcog - async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", difficulty: str = None): + async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", difficulty: str = None, + server: Server = DEFAULT_SERVER): """ Gets the sub_dungeon model given the name of a dungeon and its difficulty. """ - dungeons = database.get_dungeons_from_nickname(name.lower()) + dungeons = database.get_dungeons_from_nickname(name.lower(), server=server) if not dungeons: - dungeons = database.get_dungeons_from_name(name) + dungeons = database.get_dungeons_from_name(name, server=server) if len(dungeons) == 0: return None if len(dungeons) > 1: return dungeons dungeon = dungeons.pop() - sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty) + sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty, server=server) sub_dungeon_model = None if sub_id is None: sub_id = 0 diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index ad6f27ca..4260a4e5 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -2,10 +2,12 @@ from typing import Optional, TYPE_CHECKING from discordmenu.emoji.emoji_cache import emoji_cache +from tsutils.enums import Server +from dbcog.models.enum_types import DEFAULT_SERVER from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skill import ProcessedSkill -from dungeoncog.enemy_skills_pb2 import Condition, MonsterBehavior, Behavior, BehaviorGroup +from dungeoncog.enemy_skills_pb2 import Behavior, BehaviorGroup, Condition, MonsterBehavior from dungeoncog.grouped_skillls import GroupedSkills if TYPE_CHECKING: @@ -123,8 +125,9 @@ def process_enemy_skill(encounter: "EncounterModel", skill: "EnemySkillModel", e return non_attack_effects -def process_behavior(behavior: Behavior, database, encounter: "EncounterModel", emoji_map, parent: GroupedSkills = None): - skill = database.dungeon.get_enemy_skill(behavior.enemy_skill_id) +def process_behavior(behavior: Behavior, database, encounter: "EncounterModel", emoji_map, + parent: GroupedSkills = None, server: Server = DEFAULT_SERVER): + skill = database.dungeon.get_enemy_skill(behavior.enemy_skill_id, server=server) if skill is None: return "Unknown" skill_name = skill.name_en @@ -143,7 +146,8 @@ def process_behavior_group(group: BehaviorGroup, database, encounter: "Encounter processed_group: GroupedSkills = GroupedSkills(condition, GROUP_TYPES[group.group_type], parent) for child in group.children: if child.HasField('group'): - processed_group.add_group(process_behavior_group(child.group, database, encounter, emoji_map, processed_group)) + processed_group.add_group( + process_behavior_group(child.group, database, encounter, emoji_map, processed_group)) elif child.HasField('behavior'): processed_group.add_skill(process_behavior(child.behavior, database, encounter, emoji_map, processed_group)) return processed_group @@ -155,84 +159,85 @@ def __missing__(self, key): def process_monster(behavior: MonsterBehavior, encounter: "EncounterModel", database: "DbContext") -> DungeonMonster: - emoji_map = SafeDgEmojiDict(resolve_status=emoji_cache.get_by_name('resolve'), - fire_orb=emoji_cache.get_by_name('orb_fire'), - recover_health=emoji_cache.get_by_name('misc_autoheal'), - roulette=emoji_cache.get_by_name('spinner'), - unknown_type='Unknown', - defense_status=emoji_cache.get_by_name('defense'), - combo_absorb_status=emoji_cache.get_by_name('combo'), - absorb_status=emoji_cache.get_by_name('attribute_absorb'), - damage_absorb_status=emoji_cache.get_by_name('damage_absorb'), - damage_void_status=emoji_cache.get_by_name('vdp'), - status_shield_status=emoji_cache.get_by_name('status'), - movetime_buff_status=emoji_cache.get_by_name('time_buff'), - movetime_debuff_status=emoji_cache.get_by_name('time_debuff'), - dispel_status='Dispel', - leader_swap_status=emoji_cache.get_by_name('swap'), - skill_delay=emoji_cache.get_by_name('misc_sb'), - lock_orbs=emoji_cache.get_by_name('lock'), - tape_status=emoji_cache.get_by_name('res_seal'), - fixed_start=emoji_cache.get_by_name('orb_start'), - cloud_status=emoji_cache.get_by_name('res_cloud'), - gravity=emoji_cache.get_by_name('gravity'), - invincible_status=emoji_cache.get_by_name('invincible'), - invincible_off_status=emoji_cache.get_by_name('invincible'), # TODO - force_target_status=emoji_cache.get_by_name('force_target'), - leader_alter_status=emoji_cache.get_by_name('transform'), - board_size_status=emoji_cache.get_by_name('board_size'), - super_resolve_status=emoji_cache.get_by_name('super_resolve'), - turn_change=emoji_cache.get_by_name('turn_change'), - enrage_status=emoji_cache.get_by_name('enrage'), - active_skill_bind=emoji_cache.get_by_name('skill_bound'), - do_nothing='\N{SLEEPING SYMBOL}', - awoken_skill_bind=emoji_cache.get_by_name('awoken_skill_bind'), - no_skyfall_status=emoji_cache.get_by_name('no_skyfall'), - bind=emoji_cache.get_by_name('res_bind'), - skyfall_status='\N{CLOUD WITH RAIN}', - blind=emoji_cache.get_by_name('black_circle_real'), - super_blind=emoji_cache.get_by_name('blind_orb'), - to='\N{BLACK RIGHTWARDS ARROW}️', - attack=emoji_cache.get_by_name('single_hit'), - multi_attack=emoji_cache.get_by_name('multi_hit'), - target_self='Self', - health=emoji_cache.get_by_name('health'), - combo_orb=emoji_cache.get_by_name('orb_combo'), - locked_bomb_orb=emoji_cache.get_by_name('locked_bomb_orb'), - random_attribute='[Random Att]', - water_orb=emoji_cache.get_by_name('orb_water'), - wood_orb=emoji_cache.get_by_name('orb_wood'), - light_orb=emoji_cache.get_by_name('orb_light'), - dark_orb=emoji_cache.get_by_name('orb_dark'), - healer_orb=emoji_cache.get_by_name('heal_orb'), - jammer_orb=emoji_cache.get_by_name('jammer_orb'), - poison_orb=emoji_cache.get_by_name('poison_orb'), - bomb_orb=emoji_cache.get_by_name('bomb_orb'), - mortal_poison_orb=emoji_cache.get_by_name('mortal_poison_orb'), - evo_material_type=emoji_cache.get_by_name('killer_evomat'), - balanced_type=emoji_cache.get_by_name('killer_balance'), - physical_type=emoji_cache.get_by_name('killer_physical'), - healer_type=emoji_cache.get_by_name('killer_healer'), - dragon_type=emoji_cache.get_by_name('killer_dragon'), - god_type=emoji_cache.get_by_name('killer_god'), - attacker_type=emoji_cache.get_by_name('killer_attacker'), - devil_type=emoji_cache.get_by_name('killer_devil'), - machine_type=emoji_cache.get_by_name('killer_machine'), - awakening_material_type=emoji_cache.get_by_name('killer_awoken'), - enhance_material_type=emoji_cache.get_by_name('killer_enhancemat'), - redeemable_material_type='\U0001fa99', # Coin Emoji - no_match_fire=emoji_cache.get_by_name('no_match_fire'), - no_match_wood=emoji_cache.get_by_name('no_match_wood'), - no_match_water=emoji_cache.get_by_name('no_match_water'), - no_match_dark=emoji_cache.get_by_name('no_match_dark'), - no_match_light=emoji_cache.get_by_name('no_match_light'), - no_match_heart=emoji_cache.get_by_name('no_match_heart'), - no_match_jammer=emoji_cache.get_by_name('no_match_jammer'), - no_match_poison=emoji_cache.get_by_name('no_match_poison'), - no_match_heal=emoji_cache.get_by_name('no_match_heal'), - recover_debuff_status=emoji_cache.get_by_name('recover_debuff_status'), - attack_debuff_status='\N{CROSSED SWORDS}\N{DOWNWARDS BLACK ARROW}️' - ) + emoji_map = SafeDgEmojiDict( + resolve_status=emoji_cache.get_by_name('resolve'), + fire_orb=emoji_cache.get_by_name('orb_fire'), + recover_health=emoji_cache.get_by_name('misc_autoheal'), + roulette=emoji_cache.get_by_name('spinner'), + unknown_type='Unknown', + defense_status=emoji_cache.get_by_name('defense'), + combo_absorb_status=emoji_cache.get_by_name('combo'), + absorb_status=emoji_cache.get_by_name('attribute_absorb'), + damage_absorb_status=emoji_cache.get_by_name('damage_absorb'), + damage_void_status=emoji_cache.get_by_name('vdp'), + status_shield_status=emoji_cache.get_by_name('status'), + movetime_buff_status=emoji_cache.get_by_name('time_buff'), + movetime_debuff_status=emoji_cache.get_by_name('time_debuff'), + dispel_status='Dispel', + leader_swap_status=emoji_cache.get_by_name('swap'), + skill_delay=emoji_cache.get_by_name('misc_sb'), + lock_orbs=emoji_cache.get_by_name('lock'), + tape_status=emoji_cache.get_by_name('res_seal'), + fixed_start=emoji_cache.get_by_name('orb_start'), + cloud_status=emoji_cache.get_by_name('res_cloud'), + gravity=emoji_cache.get_by_name('gravity'), + invincible_status=emoji_cache.get_by_name('invincible'), + invincible_off_status=emoji_cache.get_by_name('invincible'), # TODO + force_target_status=emoji_cache.get_by_name('force_target'), + leader_alter_status=emoji_cache.get_by_name('transform'), + board_size_status=emoji_cache.get_by_name('board_size'), + super_resolve_status=emoji_cache.get_by_name('super_resolve'), + turn_change=emoji_cache.get_by_name('turn_change'), + enrage_status=emoji_cache.get_by_name('enrage'), + active_skill_bind=emoji_cache.get_by_name('skill_bound'), + do_nothing='\N{SLEEPING SYMBOL}', + awoken_skill_bind=emoji_cache.get_by_name('awoken_skill_bind'), + no_skyfall_status=emoji_cache.get_by_name('no_skyfall'), + bind=emoji_cache.get_by_name('res_bind'), + skyfall_status='\N{CLOUD WITH RAIN}', + blind=emoji_cache.get_by_name('black_circle_real'), + super_blind=emoji_cache.get_by_name('blind_orb'), + to='\N{BLACK RIGHTWARDS ARROW}️', + attack=emoji_cache.get_by_name('single_hit'), + multi_attack=emoji_cache.get_by_name('multi_hit'), + target_self='Self', + health=emoji_cache.get_by_name('health'), + combo_orb=emoji_cache.get_by_name('orb_combo'), + locked_bomb_orb=emoji_cache.get_by_name('locked_bomb_orb'), + random_attribute='[Random Att]', + water_orb=emoji_cache.get_by_name('orb_water'), + wood_orb=emoji_cache.get_by_name('orb_wood'), + light_orb=emoji_cache.get_by_name('orb_light'), + dark_orb=emoji_cache.get_by_name('orb_dark'), + healer_orb=emoji_cache.get_by_name('heal_orb'), + jammer_orb=emoji_cache.get_by_name('jammer_orb'), + poison_orb=emoji_cache.get_by_name('poison_orb'), + bomb_orb=emoji_cache.get_by_name('bomb_orb'), + mortal_poison_orb=emoji_cache.get_by_name('mortal_poison_orb'), + evo_material_type=emoji_cache.get_by_name('killer_evomat'), + balanced_type=emoji_cache.get_by_name('killer_balance'), + physical_type=emoji_cache.get_by_name('killer_physical'), + healer_type=emoji_cache.get_by_name('killer_healer'), + dragon_type=emoji_cache.get_by_name('killer_dragon'), + god_type=emoji_cache.get_by_name('killer_god'), + attacker_type=emoji_cache.get_by_name('killer_attacker'), + devil_type=emoji_cache.get_by_name('killer_devil'), + machine_type=emoji_cache.get_by_name('killer_machine'), + awakening_material_type=emoji_cache.get_by_name('killer_awoken'), + enhance_material_type=emoji_cache.get_by_name('killer_enhancemat'), + redeemable_material_type='\U0001fa99', # Coin Emoji + no_match_fire=emoji_cache.get_by_name('no_match_fire'), + no_match_wood=emoji_cache.get_by_name('no_match_wood'), + no_match_water=emoji_cache.get_by_name('no_match_water'), + no_match_dark=emoji_cache.get_by_name('no_match_dark'), + no_match_light=emoji_cache.get_by_name('no_match_light'), + no_match_heart=emoji_cache.get_by_name('no_match_heart'), + no_match_jammer=emoji_cache.get_by_name('no_match_jammer'), + no_match_poison=emoji_cache.get_by_name('no_match_poison'), + no_match_heal=emoji_cache.get_by_name('no_match_heal'), + recover_debuff_status=emoji_cache.get_by_name('recover_debuff_status'), + attack_debuff_status='\N{CROSSED SWORDS}\N{DOWNWARDS BLACK ARROW}️' + ) monster_model = database.graph.get_monster(encounter.monster_id) monster = DungeonMonster( hp=encounter.hp, diff --git a/dungeoncog/view/dungeon.py b/dungeoncog/view/dungeon.py index 39835947..cfdb10e1 100644 --- a/dungeoncog/view/dungeon.py +++ b/dungeoncog/view/dungeon.py @@ -7,6 +7,7 @@ from discordmenu.embed.components import EmbedMain, EmbedField from discordmenu.embed.view import EmbedView from discordmenu.embed.view_state import ViewState +from tsutils.enums import Server from tsutils.menu.components.footers import embed_footer_with_state from tsutils.tsubaki.monster_header import MonsterHeader @@ -73,7 +74,7 @@ async def deserialize(cls, dbcog, ims: dict, inc_floor: int = 0, inc_index: int verbose = not verbose # get encounter models for the floor - floor_models = dbcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor) + floor_models = dbcog.database.dungeon.get_floor_from_sub_dungeon(sub_dungeon_id, floor, server=Server.COMBINED) # check if we are on final monster of the floor if floor_index >= len(floor_models): diff --git a/padmonitor/padmonitor.py b/padmonitor/padmonitor.py index 69c0961b..d4b21e80 100644 --- a/padmonitor/padmonitor.py +++ b/padmonitor/padmonitor.py @@ -1,30 +1,32 @@ import asyncio import logging - -import discord from io import BytesIO - -from redbot.core import checks, commands -from redbot.core.utils.chat_formatting import box, pagify - from typing import Dict, List, Optional, TYPE_CHECKING -from tsutils.cog_settings import CogSettings +import discord +from redbot.core import Config, checks, commands +from redbot.core.utils.chat_formatting import box, pagify from tsutils.formatting import contains_ja if TYPE_CHECKING: - from dbcog.database_context import DbContext from dbcog.models.monster_model import MonsterModel + from dbcog.models.dungeon_model import DungeonModel logger = logging.getLogger('red.padbot-cogs.padmonitor') class PadMonitor(commands.Cog): """Monitor PAD and announce new cards""" + def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot - self.settings = PadMonitorSettings("padmonitor") + + self.config = Config.get_conf(self, identifier=9407071702) + self.config.register_global(seen_jp_monsters=[], seen_na_monsters=[], + monster_announce_channels=[], + seen_jp_dungeons=[], seen_na_dungeons=[], + dungeon_announce_channels=[], ) async def red_get_data_for_user(self, *, user_id): """Get a user's personal data.""" @@ -42,27 +44,23 @@ async def check_seen_loop(self): await self.bot.wait_until_ready() while self == self.bot.get_cog('PadMonitor'): try: - await self.check_seen() - except Exception as ex: - logger.exception("check seen loop caught exception " + str(ex)) + await self.check_seen_monsters() + await self.check_seen_dungeons() + except Exception: + logger.exception("Error in PadMonitor loop:") await asyncio.sleep(60 * 5) - async def check_seen(self): + async def check_seen_monsters(self): """Refresh the monster indexes.""" - dbcog = self.bot.get_cog('DBCog') - await dbcog.wait_until_ready() - all_monsters = [*dbcog.database.get_all_monsters()] - jp_monster_map = {m.monster_id: m for m in all_monsters if m.on_jp} - na_monster_map = {m.monster_id: m for m in all_monsters if m.on_na} - def process(existing: List[int], new_map: Dict[int, "MonsterModel"], name: str)\ - -> Optional[str]: + def process(existing: List[int], # Will be mutated + new_map: Dict[int, "MonsterModel"], + region: str) -> Optional[str]: if not existing: # If everything is new, assume nothing is and log all current monsters as seen - logger.info('preloading %i', len(new_map)) + logger.info('preloading %i monsters', len(new_map)) existing.extend(new_map.keys()) - self.settings.save_settings() return None delta_set = set(new_map.keys()).difference(existing) @@ -72,29 +70,70 @@ def process(existing: List[int], new_map: Dict[int, "MonsterModel"], name: str)\ return None existing.extend(delta_set) - self.settings.save_settings() - msg = 'New monsters added to {}:'.format(name) + msg = f'New monsters added to {region}:' for mid in sorted(delta_set): m = new_map[mid] msg += f'\n\t[{m.monster_id}] {m.unoverridden_name_en}' - if contains_ja(m.name_en) and m.name_en_override is not None: + if contains_ja(m.name_en) and m.name_en_override: msg += f' ({m.name_en_override})' return msg - if (jp_results := process(self.settings.jp_seen(), jp_monster_map, 'JP')): - for channel_id in self.settings.new_monster_channels(): - await self.announce(channel_id, jp_results) - if (na_results := process(self.settings.na_seen(), na_monster_map, 'NA')): - for channel_id in self.settings.new_monster_channels(): - await self.announce(channel_id, na_results) + dbcog = self.bot.get_cog('DBCog') + await dbcog.wait_until_ready() + all_monsters = dbcog.database.get_all_monsters() + + jp_monster_map = {m.monster_id: m for m in all_monsters if m.on_jp} + async with self.config.seen_jp_monsters() as jp_seen: + if (jp_results := process(jp_seen, jp_monster_map, 'JP')): + for channel_id in await self.config.monster_announce_channels(): + await self.announce(channel_id, jp_results) + na_monster_map = {m.monster_id: m for m in all_monsters if m.on_na} + async with self.config.seen_na_monsters() as na_seen: + if (na_results := process(na_seen, na_monster_map, 'NA')): + for channel_id in await self.config.monster_announce_channels(): + await self.announce(channel_id, na_results) + + async def check_seen_dungeons(self): + """Refresh the dungeons indexes.""" + + def process(existing: List[int], # Will be mutated + new_map: Dict[int, "DungeonModel"]) -> Optional[str]: + # TODO: Don't repeat so much code here + if not existing: + # If everything is new, assume nothing is and log all current dungeons as seen + logger.info('preloading %i dungeons', len(new_map)) + existing.extend(new_map.keys()) + return None + + delta_set = set(new_map.keys()).difference(existing) + + if not delta_set: + # There are no new dungeons + return None + + existing.extend(delta_set) + msg = f'New dungeons added:' + for mid in sorted(delta_set): + msg += f'\n\t{new_map[mid].name_en}' + return msg + + dbcog = self.bot.get_cog('DBCog') + await dbcog.wait_until_ready() + all_dungeons = dbcog.database.dungeon.get_all_dungeons() + + dungeon_map = {m.dungeon_id: m for m in all_dungeons} # TODO: Find a way to test server + async with self.config.seen_jp_dungeons() as seen: + if (results := process(seen, dungeon_map)): + for channel_id in await self.config.dungeon_announce_channels(): + await self.announce(channel_id, results) async def announce(self, channel_id: int, message: str): try: - channel = self.bot.get_channel(int(channel_id)) + channel = self.bot.get_channel(channel_id) for page in pagify(message): await channel.send(box(page)) except Exception as ex: - logger.exception('failed to send message to {}:'.format(channel_id)) + logger.exception(f'Failed to send message to {channel_id}:') @commands.group(aliases=['pdm']) async def padmonitor(self, ctx): @@ -106,68 +145,64 @@ async def padmonitor(self, ctx): async def reload(self, ctx): """Update PDM channels""" async with ctx.typing(): - await self.check_seen() + await self.check_seen_monsters() + await self.check_seen_dungeons() await ctx.tick() - @padmonitor.command() + @padmonitor.group(name='monsters', aliases=['monster']) + async def pdm_monsters(self, ctx): + """New monster announcement""" + pass + + @pdm_monsters.command(name='addchannel') @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def addchannel(self, ctx, channel: discord.TextChannel = None): + async def pdm_m_addchannel(self, ctx, channel: discord.TextChannel = None): """Sets announcements for the current channel.""" - if channel is None: - channel = ctx.channel - self.settings.add_new_monster_channel(channel.id) + channel = channel or ctx.channel + async with self.config.monster_announce_channels() as announce_channels: + if channel.id in announce_channels: + return await ctx.send(f"{channel} is already a monster announcement channel.") + announce_channels.append(channel.id) await ctx.tick() - @padmonitor.command() + @pdm_monsters.command(name='rmchannel', aliases=['removechannel']) @commands.guild_only() @checks.mod_or_permissions(manage_guild=True) - async def rmchannel(self, ctx, channel: discord.TextChannel = None): + async def pdm_m_rmchannel(self, ctx, channel: discord.TextChannel = None): """Removes announcements for the current channel.""" - if channel is None: - channel = ctx.channel - self.settings.rm_new_monster_channel(channel.id) + channel = channel or ctx.channel + async with self.config.monster_announce_channels() as announce_channels: + if channel.id not in announce_channels: + return await ctx.send(f"{channel} is not a monster announcement channel.") + announce_channels.remove(channel.id) await ctx.tick() + @padmonitor.group(name='dungeons', aliases=['dungeon']) + async def pdm_dungeons(self, ctx): + """New dungeon announcement""" + pass + + @pdm_dungeons.command(name='addchannel') + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def pdm_d_addchannel(self, ctx, channel: discord.TextChannel = None): + """Sets announcements for the current channel.""" + channel = channel or ctx.channel + async with self.config.dungeon_announce_channels() as announce_channels: + if channel.id in announce_channels: + return await ctx.send(f"{channel} is already a dungeon announcement channel.") + announce_channels.append(channel.id) + await ctx.tick() -class PadMonitorSettings(CogSettings): - def make_default_settings(self): - config = { - 'jp_seen_ids': [], - 'na_seen_ids': [], - 'new_monster_channels': [], - } - return config - - def jp_seen(self) -> List[int]: - return self.bot_settings['jp_seen_ids'] - - def na_seen(self) -> List[int]: - return self.bot_settings['na_seen_ids'] - - def add_jp_seen(self, monster_id: int) -> None: - ids = self.jp_seen() - if monster_id not in ids: - ids.append(monster_id) - self.save_settings() - - def add_na_seen(self, monster_id: int) -> None: - ids = self.na_seen() - if monster_id not in ids: - ids.append(monster_id) - self.save_settings() - - def new_monster_channels(self) -> List[int]: - return self.bot_settings['new_monster_channels'] - - def add_new_monster_channel(self, channel_id: int) -> None: - channels = self.new_monster_channels() - if channel_id not in channels: - channels.append(channel_id) - self.save_settings() - - def rm_new_monster_channel(self, channel_id: int) -> None: - channels = self.new_monster_channels() - if channel_id in channels: - channels.remove(channel_id) - self.save_settings() + @pdm_dungeons.command(name='rmchannel', aliases=['removechannel']) + @commands.guild_only() + @checks.mod_or_permissions(manage_guild=True) + async def pdm_d_rmchannel(self, ctx, channel: discord.TextChannel = None): + """Removes announcements for the current channel.""" + channel = channel or ctx.channel + async with self.config.dungeon_announce_channels() as announce_channels: + if channel.id not in announce_channels: + return await ctx.send(f"{channel} is not a dungeon announcement channel.") + announce_channels.remove(channel.id) + await ctx.tick() From 5a2e53670eadba2f2729681fa90a8ad369820765 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Fri, 24 Jun 2022 15:21:37 -0500 Subject: [PATCH 1158/1220] Remove crosscog dbcog import (#1755) --- dbcog/dungeon_context.py | 2 +- dungeoncog/dungeoncog.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dbcog/dungeon_context.py b/dbcog/dungeon_context.py index 00a96fff..dc66bc78 100644 --- a/dbcog/dungeon_context.py +++ b/dbcog/dungeon_context.py @@ -306,7 +306,7 @@ def get_enemy_skill(self, enemy_skill_id: int, *, server: Server) -> EnemySkillM enemy_skill_query = self.database.query_one(format_with_suffix(ES_QUERY, server), (enemy_skill_id,)) return EnemySkillModel(**enemy_skill_query) - def get_sub_dungeon_id_from_name(self, dungeon_id: int, sub_dungeon_name: Optional[str], *, server: Server) \ + def get_sub_dungeon_id_from_name(self, dungeon_id: int, sub_dungeon_name: Optional[str], *, server: Server = DEFAULT_SERVER) \ -> Optional[int]: if sub_dungeon_name is None: sub_dungeon_name = "" diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 00971647..7343c604 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -15,7 +15,6 @@ from tsutils.query_settings.query_settings import QuerySettings from tsutils.user_interaction import send_cancellation_message -from dbcog.models.enum_types import DEFAULT_SERVER from dungeoncog.enemy_skills_pb2 import MonsterBehavior from dungeoncog.menu.closable_embed import ClosableEmbedMenu from dungeoncog.menu.dungeon import DungeonMenu @@ -92,7 +91,7 @@ async def get_dbcog(self): return dbcog async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", difficulty: str = None, - server: Server = DEFAULT_SERVER): + server: Server = None): """ Gets the sub_dungeon model given the name of a dungeon and its difficulty. """ @@ -104,7 +103,14 @@ async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", di if len(dungeons) > 1: return dungeons dungeon = dungeons.pop() - sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty, server=server) + if server is not None: + # slightly bullshit handling here because we can't have DEFAULT_SERVER imported + # here due to cross-cog bullshit. + # please move DEFAULT_SERVER to tsutils or do a proper cross-cog import + # for a proper patch but this is a hotfix okay + sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty, server=server) + else: + sub_id = database.get_sub_dungeon_id_from_name(dungeon.dungeon_id, difficulty) sub_dungeon_model = None if sub_id is None: sub_id = 0 From 8967402038ad83e7aae78b409e01e21916b1ed40 Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 25 Jun 2022 10:00:37 -0700 Subject: [PATCH 1159/1220] Stop Caching Monster Database --- dbcog/database_context.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dbcog/database_context.py b/dbcog/database_context.py index 6d71859a..2fc4a158 100644 --- a/dbcog/database_context.py +++ b/dbcog/database_context.py @@ -49,19 +49,15 @@ def get_monsters_where(self, f: Callable[[MonsterModel], bool], *, server: Serve self.cached_filters[(cache_key, server)] = self.get_monsters_where(f, server=server) return self.cached_filters[(cache_key, server)] - @lru_cache(maxsize=None) def get_monsters_by_series(self, series_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.series_id == series_id, server=server) - @lru_cache(maxsize=None) def get_monsters_by_active(self, active_skill_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.active_skill_id == active_skill_id, server=server) - @lru_cache(maxsize=None) def get_monsters_by_leader(self, leader_skill_id: int, *, server: Server) -> List[MonsterModel]: return self.get_monsters_where(lambda m: m.leader_skill_id == leader_skill_id, server=server) - @lru_cache(maxsize=None) def get_all_monster_ids(self, server: Server) -> Iterable[int]: # We don't need to query if we're in debug mode. We already know exactly which monsters we're working with if self.debug_monster_ids is not None: @@ -71,16 +67,13 @@ def get_all_monster_ids(self, server: Server) -> Iterable[int]: query = self.database.query_many(f"SELECT monster_id FROM monsters{suffix}", as_generator=True) return (m.monster_id for m in query) - @lru_cache(maxsize=None) def get_all_monsters(self, server: Server = DEFAULT_SERVER) -> List[MonsterModel]: return [self.graph.get_monster(mid, server=server) for mid in self.get_all_monster_ids(server)] - @lru_cache(maxsize=None) def get_all_awoken_skills(self) -> List[AwokenSkillModel]: result = self.database.query_many("SELECT * FROM awoken_skills") return [AwokenSkillModel(**r) for r in result] - @lru_cache(maxsize=None) def get_all_series(self) -> List[SeriesModel]: result = self.database.query_many("SELECT * FROM series") return [SeriesModel(**r) for r in result] From 74198b0f658aabf000b0a64a76dcfbb1aa546072 Mon Sep 17 00:00:00 2001 From: AA Date: Sun, 26 Jun 2022 01:39:59 -0700 Subject: [PATCH 1160/1220] Add get_all_events to DatabaseContext --- dbcog/database_context.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dbcog/database_context.py b/dbcog/database_context.py index 2fc4a158..529a7309 100644 --- a/dbcog/database_context.py +++ b/dbcog/database_context.py @@ -78,6 +78,15 @@ def get_all_series(self) -> List[SeriesModel]: result = self.database.query_many("SELECT * FROM series") return [SeriesModel(**r) for r in result] + def get_all_events(self) -> Iterable[ScheduledEventModel]: + result = self.database.query_many(SCHEDULED_EVENT_QUERY) + for se in result: + se['dungeon_model'] = DungeonModel(name_ja=se['d_name_ja'], + name_en=se['d_name_en'], + name_ko=se['d_name_ko'], + **se) + yield ScheduledEventModel(**se) + def has_database(self) -> bool: return self.database.has_database() From b76b4574398fc7d7029f13727b8532aca4e1ad88 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 29 Jun 2022 01:25:18 -0700 Subject: [PATCH 1161/1220] Remove Cross-Cog import --- dungeoncog/processors.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dungeoncog/processors.py b/dungeoncog/processors.py index 4260a4e5..c471731a 100644 --- a/dungeoncog/processors.py +++ b/dungeoncog/processors.py @@ -4,7 +4,6 @@ from discordmenu.emoji.emoji_cache import emoji_cache from tsutils.enums import Server -from dbcog.models.enum_types import DEFAULT_SERVER from dungeoncog.dungeon_monster import DungeonMonster from dungeoncog.enemy_skill import ProcessedSkill from dungeoncog.enemy_skills_pb2 import Behavior, BehaviorGroup, Condition, MonsterBehavior @@ -15,6 +14,9 @@ from dbcog.database_context import DbContext from dbcog.models.encounter_model import EncounterModel + +DEFAULT_SERVER = Server.COMBINED + GROUP_TYPES = { 0: "Unspecified", 1: "Passive", From 9e0e4daac93a7c8ac7a36a1cbe48f24aebb762b6 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 29 Jun 2022 19:52:10 -0700 Subject: [PATCH 1162/1220] Update to Dev (#1709) --- azurlane/__init__.py | 4 ++-- crowddata/__init__.py | 4 ++-- crud/__init__.py | 4 ++-- damagecalc/__init__.py | 4 ++-- dbcog/__init__.py | 4 ++-- dbcog/dbcog.py | 7 ++++++- dungeoncog/__init__.py | 4 ++-- feedback/__init__.py | 4 ++-- monidlistener/__init__.py | 4 ++-- padboard/__init__.py | 4 ++-- padbuildimg/__init__.py | 4 ++-- padbuilds/__init__.py | 4 ++-- padevents/__init__.py | 4 ++-- padglobal/__init__.py | 4 ++-- padinfo/__init__.py | 4 ++-- padmonitor/__init__.py | 4 ++-- padsearch/__init__.py | 4 ++-- pipelineui/__init__.py | 4 ++-- pricecheck/__init__.py | 4 ++-- profile/__init__.py | 4 ++-- 20 files changed, 44 insertions(+), 39 deletions(-) diff --git a/azurlane/__init__.py b/azurlane/__init__.py index 55de70c1..a71dfdd9 100644 --- a/azurlane/__init__.py +++ b/azurlane/__init__.py @@ -3,8 +3,8 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): +async def setup(bot): n = AzurLane(bot) - bot.add_cog(n) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) bot.loop.create_task(n.register_menu()) bot.loop.create_task(n.reload_al()) diff --git a/crowddata/__init__.py b/crowddata/__init__.py index 693d1bc2..71856a25 100644 --- a/crowddata/__init__.py +++ b/crowddata/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "All explicitly stored user preferences are kept persistantly." -def setup(bot): - bot.add_cog(CrowdData(bot)) +async def setup(bot): + bot.add_cog(CrowdData(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(CrowdData(bot)) diff --git a/crud/__init__.py b/crud/__init__.py index 6dcb4639..267055e3 100644 --- a/crud/__init__.py +++ b/crud/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "User email addresses are stored persistently." -def setup(bot): - bot.add_cog(Crud(bot)) +async def setup(bot): + bot.add_cog(Crud(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(Crud(bot)) diff --git a/damagecalc/__init__.py b/damagecalc/__init__.py index 58a095fc..c0864e7b 100644 --- a/damagecalc/__init__.py +++ b/damagecalc/__init__.py @@ -3,6 +3,6 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): +async def setup(bot): n = DamageCalc(bot) - bot.add_cog(n) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) diff --git a/dbcog/__init__.py b/dbcog/__init__.py index a788f06c..eddb989f 100644 --- a/dbcog/__init__.py +++ b/dbcog/__init__.py @@ -7,7 +7,7 @@ __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] -def setup(bot): +async def setup(bot): n = DBCog(bot) - bot.add_cog(n) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) n.bot.loop.create_task(n.reload_data_task()) diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 2af16ae1..5fdae1dc 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -16,7 +16,6 @@ import discord from redbot.core import Config, checks, commands, data_manager -from redbot.core.commands import Literal as LiteralConverter from redbot.core.utils.chat_formatting import box, pagify, inline, text_to_file from tabulate import tabulate from tsutils.cogs.globaladmin import auth_check @@ -44,6 +43,12 @@ def _data_file(file_name: str) -> str: return os.path.join(str(data_manager.cog_data_path(raw_name='dbcog')), file_name) +# TODO: Find a way to get literal back +class LiteralConverter: + def __class_getitem__(cls, *_): + return Any + + class DBCog(commands.Cog, IdTest): """Database manager""" diff --git a/dungeoncog/__init__.py b/dungeoncog/__init__.py index 22202afc..bf137dc9 100644 --- a/dungeoncog/__init__.py +++ b/dungeoncog/__init__.py @@ -7,9 +7,9 @@ __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] -def setup(bot): +async def setup(bot): n = DungeonCog(bot) - bot.add_cog(n) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) bot.loop.create_task(n.load_aliases()) bot.loop.create_task(n.register_menu()) bot.loop.create_task(n.load_emojis()) diff --git a/feedback/__init__.py b/feedback/__init__.py index 1cc4f2ff..8e829606 100644 --- a/feedback/__init__.py +++ b/feedback/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): - bot.add_cog(Feedback(bot)) +async def setup(bot): + bot.add_cog(Feedback(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(Feedback(bot)) diff --git a/monidlistener/__init__.py b/monidlistener/__init__.py index b2de73a6..e0abc280 100644 --- a/monidlistener/__init__.py +++ b/monidlistener/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): - bot.add_cog(MonIdListener(bot)) +async def setup(bot): + bot.add_cog(MonIdListener(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(MonIdListener(bot)) diff --git a/padboard/__init__.py b/padboard/__init__.py index 37824925..fcf19345 100644 --- a/padboard/__init__.py +++ b/padboard/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "No personal data is stored persistantly." -def setup(bot): - bot.add_cog(PadBoard(bot)) +async def setup(bot): + bot.add_cog(PadBoard(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(PadBoard(bot)) diff --git a/padbuildimg/__init__.py b/padbuildimg/__init__.py index b52bc970..732053f2 100644 --- a/padbuildimg/__init__.py +++ b/padbuildimg/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): - bot.add_cog(PadBuildImage(bot)) +async def setup(bot): + bot.add_cog(PadBuildImage(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(PadBuildImage(bot)) diff --git a/padbuilds/__init__.py b/padbuilds/__init__.py index 61784182..a092012c 100644 --- a/padbuilds/__init__.py +++ b/padbuilds/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): - bot.add_cog(PadBuilds(bot)) +async def setup(bot): + bot.add_cog(PadBuilds(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(PadBuilds(bot)) diff --git a/padevents/__init__.py b/padevents/__init__.py index 915c1c04..4fed2fd8 100644 --- a/padevents/__init__.py +++ b/padevents/__init__.py @@ -7,5 +7,5 @@ __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] -def setup(bot): - bot.add_cog(PadEvents(bot)) +async def setup(bot): + bot.add_cog(PadEvents(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(PadEvents(bot)) diff --git a/padglobal/__init__.py b/padglobal/__init__.py index cc77b969..e70e8e8d 100644 --- a/padglobal/__init__.py +++ b/padglobal/__init__.py @@ -3,7 +3,7 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): +async def setup(bot): n = PadGlobal(bot) - bot.add_cog(n) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) bot.loop.create_task(n.register_menu()) diff --git a/padinfo/__init__.py b/padinfo/__init__.py index b71d631e..5f5aba83 100644 --- a/padinfo/__init__.py +++ b/padinfo/__init__.py @@ -7,8 +7,8 @@ __red_end_user_data_statement__ = json.load(file)['end_user_data_statement'] -def setup(bot): +async def setup(bot): n = PadInfo(bot) - bot.add_cog(n) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) bot.loop.create_task(n.register_menu()) bot.loop.create_task(n.reload_nicknames()) diff --git a/padmonitor/__init__.py b/padmonitor/__init__.py index 4191866a..ce250b22 100644 --- a/padmonitor/__init__.py +++ b/padmonitor/__init__.py @@ -3,7 +3,7 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): +async def setup(bot): n = PadMonitor(bot) - bot.add_cog(n) + bot.add_cog(n) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(n) bot.loop.create_task(n.check_seen_loop()) diff --git a/padsearch/__init__.py b/padsearch/__init__.py index 8e3dd63d..1e34f1ed 100644 --- a/padsearch/__init__.py +++ b/padsearch/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): - bot.add_cog(PadSearch(bot)) +async def setup(bot): + bot.add_cog(PadSearch(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(PadSearch(bot)) diff --git a/pipelineui/__init__.py b/pipelineui/__init__.py index 3aebfb03..8fe3bb2c 100644 --- a/pipelineui/__init__.py +++ b/pipelineui/__init__.py @@ -3,6 +3,6 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): +async def setup(bot): pdb = PipelineUI(bot) - bot.add_cog(pdb) + bot.add_cog(pdb) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(pdb) diff --git a/pricecheck/__init__.py b/pricecheck/__init__.py index 0544a15c..ef808853 100644 --- a/pricecheck/__init__.py +++ b/pricecheck/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "No personal data is stored." -def setup(bot): - bot.add_cog(PriceCheck(bot)) +async def setup(bot): + bot.add_cog(PriceCheck(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(PriceCheck(bot)) diff --git a/profile/__init__.py b/profile/__init__.py index 166c8b44..047b785d 100644 --- a/profile/__init__.py +++ b/profile/__init__.py @@ -3,5 +3,5 @@ __red_end_user_data_statement__ = "Profile data is stored persistantly." -def setup(bot): - bot.add_cog(Profile(bot)) +async def setup(bot): + bot.add_cog(Profile(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(Profile(bot)) From 944ee4a207bd380584c3bd2631224f9e91310cee Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 29 Jun 2022 20:03:22 -0700 Subject: [PATCH 1163/1220] Remove PadSearch (#1760) GOD PADSEARCH SUCKS SO MUCH I'M SO GLAD WE'RE ***FINALLY*** GETTING RID OF THIS --- dbcog/models/monster_model.py | 106 ----- padsearch/__init__.py | 7 - padsearch/info.json | 28 -- padsearch/padsearch.py | 792 ---------------------------------- 4 files changed, 933 deletions(-) delete mode 100644 padsearch/__init__.py delete mode 100644 padsearch/info.json delete mode 100644 padsearch/padsearch.py diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index e38293ca..a1d7c466 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -1,5 +1,4 @@ import re -from collections import defaultdict from typing import Set import romkan @@ -112,8 +111,6 @@ def __init__(self, **m): self.server_priority = m['server_priority'] - self.search = MonsterSearchHelper(self) - @property def killers(self): type_to_killers_map = { @@ -207,106 +204,3 @@ def get_difference(self, other): def __repr__(self): server_prefix = self.server_priority.name + " " if self.server_priority != Server.COMBINED else "" return "Monster<{}{} ({})>".format(server_prefix, self.name_en, self.monster_id) - - -class MonsterSearchHelper: - def __init__(self, m: MonsterModel): - self.name = '{} {}'.format(m.name_en, m.name_ja).lower() - leader_skill = m.leader_skill - self.leader = leader_skill.desc.lower() if leader_skill else '' - active_skill = m.active_skill - self.active_name = active_skill.name.lower() if active_skill and active_skill.name else '' - self.active_desc = active_skill.desc.lower() if active_skill and active_skill.desc else '' - self.active = '{} {}'.format(self.active_name, self.active_desc) - self.active_min = active_skill.cooldown_turns_min if active_skill else None - self.active_max = active_skill.cooldown_turns_max if active_skill else None - - self.color = [m.attr1.name.lower()] - self.hascolor = [c.name.lower() for c in [m.attr1, m.attr2] if c] - - self.hp, self.atk, self.rcv, self.weighted_stats = m.stats(lv=110) - - self.types = [t.name for t in m.types] - - def replace_colors(txt: str): - return txt.replace('red', 'fire').replace('blue', 'water').replace('green', 'wood') - - self.leader = replace_colors(self.leader) - self.active = replace_colors(self.active) - self.active_name = replace_colors(self.active_name) - self.active_desc = replace_colors(self.active_desc) - - self.board_change = [] - self.orb_convert = defaultdict(list) - self.row_convert = [] - self.column_convert = [] - - def color_txt_to_list(txt): - txt = txt.replace('and', ' ') - txt = txt.replace(',', ' ') - txt = txt.replace('orbs', ' ') - txt = txt.replace('orb', ' ') - txt = txt.replace('mortal poison', 'mortalpoison') - txt = txt.replace('jammers', 'jammer') - txt = txt.strip() - return txt.split() - - def strip_prev_clause(txt: str, sep: str): - prev_clause_start_idx = txt.find(sep) - if prev_clause_start_idx >= 0: - prev_clause_start_idx += len(sep) - txt = txt[prev_clause_start_idx:] - return txt - - def strip_next_clause(txt: str, sep: str): - next_clause_start_idx = txt.find(sep) - if next_clause_start_idx >= 0: - txt = txt[:next_clause_start_idx] - return txt - - active_desc = self.active_desc - active_desc = active_desc.replace(' rows ', ' row ') - active_desc = active_desc.replace(' columns ', ' column ') - active_desc = active_desc.replace(' into ', ' to ') - active_desc = active_desc.replace('changes orbs to', 'all orbs to') - - board_change_txt = 'all orbs to' - if board_change_txt in active_desc: - text = strip_prev_clause(active_desc, board_change_txt) - text = strip_next_clause(text, 'orbs') - text = strip_next_clause(text, ';') - self.board_change = color_txt_to_list(text) - - text = active_desc - if 'row' in text: - parts = re.split(r'\Wand\W|;\W', text) - for i in range(0, len(parts)): - if 'row' in parts[i]: - self.row_convert.append(strip_next_clause( - strip_prev_clause(parts[i], 'to '), ' orbs')) - - text = active_desc - if 'column' in text: - parts = re.split(r'\Wand\W|;\W', text) - for i in range(0, len(parts)): - if 'column' in parts[i]: - self.column_convert.append(strip_next_clause( - strip_prev_clause(parts[i], 'to '), ' orbs')) - - convert_done = self.board_change or self.row_convert or self.column_convert - - change_txt = 'change ' - if not convert_done and change_txt in active_desc and 'orb' in active_desc: - text = active_desc - parts = re.split(r'\Wand\W|;\W', text) - for i in range(0, len(parts)): - parts[i] = strip_prev_clause(parts[i], change_txt) if change_txt in parts[i] else '' - - for part in parts: - sub_parts = part.split(' to ') - if len(sub_parts) > 1: - source_orbs = color_txt_to_list(sub_parts[0]) - dest_orbs = color_txt_to_list(sub_parts[1]) - for so in source_orbs: - for do in dest_orbs: - self.orb_convert[so].append(do) diff --git a/padsearch/__init__.py b/padsearch/__init__.py deleted file mode 100644 index 1e34f1ed..00000000 --- a/padsearch/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .padsearch import PadSearch - -__red_end_user_data_statement__ = "No personal data is stored." - - -async def setup(bot): - bot.add_cog(PadSearch(bot)) if not __import__('asyncio').iscoroutinefunction(bot.add_cog) else await bot.add_cog(PadSearch(bot)) diff --git a/padsearch/info.json b/padsearch/info.json deleted file mode 100644 index 422108f9..00000000 --- a/padsearch/info.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "author": [ - "The Tsubaki Bot Team" - ], - "description": "Search for cards using advanced filters.", - "end_user_data_statement": "No personal data is stored.", - "install_msg": "Use [p]helpsearch for help.", - "short": "Card search", - "min_bot_version": "3.4.0", - "max_bot_version": "0.0.0", - "min_python_version": [ - 3, - 8, - 1 - ], - "hidden": false, - "disabled": false, - "required_cogs": { - "dbcog": "https://github.com/TsubakiBotPad/pad-cogs" - }, - "requirements": [ - "tsutils", - "ply" - ], - "tags": [ - "PAD" - ] -} diff --git a/padsearch/padsearch.py b/padsearch/padsearch.py deleted file mode 100644 index d56971f4..00000000 --- a/padsearch/padsearch.py +++ /dev/null @@ -1,792 +0,0 @@ -import logging -import re -from fnmatch import fnmatch -from io import BytesIO - -from ply import lex -from redbot.core import commands -from redbot.core.utils.chat_formatting import box, pagify -from tsutils.helper_functions import timeout_after - -from dbcog.database_context import DbContext - -logger = logging.getLogger('red.padbot-cogs.padsearch') - -HELP_MSG = """ -{0.prefix}search - -To get more than 10 results, include the word 'all' in your search. - -Colors can be any of: - fire water wood light dark null -Additionally, orb colors can be any of: - heart jammer poison mortal and bomb - -Options which take multiple colors should be comma-separated. - -Single instance filters -* hp(n) : Max HP >= n -* atk(n) : Max ATK >= n -* rcv(n) : Max RCV >= n -* weighted(n) : Weighted stat >= n -* cd(n) : Min cd <= n -* farmable : Obtainable outside REM -* haste(n) : Skill cds reduced by n -* inheritable : Can be inherited -* shuffle : Board shuffle (aka refresh) -* unlock : Orb unlock -* delay(n) : Delay enemies by n -* attabsorb : Attribute Absorb shield null -* absorbnull : Damage Abasorb shield null -* combo(n) : Increase combo count by n -* shield(n) uce damage taken by n% -* resolve : Leader skill where you survive when HP reduced to 0 - -Multiple instance filters -* active(str) : Active skill name/description -* board(colors,) : Board change to a comma-sep list of colors -* color(color) : Primary monster color -* column(color) : Creates a column of a color -* hascolor(color) : Primary or secondary monster color -* leader(str) : Leader skill description -* name(str) : Monster name -* row(color) : Creates a row of a color -* type(str) : Monster type -* convert(c1, c2) : Convert from color 1 to color 2, accepts any as entry as well -""" - - -@timeout_after(1) -def filt_timeout(filts, ms): - for f in filts: - ms = [m for m in ms if f(m)] - return ms - - -COLORS = [ - 'fire', - 'water', - 'wood', - 'light', - 'dark', - 'nil', -] - -TYPES = [ - "attacker", - "awoken", - "balanced", - "devil", - "dragon", - "enhance", - "evolve", - "god", - "healer", - "machine", - "physical", - "protected", - "vendor", -] - -ORB_TYPES = [ - 'any', - 'fire', - 'water', - 'wood', - 'light', - 'dark', - 'heal', - 'jammer', - 'poison', - 'mortal', -] - - -def assert_color(value): - value = replace_named_color(value) - if value not in COLORS: - raise commands.UserFeedbackCheckFailure( - 'Unexpected color {}, expected one of {}'.format(value, COLORS)) - return value - - -def assert_orbcolor(value): - value = replace_named_color(value) - if value not in ORB_TYPES: - raise commands.UserFeedbackCheckFailure( - 'Unexpected orb {}, expected one of {}'.format(value, ORB_TYPES)) - return value - - -def assert_orbcolors(values): - return [assert_orbcolor(c) for c in values] - - -def split_csv_orbcolors(value): - parts = [p.strip() for p in value.split(',')] - return assert_orbcolors(parts) - - -COLOR_REPLACEMENTS = { - 'red': 'fire', - 'r': 'fire', - 'blue': 'water', - 'b': 'water', - 'green': 'wood', - 'g': 'wood', - 'l': 'light', - 'd': 'dark', - 'x': 'nil', - 'null': 'nil', - 'heart': 'heal', - 'h': 'heal', - 'p': 'poison', - 'mortal': 'mortalpoison', - 'j': 'jammer', - 'o': 'bomb', -} - - -def replace_named_color(color: str): - color = color.lower().strip() - return COLOR_REPLACEMENTS.get(color, color) - - -def replace_colors_in_text(text: str): - text = text.replace('red', 'fire') - text = text.replace('blue', 'water') - text = text.replace('green', 'wood') - text = text.replace('heart', 'heal') - return text - - -def clean_name(txt, name): - return txt.replace(name, '').strip('() ') - - -def re_clean_name(txt, name): - return clean_name(txt, name)[2:-1] - - -def board_filter(colors): - def fn(m, colors=colors): - # Copy for safety - colors = list(colors) - m_colors = list(m.search.board_change) - - if len(m_colors) != len(colors): - return False - - any_values = 0 - for c in colors: - if c == 'any': - any_values += 1 - else: - if c in m_colors: - m_colors.remove(c) - else: - return False - - # Check remaining anys - return any_values == len(m_colors) - - return fn - - -class PadSearchLexer(object): - tokens = [ - 'ACTIVE', - 'ALL', - 'BOARD', - 'CD', - 'COLOR', - 'COLUMN', - 'FARMABLE', - 'HASCOLOR', - 'HASTE', - 'INHERITABLE', - 'LEADER', - 'NAME', - 'ROW', - 'TYPE', - 'SHUFFLE', - 'UNLOCK', - 'RESOLVE', - 'DELAY', - 'REMOVE', - 'CONVERT', - 'COMBO', - 'ABSORBNULL', - 'ATTABSORB', - 'SHIELD', - 'HP', - 'ATK', - 'RCV', - 'WEIGHTED', - 'REACTIVE', - 'RELEADER', - ] - - def t_ACTIVE(self, t): - r'\bactive\(.+?\)' - t.value = clean_name(t.value, 'active') - t.value = replace_colors_in_text(t.value) - return t - - def t_REACTIVE(self, t): - r"reactive\(r'.+?'\)" - t.value = re_clean_name(t.value, 'reactive') - t.value = replace_colors_in_text(t.value) - return t - - def t_ALL(self, t): - r'all(\(\))?' - return t - - def t_BOARD(self, t): - r'board\([a-zA-z, ]+\)' - t.value = clean_name(t.value, 'board') - t.value = replace_colors_in_text(t.value) - return t - - def t_CD(self, t): - r'cd\(\d+\)' - t.value = clean_name(t.value, 'cd') - t.value = int(t.value) - return t - - def t_COLOR(self, t): - r'color\([a-zA-z]+\)' - t.value = clean_name(t.value, 'color') - t.value = replace_colors_in_text(t.value) - return t - - def t_COLUMN(self, t): - r'column\([a-zA-z]+\)' - t.value = clean_name(t.value, 'column') - t.value = replace_colors_in_text(t.value) - return t - - def t_FARMABLE(self, t): - r'farmable(\(\))?' - return t - - def t_HASCOLOR(self, t): - r'hascolor\([a-zA-z]+\)' - t.value = clean_name(t.value, 'hascolor') - t.value = replace_colors_in_text(t.value) - return t - - def t_HASTE(self, t): - r'haste\(\d+\)' - t.value = clean_name(t.value, 'haste') - t.value = int(t.value) - return t - - def t_INHERITABLE(self, t): - r'inheritable(\(\))?' - return t - - def t_LEADER(self, t): - r'\bleader\(.+?\)' - t.value = clean_name(t.value, 'leader') - t.value = replace_colors_in_text(t.value) - return t - - def t_RELEADER(self, t): - r"releader\(r'.+?'\)" - t.value = re_clean_name(t.value, 'releader') - t.value = replace_colors_in_text(t.value) - return t - - def t_NAME(self, t): - r'name\([a-zA-Z0-9 ]+\)' - t.value = clean_name(t.value, 'name') - return t - - def t_ROW(self, t): - r'row\([a-zA-Z]+\)' - t.value = clean_name(t.value, 'row') - t.value = replace_colors_in_text(t.value) - return t - - def t_SHUFFLE(self, t): - r'shuffle(\(\))?' - return t - - def t_TYPE(self, t): - r'type\([a-zA-z]+\)' - t.value = clean_name(t.value, 'type') - return t - - def t_UNLOCK(self, t): - r'unlock(\(\))?' - return t - - def t_RESOLVE(self, t): - r'resolve(\(\))?' - return t - - def t_DELAY(self, t): - r'delay\(\d+\)' - t.value = clean_name(t.value, 'delay') - t.value = int(t.value) - return t - - def t_REMOVE(self, t): - r'remove\([a-zA-Z0-9 ]+\)' - t.value = t.value.replace('remove', '').strip('()') - return t - - def t_CONVERT(self, t): - r'convert\([a-zA-z, ]+\)' - t.value = clean_name(t.value, 'convert') - i = t.value.split(',') - i[0] = replace_named_color(i[0]).lower() - i[1] = replace_named_color(i[1]).lower() - t.value = i - return t - - def t_COMBO(self, t): - r'combo\(\d+\)' - t.value = clean_name(t.value, 'combo') - t.value = int(t.value) - return t - - def t_ABSORBNULL(self, t): - r'absorbnull(\(\))?' - return t - - def t_ATTABSORB(self, t): - r'attabsorb(\(\))?' - return t - - def t_SHIELD(self, t): - r'shield\(\d+[\d%]\)' - t.value = clean_name(t.value, 'shield') - t.value = t.value.strip('%') - t.value = int(t.value) - return t - - def t_ATK(self, t): - r'atk\(\d+\)' - t.value = clean_name(t.value, 'atk') - t.value = int(t.value) - return t - - def t_HP(self, t): - r'hp\(\d+\)' - t.value = clean_name(t.value, 'hp') - t.value = int(t.value) - return t - - def t_RCV(self, t): - r'rcv\(\d+\)' - t.value = clean_name(t.value, 'rcv') - t.value = int(t.value) - return t - - def t_WEIGHTED(self, t): - r'weighted\(\d+\)' - t.value = clean_name(t.value, 'weighted') - t.value = int(t.value) - return t - - t_ignore = ' \t\n' - - def t_error(self, t): - raise commands.UserFeedbackCheckFailure("Unknown text '%s'" % (t.value,)) - - def build(self, **kwargs): - # pass debug=1 to enable verbose output - self.lexer = lex.lex(module=self) - return self.lexer - - -class SearchConfig(object): - - def __init__(self, lexer, db_context: "DbContext"): - self.all = False - self.cd = None - self.farmable = None - self.haste = None - self.inheritable = None - self.shuffle = None - self.unlock = None - self.resolve = None - self.delay = None - self.combo = None - self.absorbnull = None - self.attabsorb = None - self.shield = None - self.hp = None - self.atk = None - self.rcv = None - self.weighted = None - - self.active = [] - self.reactive = [] - self.board = [] - self.column = [] - self.color = [] - self.hascolor = [] - self.leader = [] - self.releader = [] - self.name = [] - self.row = [] - self.types = [] - self.remove = [] - self.convert = [] - for tok in iter(lexer.token, None): - type = tok.type - value = tok.value - self.all |= type == 'ALL' - self.cd = self.setIfType('CD', type, self.cd, value) - self.farmable = self.setIfType('FARMABLE', type, self.farmable, value) - self.haste = self.setIfType('HASTE', type, self.haste, value) - self.inheritable = self.setIfType('INHERITABLE', type, self.inheritable, value) - self.shuffle = self.setIfType('SHUFFLE', type, self.shuffle, value) - self.unlock = self.setIfType('UNLOCK', type, self.unlock, value) - self.resolve = self.setIfType('RESOLVE', type, self.resolve, value) - self.delay = self.setIfType('DELAY', type, self.delay, value) - self.combo = self.setIfType('COMBO', type, self.combo, value) - self.absorbnull = self.setIfType('ABSORBNULL', type, self.absorbnull, value) - self.attabsorb = self.setIfType('ATTABSORB', type, self.attabsorb, value) - self.shield = self.setIfType('SHIELD', type, self.shield, value) - self.atk = self.setIfType('ATK', type, self.atk, value) - self.hp = self.setIfType('HP', type, self.hp, value) - self.rcv = self.setIfType('RCV', type, self.rcv, value) - self.weighted = self.setIfType('WEIGHTED', type, self.weighted, value) - - if type == 'ACTIVE': - self.active.append(value) - if type == 'REACTIVE': - self.reactive.append(value) - if type == 'BOARD': - self.board.append(split_csv_orbcolors(value)) - if type == 'COLOR': - self.color.append(assert_color(value)) - if type == 'COLUMN': - self.column.append(assert_orbcolor(value)) - if type == 'HASCOLOR': - self.hascolor.append(assert_color(value)) - if type == 'LEADER': - self.leader.append(value) - if type == 'RELEADER': - self.releader.append(value) - if type == 'NAME': - self.name.append(value) - if type == 'ROW': - self.row.append(assert_orbcolor(value)) - if type == 'TYPE': - if value not in TYPES: - raise commands.UserFeedbackCheckFailure( - 'Unexpected type {}, expected one of {}'.format(value, TYPES)) - self.types.append(value) - if type == 'REMOVE': - self.remove.append(value) - if type == 'CONVERT': - self.convert.append(value) - - self.filters = list() - self.re_filters = list() - self.gl_filters = list() - self.regeces = list() - self.globs = list() - - # Single - if self.cd: - self.filters.append(lambda m: m.search.active_min and m.search.active_min <= self.cd) - - if self.farmable: - self.filters.append(lambda m: db_context.graph.monster_is_farmable_evo(m)) - - if self.haste: - text = "charge allies' skill by {}".format(self.haste) - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.inheritable: - self.filters.append(lambda m: m.is_inheritable) - - if self.shuffle: - text = 'replace all' - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.unlock: - text = 'unlock all orbs' - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.resolve: - text = 'may survive when' - self.filters.append(lambda m, t=text: t in m.search.leader) - - if self.delay: - text = 'delay enemies for {}'.format(self.delay) - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.combo: - text = 'increase combo count by {}'.format(self.combo) - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.convert: - text_from = self.convert[0][0] - text_to = self.convert[0][1] - self.filters.append(lambda m, - tt=text_to, - tf=text_from: - [tt] in m.search.orb_convert.values() if text_from == 'any' else - (tf in m.search.orb_convert.keys() if text_to == 'any' else - (tf in m.search.orb_convert.keys() and - tt in m.search.orb_convert[tf]))) - - if self.absorbnull: - text = 'damage absorb shield' - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.attabsorb: - text = 'att. absorb shield' - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.shield: - text = 'damage taken by {}%'.format(self.shield) - self.filters.append(lambda m, t=text: t in m.search.active_desc) - - if self.atk: - self.filters.append(lambda m: m.search.atk and m.search.atk >= self.atk) - - if self.hp: - self.filters.append(lambda m: m.search.hp and m.search.hp >= self.hp) - - if self.rcv: - self.filters.append(lambda m: m.search.rcv and m.search.rcv >= self.rcv) - - if self.weighted: - self.filters.append( - lambda m: m.search.weighted_stats and m.search.weighted_stats >= self.weighted) - - # Multiple - if self.reactive: - filters = [] - for ft in self.reactive: - text = ft.lower() - filters.append(lambda m, t=text: re.search(t, m.search.active)) - self.regeces.append(text) - self.re_filters.extend(filters) - - if self.active: - filters = [] - for ft in self.active: - text = "*" + ft.lower() + "*" - filters.append(lambda m, t=text: fnmatch(m.search.active, t)) - self.globs.append(text) - self.gl_filters.extend(filters) - - if self.board: - filters = [] - for colors in self.board: - filters.append(board_filter(colors)) - self.filters.append(self.or_filters(filters)) - - if self.color: - filters = [] - for ft in self.color: - text = ft.lower() - filters.append(lambda m, c=text: c in m.search.color) - self.filters.append(self.or_filters(filters)) - - if self.column: - filters = [] - for ft in self.column: - text = ft.lower() - if text == 'any': - filters.append(lambda m: m.search.column_convert) - else: - filters.append(lambda m, t=text: t in m.search.column_convert) - self.filters.append(self.or_filters(filters)) - - if self.hascolor: - filters = [] - for ft in self.hascolor: - text = ft.lower() - filters.append(lambda m, c=text: c in m.search.hascolor) - self.filters.append(self.or_filters(filters)) - - if self.releader: - filters = [] - for ft in self.releader: - text = ft.lower() - filters.append(lambda m, t=text: re.search(t, m.search.leader)) - self.regeces.append(text) - self.re_filters.extend(filters) - - if self.leader: - filters = [] - for ft in self.leader: - text = "*" + ft.lower() + "*" - filters.append(lambda m, t=text: fnmatch(m.search.leader, t)) - self.globs.append(text) - self.gl_filters.extend(filters) - - if self.name: - filters = [] - for ft in self.name: - text = ft.lower() - filters.append(lambda m, t=text: t in m.search.name) - self.filters.append(self.or_filters(filters)) - - if self.row: - filters = [] - for ft in self.row: - text = ft.lower() - if text == 'any': - filters.append(lambda m: m.search.row_convert) - else: - filters.append(lambda m, t=text: t in m.search.row_convert) - self.filters.append(self.or_filters(filters)) - - if self.types: - filters = [] - for ft in self.types: - text = ft.lower() - filters.append(lambda m, t=text: t in m.search.types) - self.filters.append(self.or_filters(filters)) - - if self.remove: - filters = [] - for ft in self.remove: - text = ft.lower() - filters.append(lambda m, t=text: t not in m.search.name) - self.filters.append(self.or_filters(filters)) - if not self.filters and not self.re_filters and not self.gl_filters: - raise commands.UserFeedbackCheckFailure('You need to specify at least one filter') - - def check_filters(self, m): - for f in self.filters: - if not f(m): - return False - return True - - async def check_re_filters(self, ms, ctx): - try: - return filt_timeout(self.re_filters, ms) - except TimeoutError: - logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( - '", "'.join(self.regeces), ctx.author.name, ctx.author.id)) - raise commands.UserFeedbackCheckFailure("Regex took too long to compile. Stop trying to break the bot") - except re.error as e: - raise commands.UserFeedbackCheckFailure("Regex search threw error '{}'".format(e.msg)) - - async def check_glob_filters(self, ms, ctx): - try: - return filt_timeout(self.gl_filters, ms) - except TimeoutError: - logger.error("Timeout with patttern: \"{}\" by user {} ({})".format( - '", "'.join(self.globs), ctx.author.name, ctx.author.id)) - raise commands.UserFeedbackCheckFailure("Glob took too long to compile. Stop trying to break the bot") - - def or_filters(self, filters): - def fn(m, filters=filters): - for f in filters: - if f(m): - return True - return False - - return fn - - def setIfType(self, expected_type, given_type, current_value, new_value): - if expected_type != given_type: - return current_value - if current_value is not None: - raise commands.UserFeedbackCheckFailure('You set {} more than once'.format(given_type)) - return new_value - - -class PadSearch(commands.Cog): - """PAD data searching.""" - - def __init__(self, bot, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bot = bot - - async def red_get_data_for_user(self, *, user_id): - """Get a user's personal data.""" - data = "No data is stored for user with ID {}.\n".format(user_id) - return {"user_data.txt": BytesIO(data.encode())} - - async def red_delete_data_for_user(self, *, requester, user_id): - """Delete a user's personal data. - - No personal data is stored in this cog. - """ - return - - @commands.command() - async def helpsearch(self, ctx): - """Help info for the search command.""" - await ctx.author.send(box(HELP_MSG.format(ctx))) - - @commands.command() - async def search(self, ctx, *, filter_spec: str): - """Searches for monsters based on a filter you specify. - Use [p]helpsearch for more info. - """ - try: - config = self._make_search_config(filter_spec) - except Exception as ex: - # Try to correct for missing closing tag - try: - config = self._make_search_config(filter_spec + ')') - except: - # If it still failed, raise the original exception - raise ex - dbcog = self.bot.get_cog('DBCog') - if dbcog is None: - await ctx.send("DBCog cog not loaded.") - return - monsters = dbcog.database.get_all_monsters() - - rmvGemFilter = self._make_search_config('remove( gem)') - monsters = list(filter(rmvGemFilter.check_filters, monsters)) - - matched_monsters = [m for m in monsters if config.check_filters(m)] - # Running regeces/globs on a timer - matched_monsters = await config.check_re_filters(matched_monsters, ctx) - matched_monsters = await config.check_glob_filters(matched_monsters, ctx) - - # Removing entry with names that have gems in it - - matched_monsters.sort(key=lambda m: m.monster_no_na, reverse=True) - - msg = 'Matched {} monsters'.format(len(matched_monsters)) - dm_required = False - if len(matched_monsters) == len(monsters): - await ctx.send("All monsters were matched. Try with a more specific query.") - return - if len(matched_monsters) > 10: - if not config.all: - msg += " (limited to 10, use 'all' to get more)" - matched_monsters = matched_monsters[0:10] - else: - dm_required = True - header = msg - - if len(matched_monsters) > 200: - msg += " (limited to 200)" - matched_monsters = matched_monsters[0:200] - - for m in matched_monsters: - msg += '\n\t[{}] {}'.format(m.monster_no_na, m.name_en) - - if dm_required: - header += '\nList too long to display; sent via DM' - await ctx.send(box(header)) - for page in pagify(msg): - await ctx.author.send(box(page)) - else: - await ctx.send(box(msg)) - - def _make_search_config(self, input): - lexer = PadSearchLexer().build() - lexer.input(input) - db_context = self.bot.get_cog("DBCog").database - return SearchConfig(lexer, db_context) From 4d2e2bcb76f47385d1a487323bc9d05298bf4b0a Mon Sep 17 00:00:00 2001 From: AA Date: Fri, 1 Jul 2022 22:17:33 -0700 Subject: [PATCH 1164/1220] Fix feedback (#1761) --- feedback/feedback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feedback/feedback.py b/feedback/feedback.py index 7f07fb36..fc55c8b2 100644 --- a/feedback/feedback.py +++ b/feedback/feedback.py @@ -47,8 +47,8 @@ async def _send_feedback(self, ctx, message: str, feedback_channel, success_mess description = "Sent by {} {}".format(author, source) e = discord.Embed(description=message) - if author.avatar_url: - e.set_author(name=description, icon_url=author.avatar_url) + if author.avatar.url: + e.set_author(name=description, icon_url=ctx.author.avatar.url) else: e.set_author(name=description) e.set_footer(text=footer) @@ -93,7 +93,7 @@ async def credits(self, ctx): embed.add_field(name="About the bot", value=about, inline=False) embed.add_field(name="Using the bot", value=using.format(ctx), inline=False) embed.add_field(name="Avatar credits", value=avatar, inline=False) - embed.set_thumbnail(url=self.bot.user.avatar_url) + embed.set_thumbnail(url=self.bot.user.avatar.url) await ctx.send(embed=embed) From 21dcfdaa27a8eb92e93ab5109b3e6f7e704a1cfc Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 2 Jul 2022 08:33:58 -0700 Subject: [PATCH 1165/1220] Remove pronunciation_ja from MonsterModel (#1762) --- dbcog/models/monster_model.py | 1 - dbcog/monster_graph.py | 1 - 2 files changed, 2 deletions(-) diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index a1d7c466..9b972cec 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -105,7 +105,6 @@ def __init__(self, **m): self.voice_id_jp = m['voice_id_jp'] self.voice_id_na = m['voice_id_na'] - self.pronunciation_ja = m['pronunciation_ja'] self.has_animation = m['has_animation'] self.has_hqimage = m['has_hqimage'] diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index af2c568c..76f3f7f2 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -346,7 +346,6 @@ def build_graph(self, server: Server) -> MultiDiGraph: exp=m.exp, fodder_exp=m.fodder_exp, limit_mult=m.limit_mult, - pronunciation_ja=m.pronunciation_ja, voice_id_jp=m.voice_id_jp, voice_id_na=m.voice_id_na, hp_max=m.hp_max, From fd27b0189a1646d116f1f58e4138c090773a409e Mon Sep 17 00:00:00 2001 From: AA Date: Mon, 4 Jul 2022 01:33:34 -0700 Subject: [PATCH 1166/1220] server isn't Optional[Server] (#1764) --- dbcog/dungeon_context.py | 2 +- dungeoncog/dungeoncog.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dbcog/dungeon_context.py b/dbcog/dungeon_context.py index dc66bc78..ba6a0095 100644 --- a/dbcog/dungeon_context.py +++ b/dbcog/dungeon_context.py @@ -257,7 +257,7 @@ def get_dungeons_from_name(self, name: str, *, server: Server) -> List[DungeonMo (s['sub_dungeon_id'],)) ems = [] for e in encounters: - data = self.database.query_one(ENEMY_DATA_QUERY, (e["enemy_id"],)) + data = self.database.query_one(format_with_suffix(ENEMY_DATA_QUERY, server), (e["enemy_id"],)) if data is not None: edm = EnemyDataModel(**data) else: diff --git a/dungeoncog/dungeoncog.py b/dungeoncog/dungeoncog.py index 7343c604..70e5763d 100644 --- a/dungeoncog/dungeoncog.py +++ b/dungeoncog/dungeoncog.py @@ -28,6 +28,7 @@ logger = logging.getLogger('red.padbot-cogs.dungeoncog') EMBED_NOT_GENERATED = -1 +DEFAULT_SERVER = Server.COMBINED DUNGEON_ALIASES = "https://docs.google.com/spreadsheets/d/e/" \ "2PACX-1vQ3F4shS6w2na4FXA-vZyyhKcOQ0zRA1B3T7zaX0Bm4cEjW-1IVw91josPtLgc9Zh_TGh8GTD6zFmd0" \ @@ -91,7 +92,7 @@ async def get_dbcog(self): return dbcog async def find_dungeon_from_name(self, ctx, name, database: "DungeonContext", difficulty: str = None, - server: Server = None): + server: Server = DEFAULT_SERVER): """ Gets the sub_dungeon model given the name of a dungeon and its difficulty. """ From 66d43cf8a5725fe6a455a426a84248a98fff4186 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 6 Jul 2022 21:22:17 -0700 Subject: [PATCH 1167/1220] Create TempCog (#1765) --- tempcog/__init__.py | 8 ++++++++ tempcog/info.json | 21 +++++++++++++++++++++ tempcog/tempcog.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 tempcog/__init__.py create mode 100644 tempcog/info.json create mode 100644 tempcog/tempcog.py diff --git a/tempcog/__init__.py b/tempcog/__init__.py new file mode 100644 index 00000000..239b25f0 --- /dev/null +++ b/tempcog/__init__.py @@ -0,0 +1,8 @@ +from .tempcog import TempCog + +__red_end_user_data_statement__ = "No personal data is stored." + + +async def setup(bot): + n = TempCog(bot) + await bot.add_cog(n) diff --git a/tempcog/info.json b/tempcog/info.json new file mode 100644 index 00000000..24c66a83 --- /dev/null +++ b/tempcog/info.json @@ -0,0 +1,21 @@ +{ + "author": [ + "Aradia Megido#2552" + ], + "description": "Holds temporary things that will be removed later.", + "end_user_data_statement": "No personal data is stored.", + "install_msg": "I don't even know what I'm supposed to put in these.", + "short": "Temporary Utilities", + "min_bot_version": "3.4.0", + "max_bot_version": "0.0.0", + "min_python_version": [ + 3, + 8, + 1 + ], + "hidden": true, + "disabled": false, + "required_cogs": {}, + "requirements": [], + "tags": [] +} diff --git a/tempcog/tempcog.py b/tempcog/tempcog.py new file mode 100644 index 00000000..e67a8154 --- /dev/null +++ b/tempcog/tempcog.py @@ -0,0 +1,36 @@ +import logging +from io import BytesIO + +from redbot.core import Config, commands + +logger = logging.getLogger('red.padbot-cogs.tempcog') + + +class TempCog(commands.Cog): + """Temporary Utilities.""" + + def __init__(self, bot, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bot = bot + + self.config = Config.get_conf(self, identifier=7379306) + + async def red_get_data_for_user(self, *, user_id): + """Get a user's personal data.""" + data = "No data is stored for user with ID {}.\n".format(user_id) + return {"user_data.txt": BytesIO(data.encode())} + + async def red_delete_data_for_user(self, *, requester, user_id): + """Delete a user's personal data. + + No personal data is stored in this cog. + """ + return + + @commands.Cog.listener('on_member_join') + async def mod_user_join(self, member): + if member.guild.id != 89391974502649856: + return + await self.bot.get_channel(263512015635611650).send( + "{0.mention} Join name: {0.name} ({0.id})" + " joined the server. Created: {0.created_at}".format(member)) From bfc64f131042f419198c29b30475d60e5d05205b Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 6 Jul 2022 23:50:45 -0700 Subject: [PATCH 1168/1220] Add propagate command (#1766) --- crud/editseries.py | 37 ++++++++++++++++++++++++++++++++++- dbcog/models/monster_model.py | 1 + dbcog/monster_graph.py | 1 + 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/crud/editseries.py b/crud/editseries.py index 0062c47b..21f19b2f 100644 --- a/crud/editseries.py +++ b/crud/editseries.py @@ -45,9 +45,15 @@ async def convert(self, ctx, argument) -> str: 'group': ('groups'), 'collab': ('collabs'), }) + PropagateTarget = AliasedKeywordConverter({ + 'series': None, + 'group': ('groups'), + 'collab': ('collabs'), + }) else: EditSeriesCommand = Literal['add', 'remove', 'promote'] EditSeriesTarget = Literal['monster', 'tree', 'group', 'collab'] + PropagateTarget = Literal['series', 'group', 'collab'] def disjoint_sets(superset: Iterable["MonsterModel"], f: Callable[["MonsterModel"], bool]) \ @@ -81,7 +87,7 @@ async def es_execute_write(self, ctx, sql: str, replacements: Sequence = ()) -> await ctx.send("{} monster(s) affected.".format(affected)) return affected - @commands.command() + @commands.group(aliases=['seriesedit'], invoke_without_command=True) async def editseries(self, ctx, command: EditSeriesCommand, series_id: int, target: EditSeriesTarget, *, target_str): """Affect the series of a target. @@ -138,6 +144,35 @@ async def tryint(arg): elif command == 'promote': await self.es_promote(ctx, series_id, monsters) + @editseries.command() + async def propagate(self, ctx, target: PropagateTarget, *, target_ids): + target_ids = [int(d) for d in re.split(r'\D+', target_ids)] + dbcog = await self.get_dbcog() + + monsters = None + if target == "series": + if target_ids == 0: + return await ctx.send("You cannot propagate the unsorted series.") + monsters = {evo + for monster in dbcog.database.get_monsters_where(lambda m: m.series_id in target_ids, + server=Server.COMBINED) + for evo in dbcog.database.graph.get_alt_monsters(monster)} + elif target == "group": + monsters = {monster for monster in dbcog.database.get_monsters_where(lambda m: m.group_id in target_ids, + server=Server.COMBINED)} + elif target == "collab": + monsters = {monster for monster in dbcog.database.get_monsters_where(lambda m: m.collab_id in target_ids, + server=Server.COMBINED)} + + if not monsters: + return await ctx.send("No monsters found.") + + series = {m.series_id for m in monsters}.difference({0}) + if len(series) != 1: + return await ctx.send("The requested series has inconsistant trees.") + + await self.es_promote(ctx, next(iter(series)), sorted(monsters, key=lambda m: m.monster_id)) + async def es_add(self, ctx, series_id: int, monsters: List["MonsterModel"]): monsters_str = "(" + ",".join(str(m.monster_id) for m in monsters) + ")" async with await self.get_cursor() as cursor: diff --git a/dbcog/models/monster_model.py b/dbcog/models/monster_model.py index 9b972cec..702ffa8a 100644 --- a/dbcog/models/monster_model.py +++ b/dbcog/models/monster_model.py @@ -37,6 +37,7 @@ def __init__(self, **m): self.all_series: Set[SeriesModel] = m['all_series'] self.series_id = m['series_id'] self.group_id = m['group_id'] + self.collab_id = m['collab_id'] self.name_ja = m['name_ja'] self.name_ko = m['name_ko'] self.name_en = self.unoverridden_name_en = m['name_en'] diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 76f3f7f2..18fb1bef 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -316,6 +316,7 @@ def build_graph(self, server: Server) -> MultiDiGraph: all_series=monster_series[m.monster_id] or {series[0]}, series_id=m.s_series_id, group_id=m.group_id, + collab_id=m.collab_id, attribute_1_id=m.attribute_1_id, attribute_2_id=m.attribute_2_id, name_ja=m.name_ja, From e036d4b9763979aae782b3ee24e4794bd2e0b1ca Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 9 Jul 2022 07:28:42 -0500 Subject: [PATCH 1169/1220] Comment out uvloop in requirements (#1771) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c4616eed..269f836e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,7 +54,7 @@ six==1.16.0 tabulate==0.8.7 tsutils typing-extensions==3.10.0.2 -uvloop==0.16.0 +# uvloop==0.16.0 wcwidth==0.2.5 wheel yarl==1.6.3 \ No newline at end of file From 232e571b739881926c7ac8987e4d6c5f6b1fbf99 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Sat, 9 Jul 2022 07:41:52 -0500 Subject: [PATCH 1170/1220] A couple README fixes (#1772) --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba1cfef4..1326570d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ I suggest using [PyCharm Community](https://www.jetbrains.com/pycharm/download) ## Check out the repo -Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/pad-cogs). +Fork this repo (you are probably already on the page, but if not [go here](https://github.com/TsubakiBotPad/pad-cogs)). > Note: If you are developing a cog that is in the [misc-cogs repo](https://github.com/TsubakiBotPad/misc-cogs), you may need to complete some steps for both pad-cogs AND misc-cogs. @@ -67,11 +67,18 @@ e.g ^addpath /Users/Tsubaki/src/pad-cogs ``` -> Note: you may have to do this for `pad-cogs`, `core-cogs`, and `misc-cogs`. +> Note: you may have to do this for `pad-cogs`, `core-cogs`, and `misc-cogs`. If you have forked and cloned core & misc cogs, this process is the same! However, if you are only developing in `pad-cogs`, and installing `core-cogs` & `misc-cogs` from remote repos then: +> +> 1. `^load downloader` (this cog is always installed by default) +> 2. `^repo install misc https://github.com/TsubakiBotPad/misc-cogs` +> 3. `^repo install core https://github.com/TsubakiBotPad/core-cogs` +> 4. Now you can install, for example, the `menulistener` cog with `^repo install core menulistener`. The `menulistener` cog is what makes the `^id` menus interactive instead of single-panel static screens with unusable emoji below them. ### Load cogs -Load relevant cogs using the `^load` command. +Load relevant cogs (whichever cogs you are developing, `dbcog`, and `padinfo`) using the `^load` command. + +> Node: We suggest you load `dbcog` and `padinfo` so that you will have something to test that your bot installation correctly installed; however, if your development does not involve dbcog as a dependency, you may want to keep it unloaded (`^unload dbcog`, get it back with `^load dbcog` at any time), simply because it is very slow for the bot to initialize dbcog on startup, and you will be restarting your bot a lot during development. ``` Syntax: ^load ... @@ -91,6 +98,7 @@ You should now be able to run basic commands like `^id` ``` Should return something like: + image Happy developing! @@ -115,7 +123,7 @@ Most cogs here relate to the mobile game 'Puzzle & Dragons'. | Cog | Purpose | | ------------- | --------------------------------------------------------------- | -| azurelane | **DISCONTINUED** Azure Lane card lookup | +| azurlane | Azur Lane card lookup | | crowddata | Crowdsourced data collection | | crud | Database editor. Not useful for bots other than prod Tsubaki | | dbcog | Central cog to host all PAD data | @@ -128,10 +136,9 @@ Most cogs here relate to the mobile game 'Puzzle & Dragons'. | padbuilds | See user-made builds for dungeons i think??? (nobody uses this) | | padevents | A scheduler cog to help players see upcoming daily GH dungeons. | | padglobal | Global PAD info commands | +| padle | A wordle-inspired monster guessing game | | padinfo | Monster lookup and info display | | padmonitor | Keeps an eye out for PAD updates | -| padsearch | Search for monsters with specific attributes | | pipelineui | Interface for running the pipeline (Tsubaki-only) | | pricecheck | Lumon's thing. Something about stamina trade equivalence | | profile | Global user PAD profile storage and lookup | -| schoolidol | **DISCONTINUED** Love Live! School Idol Festival card lookup | From cfc5da0cc91678887ce0e153d90a8a6db9241afd Mon Sep 17 00:00:00 2001 From: AA Date: Sat, 9 Jul 2022 22:53:32 -0700 Subject: [PATCH 1171/1220] Add () and restrict near-matching in subqueries (#1775) * Add tree subqueries (denoted by parentheses) * Negating tokens is now exact-matching * Subquery matching (e.g. seriesof) now prefers to exact-match tokens, and will avoid near-matching if there is an exact-match monster available in the subquery result set * Fixes a bug where `[-trailo -kunda]` errored See the PR comments for a full set of tests that this commit resolves. --- dbcog/find_monster/find_monster.py | 43 ++------- .../tokens/find_monster_tokens.py | 94 +++++++++++++++++-- padinfo/padinfo.py | 2 +- 3 files changed, 93 insertions(+), 46 deletions(-) diff --git a/dbcog/find_monster/find_monster.py b/dbcog/find_monster/find_monster.py index dd537635..692b0050 100644 --- a/dbcog/find_monster/find_monster.py +++ b/dbcog/find_monster/find_monster.py @@ -7,7 +7,8 @@ from tsutils.formatting import rmdiacritics from tsutils.query_settings.query_settings import QuerySettings -from dbcog.find_monster.tokens.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, QueryToken, SpecialToken, \ +from dbcog.find_monster.tokens.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, MatchMap, \ + MonsterInfo, MonsterMatch, QueryToken, SpecialToken, \ TOKEN_JW_DISTANCE, TokenMatch, calc_ratio_modifier, calc_ratio_name, string_to_token from dbcog.models.monster_model import MonsterModel @@ -22,35 +23,6 @@ } -class MonsterMatch: - def __init__(self): - # The total score of the monster - self.score: float = 0 - - # Match data for all name tokens - self.name: Set[TokenMatch] = set() - - # Match data for all modifier tokens - self.mod: Set[TokenMatch] = set() - - def __repr__(self): - return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) - - -MatchMap = Mapping[MonsterModel, MonsterMatch] - - -class MonsterInfo(NamedTuple): - # The best matching monster - matched_monster: Optional[MonsterModel] - - # How the user tokens matched each monster - monster_matches: MatchMap - - # All monsters that were not rejected by the query - valid_monsters: Set[MonsterModel] - - class SubqueryData(NamedTuple): # Subquery token type matched label: str @@ -115,11 +87,11 @@ def _merge_multi_word_tokens(self, tokens: List[str]) -> List[str]: async def _monster_has_modifier(self, monster: MonsterModel, token: QueryToken, matches: MatchMap) -> bool: score, match = await token.matches(monster, self.index) if not score: - return False + return False ^ token.negated matches[monster].score += score matches[monster].mod.add(match) - return True + return True ^ (token.negated and match.match_data.can_negate) async def _interpret_query(self, tokenized_query: List[str]) -> Tuple[Set[QueryToken], Set[QueryToken]]: modifiers = [] @@ -217,7 +189,7 @@ async def _process_modifiers(self, mod_tokens: Set[QueryToken], potential_evos: matches: MatchMap) -> Set[MonsterModel]: for mod_token in mod_tokens: potential_evos = {m for m in potential_evos if - await self._monster_has_modifier(m, mod_token, matches) ^ mod_token.negated} + await self._monster_has_modifier(m, mod_token, matches)} if not potential_evos: return set() return potential_evos @@ -263,8 +235,8 @@ def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) for evo in self.dbcog.database.graph.get_alt_monsters(monster): monster_evos.add(evo) if matches[evo].score < matches[monster].score: - matches[evo].name = {(t[0], t[1], - f'(from evo {monster.monster_id})') for t in matches[monster].name} + matches[evo].name = {TokenMatch(t[0], t[1], MatchData(t.match_data.token, from_evo=monster.monster_id)) + for t in matches[monster].name} matches[evo].score = matches[monster].score - EPSILON * 3 return monster_evos @@ -344,6 +316,7 @@ async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: # Keep some spaces query = re.sub(r':=?r?("[^"]+"|\'[^\']+\')', lambda m: m.group(0).replace(' ', '\0'), query) query = re.sub(r'\[[^[]+]', lambda m: m.group(0).replace(' ', '\0'), query) + query = re.sub(r'\([^(]+\)', lambda m: m.group(0).replace(' ', '\0'), query) tokenized_query = (await self._process_settings(query)).split() tokenized_query = [token.replace('\0', ' ') for token in tokenized_query] diff --git a/dbcog/find_monster/tokens/find_monster_tokens.py b/dbcog/find_monster/tokens/find_monster_tokens.py index 91315b07..2b5a639f 100644 --- a/dbcog/find_monster/tokens/find_monster_tokens.py +++ b/dbcog/find_monster/tokens/find_monster_tokens.py @@ -1,11 +1,11 @@ from abc import ABC, abstractmethod from collections import namedtuple from fnmatch import fnmatch -from itertools import chain -from typing import Iterable, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union +from typing import Iterable, List, Mapping, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union import regex as re from Levenshtein import jaro, jaro_winkler +from itertools import chain from tsutils.helper_classes import DummyObject from tsutils.tsubaki.monster_header import MonsterHeader @@ -27,6 +27,35 @@ EPSILON = .001 +class MonsterMatch: + def __init__(self): + # The total score of the monster + self.score: float = 0 + + # Match data for all name tokens + self.name: Set[TokenMatch] = set() + + # Match data for all modifier tokens + self.mod: Set[TokenMatch] = set() + + def __repr__(self): + return str((self.score, [t[0] for t in self.name], [t[0] for t in self.mod])) + + +MatchMap = Mapping[MonsterModel, MonsterMatch] + + +class MonsterInfo(NamedTuple): + # The best matching monster + matched_monster: Optional[MonsterModel] + + # How the user tokens matched each monster + monster_matches: MatchMap + + # All monsters that were not rejected by the query + valid_monsters: Set[MonsterModel] + + class MatchData(NamedTuple): # The actual matching token token: "QueryToken" @@ -43,6 +72,12 @@ class MatchData(NamedTuple): # Which or result was chosen or_index: Optional[Tuple[int, str]] = None + # Is negatable + can_negate: bool = True + + # From Evolution + from_evo: int = None + def __repr__(self): ret = [] if self.name_type is not None: @@ -53,6 +88,8 @@ def __repr__(self): ret.append("[Subattr]") if self.or_index is not None: ret.append(f"[Matched `{self.or_index[1]}`]") + if self.from_evo is not None: + ret.append(f"[From evo {self.from_evo}]") return " ".join(ret) def __or__(self, other) -> "MatchData": @@ -85,7 +122,7 @@ class QueryToken(ABC): def __init__(self, value: str, *, negated: bool = False, exact: bool = False): self.value = value self.negated = negated - self.exact = exact + self.exact = exact or negated @abstractmethod async def matches(self, monster: MonsterModel, index: MonsterIndex) -> Tuple[float, Optional[TokenMatch]]: @@ -278,8 +315,9 @@ def __init__(self, fullvalue, subquery, *, negated=False, exact=False, dbcog): async def prepare(self): m_info, _ = await self.dbcog.find_monster_debug(self.subquery) - self.sub_matches = m_info.monster_matches - self.valid_monsters = m_info.valid_monsters + pruned_m_info = await prune_results_subquery(m_info) + self.sub_matches = pruned_m_info.monster_matches + self.valid_monsters = pruned_m_info.valid_monsters self.max_score = max((self.sub_matches[mon].score for mon in self.valid_monsters), default=0) @abstractmethod @@ -292,7 +330,7 @@ async def special_matches(self, monster): return False, MatchData(self) matched, score = max(((mon, self.sub_matches.get(mon, DummyObject(score=0)).score) for mon in mons), key=lambda x: x[1]) - return score / self.max_score, MatchData(self, subquery_result=matched) + return normalize_score(score, self.max_score), MatchData(self, subquery_result=matched) SQT = TypeVar("SQT", bound=Type[SubqueryToken]) @@ -343,6 +381,18 @@ def get_matching_monsters(self, monster): cache_key=('series_id', monster.series_id)) +class SameEvoTree(SubqueryToken): + label = 'sametree' + RE_MATCH = r"\((.+)\)" + + def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): + subquery, = re.fullmatch(self.RE_MATCH, fullvalue.lower()).groups() + super().__init__(fullvalue, subquery, negated=negated, exact=exact, dbcog=dbcog) + + def get_matching_monsters(self, monster): + return self.dbcog.database.graph.get_alt_monsters(monster) + + class AttributeToken(SpecialToken): RE_MATCH = r"[rbgldx]|red|fire|blue|water|green|wood|light|yellow|dark|purple|nil|none|null|white" @@ -366,7 +416,7 @@ async def special_matches(self, monster): if monster.attr1 == self.attr: return True, MatchData(self) if monster.attr2 == self.attr: - return 1 - EPSILON, MatchData(self, subattr_match=True) + return 1 - EPSILON, MatchData(self, subattr_match=True, can_negate=False) return False, MatchData(self) @@ -378,7 +428,6 @@ def __init__(self, fullvalue, *, negated=False, exact=False, dbcog): for s in re.sub(r'"[^"]+"|\'[^\']+\'', lambda m: m.group(0).replace(' ', '\0')[1:-1], fullvalue[1:-1]).split()] - print(self.subqueries) self.tokens: Optional[List[QueryToken]] = None self.results = [] @@ -403,8 +452,8 @@ async def special_matches(self, monster): if monster not in subquery.valid_monsters: continue score = subquery.sub_matches.get(monster, DummyObject(score=0)).score - if best_score < score / subquery.max_score: - best_score = score / subquery.max_score + if best_score < normalize_score(score, subquery.max_score): + best_score = normalize_score(score, subquery.max_score) best_data = MatchData(self, or_index=(c, self.subqueries[c])) return best_score, best_data @@ -474,6 +523,30 @@ async def string_to_token(string: str, dbcog: "DBCog") -> QueryToken: return RegularToken(string, negated=negated, exact=exact) +async def prune_results_subquery(monster_info: "MonsterInfo") -> "MonsterInfo": + exacts = set() + for monster in monster_info.valid_monsters: + matched = False + for token in monster_info.monster_matches[monster].name: + if token.match_data.name_type != "fluff": + if token.matched == token.token: + matched = True + else: + break + else: + if matched: + exacts.add(monster) + return MonsterInfo(monster_info.matched_monster, + monster_info.monster_matches, + exacts or monster_info.valid_monsters) + + +def normalize_score(score: float, max_score: float) -> float: + if max_score == 0: + return 1.0 + return score / max_score + + SPECIAL_TOKEN_TYPES: Set[Type[SpecialToken]] = { MultipleAwakeningToken, MonsterAttributeNumeric, @@ -482,6 +555,7 @@ async def string_to_token(string: str, dbcog: "DBCog") -> QueryToken: SelfHasMaterial, HasMaterial, SeriesOf, + SameEvoTree, AttributeToken, OrToken, IDRangeToken, diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index 9157f475..a17f3a22 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -470,7 +470,7 @@ async def links(self, ctx, *, query: str): await self.log_id_result(ctx, monster.monster_id) self.save_historic_data(query, monster) - @commands.command() + @commands.command(aliases=['lu']) @checks.bot_has_permissions(embed_links=True) async def lookup(self, ctx, *, query: str): """Short info results for a monster query""" From 2c647d33ca65a2fc7d30d59b0b1c55576d92c86a Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 12 Jul 2022 23:17:14 -0500 Subject: [PATCH 1172/1220] Add subheadings when there are subqueries in ^ids (#1778) --- dbcog/dbcog.py | 8 +- dbcog/find_monster/extra_info.py | 72 +++++++++++++++++ dbcog/find_monster/find_monster.py | 41 ++-------- padinfo/padinfo.py | 28 +++---- padinfo/view/monster_list/all_mats.py | 6 +- padinfo/view/monster_list/id_search.py | 16 ++-- padinfo/view/monster_list/monster_list.py | 95 +++++++++++++++++++---- padinfo/view/monster_list/scroll.py | 20 ++--- 8 files changed, 196 insertions(+), 90 deletions(-) create mode 100644 dbcog/find_monster/extra_info.py diff --git a/dbcog/dbcog.py b/dbcog/dbcog.py index 5fdae1dc..8a50b39b 100644 --- a/dbcog/dbcog.py +++ b/dbcog/dbcog.py @@ -27,7 +27,8 @@ from .find_monster import token_mappings from .database_context import DbContext from .database_loader import load_database -from dbcog.find_monster.find_monster import ExtraInfo, FindMonster, MonsterInfo +from dbcog.find_monster.find_monster import FindMonster, MonsterInfo +from .find_monster.extra_info import ExtraInfo from dbcog.find_monster.idtest_mixin import IdTest from .models.enum_types import DEFAULT_SERVER, SERVERS from .models.monster_model import MonsterModel @@ -178,7 +179,6 @@ async def check_index(self): for index in self.indexes.values(): issues.extend(index.issues) for class_attributes in MONSTER_CLASS_ATTRIBUTES: - good = False for monster in self.database.get_all_monsters(): val: Any = monster for ca in class_attributes: @@ -344,9 +344,9 @@ async def find_monster(self, query: str, author_id: int = 0) -> Optional[Monster monster, e_info = await FindMonster(self, await self.get_fm_flags(author_id)).find_monster(query) return monster - async def find_monsters(self, query: str, author_id: int = 0) -> List[MonsterModel]: + async def find_monsters(self, query: str, author_id: int = 0) -> Tuple[List[MonsterModel], ExtraInfo]: monsters, e_info = await FindMonster(self, await self.get_fm_flags(author_id)).find_monsters(query) - return monsters + return monsters, e_info async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: return await FindMonster(self, self.fm_flags_default).find_monster_debug(query) diff --git a/dbcog/find_monster/extra_info.py b/dbcog/find_monster/extra_info.py new file mode 100644 index 00000000..d22c5a84 --- /dev/null +++ b/dbcog/find_monster/extra_info.py @@ -0,0 +1,72 @@ +from collections import defaultdict +from typing import NamedTuple, Mapping, Set + +from frozendict import frozendict + +from dbcog.find_monster.tokens.find_monster_tokens import MatchMap +from dbcog.models.monster_model import MonsterModel + + +class SubqueryData(NamedTuple): + # Subquery token type matched + label: str + + # Subquery given + subquery: str + + # Immutable Parent-to-Child monster mapping + map: Mapping[int, int] + + +class SubqueryMonsters(NamedTuple): + map: Mapping[int, MonsterModel] + + +class ExtraInfo(NamedTuple): + # Subquery Result + subquery_data: Set[SubqueryData] + + subquery_monsters: Set[SubqueryMonsters] + + # Unused + return_code: int + + def get_subquery_mon(self, matched_mon_id: int) -> int: + """Returns the subquery monster ID, preferring the one that restricted the query by the most (i.e. returned the fewest options in the result set)""" + best_weight = 100000000000 # infinity + subquery_mon = None + item: SubqueryData + for item in list(self.subquery_data): + if matched_mon_id not in item.map: + continue + # we want to show the most restrictive query possible + if len(item.map) <= best_weight: + subquery_mon = item.map[matched_mon_id] + if subquery_mon is None: + raise KeyError + return subquery_mon + + def get_monster(self, m_id) -> MonsterModel: + for item in list(self.subquery_monsters): + if m_id in item.map: + return item.map[m_id] + raise KeyError + + @staticmethod + def build_extra_info(monster_matches: MatchMap) -> "ExtraInfo": + subquery_data = defaultdict(dict) + subquery_mons = defaultdict() + for monster, match in monster_matches.items(): + for mod_token in match.mod: + match_data = mod_token.match_data + if match_data.subquery_result: + subquery_data[match_data.token][monster.monster_id] = match_data.subquery_result.monster_id + if match_data.subquery_result.monster_id not in subquery_mons: + subquery_mons[match_data.subquery_result.monster_id] = match_data.subquery_result + subquery_data = {SubqueryData(token.label, token.subquery, frozendict(m)) + for token, m in subquery_data.items()} + + subquery_mons = {SubqueryMonsters(frozendict({token: mon})) + for token, mon in subquery_mons.items()} + + return ExtraInfo(subquery_data, subquery_mons, 0) diff --git a/dbcog/find_monster/find_monster.py b/dbcog/find_monster/find_monster.py index 692b0050..873fe3ef 100644 --- a/dbcog/find_monster/find_monster.py +++ b/dbcog/find_monster/find_monster.py @@ -1,12 +1,12 @@ import re from collections import defaultdict -from typing import Any, DefaultDict, Dict, Iterable, List, Mapping, NamedTuple, Optional, Set, Tuple, Union +from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Set, Tuple, Union -from frozendict import frozendict from tsutils.enums import Server from tsutils.formatting import rmdiacritics from tsutils.query_settings.query_settings import QuerySettings +from dbcog.find_monster.extra_info import ExtraInfo from dbcog.find_monster.tokens.find_monster_tokens import EPSILON, MODIFIER_JW_DISTANCE, MatchData, MatchMap, \ MonsterInfo, MonsterMatch, QueryToken, SpecialToken, \ TOKEN_JW_DISTANCE, TokenMatch, calc_ratio_modifier, calc_ratio_name, string_to_token @@ -23,25 +23,6 @@ } -class SubqueryData(NamedTuple): - # Subquery token type matched - label: str - - # Subquery given - subquery: str - - # Immutable Parent-to-Child monster mapping - map: Mapping[MonsterModel, MonsterModel] - - -class ExtraInfo(NamedTuple): - # Subquery Result - subquery_data: Set[SubqueryData] - - # Unused - return_code: int - - class FindMonster: TOKEN_JW_DISTANCE = TOKEN_JW_DISTANCE MODIFIER_JW_DISTANCE = MODIFIER_JW_DISTANCE @@ -235,8 +216,9 @@ def _get_monster_evos(self, matched_mons: Set[MonsterModel], matches: MatchMap) for evo in self.dbcog.database.graph.get_alt_monsters(monster): monster_evos.add(evo) if matches[evo].score < matches[monster].score: - matches[evo].name = {TokenMatch(t[0], t[1], MatchData(t.match_data.token, from_evo=monster.monster_id)) - for t in matches[monster].name} + matches[evo].name = { + TokenMatch(t[0], t[1], MatchData(t.match_data.token, from_evo=monster.monster_id)) + for t in matches[monster].name} matches[evo].score = matches[monster].score - EPSILON * 3 return monster_evos @@ -251,17 +233,6 @@ def get_most_eligable_monster(self, monsters: Iterable[MonsterModel], tokenized_ return max(monsters, key=lambda m: self.get_priority_tuple(m, tokenized_query, matches)) - def build_extra_info(self, monster_matches: MatchMap) -> ExtraInfo: - subquery_data = defaultdict(dict) - for monster, match in monster_matches.items(): - for mod_token in match.mod: - if mod_token.match_data.subquery_result: - subquery_data[mod_token.match_data.token][monster] = mod_token.match_data.subquery_result - subquery_data = {SubqueryData(token.label, token.subquery, frozendict(m)) - for token, m in subquery_data.items()} - - return ExtraInfo(subquery_data, 0) - async def _find_monster_search(self, tokenized_query: List[str]) -> \ Tuple[Optional[MonsterModel], MatchMap, Set[MonsterModel]]: mod_tokens, name_query_tokens = await self._interpret_query(tokenized_query) @@ -330,7 +301,7 @@ async def find_monster_debug(self, query: str) -> Tuple[MonsterInfo, ExtraInfo]: ) monster_info = MonsterInfo(best_monster, matches_dict, valid_monsters) - extra_info = self.build_extra_info(matches_dict) + extra_info = ExtraInfo.build_extra_info(matches_dict) return monster_info, extra_info async def find_monster(self, query: str) -> Tuple[Optional[MonsterModel], ExtraInfo]: diff --git a/padinfo/padinfo.py b/padinfo/padinfo.py index a17f3a22..1996be3b 100644 --- a/padinfo/padinfo.py +++ b/padinfo/padinfo.py @@ -63,7 +63,7 @@ from padinfo.view.materials import MaterialsViewState from padinfo.view.monster_list.all_mats import AllMatsViewState from padinfo.view.monster_list.id_search import IdSearchViewState -from padinfo.view.monster_list.monster_list import MonsterListViewState +from padinfo.view.monster_list.monster_list import MonsterListViewState, MonsterListQueriedProps from padinfo.view.monster_list.scroll import ScrollViewState from padinfo.view.monster_list.static_monster_list import StaticMonsterListViewState from padinfo.view.otherinfo import OtherInfoViewState @@ -532,13 +532,13 @@ async def _do_idsearch(self, ctx, query, child_menu_type=None, child_reaction_list=None): dbcog = await self.get_dbcog() query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) - monster_list = await IdSearchViewState.do_query(dbcog, query, ctx.author.id, query_settings) + queried_props = await IdSearchViewState.do_query(dbcog, query, ctx.author.id, query_settings) - if not monster_list: + if not queried_props or not queried_props.monster_list: await ctx.send("No monster matched.") return - await self._do_monster_list(ctx, dbcog, query, monster_list, 'ID Search Results', + await self._do_monster_list(ctx, dbcog, query, queried_props, 'ID Search Results', IdSearchViewState, child_menu_type=child_menu_type, child_reaction_list=child_reaction_list) @@ -561,7 +561,7 @@ async def allmats(self, ctx, *, query: str): _, usedin, _, gemusedin, _, _, _, _ = await MaterialsViewState.do_query(dbcog, monster) title = 'Material For' if usedin else 'Gem is Material For' - await self._do_monster_list(ctx, dbcog, query, monster_list, title, AllMatsViewState) + await self._do_monster_list(ctx, dbcog, query, MonsterListQueriedProps(monster_list), title, AllMatsViewState) @commands.command() @checks.bot_has_permissions(embed_links=True) @@ -577,9 +577,10 @@ async def evolist(self, ctx, *, query): if monster_list is None: await self.send_invalid_monster_message(ctx, query, monster, ', which has no alt evos') return - await self._do_monster_list(ctx, dbcog, query, monster_list, 'Evolution List', StaticMonsterListViewState) + await self._do_monster_list(ctx, dbcog, query, MonsterListQueriedProps(monster_list), + 'Evolution List', StaticMonsterListViewState) - async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterModel"], + async def _do_monster_list(self, ctx, dbcog, query, queried_props: MonsterListQueriedProps, title, view_state_type: Type[MonsterListViewState], child_menu_type: Optional[str] = None, child_reaction_list: Optional[List] = None @@ -587,7 +588,7 @@ async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterM raw_query = query original_author_id = ctx.message.author.id query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) - initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(monster_list)) + initial_reaction_list = MonsterListMenuPanes.get_initial_reaction_list(len(queried_props.monster_list)) instruction_message = 'Click a reaction to see monster details!' if child_menu_type is None: @@ -596,7 +597,7 @@ async def _do_monster_list(self, ctx, dbcog, query, monster_list: List["MonsterM child_reaction_list = child_panes_class.emoji_names() state = view_state_type(original_author_id, view_state_type.VIEW_STATE_TYPE, query, - monster_list, query_settings, + queried_props, query_settings, title, instruction_message, child_menu_type=child_menu_type, child_reaction_list=child_reaction_list, @@ -636,16 +637,16 @@ async def scroll(self, ctx, *, query: str): if not monster: await self.send_id_failure_message(ctx, query) return - monster_list = await ScrollViewState.do_query(dbcog, monster) + queried_props = await ScrollViewState.do_query(dbcog, monster) title = 'Monster Book Scroll' raw_query = query original_author_id = ctx.message.author.id query_settings = await QuerySettings.extract_raw(ctx.author, self.bot, query) - initial_reaction_list = ScrollMenuPanes.get_initial_reaction_list(len(monster_list)) + initial_reaction_list = ScrollMenuPanes.get_initial_reaction_list(len(queried_props.monster_list)) instruction_message = 'Click a reaction to see monster details!' state = ScrollViewState(original_author_id, ScrollViewState.VIEW_STATE_TYPE, query, - monster_list, query_settings, + queried_props, query_settings, title, instruction_message, monster.monster_id, child_menu_type=IdMenu.MENU_TYPE, child_reaction_list=IdMenuPanes.emoji_names(), @@ -878,7 +879,8 @@ async def idhistory(self, ctx): if not monster_list: await ctx.send('Did not find any recent queries in history.') return - await self._do_monster_list(ctx, dbcog, '', monster_list, 'Result History', StaticMonsterListViewState) + queried_props = MonsterListQueriedProps(monster_list) + await self._do_monster_list(ctx, dbcog, '', queried_props, 'Result History', StaticMonsterListViewState) @commands.command() async def padsay(self, ctx, server, *, query: str = None): diff --git a/padinfo/view/monster_list/all_mats.py b/padinfo/view/monster_list/all_mats.py index 17d33576..c9ecc2d1 100644 --- a/padinfo/view/monster_list/all_mats.py +++ b/padinfo/view/monster_list/all_mats.py @@ -1,7 +1,7 @@ from typing import List, Optional, TYPE_CHECKING from padinfo.view.materials import MaterialsViewState -from padinfo.view.monster_list.monster_list import MonsterListViewState +from padinfo.view.monster_list.monster_list import MonsterListViewState, MonsterListQueriedProps if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -19,7 +19,7 @@ async def do_query(cls, dbcog, monster: "MonsterModel") -> Optional[List["Monste return monster_list @classmethod - async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: + async def query_from_ims(cls, dbcog, ims) -> MonsterListQueriedProps: monster = await dbcog.find_monster(ims['raw_query'], ims['original_author_id']) monster_list = await cls.do_query(dbcog, monster) - return monster_list + return MonsterListQueriedProps(monster_list) diff --git a/padinfo/view/monster_list/id_search.py b/padinfo/view/monster_list/id_search.py index 400a7005..0826f192 100644 --- a/padinfo/view/monster_list/id_search.py +++ b/padinfo/view/monster_list/id_search.py @@ -3,7 +3,7 @@ from tsutils.query_settings.enums import EvoGrouping from tsutils.query_settings.query_settings import QuerySettings -from padinfo.view.monster_list.monster_list import MonsterListViewState +from padinfo.view.monster_list.monster_list import MonsterListViewState, MonsterListQueriedProps if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -14,8 +14,8 @@ class IdSearchViewState(MonsterListViewState): @classmethod async def do_query(cls, dbcog, query, original_author_id, query_settings: QuerySettings) \ - -> Optional[List["MonsterModel"]]: - found_monsters = await dbcog.find_monsters(query, original_author_id) + -> Optional[MonsterListQueriedProps]: + found_monsters, extra_info = await dbcog.find_monsters(query, original_author_id) if not found_monsters: return None @@ -31,10 +31,10 @@ async def do_query(cls, dbcog, query, original_author_id, query_settings: QueryS used.add(base_id) monster_list.append(mon) - return monster_list + return MonsterListQueriedProps(monster_list, extra_info) @classmethod - async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: - monster_list = await cls.do_query(dbcog, ims['raw_query'], ims['original_author_id'], - QuerySettings.deserialize(ims['query_settings'])) - return monster_list + async def query_from_ims(cls, dbcog, ims) -> MonsterListQueriedProps: + queried_props = await cls.do_query(dbcog, ims['raw_query'], ims['original_author_id'], + QuerySettings.deserialize(ims['query_settings'])) + return queried_props diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index 4267f8bf..b35fccdc 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -1,4 +1,4 @@ -from typing import List, Optional, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING, Dict from discordmenu.embed.base import Box from discordmenu.embed.components import EmbedField, EmbedMain @@ -13,6 +13,13 @@ if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel + from dbcog.find_monster.extra_info import SubqueryData, ExtraInfo + + +class MonsterListQueriedProps: + def __init__(self, monster_list: List["MonsterModel"], extra_info: "ExtraInfo" = None): + self.extra_info = extra_info + self.monster_list = monster_list class MonsterListViewState(ViewStateBase): @@ -20,7 +27,7 @@ class MonsterListViewState(ViewStateBase): MAX_ITEMS_PER_PANE = 11 def __init__(self, original_author_id, menu_type, query, - monster_list: List["MonsterModel"], query_settings: QuerySettings, + queried_props: MonsterListQueriedProps, query_settings: QuerySettings, title, message, *, current_page: int = 0, @@ -33,7 +40,9 @@ def __init__(self, original_author_id, menu_type, query, ): super().__init__(original_author_id, menu_type, query, extra_state=extra_state) - paginated_monsters = self.paginate(monster_list) + paginated_monsters = self.paginate(queried_props.monster_list) + self.queried_props = queried_props + self.extra_info = self.queried_props.extra_info self.current_index = current_index self.current_page = current_page self.page_count = len(paginated_monsters) @@ -91,7 +100,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): return None title = ims['title'] - monster_list = await cls.query_from_ims(dbcog, ims) + queried_props = await cls.query_from_ims(dbcog, ims) current_page = ims['current_page'] current_index = ims.get('current_index') @@ -106,7 +115,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): child_reaction_list = ims.get('child_reaction_list') idle_message = ims.get('idle_message') return cls(original_author_id, menu_type, query, - monster_list, query_settings, + queried_props, query_settings, title, idle_message, current_page=current_page, current_index=current_index, @@ -118,7 +127,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): ) @classmethod - async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: + async def query_from_ims(cls, dbcog, ims) -> MonsterListQueriedProps: ... @staticmethod @@ -129,7 +138,8 @@ def paginate(monster_list: List["MonsterModel"]) -> List[List["MonsterModel"]]: @classmethod async def query_paginated_from_ims(cls, dbcog, ims) -> List[List["MonsterModel"]]: - return cls.paginate(await cls.query_from_ims(dbcog, ims)) + queried_props = await cls.query_from_ims(dbcog, ims) + return cls.paginate(queried_props.monster_list) def increment_page(self): if self.current_page < len(self.paginated_monsters) - 1: @@ -190,30 +200,81 @@ class MonsterListView: VIEW_TYPE = 'MonsterList' @classmethod - def monster_list(cls, monsters: List["MonsterModel"], current_monster_id: int, query_settings: QuerySettings): + def monster_list(cls, monsters: List["MonsterModel"], current_monster_id: int, query_settings: QuerySettings, + offset=0): if not len(monsters): return [] return [MonsterHeader.box_with_emoji( - mon, link=True, prefix=cls.get_emoji(i, current_monster_id), + mon, link=True, prefix=cls.get_emoji(offset + i, current_monster_id), query_settings=query_settings) for i, mon in enumerate(monsters)] @classmethod def get_emoji(cls, i: int, _current_monster_id: int): return char_to_emoji(str(i)) + @classmethod + def has_subqueries(cls, state): + if state.queried_props.extra_info is None: + return False + return state.queried_props.extra_info.subquery_data + + @classmethod + def get_title(cls, state): + if not cls.has_subqueries(state): + return None + return state.title + @classmethod def embed(cls, state: MonsterListViewState): - fields = [ - EmbedField(state.title, - Box(*cls.monster_list(state.monster_list, state.current_monster_id, state.query_settings))), - EmbedField(BoldText('Page'), - Box('{} of {}'.format(state.current_page + 1, state.page_count)), - inline=True - ) - ] + fields = [] + print(state.extra_info) + if not cls.has_subqueries(state): + fields.append( + EmbedField(state.title, + Box(*cls.monster_list(state.monster_list, state.current_monster_id, state.query_settings))) + ) + + else: + cur_subq_id = None + cur_mon_list = [] + offset = 0 + i = 0 + for m in state.monster_list: + subq_id = state.extra_info.get_subquery_mon(m.monster_id) + if cur_mon_list and subq_id != cur_subq_id: + title = MonsterHeader.box_with_emoji( + state.extra_info.get_monster(cur_subq_id), query_settings=state.query_settings, + link=False) + fields.append( + EmbedField( + title, + Box(*cls.monster_list( + cur_mon_list, state.current_monster_id, state.query_settings, offset=offset))) + ) + cur_mon_list = [] + offset += i + i = 0 + cur_mon_list.append(m) + cur_subq_id = subq_id + i += 1 + + title = MonsterHeader.box_with_emoji( + state.extra_info.get_monster(cur_subq_id), query_settings=state.query_settings, link=False) + fields.append( + EmbedField( + title, + Box(*cls.monster_list( + cur_mon_list, state.current_monster_id, state.query_settings, offset=offset))) + ) + + fields.append(EmbedField(BoldText('Page'), + Box('{} of {}'.format(state.current_page + 1, state.page_count)), + inline=True + )) return EmbedView( EmbedMain( + title=cls.get_title(state), color=state.query_settings.embedcolor, ), embed_footer=embed_footer_with_state(state), diff --git a/padinfo/view/monster_list/scroll.py b/padinfo/view/monster_list/scroll.py index 08b1f3a9..78194633 100644 --- a/padinfo/view/monster_list/scroll.py +++ b/padinfo/view/monster_list/scroll.py @@ -3,7 +3,7 @@ from tsutils.menu.components.config import UserConfig from tsutils.query_settings.query_settings import QuerySettings -from padinfo.view.monster_list.monster_list import MonsterListView, MonsterListViewState +from padinfo.view.monster_list.monster_list import MonsterListView, MonsterListViewState, MonsterListQueriedProps if TYPE_CHECKING: from dbcog.models.monster_model import MonsterModel @@ -14,7 +14,7 @@ class ScrollViewState(MonsterListViewState): VIEW_STATE_TYPE = "Scroll" def __init__(self, original_author_id, menu_type, query, - monster_list: List["MonsterModel"], query_settings: QuerySettings, + queried_props: MonsterListQueriedProps, query_settings: QuerySettings, title, message, current_monster_id: int, *, current_page: int = 0, @@ -25,7 +25,7 @@ def __init__(self, original_author_id, menu_type, query, extra_state=None, child_message_id=None ): - super().__init__(original_author_id, menu_type, query, monster_list, query_settings, title, message, + super().__init__(original_author_id, menu_type, query, queried_props, query_settings, title, message, current_page=current_page, current_index=current_index, child_menu_type=child_menu_type, child_reaction_list=child_reaction_list, reaction_list=reaction_list, extra_state=extra_state, child_message_id=child_message_id) @@ -49,7 +49,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): return None title = ims['title'] - monster_list = await cls.query_from_ims(dbcog, ims) + queried_props = await cls.query_from_ims(dbcog, ims) current_page = ims['current_page'] current_index = ims.get('current_index') @@ -64,7 +64,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): child_reaction_list = ims.get('child_reaction_list') idle_message = ims.get('idle_message') return ScrollViewState(original_author_id, menu_type, query, - monster_list, query_settings, + queried_props, query_settings, title, idle_message, ims['current_monster_id'], current_page=current_page, current_index=current_index, @@ -76,7 +76,7 @@ async def deserialize(cls, dbcog, user_config: UserConfig, ims: dict): ) @classmethod - async def do_query(cls, dbcog, monster) -> Optional[List["MonsterModel"]]: + async def do_query(cls, dbcog, monster) -> Optional[MonsterListQueriedProps]: prev_monster = dbcog.database.graph.numeric_prev_monster(monster) next_monster = dbcog.database.graph.numeric_next_monster(monster) @@ -87,7 +87,7 @@ async def do_query(cls, dbcog, monster) -> Optional[List["MonsterModel"]]: if next_monster is not None: monster_list.append(next_monster) - return monster_list + return MonsterListQueriedProps(monster_list) async def decrement_index(self, dbcog): db_context: "DbContext" = dbcog.database @@ -115,10 +115,10 @@ async def increment_index(self, dbcog): self.paginated_monsters = [monster_list] @classmethod - async def query_from_ims(cls, dbcog, ims) -> List["MonsterModel"]: + async def query_from_ims(cls, dbcog, ims) -> MonsterListQueriedProps: monster = await dbcog.find_monster(ims['raw_query'], ims['original_author_id']) - monster_list = await cls.do_query(dbcog, monster) - return monster_list + queried_props = await cls.do_query(dbcog, monster) + return queried_props class ScrollView(MonsterListView): From 4738246fd626fe38413a5d1468637ec225bc8636 Mon Sep 17 00:00:00 2001 From: River <18037011+RheingoldRiver@users.noreply.github.com> Date: Tue, 12 Jul 2022 23:21:17 -0500 Subject: [PATCH 1173/1220] remove print (#1780) --- padinfo/view/monster_list/monster_list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padinfo/view/monster_list/monster_list.py b/padinfo/view/monster_list/monster_list.py index b35fccdc..0cfabb2a 100644 --- a/padinfo/view/monster_list/monster_list.py +++ b/padinfo/view/monster_list/monster_list.py @@ -227,7 +227,6 @@ def get_title(cls, state): @classmethod def embed(cls, state: MonsterListViewState): fields = [] - print(state.extra_info) if not cls.has_subqueries(state): fields.append( EmbedField(state.title, From 0a39b66dafe9bb4c223a944de2a2d631dcc3e7a7 Mon Sep 17 00:00:00 2001 From: AA Date: Tue, 19 Jul 2022 21:38:50 -0700 Subject: [PATCH 1174/1220] Add board references to active skills (#1783) --- dbcog/models/active_skill_model.py | 2 ++ dbcog/monster_graph.py | 4 ++++ padinfo/view/components/base_id_main_view.py | 16 ++++++++++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/dbcog/models/active_skill_model.py b/dbcog/models/active_skill_model.py index 742abc67..81930062 100644 --- a/dbcog/models/active_skill_model.py +++ b/dbcog/models/active_skill_model.py @@ -42,6 +42,8 @@ def __init__(self, *, active_parts: Sequence[Union[ActivePartModel, Dict[str, An self.desc_templated_ja = kwargs['desc_templated_ja'] self.desc_templated_en = kwargs['desc_templated_en'] self.desc_templated_ko = kwargs['desc_templated_ko'] + self.board_65 = kwargs['board_65'] + self.board_76 = kwargs['board_76'] self.cooldown = kwargs['cooldown'] self.active_parts = active_parts diff --git a/dbcog/monster_graph.py b/dbcog/monster_graph.py index 18fb1bef..ab21a337 100644 --- a/dbcog/monster_graph.py +++ b/dbcog/monster_graph.py @@ -87,6 +87,8 @@ ass.desc_templated_ja AS ass_desc_templated_ja, ass.desc_templated_en AS ass_desc_templated_en, ass.desc_templated_ko AS ass_desc_templated_ko, + ass.board_65 AS ass_board_65, + ass.board_76 AS ass_board_76, ass.cooldown AS ass_cooldown, ap.active_part_id, ap.active_skill_type_id, @@ -236,6 +238,8 @@ def build_graph(self, server: Server) -> MultiDiGraph: 'desc_templated_ja': row.ass_desc_templated_ja, 'desc_templated_en': row.ass_desc_templated_en, 'desc_templated_ko': row.ass_desc_templated_ko, + 'board_65': row.ass_board_65, + 'board_76': row.ass_board_76, 'cooldown': row.ass_cooldown, 'active_parts': [] diff --git a/padinfo/view/components/base_id_main_view.py b/padinfo/view/components/base_id_main_view.py index d8eb5c76..d0eb5f9f 100644 --- a/padinfo/view/components/base_id_main_view.py +++ b/padinfo/view/components/base_id_main_view.py @@ -28,14 +28,21 @@ def _killer_latent_emoji(latent_name: str): return get_emoji('latent_killer_{}'.format(latent_name.lower())) -def _make_prefix(idx, compound_skill_type_id, subskill): - if compound_skill_type_id == 1: +def _make_prefix(idx, skill, subskill): + if skill.compound_skill_type_id == 1: return emoji_cache.get_emoji('bd') + ' ' - elif compound_skill_type_id in (2, 3): + elif skill.compound_skill_type_id in (2, 3): return number_emoji_small(idx) + (f' **[{subskill.cooldown}cd]** ' if idx != 1 else ' ') return '' +def _make_suffix(idx, skill, subskill): + if subskill.board_76 != "Z" * (7 * 6): + return (f" [see 6x5](https://pad.dawnglare.com/?patt={subskill.board_65}&showfill=1) ·" + f" [see 7x6](https://pad.dawnglare.com/?patt={subskill.board_76}&showfill=1&height=6&width=7)") + return "" + + class BaseIdMainView(BaseIdView, ABC): transform_emoji_names = ['downr', 'downo', 'downy', 'downg', 'downb', 'downp'] up_emoji_name = 'upgr' @@ -125,8 +132,9 @@ def active_skill_text(cls, active_skill: Optional["ActiveSkillModel"], if active_skill is None: return 'None' - return "\n".join(_make_prefix(c, active_skill.compound_skill_type_id, subskill) + return "\n".join(_make_prefix(c, active_skill, subskill) + jinja2.Template(subskill.desc_templated).render(**jinja2_replacements) + + _make_suffix(c, active_skill, subskill) for c, subskill in enumerate(active_skill.active_subskills, 1)) @staticmethod From edc19675b34ece4cd2fc891d6c0faf243757d3e6 Mon Sep 17 00:00:00 2001 From: AA Date: Wed, 20 Jul 2022 19:25:21 -0700 Subject: [PATCH 1175/1220] Add New Awakenings (#1788) --- dbcog/find_monster/token_mappings.py | 19 ++++++++++++++----- dbcog/models/enum_types.py | 9 +++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/dbcog/find_monster/token_mappings.py b/dbcog/find_monster/token_mappings.py index a66d9eaa..4dc7654d 100644 --- a/dbcog/find_monster/token_mappings.py +++ b/dbcog/find_monster/token_mappings.py @@ -184,11 +184,20 @@ class EvoTypes(Enum): AwokenSkills.ADDTYPEATTACKER: (), AwokenSkills.ADDTYPEPHYSICAL: (), AwokenSkills.ADDTYPEHEALER: (), - AwokenSkills.SUBATTRRED: (), - AwokenSkills.SUBATTRBLUE: (), - AwokenSkills.SUBATTRGREEN: (), - AwokenSkills.SUBATTRLIGHT: (), - AwokenSkills.SUBATTRDARK: (), + AwokenSkills.SUBATTRRED: ('subattrr', '?r+'), + AwokenSkills.SUBATTRBLUE: ('subattrb', '?b+'), + AwokenSkills.SUBATTRGREEN: ('subattrg', '?g+'), + AwokenSkills.SUBATTRLIGHT: ('subattrl', '?l+'), + AwokenSkills.SUBATTRDARK: ('subattrd', '?d+'), + AwokenSkills.TPAPLUS: ('tpa+', 'pronged+'), + AwokenSkills.SKILLCHARGEPLUS: ('rainbowhaste+', 'skillcharge+', 'hasteawo+'), + AwokenSkills.AUTOHEALPLUS: ('autoheal+',), + AwokenSkills.ENHANCEDREDPLUS: ('oer+', 'oe+'), + AwokenSkills.ENHANCEDBLUEPLUS: ('oeb+', 'oe+'), + AwokenSkills.ENHANCEDGREENPLUS: ('oeg+', 'oe+'), + AwokenSkills.ENHANCEDLIGHTPLUS: ('oel+', 'oe+'), + AwokenSkills.ENHANCEDDARKPLUS: ('oed+', 'oe+'), + AwokenSkills.ENHANCEDHEALPLUS: ('htpa+', 'oeh+'), } diff --git a/dbcog/models/enum_types.py b/dbcog/models/enum_types.py index b20221d4..0b2b529c 100644 --- a/dbcog/models/enum_types.py +++ b/dbcog/models/enum_types.py @@ -154,3 +154,12 @@ class AwokenSkills(Enum): SUBATTRGREEN = 93 SUBATTRLIGHT = 94 SUBATTRDARK = 95 + TPAPLUS = 96 + SKILLCHARGEPLUS = 97 + AUTOHEALPLUS = 98 + ENHANCEDREDPLUS = 99 + ENHANCEDBLUEPLUS = 100 + ENHANCEDGREENPLUS = 101 + ENHANCEDLIGHTPLUS = 102 + ENHANCEDDARKPLUS = 103 + ENHANCEDHEALPLUS = 104 From 765a13e8ab51692430d3bf803179100a52e042e2 Mon Sep 17 00:00:00 2001 From: ViciousSlayer <94924966+ViciousSlayer@users.noreply.github.com> Date: Thu, 21 Jul 2022 18:53:21 -0300 Subject: [PATCH 1176/1220] Add grey bases for new latents, all 120s, dblcap (#1792) --- padbuildimg/assets/lat/abs120.png | Bin 0 -> 5983 bytes padbuildimg/assets/lat/att120.png | Bin 0 -> 5907 bytes padbuildimg/assets/lat/cltp.png | Bin 0 -> 5078 bytes padbuildimg/assets/lat/cltp120.png | Bin 0 -> 5832 bytes padbuildimg/assets/lat/dbl.png | Bin 0 -> 5107 bytes padbuildimg/assets/lat/ls120.png | Bin 0 -> 5963 bytes padbuildimg/assets/lat/nabs120.png | Bin 0 -> 5482 bytes padbuildimg/assets/lat/ncltp.png | Bin 0 -> 4481 bytes padbuildimg/assets/lat/ncltp120.png | Bin 0 -> 5445 bytes padbuildimg/assets/lat/ndbl.png | Bin 0 -> 5471 bytes padbuildimg/assets/lat/nsb++.png | Bin 0 -> 4265 bytes padbuildimg/assets/lat/nsb++120.png | Bin 0 -> 5459 bytes padbuildimg/assets/lat/nspn120.png | Bin 0 -> 5513 bytes padbuildimg/assets/lat/nunm120.png | Bin 0 -> 5510 bytes padbuildimg/assets/lat/sb++.png | Bin 0 -> 4923 bytes padbuildimg/assets/lat/sb++120.png | Bin 0 -> 6019 bytes padbuildimg/assets/lat/spn120.png | Bin 0 -> 5797 bytes padbuildimg/assets/lat/unm120.png | Bin 0 -> 5850 bytes padbuildimg/assets/lat/vdp120.png | Bin 0 -> 5922 bytes 19 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 padbuildimg/assets/lat/abs120.png create mode 100644 padbuildimg/assets/lat/att120.png create mode 100644 padbuildimg/assets/lat/cltp.png create mode 100644 padbuildimg/assets/lat/cltp120.png create mode 100644 padbuildimg/assets/lat/dbl.png create mode 100644 padbuildimg/assets/lat/ls120.png create mode 100644 padbuildimg/assets/lat/nabs120.png create mode 100644 padbuildimg/assets/lat/ncltp.png create mode 100644 padbuildimg/assets/lat/ncltp120.png create mode 100644 padbuildimg/assets/lat/ndbl.png create mode 100644 padbuildimg/assets/lat/nsb++.png create mode 100644 padbuildimg/assets/lat/nsb++120.png create mode 100644 padbuildimg/assets/lat/nspn120.png create mode 100644 padbuildimg/assets/lat/nunm120.png create mode 100644 padbuildimg/assets/lat/sb++.png create mode 100644 padbuildimg/assets/lat/sb++120.png create mode 100644 padbuildimg/assets/lat/spn120.png create mode 100644 padbuildimg/assets/lat/unm120.png create mode 100644 padbuildimg/assets/lat/vdp120.png diff --git a/padbuildimg/assets/lat/abs120.png b/padbuildimg/assets/lat/abs120.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff71b0a5b1d35cdd4a049ad51a065f5ed25e150 GIT binary patch literal 5983 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 J&sh5J`XBnSmh}Jt literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/att120.png b/padbuildimg/assets/lat/att120.png new file mode 100644 index 0000000000000000000000000000000000000000..8e76c3389cb3afdb2ab1cec8525c24f8289a5501 GIT binary patch literal 5907 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 BPCftt literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/nabs120.png b/padbuildimg/assets/lat/nabs120.png new file mode 100644 index 0000000000000000000000000000000000000000..f109d2b245904aa09cde92a4d9be92e1f8af0e46 GIT binary patch literal 5482 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 KG`nWv8Tmixj3T1| literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/ncltp.png b/padbuildimg/assets/lat/ncltp.png new file mode 100644 index 0000000000000000000000000000000000000000..67a5a9c1d1320f08f411f7d82e1ca017351e0b18 GIT binary patch literal 4481 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/nsb++.png b/padbuildimg/assets/lat/nsb++.png new file mode 100644 index 0000000000000000000000000000000000000000..fe8e7a7132d900a755720c70c9ea695e32e1d9da GIT binary patch literal 4265 zcmcIoc~}$Y_712Nv=qxOAckONk;yWdtP&w%M?z(*q6kiA1_)#`37ZuJTu`(w6=hLW ziU<`&M37oG(XuI`La!j;4x*x>a$S%M{SAWNdw)-FFaO-kGc(`JobNsFIp>}8&6DlR zeLZw%o6RN=2)bULuKxHL#rJvITKKUkbjLFMq@(Z*RuKqu3?_CBLe3#m0zsQB4qT~T zDfHnYaw!=`mBE_ET zl~}-f-#}!2G{QkiPD_c7@mw50im72@yi_7napQTUkGNcXJaL;sB7QVcNApPh35Ud$ z!sSF4xe_BX$y5NLQb8h%L#8nyh)H!If>e-7p)x2m5TG%+RESGs5vP76+?x^&=lZ+4 zPx->Hc%(?RTEV4I;^N}Sadfg=89||OI2;NUq<|oRTL7vAnHr7O@GN428nKHkC?0TdUM=vACBh zzJCg=3QSO76n{)5k5M9+TP!A1TYqw6hKlo+g*D+A?n$)`ds|KyPf(#Rmuc7<4m%Hvda6O5)NV6LzlAP~v|7~v2D;9yJ$U{WC(1E!1cJFHJ=-zvrUn*&S!6?K9YiX+mQ2pyv_ z5FnfZ(EywS2EYhDVo_-*CStH4m;sT9|Mcr3m&lbuIf`eOP9mZRH(aii!nl6KQaA#m zC}a^6YR1v=C_Kd~ymSdu??(W(ZfYbE6F-WM3nLR+=aGbCe#vNt`Rldy4- z7!LXu#h)}&$-~uguoB}(;FVQ)xvaVOa!*H%CvqTUB3I|}A2?JCR;s6{Ln+b5h z@NkfdibNoSOtO%O{~H#Q$`WxHAQfQH8EgP=${-+O(=dR_pfWLtK^L)@;B>KA3^s>F z15qHH%4APUUId~rK&OW@5D*e^@NL>yFvezr2n3)A2Y+*5hz+orbUa2FWV1mALS@mX zjm2g%K?n|~;VlY90FVvQaTqoR;5~!Rpt3pP9BA5DFoc0@1ZCipVGN)lc)mCsItGZC zG>pSW5EO$J-8F9$0xSAJk*N`H=2V%RjVY2(?d%DCQ< z4a~;t?EQA9XLWAz*U&-@w%O-6_x-eJLs4J_v3-$#f%oc)f>Yg|e){%56$y)rgdePa zI!4MnVYollXHMgx#kT3RyyYn~8g}_xShSh;RMuAB&Q1Dd7)z{OG3uN+vaEQ`?Prd| zM+T&avw0fV@7`T7a}{Z?_IB6QC(ha_8OrwIKdjTU>q-XW9W9e9Q;cum7CsC0fq`pB zY)rqzg|U*%{rUS?L4oODU43al?zcjr?qKKXh=z2dUqVXAx{*&_ya@kee9Sf-l$}!B zSpAqj7Umziv^RRZrluyOE@{5qH$tIsG2;^T@slU;i{A6C+4L`hf`;a0OPULJww}9^ zN$D6LKs@Q`*Bwvdjlci${wHtOQk$K~YNUa5G$C^ALH_TZoe}$aFXH>%t!At~&~V`b zfyd)HtSZ+AjK94%JW^C#T(x+!H=q1qE3A0fs4K{u>+fyo#`kWybvEKwpSh?h^B>hC zH?`$4v1Mq>kLCKlx!i`5YwiiZ6r`IZ#T+atAxA|;DXMNq<}k(2{D1KGfsCS71nvLz zS8jDEMW7A2`7PR8m1*0@%OtNMbtUAaWNi&zqW6Fs5?vV8XlglsX@xWLa8uF;vKzbC zsoDICB$w8kH{5n68)N+&SmsN^>i0U`zJKq{oa%M=!g^QK?YVl8718gX?l;>_Wmg6X zUHlBotBg)G9KF7I*8-=umX3{S!n>(PVQIk?3hCT2lQ{{p!nOg+_E+yTc-G}f{QQJM z!DzPM;oc>U*Xm9WH=E^N=Ie#b!#V?GwcL(n-}lU~eK)xFyG=WHulBUb9zqA{K{Zk5 zTpn-KCx=!yx6~a@qYS0#o_fhYQIA+ddo)rQtNBIx8&X|Y)|YI_avI4g(o*eme&hAh zHqzR@tZ{R)msOBa{*KnT%LPtCQmNFu1G+-Gu46Zoq<3S(&#!;mzOVGHPFMTvpLcs?em8hzhCEj zWJ~c@^i>%qxm7mnbmzf+0ZW@&u`dgkrh7$<9K9uIfP|#Io~z#fQlb3X{w7gh1FSlk zn)rO?(`Ki2K|D{_z)ezt4`0@`>-!-3nan!fA&WWSsMEvY$b z+T3>~;B{7ax0#?R)TQSG!~TGdsD}Jf(@K%3wE9r%H22P^t|%gDnaic9N7s%xc{O%d zM2jOMFPxJG1{IW*>1TC6lK1-2&o9ya?kn3e*V9MH>2Ho5KfdT$E&VsWe;EEzm{(?L z|KMI{eatM!uT91y!mP%-^d3Kcd=JM7?w_ftaiX}mqcOSN_?|>d@zz9;5wpfml9znR z7V|iuW3;KU(f!fYgB}m=b?mIIUVzOn8YVD;_*j@sqcE8OGwT7ileLn%0! z`sjhiHq=W$lnff!UkrKZ+n^!rXziwCHf7G%CGI^@lRLUc>2{^>*!~-bbDST=-+63m za@YLgyQ{oNBl~gK+)Hz0d^Br8cahfG8Gc#ooJ@I}_E3Xs<8{Wi@*Rv_{4&a`U(IM~ zJ+mxDoU?DK7n*duS^dmE<^&%U{1~-3Y29Vkh2PHY?t3I)a^kki>JN)6JN*#hT zY?jnm-6ibvcAKf0{kMGO#pfD2BGU(!rI(U(NKfJ(-jtTskFML&97ZN>d`BA0$uXTb zkMHfpwrFb^4$XL9unP`0-UDuPC1~2&$Tu9C!5%6&R-rzbLA+#C#Q%G)_20Xg63Mpi zulPSltxEOJ`+9Wso@C8v%dQL8bC(2PSzf5@mAxysZm_G&^6~Qw3q5UeiRvE_EZ}Q; zAD$m`PTQ{0_My%)Hrm(yh6`JFRj&hv)VUWH&a)7ccMWXLaQwZ!wH>>1o(E-&ujzcF zZcFP+BVH+cZK=CIRgg^zV2xNL-aHu$F_}tyb=m!u34PPz9#Ma_3FuxcpV<(3S_=(P}cwl oV{5Y?l*5m%RyH*@t{T^!C;xHb?ZASZi9Zv(+ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/lat/nsb++120.png b/padbuildimg/assets/lat/nsb++120.png new file mode 100644 index 0000000000000000000000000000000000000000..3c7c3d4b166d5530a866854a80b8ded4d9875fbf GIT binary patch literal 5459 zcmcIoc|4SD+a4{JsANkaVtXOB(I7@DqBdA zN>2(UOHWy|gk;P1-P+#g`+m>+y#4dd{O-B8^E%GsJdfkLuHSEBEY0_T`xaIBjDdc_#Rpaz2$^(2Qy2!0f!61 zi6{(`fx!T95(R}N;_*a`1{}Zu7&L}}#sWwzk&3}nu_XA94+6@@WwNN&hP!{{g6^~s zZhXEE6^-`y_ec5TP#mr+8cU&2&=>#>07xhT$qV%6(*ls*ysbYG3_%`)%l6^3Io|MP zL>isr%hy6cOuv`lHoA;wlP{YsxG#@k;g;_4?XP}wcKZkmG{fg%CclkkCe(?QU zV4iKD4~Vt~c^qFZ1Ki~Ydh@sbOy=Xp;d6Lyod3Y>*X#eqkjeO!(8rhS`K>)n1{(AP zy&!BJ6pQ`F&xdNj1!;T^*OtTa{3(>>&x~*Yivr+k_H1t^$Dg--nbD5{pdpP9Y9S!i z03?Ql1hBSPG8F?*aU>)LN5x>ihnhh?VAA-s-wWn2*{s0d2{kjL8hi8jG;aoIY^a5R zNTJwlCY6c9kVy<00Z9U|L?jV17LtNtV3AmmLZD-?1Tqd}ed7gvUmpidM{=03N z90r8(tD95?i9*1WiDV>}Nn=8G#bc2aki1+i0mKn-GztqO{Y10ivZ0wn^ZYC7GAky8 zh^Mnibbw4kGUyZ(FT2*>#v4r|Gh==WD*@uz_XAv z8j*&?0C)Oe6~f%^y672a$AuNkigrECK_-(<#vRx3Op-nFuiONG5{< zLWM%phfKslHPQew86Yq)B;0Rf(HI111dWb_<}ZjOFd<$P3X6hdkTC=%nZm?^_`hO( z)9fz!mYB7Qfi%T1_$KQDp86}PaT|%G4 z&CJZw>9^jzt&Lu9G`f7r5TaO{qx)fD?>b2tC_Qph&y=rP|)l9(4Dy&3W zTDoGkFWo4ySuX#6dgQ>isU?ZPt?$uZp$88tl$4anM(NRL??+7F;#pU(MzLRAJ(HFe ztMN1x%Ru?gKa7z)JN#AHVC_~Z_17Dbtgec{;GvwH9C;nba2b(sdd|#L3zNwdOR%!H z7t-CgPv(;4iM~D-000SKYUPn>>;3zUHDnB|q_{j@>3ALJkT3JLy}j)&M{#m;QdsGQ zuR?jJc~B5_*REanoC*%l29&})(xU3SFF{l?GUU%1;iGEb!;wfYr#1Bvx^P_vV=EGg zgc&7977WRgeZqQA`F*OaZ3y+6yj5}|J{9LWMJeID)84Y^m~ucyb?m`)FZrDT;&bF{vFhsjB5eZ{9j0E8ze%os-a>q<4}?|~$6xPe65sVQhsJk@95&io zzv`}Yb^_`3x_ql0n`_TD<7s{wxdnMAk68cN#(As6v~0Vg#Fwkm&U@9>rF}(b>zPiS z&eMXim*K_|PaM3BGcy8Y{$XVmVS zT^i7i(V8otiSCb>Z(skcBd9WUW}mvP|M(a~=976*aewx@`%8}7c`_Q+-YYr_KHpXM zyyE(%A?p@2ZC3tbIn$g32SxoKm0ViN~#MKnp)c;Tz)5!+sKoe2=}l(nQ^pG?PuB3 zq8FD+;>?ET=bwu1xYHvA0Eq$Py|Oxo{4cIW=N;|GYU-#@O_ZOO`e0-2P{j zuHM7qjl&F5eb18xrmM)71y!MZIfMDBq-?lHXOxQn(tx>TnuoDuZbIUvD>8lawko(? zy?@+%e9u)g6^qnWlhU_q!RB7NrTe=4Ze3U{d_0r^8S1US2yb}zAG&S z@4sn>x?K~S{_LJTWo+N71oBm*F!8u!@Q9o1HZ))CK1zrumVo7E=@Kc5%V*7?x?wbv^}CH3{Y zvXiY;^wLbXH8HrOp@$g}YvUqedFZ;t+S=oHyvwz2sg>V3*$p5`}~6*!+CShT2dbKyJ3iy!M@lDZCZvTl4O9xh&ehAR0+0G(3sysqMX z@cbV-`pl&RJ-tYmuU|e!^dOz?B~(=Gd;XS*F|XWn zOC6t>^#bUDWQ*tG zN@a6kqNm8iipS0B*Q-1`)8%5?ya}-`^9IgDrvq9*Q^(}n$_*mLZSCxC1<(0u_-k9d zw0ONiYekTxu)6F9OF#(_nZH?}3Qk|w*k2SmJUnbQ1nkMy@TAseNQ=z8vVL)zn8

o&XsL=oKPcbq0Wb-QCn0&=Iqe3DXp!CS@~r9fP7ZSaLwj zBs>&z8X!GS+9x?E`VF4xBn*53_{UEKLM92PvGv+P#!G{@8(R(=?;1N03VP4uD^6b6 zjZmGz4oonUyH9~$GGz9G#=at7QuA~#?5~+FSH2q3v?8PdE%f+&u*~M=g$mUCeDcDb z+*+XMu8_&-rd(1_;UZ>Qk~_DXCVemzs{$X6D1)LvMru*27=s~zAuxRnMvA>UHEOJT zMt;n=Z^_zuwFit9>k{Mp8at~;4OwTbI8Z<9l4ZsLb(ir6dCY^xr2JX8ZZ-ypX?rhK z+i#PX$O|7Z4v51$Z}W^7wN`D5dh{Gsu}UUm5`#zT46Z!qo_wO2cg zVdAN6D5N@1pd!e)TTUNO$`yG;7Qwcj4P+nRBr>nvu*uy3x6wfW^WA!iR%lSFTbD;z>oH>RV$>u_KKRG8P7#X&g(*)iI~`8zjHr`dHMXB1IESOh#3qUM@agRC*^HrwXyD5+ab`jp!JT8 z-=^t+Y1Tw@$%9Bo{RhGZ)}YRO&^90V*xJ}sm{(9F0=arF=v!3e@Otw=U)_-dVuVVH zvD_*tk_$y%UVdSI;kZ^Dwrb;Ba}b9u=g2vsd{34~cRNB%Zqo-U#!|yx!Tw^4NP#jnVCQfon~2m{Z07;3D&1gm;Gd~Ea z%E(v0a>zKv-O<=wt;c$$_v_~xF>F|4V-r?4O=(R-ltHdRpC{LZS;>A0q1vtBwtO=Mx{Jwa%*{RA6|sBS72@J3*_n9a7C1*nlWLb!*o^x ziqut#r;PvIM8p{Tu=Ci$KQVXZR6~N+xG8IWxH$r`HCB+ z3j5-@%eOgX@2}D)j_v)xDl7Bd(GP8EX;o>ymgP4-EK$|N_q=>ql}Ak+)0|3#*D&1k z^NG0=R^JRi5VI;s)V4|*E8W^Sp{_Ece}C7gkt2r>AA_aJn6xp<x#PtFp%D8{e2! zSvhXVp!zX`#BmvTMmSp=_MeP%@%?bl%EK zDVxY?zC{Xlyfbk5tX9Ts-`%wbPbe zZ(HR*n9;9y*_I2eY$Ffem1ZxG^eNCPjSuu3o9)pD??2>J`o)lZq*tHvFjABF^Icbt z%57VC;~cU@tt2xX`K^o9Y7rkWR?(0Bwo zqL=BYdEr%1`63w%{q0_Y>|J|_f=p(;dxdglWoCA_8T|xhfZJc$mnAl2%$hNMR)(?3 zb6$6Y(e=jPHy>ZPw0-A-^Uq&k?on;;;!H7%P84163{TI@bRvQ$C*AGNc4JoeNywRn z{hg@@Oow|*$VtXo$XSlD$fh+FmAnBjVG37`hE0+1;JSt&w0qRZ5Kp|}ty1h!eRi+8 zH`$It_FfAN8+8_$Kij4LZ#MM*&0w+p$9u(G4r@rE3y0-Eo+8V+ zdT3Ou?xotOq@>ApKDDH_W?=RTrR;;j+Bo&Z(lqFyQi}kDN#eF=5Zb1u`BwIwxn^F) zg3?~SE2{n2{m!>#6_C!rmO_nsN?lwtXJ+II@PMoxpfGmp0gcwu)6f zX0|0A8||a*wOF-8;OAVO5e_@Evx{&@6(}iD@{1%X-vwvyvXXEHtR?{+aeMi>+0Jb8 zjB}C+f$MoSgm5DirTl{4s)etEoBWpRGR-ncOa-D^61#;GTBlsAG zmC^8A|3mSrfz3K{0K6jNqC#&T*l&yYU<%=qwg@in6TvN6> zF}O@zA;yR%u_ipIwsuh1*o^%5zZy4hUa#Z3#mMoC7f--QTROI(O3Hg!O;<`Y8Y|0! zN>Oi5T24_8JfGbq?2r~ILv1;fIPx`mJVp6A3j8Bce-Auh4X)cT*_=u2z{?a`JwuA% zOoJUSh8afIehEsDhM9>ZDi|VNLKk9;^Z;_f80~*aYwlOy?E2U+g!D<;xPI|*X&(vO z**oW^jcvg7ym`*C{p!ZL=E@CU8-FaSA3ChAOjLgSS5aP6-#9E@X1sCg#K`)g!{eiW zePlE~t!*~7m-8N{0nOEn6Y%YgBWtr8MmJPV89UZ3TrCq?TEfGI4H;53dURn`;p{mR zCrs$u*jUjwv%YUV3;=!AaK*@h6$2-ocvGzLe zjE#%N4hdJs-nwD?@GI6|GvU85zvYe}2K$@w!jnH9kQg?6!Ho0fIyZfx=au9�LN9 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%0DpzNyo5m`c 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 C6>fh3 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/10.png b/padbuildimg/assets/awk/10.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9cb7e1b975b29ee78565c596ad24d9c9fe91cd GIT binary patch literal 3016 zcmbVOc|25m8@Gg1LMU#^7?Ct)Wu}>CY%^i96paSmyv=fCVwPqGg9wFeT}w0BuA5L% z%2v7&(n8I(q$si^B1%z~;bJ*mc7TK^KE5=o^Y3^a39xK7);{>a<)t^9RQKpw8MZqKx2j+ zeF)&? zTws|RpXrNpC)xkdlC20}KcSF^gTaD>gQ3AFD3^a2hQMO6OB_g~xy-^`5W*4CMCKfU z(J})G5HR>Go{+`mfR-3(bZ(%K0G4_BJp?w-#pN$zj^Ib4WGRD*XgnAK3Wu@TOL2X( z76{#e|G4qD)&fch4}iG?0&XCmAv+IWqh+uxcmFK3WGM3nw};P?9SSXg#AO7s0gjMN zB7kLIpiCAMXKR7RAT8}J;Yb8_7Yc#EqOov05_$&$OR^wY!cof{e}}chplq>33laj0 zvO^$tq3lo?3nB`GLX!|A651BAj3sjfLK=qwEbFplx<9bQf5hU5e1In8@+n+yz;Xrb z_2UY;0zWPfMBIl4nNnFCCO25HZRvQvM@s_utRR4C&*!p1-|~xN{f)k@<^PKNJC^zX zaL`+0w~XP z7W=YieGOV)l^w2$3=a5%-u_%;gZu^aypqT?4Sf>Wqxhuz(^!gm3B|+hvb~3K0?F>l zkV=X{@@}xheKt?kFj2GcB|1t@J^=rGbK^`>vo35Z^~=s-LfxQuL*2EY(D8$2h=rMWqaCxe($B|B zVv`dm?Ot%AM^bh(_Y1Z@QMsDxd%cxQI9WCdH_PfN{XHf`J!`Y{(B~ui=>yt1N+xHn z1@>0-hxyWjTI-%FyS{vUJV;@zU=u8NPBmn|n(E7^-H!)U9VMm`_Qcp}_ovvX!79|} zp=QlXUN?$TE!>alS#BhBf4v&qsG;iI`d;l|ZUb@Wu}h0LRPuh&(p}YKM-x}2P${Yl zzuqgwhZ3*lqReXlsj+VP(IU$~Pxhr_>C`Xrt*xZ)~Kz(9VtpUrpQm0oS!B+r1U+ zL$z)`wz3Lae|MiyYV5MwV}ET5N$halQau?+7STtxPl(>~Dz+x9s#d=i!!RX3tf=34 z418mL#guB7eu`rh&-;VoP$9*+z2)ic=CK?r%4zMr+U6!GDZBFgz1+k{J?qX$F!YWw zgO^YCcuzifY)#w2*UBE$gXFYVd+r@7JMrjJTu2HpdjTA>B|i$)Z5KXpt|OBCF|EVU zy_}4@sn9z9oI{zWMcsu51|Ho_ zXb+`8z4xq5og63%7tOXSRqC(xi~BUxX?hS679R`~Ya38gs@hKKZ0xOgG^O={Tvubund>oaKyekm{dLFiUgLSB)yGeZa#o9!j={+r>^ex9W6$!QT<8{3_ z@^^KRN3tqYXqrj99Um?2Piv`~#^@h5oDNrn>_4)uIGPDgC?CI!2&z^etg*Nwk0n0Sh<}i>n|#yQ<(#AWin}@)U$G@`rVC3K*%tnT zu6T4p%jK<(rm=BUt3vr4!a})ZPA9R`R`O&=#Jde|#^zSyWBj7(6q5aO4X=5%E%q6} z;p^^+ZFCzCglXP3XN_>@+zb;2p1p#;aj~~U-c1KopKr3dcL6+9o4-;ReyXZ}^yGct z`p(7G&uH2qCCziZWPXXpN zR|+44(rhwYSBHe^kAF}DJJ0i+AVy_at-z_LPm5xq^Hx&$er!1>uW0SYI$SK49_jt?(#!ZE4sz>M1ElO|EzguI5FNeO3jk!e${8i^- z-&=>qM&c#^nt1S6V>V2jnj+caSdRaoYvOdPYz@`X7}W&B7+!1NozDw!y+a>X{j%9W z+%~zmV#U#QwP2$dVr2F!DIVR=pL3b;$gKQ`)+J42oN7l45RD0a$6ak`&+-#sh58pG z|5PyAZDu|(r22H@no>TL}Jtk<3Q@)_#y2@b!Xz8MF1y)L@V zUZXgY|K;3laarL~1tyM^@e*-xbr)ZCiV7P_ddG_R$gJ;Jo%w+Qownp^3~^G(sHViQ~f mHh)Gus_WLfX85?0u3UTZWsfmtVA0aQHF=jS>8jnK=zjyr&=#No literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/11.png b/padbuildimg/assets/awk/11.png new file mode 100644 index 0000000000000000000000000000000000000000..ace29d8a1db91d5831a6ed605788e29945302ce5 GIT binary patch literal 2860 zcmbVOdpMJQA79j?6h)*XGsm2^nQgYVWQLR>#ukg#c4H4awB2S-CG`|JloX-#h(wAI ziX>9Ylbod@hqRI&FDi+YcPh{O{_$MTdtL8+-S_=F+@H_qd-#37*LBCaxj3pQuUCdZ zASzDV$?oz|NB%&U{vv;m#N^n@hjpMm9rWP)gP}|z0I_BB_W@8RE;9gd2bk=z{fz(t z0#WqmQ0X9@;*4kUxfaZMjD>_Nkh38Wf{jGLWCZ~rbRQ7F;SphD)wM7vhfRdh&=dqk zUf}j8ohlhrST7;r3_`(1<5{JXhYgk&E%Ms?HFdoR1nDaz>3kqaF z#1e7@AcxO`&MPwa@k2l&Oz!EA5V!&gBkA0lMQ&%DJ)B_^aCS#qQXh z=D8}GA6<8JYLruNa5AlvG^OvJPDrNyO^Er9=F|Q<_-IXcv^YY0r`#hx(?h%!sa~f1dnZ%}P6EvOauSht``x>!jM* zIaWK=D&&$|W`aq21u!?9^)T7-M)tD{lU;fHv>3dZ#xxyip;7Zo|2+|7eQ`NPwe}K~ zOEJ^SM)@+otQ>#El{$o^OS5L;Nc^FZBl{_~(*~Ynm|GhtuygN*y=gaon&dU2ZLahs~;(!L#!xw_|w$#pr$T67!O8o;Xv3d?O%#v8pw z4At<<%<8xM0yFk9`kSXNTWGbVhju=Di|*o?Ue zDb8$H&qQTutW@z=b277FljquQ^u}LR_@2=bXVeW=N)kSH`P83H{6*u$`0*9pC1-j# zWur?xqPdq=Wf*^K)Jf2FHq^`2i_$$YlCce_;U82e7^OyM<2y?|4?VuCq_DnkEj)W_ zsQkF}@Wp^>=*=pM$`OXe&6gGDcisNluKvUlSj(gUcZ8a^CsjsdZ9783l_RS+gI#fJ zbGK?arM`tW_Nv{|hRZ&m#bb8+?zxszKuicYkz_aYN8K?zJ&^(St6jDw6z+XD53+dYGU|jZvAuo3cWiOz9`0hGI^^XJg(-Qhj#BR~?0H zj-_2lr)93RY5$FG8LM=lC;si=#uu_}-3+?@hUKf9{Jm|+gFWUt*2WVn2U{yvJ+RAC zIB&0HWTsRTnH7nRN8B^E&Z>+O(8^l3RT3sz4encAuT@>7oFa*vyq3`YuNHmHOWu0< zMW}HDMa_6=AsBhh=J*m5Q}Cc4HOfO-V`HzDXJX2GsYX)a3jgH&1Gk7Sg^qqc9+`Q~ zvhj|H3d~w=&ONo3Tz8w?`yfnphgaFzgmW!rzLE&Tb=@r=4xB1J$Ruf_YgR+GH8!3e z76%Kz5LlA2sh*nf+j}oI!AjD1g!SloVUOLNbk5W1Rw_x83ZKHAUvAb7OXY9LA3jWN z_(aryzr?XeP@Q`_+O_0luNWHgVszqA|0GlC9XK^Q@$R+SbPTJbBj(X8yNOw!EmnU+ z6`5~2UDnvY{`Mys&NqwXxjv=sF6`0pz}T1lZ&wgKubcLylLaN^sH0&`CxLOAk|~l{0)L+zs4)5Oh05B@+7&nv#%#w`GBm3oA%I; zW*uX6^C*p-k2dWhl$8V=RF^f*P=rO=W>s4&V(#x;=Vd(ZI}kzq^VVXM4toP?mY)7z z;ukICw2R9iJ(%rlGxeyj^Tnk&g;h?hH(Ej0G2Y8fAFT=e<+Q?~rRYTO*a{%o#g#R^ z&uiI*4bAUgIp<6jS-@VUg_HP~jGu~Lct|U&Lp^u)ll={JZu@TKNl-#D| z^72zxf0fcjeMXA3g23{$fooE%#J{z*ZhB$)xYz7TkZN{;(bc5PZ{2>*(5AY{oGGkG z(pnr8^VQF(^ji|Y;v&9asB@^}ob2#ShmFnLV6px)Wl{W^<_nDaPMdC%jz@_%*$7UL z&tac2itdxZ-yf>Y)O|Zx3v;|>(6&Bfy03AoRS50H?{fhh*}iek>3rWGU*CJZ@B3S>`@Wy&`91gbUe_CAWnp}bTbP@TjqRAp zMT9kL6lXmEE>70_Q$(R2YY?ItI?`-N9yEV61;?g`CAs4OCO&99&Kift2Kv6lX|b`f zOA>4yX^u#97>48nL?2*)0X}3Fn~hCNCxDE`T*c71 zuEHh%O3D#w1u!5{Z~#@HvLXhoqy&IMfl4Z>P^gju00LHmfWWL9s;Hy_Q-#7HYJk5l zNmeup)&pjZF#0W2aXN5xPGWEz1)1RN-$-AR5lxFjpnze?~S|07GJ{(VfW0|N!1$si>l_@Ja8KqT`2 zLw$VyK~rhgxc}t)KZU8bfn*%W8b>AhQ828=c}N|EBEt+QI5dq!u_ckbf1IL~Cy7R) zdXmTh0~;vdoCAS~CHYh3|HLDaFcTt`h9+WgCJ4ABO94nAU||}nh6aX)Y6vBDBYh<$ zLuGYU4SfU{j4;xJLXFg+`aie`62{L5N2LAWV*kT6{3-Xq3O;03WCV^vxQ4?TQAj?3 zKTC!Yex8f|Pxb!hVt<~C!B4p$Rx+T2#{O5Me{8Ybb1?nKwyecJ#>Wv^Zl|zpy?i$Z zz{bX{V}j7L4H&7t6G%>77wXhm9;c7>g!mkiI4mu#t1DvFz+uR*Q9F65&bgCURI}K2 zVc3GNws6oQ&BD^yC6PeDeYtmzVyS1#8*^rwn}bU_>a*?V54X2^E)@{38O-HJx# zC*B(&q08GLxyzdkH@Mx>JLutc$CgGJezyu#x-3sz*)>k_g9suSsF|$^z1@g{NK*VB zy12tD(Bg^sv5tm-#?y9o#-#oIjLG9NgVEE|c%fzU(*8unYm6J#fj#>RjoJE}@<=HB z#)8@B&~2_OL--d3LAk>_@NkFUj8kgT7GvJ$9Pbu>GrDt#onJ?Fb(Q?lsddfGwKaN2 z|H4X!DakTfA~btxO6q90|DK~}%@e1HF|_+#otJK*x2Vf}LYtK4Yo>BdRtmDP4c`1? zO+C0!K0oPpoI7gl*w!~*!+F?iv$s)gkd*S`IrX>5 zb(Gp1B(YMVRER!^Wecqi3EQoDh28mBRUmWYnU2^-VX=x>tnAw%L5cdyn7&yV{)TYT z(9~T`&U^lJ&&J~)nPdTEu*M@LUCC^ZQFA`g``R?r2~%OqT+teiXq1H9(5t8*xnISi zySUFrhL~6^$McA$wZB$Exym_ds0P==JIX7i(|qIW3xxf06?SInXO8e%L5r#@RrT%k zrw)&qh?$mlIHpZhKIU%=lmXIE{A%$pi}FiU4U|bS@j1N-cV+NN7QA5GiWBh45_;DX zIuYut=0`n!E1ob;iJut%7FgbXzuY!z-N)F;onl>7GVCc>C?GWd2mMvEHpA_L@+Iq! zV6qi0=vS2gmBHkjL&Z4g#!kx}gy*T6rAFhYg(C0Q3lKA~YM!ed)YGLK-cp*NJ3wD_ zG$bMP*g980YB?>r$7|#Z<9yC9#QKJYt0;Cn`|fo4FXz2-P~Inmuz7k-vVvzMc^;1u zB>QJ4+g9M8o%hck+AS?>`A&Q>pwnsh{=3kxX>|zpkoE%mtx(8b^C2RrVkSGe%w#a@ zy!kz$L?0k=HDSv0ZmjJg?&jj*&$8^sr_4o%QU!U|WX$`Q&A{`{a?CHr*<;(BJ@oMp zlW(67$5LWq{i88F86mUW3ifHwT$#P;oy*o@@$xB`e^n ztdnIgD-RWpY8w-^IAwX;XdxWUZmcXK*lUQ4EM0$NgXT~e@fU7s__N=~UUjF5ww)uzPKs}94Xgt2p*`TBLNL2sq zUO%fVE69HI>+!47z}#|1e#!40_j#(YJz0Zmu{*u3Ey+pvs7xR;;~c_$bi)i}Fcv9o zq|#F6p*3W%XY-|h1(@Ldl|0d|;#EbDRdy(p(4-q^)>E6@G0f(m?~&+hi{9>|0}C;U zYfS0}$L=GDf^z!myUW^8l4hI~p1Z?gK!C?@s!JSH_e{^&=7ir+dqbay;60k`;K|F> zWKL0!*l_|?{d$0CPL9ht1_S?~CjNzCtdNj-%|@9+yrYRj8R30k_QBC$^=a;dmT{ZLI z9b7@s%%SEJpNcpyYKwap6mChqP*H*SK09=);~tq5ro3HWkny^o=llbcEyIeoOgjnD z#Ug&2+pVeLr6?~2{gxu#7|-$Q`>A33T=LX2su~LPckNuUv<41)_zh`IkQf}u-u6qw z^65eW?8nnq610WQ*DJ)^yzH>olBcT7DnoUzTtEE0rBuEm zyDhD|q`!9kx3y>wsf^1Sc8_^l1j5QL^%%Q&IX9)wY72$PD*9lEZoGKj_>Np0e$QXz z>tZ3{x{OnUp7lzSZMi*aDHxdNuh}EckW+ZFSc!`H`VeyVVt8YlWd{w^bfq=d5#07| zKF!>boSUK-Kt}K3S`pCo$fVq&oz(rJB$2 znUD=Ls>w$j#{C^#m18%dM8KTRxs&)4W}50xmSGgH{T{V9sokC*i<&Uv*ImWhaP|*} zd-_X@%D5^Gmn*VdeXYxxOXkdzE<@Ko$gh}*RWQEKO79p)oV;Vplm|T=sm}CWq|A)# zwC6fxWrV&zn#^n>+#ntHExU#}?{R7TOi^$UFtl|WzctrYUdr^j79 zyVP2m_0-eV90>Qn_TLFaM(W2NO#&vyREU@5Z>A0`8E6P}oS*WjtJz<-;pmN8v2DV7 zR~GNO#haJk>QgKmBqwa<5u^m{P1@}U%sWOaJEn(g91>F|F@_CWvzR*i4QD+^kCny5 z+!LmQy?JT~$~yYK=h_6N0Pa`Gq!IpnQFSfDEBEZH%rzz75V73XgY$c?lqUYfE%EZtDQK|11 F@ox%sJ$nEE literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/13.png b/padbuildimg/assets/awk/13.png new file mode 100644 index 0000000000000000000000000000000000000000..8917b877fffc1f48ad6da577c6b81562cfcd874c GIT binary patch literal 3379 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/14.png b/padbuildimg/assets/awk/14.png new file mode 100644 index 0000000000000000000000000000000000000000..23ad2aec003fc3f4cb86c0035bd191ad8e6cc6a7 GIT binary patch literal 3205 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/15.png b/padbuildimg/assets/awk/15.png new file mode 100644 index 0000000000000000000000000000000000000000..56a2ca82dc92fc7a1d41ba3dff7d56bfcc9d117d GIT binary patch literal 3222 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/18.png b/padbuildimg/assets/awk/18.png new file mode 100644 index 0000000000000000000000000000000000000000..bb5e25f3c55da04afa3995b73a45ef03f84bd577 GIT binary patch literal 3265 zcmaJ^c|6nqADQ|u{3a`1_6iV*;?9zu0= z<+u{8(If^pUWhY{3smoaf&yUZVY+dcoPDql8qrQ2m*}2G^6-X z%|loehY%Y_QivbPkPO*x0yg5Jg#t7RhY04<{ON2o*BJ6$7cHExZ9^g8?+}ikG34*0 zTnUH3W(*bujL^~3Cc)rvFcPT)*GC|ca4oPd46X}>316f(Tpx`FF?7-cY!qq2Zc_uCBHaq0J7abBJ7RI$PtX0+zxi zv8YTAl|cuuDH4w}0yxGHVWxi{fyVqLOK1Nmldxb=E|Cd^>%i8I^b<%R{C_Bo_6yDC z*i-(=_g{tCj=@X{)Skj-1h7cL#(8P1g<_)3SQH|M!E$6U{C`&QkPm~yVEZtbU^53K zc$W*6PG$tLcYntd2xtpBn?t0NC>B^_h)_X?N+qKWkuaEt40N9AI zz+xP^V>K>P{th!+{#tDNcs*>@(tifS+N!0g8R8EzZ)hB+7u(p)gu;obITmKkaoO5`!J@@SfFPGV6C!MO1 z%L8WF5m(f@vhPigH>jw1KmFXCC}(t%qgx$0y0bZy2PywNoMiw{ksyvuY#lNFY9MAF zPlgR;V)ZkSlZ|oO5|0+KENjg2fJ9BHz-7~Z!d+j?)_7;VA$6X=TH8EjQp>LF;~8Gx zqG}}a}<=rwgTm37b3=MHw5ZjLJqEg_Y=wO8^Q54qF; zC@q;wbxp?%6|TC~Ma>yMCZ+&^M*Ydx1G$N&5;-lQ`u(OjuH4L^0GBJcjV+U%l4 zA+rH=wtgMIYh6N?>F2ok^oNi5=bZLO+2*WEJ-?n|pd2s=t!D9B8yUo@c6|4(z(s9lR}rv;PTbJ^taaoZU3y=G~ZJ=@re* zbvBJ<6oB!ZMdo;~&ex_BpqyqA;6al4vW4J?jVZxL#$R>*^A4thg1XuSzx|jBC|8N! zW+8F~SXOQ?7oPiN^vsLZCDMkO@e~2DGk^N6%6oLV+muJQa!iMmH`!y0(|3W@87cr{+T&gvCLCI&K z%jYPtMRu=|gJ`>8Pb}eh#CS0Fx~P|vxFAHq3gleaT&pJjEns6#PuuNmJ2wqm&5$Xx zt<=`UDf9Fb8o+w9@O=m*HqP-8ni{uAaqq%?f z9RWrc*4-zzerbHSZLjiwQlc^`2*DS2(Zy+{jdFFl=WQC(iahn}2S5vEkaO5vv*Lk) zBx33KpHESoXL6kGqKxTH`8CB7xJ6Tsmnx@pb*7)yBQx;x$F@tEd@9EhC_GK}6_jIz zXN%ZAbRzi+p=%q@4jvn*=6ViMMAVHtRg)5PTY_^qB1t7$R5LZDR8`~CctT?7=zM8a zZgeY0rLZHa4lC>K8tF0@&Y$F&%uhrBb57zs!?b%YrkT@2ma?rp19%|Y&cF+BA2(ucXEnZPGQ&o`Y_+ycglq^&fgf?i6RG>@F= z+_%`?%yud0ap%iMY*bPSn5duj(CIpWijT-MS)V;Bdb4{|dj1^pd8fT;l;pO)Q~LIDgT>RcnR9ji z_|$lFeYcXz3s08@%$A%&j(vD5@RF^&zfYn%Ru4P-;2QN%?27ij$=f>@Xd@9Hfmw$W ziV6yWCVV*&TajlqaZ+c)W`>WrUTS=I;qhlRTRA9@NzRQmz)se{CblHAFZ_v{b+D@Z!_9}zfI~tA+ZDx=oi8>H#k~EDQ#uDhF`Gu0 zDzg(b1|beR@06#V?oo%4=H?DxEj*#@g-FBnm`ivnV;t@n>RazE>FhZe{h06VTy^VZ z0^MW9=+vFO58!Q-DQM7OOCW}svAGm_Yrb<&#p5OL+Y66eANuRCJ&tOn&OF+4g(+G%0xzd~4xGVZ#L_vmubyx&L@R0~}mUc6oa1 zk<;~0ZKt762Jb1{J^J$A{qV4R$%nPHXqp74_w{)rGkRX5%&&Qsb>DdWH=UsIB!l5- zKSfjh8Qn{1F&tA$l#U0gz+tdfM~(YcB#CQH=+ zWL5wBlD_xXDn>jo8a|!8g>A5=*wrtm6&Y@ME@XVpCcffw!qiyoRjAqy{;oPdPinhQ zlcsD+cTM(PU`3!phlo_1Rqqt)MZ^JKikP_9#B}uROtOm0lJ$qtq7{dZ+}$r)yqBE^ z3Tr2eAA27Si;4Sa#ia&_N2zuD?pWR%VCeR)kRR&$wbXXwkXwDWG|1oTlK3RBTKt>p z1GnZp`hw{ukJ>%gYOB=Wv~1TnkDCa%`%MxCNU}+P+&XNRzILgwz}a9gn;wh$FO21% A+W-In literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/19.png b/padbuildimg/assets/awk/19.png new file mode 100644 index 0000000000000000000000000000000000000000..64f530ee45f75b6869906e2b8d1def19f5408c30 GIT binary patch literal 2890 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/2.png b/padbuildimg/assets/awk/2.png new file mode 100644 index 0000000000000000000000000000000000000000..c71311fb2c7d7eb5e22d47a197e96a283e7706c3 GIT binary patch literal 3173 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 H+{6D3kjjW! literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/20.png b/padbuildimg/assets/awk/20.png new file mode 100644 index 0000000000000000000000000000000000000000..0963ada67d3fe03db8802085250b5ee0bbc96591 GIT binary patch literal 3333 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/23.png b/padbuildimg/assets/awk/23.png new file mode 100644 index 0000000000000000000000000000000000000000..deaf06b621719f6e2af7bd1e803f5e3115e79c59 GIT binary patch literal 3284 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/25.png b/padbuildimg/assets/awk/25.png new file mode 100644 index 0000000000000000000000000000000000000000..ac41df25db332d7afd328848d36c7d2908643a9e GIT binary patch literal 3317 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 literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/27.png b/padbuildimg/assets/awk/27.png new file mode 100644 index 0000000000000000000000000000000000000000..09c372de79bd1c7087e1ff794e21313363def66b GIT binary patch literal 2466 zcmaJ@c|4SB8=kLD9m#f*HOm;1idl@YOooQBjEt-y#F%+8G4qa@*D@mUAw;qyF@&OI zDxI{*Qjrh`m8By~QK2l^Iw7XwFCo`%3fFnSq^MYFdV;F2b zg5m52xlwHKG?0m;u40hkOtz2>gBhEKv#GQ|0D}7h{tOlYFrY3OXaJ*1}2|!eMI5UXF#fK9R-*oZ9`RX~w@T8Y z1pdkQe+qM*d29gX2ynp=4o%oNKlRm6Y`i%Kph6(W83cpYPSHL9gg|Zp$cCFc>BF^L z87w*&#@+c1Podz+EG|T4(Eu`ufDkGm84Nm}WT{8Qm|K`*3@yzu7z->>536g4MiX&H zL>$(VWU7Q6rfvjKqmZK$#}-kb0Pjz?>m?N z^IXh+%0&s2L9I6SzZ$)^C3Mf~^oMPQiyy`ZSVFgRgti_iR4#+THp-GoMCb6nO4q<3 zXN#?0rUt632PRq7FIYqSJ#6-o8EOY8ikr-~;glZsC4h~AR=s4#)F2#5_! zN{i$s+SsW7_5hyN^$=mrtIV$KL`FFs>>HRGJkrGVQ;llM?jg6=R$)gYhxl0RXqyYn z&6<73Kdh>C1v@Ib#)hMT;X&2DZ;q zBs$JzDw$cPOSbH^OJa;Y`m|r8=F9cjfh%4mEhfD#L>pC;+p7N2caME7ce&;jn|AoN zbZzXDy>~2^jOKaje+VK+6QagctKx@U4rxT-%9Kpzf{szN=927*+0mtF3OALjBDC7xp)~+j>4%Sk*1%eWKx?wFFIw zs<6*b=Bl=p6EQD8_EC!{)J`$0jZh{3ZtAD7+N4I0g5zcCvl4j$M!&0`aJ_hc{7GqC z#I4Yp_*#V|pF?=ZdHW5v=5Qr+CsdSv6t;->9HBne7xr_elsER+KC#}VEGLF$)k=c>pDfYEQ?H9g`+iZJar&VK0 z{fMCVPs6s%Gt*ViF=^z4wsHl%_1OYPxzDL`Gcv_vhoZ+ zd)Wt9Pkgw&^N8H_tmcTzQo#yfQ^d%mynl`ITu!2(O4gm53*)6%HODMW%M`z|MtSbr z)$I0=U_VgC)?m0*s~bF}rldAboSbBy5%T)uGQD~0b{BkXoXkOSx(Zv}MH;f|C+%EC z>S}!Z@@eqw3wl}wN)Rp$UaLiY?C`!$C2K@rFo=v{<3&&j$~9qpHw zHQom;`zSp+;cG7K>8xHbmz|b=;!^Bk2Ps8S;uFE#wYL#&!;KYZn+mP5*GfO+svE0K zARj+yU@|NGe4#<>2@x=UBNV!AekJYce|9|F*BQyYKjvsYEB!Y42|S$}jUhSP`5O zDdA!oyI-2UU9z~^tE9et!6p4)IXQ5fY$GY--q@HM-e&J>H|=XVs3kGo*0hVg&2kPi zMhW@wyt-u&w;_`!HcUH|`u_6El=IxN9kE7AvRMbV7cWh#%&#oT%y08A^1FI0lmNo0_+smS^<3k?#6F7tYrTJ5^P+q^bZ26+kcrJZ0IseX= zPcW}V((5KazS|X1$7g~M%X)_VT?0#7uS!Cp<2DoaugKk}ynL)M%%}Xw>aKGs59*_l z<%7G9?Qv>oSvlHX_^zk}=~|nU0W`4Boov0ia)-xa*GuGsPFf;W-7M{r+$o!0sSCUQ z3UzN_)cMelaD$Ymd$JM#MsCcNi(}g^aciQrcUV>70YS)5t@oVi8jbAa7hjnt4G|N(gzwEvb+mmQWqGxn= Quil!-7IvhoWW4NpoJ$3aIq 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 QAg>Tu{6bavu~M%2f4!Va9RL6T literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/3.png b/padbuildimg/assets/awk/3.png new file mode 100644 index 0000000000000000000000000000000000000000..89498c02ee4800cb25e2ab3cb345ef2bfcaba33e GIT binary patch literal 2947 zcmaJ@c|4SB8@3xtvKERlh{BASVK9f7F=WkHicpN17lT>Mj6Ide5(=q~%2tRIN@Y2g zh>#qm)0u49hDK5vWlQLc&Z+bLanASt-uHc$-*sL0bwBt0$CHXbctA>0SyDhiK+4V* z=gJ?|_#YS~#{a)PeFMWElvviDEH_#JE0Vwf1TZ9;KLEC)5CQ>LfIx~4>jumO1cV`E zcTbk5lOvi)qd*Cp7-$rQ&SwhB9}tMkqAG82tAG;YVYT z0?@8Fo4;f6SLTo)7K@IC!6G9gp^-=^jS&cgo0^(#Y9J8$e1twTn#v+X=~I~+-xY8G zlgJ>`S!5a&ys1d=r-if3A$+HQr9h$okfk#JJ|_Nw!J-Is7#wP_nbLQllhgl)QYb&r zOqMI~kKcb4X1Yhy0hlYmq=hqx{K5rjY=)wvtr!4-MPs9Ku(ClF1~rH3o&km|zXyrUpnj+!|?Wh%m-l z8yOkeAZ!pO7^Cl89E})G0jR9+T+%;W_PKiZbR_|fZD*ZH03mcWV?x}Q+DZtP7(J8Ns%Asn(LYnbQ@{m>e;=jg5%~Kq|D6FcpGjehS8>I?A0~|}*%m|KNCU)* z%h&^_Yo~t#pa0S+^rlW4Bj53u7ulsL_V5Zh{&_`Ftfcxx?y!5yKzq~r;%$meTGdg= zh5W`@{Xce-+m_j9slSRfhM?r4!~}hLG2FNfHWPR6)y@9PL=q>2G_q5evUT~Suw%N& z#JdFo3vAhvk!oM8s51Tu^yc#;DYAQV|Nax{2D(yozha;1LKXEI@&3_J2{|qMg}p0+ z%cg!6S22B7APqEGPS7o1q-N3Evc)RDt2JCb<-wK&zmy%;G64Vq0fuWD8E-NGpa zG@)vrQ~PaFbvW|aN1TT*wn}n5h|lJC_#twkMT<|vXX#x-H3JR6mMbC75C`wXJixh~ zTBp(aY20L+*bSkS__o6Nbjnpv~zo}YCMPE0{~#6>FBBTpH-fVNmS1$>%p?8W$C#QE|RC$ zEG6xE5jrkjUpp0cI~*>&epgmpTD7=!*0{dtd6L4N!Ol`HWvv?&cHJ{Qx@&$k6?|nV zjxL>n>3psEcv{bu?yT*wmmOTgT$-oEId02Sg{Md({VrQ4F*Q=^7Jm@NY zTUuc1Vi{qQe@1K{h`6Rzcs%n!_gvTfM!9rrkLqrPGqL?689O4(l1JxMkFysQhTdWB zX!<+a$RQB^H($P7_g2_ri!Z$Cja@|;$p@p3b1hSX)w>t6I^wg!IEzmlS_=jmmLJV<`R%zg?qBW@YV#nT7oR>^zIH`0TjhEM`KY z!LSTHRieAevWO5?eM`D@fwueUL<)oggn;ZGaTF8#2*xFUF@n!Ur1&?vN=x3b}uaC~Anx!<^eolq`Pz z>M}kTL~l5&+Dme%EhnVEF09ijU6tbUmVL)$@F!27IYKMkSQvj2%Zu*Wfxl{fRzrKI z%!{DQ#s-?=^9SF{g#PCHlPD)Vx)7C(lIOT>}w$c{K3{iUR zzV?Mdh;U-e?@^}oDC;_p zQ*Io#@+p!$VD7sDT9}hJ8M?@N8p;8;zUnh;@@dMbxgC(^v_%DzK6lYn%{&WeoMQLruJi$R&gj=#&$+tv01oe?IetZH zt*At~F?iVuSW?$_h8sB-UrPHU26c@DTGaf*Q#SvCzpgt5`7G@tw`HRH-t7xHx_-XL zBwmgyGDOfT$qFph58Uz4eooB1O^$wykfUW=RVkurqk~Z9-}LxsX#>euNgF4bYhvDP zFH>1s_fANv!MiK0sd3xV{4ukO>)UHdug+jSwnfxj*2tXdSfLw66o2%m_K z)p|F^!W4{Kzl8Lrn-8YE{#j4&LfAG!fJw=)JN<23(E4xZDsQwrJddPxXyh)B<0qhN zzf^984WBq`=p^(cb|c;@?`vYudmC=}DI@E+!F8Q&kTYk16t?E9LC&cj4{e7gp`@$U zhIy)w9xg?O5jc<~?z(Kr=kqV-wY`@o6Xs$>a62RqlZzm^jM`?>uK4lp`U)QpEz`+F zoY%(yIngfvZhK*W5WA*1kQdZLo0uAj`lTjENQGmkGe!_1Iz{X z$r$@4JcKoSRCJeGY&)SP~&{SnZE@7cS`SyEQ1_{8>@ zIf44pOv%X^@tAwlYMBg6PJQKi(st_2S@Fp`!EVLS?w22&OgvD-;-^T`dLj-OkgeJ*piUy5!=mcv7PlnTou;u^q=0E3+?~_ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/30.png b/padbuildimg/assets/awk/30.png new file mode 100644 index 0000000000000000000000000000000000000000..46cb8ff2bd7373995206b74375b6e8c6137d1b78 GIT binary patch literal 1766 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 D!`IBV literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/33.png b/padbuildimg/assets/awk/33.png new file mode 100644 index 0000000000000000000000000000000000000000..715c927dff7365d70e17eed85306189e171e64f7 GIT binary patch literal 1681 zcmV;C25$L@P)Vl&|00009a7bBm000XU z000XU0RWnu7ytkO1ZP1_K>z@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRZ?2T4RhRCwBA-2Hzh0~9C$@hTuTW}qFo2gG4Od=|t3 z2p|@yU>XoZT=D)r!@Ya&scx&Fk|2YipdiSDXMnf@h&urShz06nu*DUwoeXF9+@qPd zja`%&I(Cod82OxkLfx&(k{2V3;RBDkUb##KmZ_sz?S3l zElAn!@-&9DP!|XXB^5!av*6$aI#3A`nJ@rKNTEtlHM0=Wp#&8J2p|?@Td<{ZV~`Et zNV^A70A!YjhJsTmh~3$_hha4^ks5>ZsUX-rtKUOR3I*z$3BlpoyKGXCW3FLoD8dAJ z>WQ=O;Xw@$Kum;72cU1x-UBnhp&*EGZh1#2I7TXf7KDK*g8t`ll0 zIAKFg00YA+1z|pZ54IfSh}En2Af*YULK_(2pm^>GHD(9} zIvAuX4CrH6NGlnm<|2RqLW%SDNYL0AlCMF8ruzKPUw50TqE9 z05cer;AZVP3rt-15Qz^c?@}&^k%<8Uhy_tJzGo;;3x#Akkbyur3z$4%DRWgvIm4{g zs~OG$Ep?d*bPO;!@4bg4#PXRg5XYnmGIUn#VMxD+TAr7u8H1w_oHb{yhRXp25W;d$ zHU=dU7h_<)g}5A=V1WtlEI3HIuvtJgnAL7*=vi?5uL7!DJqr>YpfWriXwJ+I7huRiLK9TEz`_n7fKaj;C{cxl ztp+RV0BR6~<~I-@*;^FT{Laa^93 z2K4c2pv8L-K1M1~0Ro5x&2D(T>zWRXJD@=zmjmqw8wd&k6IWp605x0*SUueXigf~m zc@L;sHioDJ8CDL8x^!T{wE7-OI{~>c2M8c0M2W(HQgDDQ0Httc<<+p76C5p|AO@-K zTn$ROkXq^<10*V7r5A`)28Q0OJrIXtR@o3c0Ro5xLkukqfD#=jZ!1A723R@(@m;_L zj}lxCW(G(-C|@fHVlfvP1gC6Z{N39_{q7Q|bOT2X)Sh(Zvu6PUh!Gl<@5*;8ffEm{0jO5%bOqOL$^Zex zwCcYxsJM2mFuBJtE9@S;K~Hr{f#$9TR`eChV2i_m_TB>sAe4}Sc9H+n5U*l@XHo_R bfB*vkC5xLZ2vU^-00000NkvXXu0mjf8RnW^ literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/34.png b/padbuildimg/assets/awk/34.png new file mode 100644 index 0000000000000000000000000000000000000000..90b92881e22d57fc5a5cb488d5724f518e5ecc15 GIT binary patch literal 1621 zcmV-b2CDgqP)Vl&|00009a7bBm000XU z000XU0RWnu7ytkO1ZP1_K>z@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRZ>%Sl8*RCwBA-2Hzh0~9C$@hTuTW}qFo2gG4Od=|t3 z2p|@yU>XoZT=D)r!@Ya&scx&Fk|2YipdiSDXMnf@h&urShz06nu*DUwoeXF9+@qPd zja`%&I(Cod82OxkLfx&(k{2V3sB;reJe{4wwZr8{`0R z2!MhZXb;#vU>X1jASSRANc64Xdyob}2AH-ztM7pw4vbQUnMj7sggOQU&dx$ahZ0l_ zAb^;VgY7*7CI%6!%F7u*?uPL}35NlkLV!uOa}UETpwErL`4r-y)xZz|VXy;$jsYnL z#VpK`FaQuhEcks0G_V7lI2bz0LmAEj;n&!BMJAo2hK#DX)Tkt}`>^##OYUE2n7zC9V;O?$~ z7E0xUAfE~XErjxc7FGZq5QbD}ffF{wL%mP|!3{(ShKFGr$i59dl+?2RN&MieZ?;z=bv_w19d$(u^6((_9$#fW+=WEC&0> z7$sI200Ial&S8$|NCPFV_wbOo2efn+Fba%;uAT`@2g)WDkUS3Z={-oEEiZQghZHEm zfl@zm;sePmxnL$LfB-_tYGCRfB;%(6gVn_a65o4(ODY5!%8+pLVFKS?EnG9f>f5lnV@PF>;O<|78C?0GLS*D zI?pn6bO7TPT8_i?l+SblGwuOHYGy|$JiDYv2PUvIU_1&U2NMId0RpNv@4*@)_Xt*000G2;9_pa@a|s29J2;`ezjqHD z0;_;NHUXw)S72W60H*9!Kp%pA^_~G%MG{^0!T>-3F<}vd2aSu1610&3iI3et2e=qR z@;syREO6+690*DWpn3oq6Ja+4Kmeh}Ihv}Ot6V^t9BetbK>)O56;KRRJAul9)j%4Q z7Sb4$7>I5RAUOdbfLL%j3sm8E0*lS^nZWox3k?lWTtc%DsPZ^_b~QYx1eGu|tRMpx zHzAeb00D%vVgY51PN2^~dOLs)*aOTm>A=(;3QTF$L;F6{UDMMzxX8;HwMo{ys%0^B};+LNw)_AEdEF+y8N@5*U0Ga(+tW00mQWGzcDD< zT`Nq06$h{qzIXOLZ7c<~a)1?mg)-RUFrdBn00IcD)dub&|ED2d#ekGI00ImET`^(- T=u~;S00000NkvXXu0mjfx%-4c literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/35.png b/padbuildimg/assets/awk/35.png new file mode 100644 index 0000000000000000000000000000000000000000..68ecbdd69e347d8cbb116d74001cda621de93833 GIT binary patch literal 1718 zcmWkve^gWV6~FKMe&r|05JCn)9u}A)!jn;m7V!ls4Tu;$wnIwW2U?95kHH@pZB||q zi*VMupinr>_5@@c>(E8Ciq5$ppn$H!V{BvRk5G`~n6iLl4Qriy{&DZ;oO{o`=W~CY zd+SZbZzyHaG5}B(81vSP7$*uNp+r6DIBpO@YB6ry4IrQQQZQ&erWK9ER>K<6dFHU~ z$8|?vMr<;!UjuL;5kPPP%)Jot9{_tvt9Rg5?O0 z@Wb>LW@4*5HiY%pbC<*WF$tqN5zsNk=xmOSw*~|FogI-THLq5y1CZW~R}pF&5o)>b zI{%v(l*E?lgTj^vnHCdo_M4#Mzj&3NgQdv%JjT_Fb%$_uvR)`N`WYUUX6W^7hE5N^ zQiZ=ZB1~EjQKTo^IMs_W(*~jZD(1p-ej+7I64N_9ikqBXnsUPTDO(^p4N5C{9;yNN z<+Kct6jCTDWLfGOV0QOTEEd`$yAJ=H5rmwbAzC&mSB1fS`@>?=EhVLx0$Co`z`!dB zMrD$4yHG%QA3C8OtdXbMxPzddF-+)LJw%EX&1+d(#5#SLvk11oI#{7yAu>8K;ALJ< zS2X^XJB-=oa`mk?h{Q#Pn{TgV1K0RIjgGfigphh-jFhLc85ugB6-ue_`>v%>iaU@!5SG?UQLj2mlrhM?*uVx)yAE)`Y< zymZfV!20vp_FH4I=E7V7#}_s@9KXYK=a3?byJ8Ds$2RG>E99Y@v`1@fa-xh&sWyRr zm4ngQi%@bDX%};9nUE8UysSGgo#;#SqZvixrTr{ zYvB)cRI>ZX6TE+Qu%Gt*%ftu``^7es1B*y7Y6SuGy*Zv0xb-qJxVPWFBoI8MxbgTGQtTiZX z`=mD>Yy2*YB@&7t`W|!|Y{pyCJs|?G1@jPUH>^90X=A1C3at_d=1W-v94s%HVw;XO z3=XZp>c^MquN)kg4i4qCg>NjVKD*=ej?|&O%f7Dub%SJ|pKVHi78<$BcNVV6zHsAd z-sEITeXjEdZ(+Oq?F$#Jp7upA{&@T;xgpM1*-$s~LwCXFZ(21!A&L1S8ktckvWhSB zgLNJ#4Pv@xiX*AO<)L18TRe77vY>kOn%oorv#@Bd?F+2&quwBjxW1BUOM(q)nC}-@ z(Lzl$L&F^RXXf~V>sU#_Dq%%J2x z6B1}OmoiwpQy+wyZHr<0K5em|5wN%3TEyL$l?S4y(7Vqm*5c=8m@eTJ30H03o@F8_u>o{aHe5(CoElO_%@6n>y$q}L zz=OcSbLOf;?W-KfKTT_FJV=;)=0$NIi6@fgQS4;Xe4YEFeNvx`6~4ik4`qr}8zEs_ zk7l^$2Wfuj`bt4)%}Va#`?=6_;JkRSI>|W2eDuzGUmz%y8?NG(m`{^FLw~&wU5{fF yUd=xAbOlm#gEQ{1u~_DRZ>+M>+f`a1w9u0N;a8iEUz-wt3>3UxoOe2xcl;m1;bH9n literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/36.png b/padbuildimg/assets/awk/36.png new file mode 100644 index 0000000000000000000000000000000000000000..7de1b26ed0a46e708432c819678f0bb68cfcf5e3 GIT binary patch literal 1643 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 PSpZn$?6J4aJYxP0&m>`Y literal 0 HcmV?d00001 diff --git a/padbuildimg/assets/awk/40.png b/padbuildimg/assets/awk/40.png new file mode 100644 index 0000000000000000000000000000000000000000..1ca2a1def38ef084a5304cd67e29e578256ec6c9 GIT binary patch literal 2052 zcmWlac~nzZ9>;%g$$KFi5kd%?K97LWA_N*~aN{LV1f&`f6&V~Klq#i2gG)sZmJ11C z(MpDCDWy~&;y8@s5*C%Z^+iyrSk0-7gARkyx*z8pT#J_4iGTd=_uPBW@1AqN_jf