From 6ebb70f6d26114ca93bafa55c0a615bfae65e0dd Mon Sep 17 00:00:00 2001 From: dinhduckhoi Date: Fri, 3 Jul 2026 16:00:06 +0200 Subject: [PATCH] feat: add inline emote rendering with :shortcode: tokens via bitmap font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implements generic processEmotes() tree-walker replacing legacy processCatplush(); preserves per-element styles (colour, font, hover). - Adds EmoteRegistry loaded from auto-generated emotes_manifest.json. - Adds build.gradle generateEmoteAssets task producing font/emotes.json and emotes_manifest.json from textures/catplush/*.png. - ClientPacketListenerMixin wraps displayClientMessage in Minecraft.execute() to prevent cross-thread render crash. - Changes shield prefix colour from GREEN to AQUA for Wynntils guide tab compatibility. - Resizes 21 catplush PNGs to 18×18 for bitmap font provider. - Updates .gitignore to exclude generated emote artifacts. --- .gitignore | 2 + build.gradle | 67 ++++++++++++ .../edenmod/textures/catplush/catplush.png | Bin 0 -> 1336 bytes .../edenmod/textures/catplush/catplush10.png | Bin 0 -> 1389 bytes .../edenmod/textures/catplush/catplush11.png | Bin 0 -> 1541 bytes .../edenmod/textures/catplush/catplush110.png | Bin 0 -> 1430 bytes .../edenmod/textures/catplush/catplush12.png | Bin 0 -> 1226 bytes .../edenmod/textures/catplush/catplush13.png | Bin 0 -> 1343 bytes .../edenmod/textures/catplush/catplush14.png | Bin 0 -> 1503 bytes .../edenmod/textures/catplush/catplush15.png | Bin 0 -> 1346 bytes .../edenmod/textures/catplush/catplush16.png | Bin 0 -> 1340 bytes .../edenmod/textures/catplush/catplush17.png | Bin 0 -> 1360 bytes .../edenmod/textures/catplush/catplush18.png | Bin 0 -> 1440 bytes .../edenmod/textures/catplush/catplush19.png | Bin 0 -> 1288 bytes .../edenmod/textures/catplush/catplush2.png | Bin 0 -> 1245 bytes .../edenmod/textures/catplush/catplush20.png | Bin 0 -> 1545 bytes .../edenmod/textures/catplush/catplush3.png | Bin 0 -> 1244 bytes .../edenmod/textures/catplush/catplush4.png | Bin 0 -> 1281 bytes .../edenmod/textures/catplush/catplush5.png | Bin 0 -> 1239 bytes .../edenmod/textures/catplush/catplush6.png | Bin 0 -> 1240 bytes .../edenmod/textures/catplush/catplush7.png | Bin 0 -> 1327 bytes .../edenmod/textures/catplush/catplush8.png | Bin 0 -> 1225 bytes .../edenmod/textures/catplush/catplush9.png | Bin 0 -> 1369 bytes .../eden/mod/chat/DiscordChatFormatter.java | 87 ++++++++++++++-- src/tel/eden/mod/chat/EmoteRegistry.java | 97 ++++++++++++++++++ .../mod/mixin/ClientPacketListenerMixin.java | 23 +++-- 26 files changed, 261 insertions(+), 15 deletions(-) create mode 100644 resources/assets/edenmod/textures/catplush/catplush.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush10.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush11.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush110.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush12.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush13.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush14.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush15.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush16.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush17.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush18.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush19.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush2.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush20.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush3.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush4.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush5.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush6.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush7.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush8.png create mode 100644 resources/assets/edenmod/textures/catplush/catplush9.png create mode 100644 src/tel/eden/mod/chat/EmoteRegistry.java diff --git a/.gitignore b/.gitignore index 6b3ef1c..ba17279 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ Thumbs.db # Backups *.bak +resources/assets/edenmod/font/emotes.json +resources/assets/edenmod/emotes_manifest.json diff --git a/build.gradle b/build.gradle index 6e198a0..3875ada 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,73 @@ processResources { } } +ext.emoteFontId = 'edenmod:emotes' +ext.emoteCodepointBase = 0xF000 +ext.emoteDirs = ['catplush'] +ext.emoteAscent = 7 +ext.emoteHeight = 8 + +tasks.register('generateEmoteAssets') { + def assetsDir = file('resources/assets/edenmod') + def texturesDir = new File(assetsDir, 'textures') + def fontFile = new File(assetsDir, 'font/emotes.json') + def manifestFile = new File(assetsDir, 'emotes_manifest.json') + + inputs.files(fileTree(texturesDir) { include '**/*.png' }).skipWhenEmpty(false) + outputs.files(fontFile, manifestFile) + + doLast { + def images = [] + emoteDirs.each { dirName -> + def dir = new File(texturesDir, dirName) + if (dir.directory) { + dir.listFiles({ f -> f.isFile() && f.name.toLowerCase().endsWith('.png') } as FileFilter)?.each { + images << [dir: dirName, file: it] + } + } + } + // Natural sort so catplush2 sorts before catplush10. + images.sort { a, b -> + def numOf = { java.io.File f -> + def m = (f.name =~ /(\d+)/) + m.find() ? m.group(1).toInteger() : 0 + } + def na = numOf(a.file) + def nb = numOf(b.file) + na != nb ? na <=> nb : a.file.name <=> b.file.name + } + + def providers = [] + def emotes = [:] + images.eachWithIndex { img, idx -> + def name = img.file.name + def shortcode = name.take(name.lastIndexOf('.')) + int codepoint = emoteCodepointBase + idx + // "file" is resolved under assets//textures/, so it must + // NOT repeat the "textures" segment. + providers << [ + type : 'bitmap', + file : "edenmod:${img.dir}/${name}".toString(), + ascent: emoteAscent, + height: emoteHeight, + chars : [new String(Character.toChars(codepoint))] + ] + emotes[shortcode] = codepoint + } + + fontFile.parentFile.mkdirs() + fontFile.setText(groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([providers: providers])), 'UTF-8') + manifestFile.setText(groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([font: emoteFontId, emotes: emotes])), 'UTF-8') + + logger.lifecycle("EdenMod: generated ${images.size()} chat emote(s)" + (images.isEmpty() ? '' : ": " + emotes.keySet().join(', '))) + } +} + +processResources.dependsOn generateEmoteAssets +tasks.named('sourcesJar').configure { + dependsOn 'generateEmoteAssets' +} + jar { from('LICENSE') { rename { "${it}_${project.archivesBaseName}" } diff --git a/resources/assets/edenmod/textures/catplush/catplush.png b/resources/assets/edenmod/textures/catplush/catplush.png new file mode 100644 index 0000000000000000000000000000000000000000..e0f1ee52ce51b5c2fbd89ec81afb079671aa9c7e GIT binary patch literal 1336 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G(@^*J&_z!{$_AZ|c6yYrJh%9Dc;5!V$jK}j= zqyPokOFVsD*o9m|GcIS{YkHG;EluR|eFe0k@$fGdH!kBr&%D zS&yNK70@0NGl(9YXW1GI3@pr^E{-7IemV7)sG)SEFP0u%CdX7?Q71T zEWdw$|IW(xr%#X1H#=4{BmL&~mj&OKTw5iw+<(5CrsL|Xb=tCgcASdqX5E^1w!0={ zt*PN8EzNvizmoUUCtcrDoR*ncX>cuL&xGfdQBuEU3L>-DT2@sA#{7T1{phKP(A!l9 zZ~Z!wx2?OlPeS=x)b4E?m$e_B7%^kF`Rv=b_L*FKxnb|-%Xja+Jg>P_?{$gEqx1XM z?@xO5tMs+*<=@{7EqAuB-XC-DiriWerwJDMD`U@|Q`oz)Xm@#Ha`NMu_WypwTE72o z^?U;N=g*hDR+ra&m>i`g)^B^{U#+ppY+otQCJ`o0N6p4-Lc!tRLzY?WD@?b2m$rDp z5*bg;37(T|c3gTbySwyrKHu5<$8C)_msrQDS;t-r&)d9jSKAb!wI*wyNh~vBKfS18 z<;@HZf!)`3M!Pif9DMIP>DY~p%KQf91@qSL|1~Ktzctyt-&m&SMgIE<8m`XjCAV{r z-PWBVQk7OEwf3u5`K+@G1B@6UgJ;Sf{5uI;HejgeJ* zqjU1^F4W;pYU%V7S4}jykyyOCJ}tHM^;751oBh_@i}Rke~&p)p|Y7ORxO7XUeBtFTRF#8p`!e>`(4F&y}Hcpt`@D zFZlYDRL@xHm{?=GdxvIBQaZ2M)%CUB+)qQ(UNm~PX6nq%m*1D~x_hqu-tXs(* zmQH(MYsfZb(w3;?^3#&dn%Z5963n&td2D{eyLa0*r`2~KUicI9tXaBEU}f*ZZPOme zGI(*d2>f{P@t$x^r=jVwh&|u?&9=_F=G|}WG;5iJaDbPe(~^rBi%Ok(8U>UOS|@RE zD0V0&^dy>`DmeFOZumh~MaCsl&YbaKT=aVHajRl)ir_my3T-NUH0sz8CyKZi{8)l{_ywa5R?ktu+-EmUZgbxb0o^+Nl>A znh!Rp&$nyZ`qkcvBk9YR?dGR9ZIj;~{Wh!a&wWbux6M?LWU**AScPDTPw{M>Ma ztD9TvUz6UA=gantztHxS+0%4XO}$f3Haq&wQIgs&s=xm=+YzKr7rivP+wQEM04cD> zU%N_uKJRaScdEuE~TTiCQrJgxX2CtXa1n;?A+xEWt zQjco0yjuuU()-^$a&3T*OMVQ<;o6Xa)o3}4&Z++D(JiLlRaT7<4F2T|z zEReZElmVp%9ret11N?f~q|6(hy;f7M(fXJ6l4orhNtKM5)B}Nhnr|Qd?j5H^MOkg9 z!~|INzOaR?W2>tWr%GZq{Bk;4cS7skArrGcm~g1fo7IkcF#z;B#Ewt7sq>xr}=)EOhmCJyE;#Koc1e z^Sq5*8n4@A-m_CQy-DS)O-wY`@~YGD%WDS1m-mmX5ht1sa$0dM!Xc{}v>cpn5FWx>OoeW>EpcT~_l)sGqrO&6WwmA90P3Y2qnZPr~3veleLLNf%gkR`!- zMiZiHfk}hV9|ku>7g1_NItkTFg--F6k z*rbYBt8ZQliZut6q}A{Blus#Tko(a3IDbm?@1LW!`FsWKm{0lv4K+}TPD@Egy?jWI z2{bS7(J9wiP8m<;Ghvc3vf&GCQ&nWe+;`e0b_35=S6W(HT-#V&TiB5rB3D2<6Jn%Y zdwy^Li`p)Cid0g?4fI(KBa-y5^EF3=aYRx%DeNpp*`35qU7uVeKUyEl%Fx*LYY#$GWtnN`-u z6uTevnGKign~{GS{0hmPZWlD}rCoS9Rkwu)eP?(EXWOg|5@oWSu z&i8S+?p6>w-yu#8E#oTi1D{jhcrSps9D+}0uaY*_7_mt6l;|WT0 zR3ve9aoGK1fpNcTow22c^2o|~SE0GZo)U><;F0PdT6wpf3T}8W`!Ry8gJXQr`|nf^ zA%nWag;Vj^8!i|MoenA)jXiHapY_VYM6nxsZuIGmHAmnq$Qlb94?F1WryIS!b>0TE gL!(Xq#22maMyzl0{>BWoO8*XUB)Zzw+EOn43;sfDLjV8( literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush11.png b/resources/assets/edenmod/textures/catplush/catplush11.png new file mode 100644 index 0000000000000000000000000000000000000000..65566e3d61857b9a1c0faf302d11312a54efe49f GIT binary patch literal 1541 zcmbVKX;70_6#dXb1Piv<4%*tGweDL32xvfS6{?J~McJ%CkR^ix+!g@t9{_-u1OQkETQM>K2!#TG0s#Q9d;l;&7j-*Y z0>G9Phbz~t!3z?Zd~xy3c^liXhzN;HDiTR_K;PBMt1pef-yn!OD zYh^Q7!nV;l3PbpcDMnUImUS}7bOCO3KCNr^aqW;mwv^K|gQx( za_20SE}ov16Q;yzcy4|xt9OVuOyzEFZl>e8sQmuo&IvwOlG(%a&%(!76LMbiq8g{r z1-*qhYUv<9qMjL0O!Z9bL1w)SDSYLF#k(hW!DE`>G0jf#tuArR$TS>}$5k$W6NfSlXNEBB0 zah`RKMKp0Rb?o4($-r{ft69MI{=(?#vnv%g(4^!oJcy8Epc0d^u zkwfys4m@q0>K+sfkzNxiqVNWGP$kR1Wb9@h*&(6bF}@v@I~d+Dg{ovCi$}flDL2yl z9pl?v9^nGZ$9xM%5ZOcS8GY`lcvKm~yMXKome>IgY@bh7uU9s~1KaPO)(bw$qYrv& z@ZYCm;C5F*{2xcl=*ZUwdRxA`us29P$6yjFn5*f#nOUyy>0iakj|3}^{r0EwC&%?A zvxAaB-95Y_{jui#8ruEsI+`ywg!eScPvVHeBy+0EN zZliJY@*h+PZDA zSrn1x)+SwpEX5_Pt!4ok@jh06X!ac5Gq-;m4`q z=`R1}+{W3x6;}ObPkto@3R*+hsxuMqhKWhBi9_={e>XgJPqu`uAn9lA>p!1KIZU!I zvJ73PaYE|gOylo=;NVu44NiD;M~W5gDYRp!GcqFY(pR^e2l2b3q^e%7rDjIP&FFy- zkR66+qTk|LYF%x1@2iBv*BAas&TIDQJAAa+L34Xuoy*yX4b}L@4#Vg5rY8r?Zuzq( zqSxPxm7VI)RT$z+E%)As#A?HzADDLH6yKgp*!6kp%!~!Br#m9W_DbvxKm!tmMBfrR QgWdx+)^=7+=Miy#0oZ&j4gdfE literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush110.png b/resources/assets/edenmod/textures/catplush/catplush110.png new file mode 100644 index 0000000000000000000000000000000000000000..cea25b261d14383b3dcac0af5fb651cadc4b731a GIT binary patch literal 1430 zcmbVKYitx%6rS>0sk9o@P+BA)R3K2??bf!gR0UdTgCz)!lvf1Gf>0}^UBEU%2vUg> z8%ab7QZ#@@Di8^%)FOh^D1unFg?8WDo!Q-)JMWpD_wJ6jAtCtJ$-Vd7IXT}s-}&zO zaOsjoS(&++2!do)RhBP|#3?}VBoIt9MUjFiXe1CRQlv?qrbHsn6FAS% zJjV$`;TRxTKyb;F$+Ds>>t_FHpEnr8$Z#lXrcIU>0=}@j*NsN-pg*jt2EdtLF8`(} zNuHC?a5NGKVIfrD6%waR)9kt0Lz6Uy;_(;(2nqn2z$uDivEiZ|9wj1S)b9-q-u1CG z!!s<;astOEM97K)I2Vtx@WQjQz^gnb16+(_Bt`N7=c5>)DS@RWhLZ3&&CsIAs~n@y zl*BR#DWQdeC?rV&j%Ou~m7$Xt6je2Yfe6I1yg^b5WEc)%Xe2I(DVEb|3aZ42i6qbK zP#jrK0)hqv6ATW)JhE))deZ0d!v$~};1r3|eZBYk-GeYsz#oFnp+IQJA0jY37>q=Q zFbF4b61E(}U^D}8HWno@49DZ(ff_W$vNXp~91#OU!H9Ptz_F6R$*^ltP!%~TiiRlY zJg-1Xq97}>uBk~`GGs|t6~o|Y?>*n3C#0w*Nw5MZ$+D(tNyA7blW9Xw>v~GplBom_ z;>6&9U)RkjN-(sbD0(WDPO322@W74$Wf*Dr=x8Jcb&wK@0TZeE_3MA0J$qi)(^A5K z?TDhRD4L5XqETM$}uAz%dEte-OnAl_3Ot?WjSqOLymtt zvZSXwE2H2J_2N#yS~JkTwbP@Vs-ERbsR{SSG4G#VI4@`H7uU#?LH9mCA}_n3cF$)A zSIo_Q_4a{VkWt2l#f!>k{qvaaC1USb!xXJ8SOv z?Yp|5E8kU7>)N>9<#g0FIiW!ct%bz}){+9Nt;S||*sTt0$!se;t+8(L%l`lxwr$+9 zspVmSwZvg7brhA}2Z+R?Cb$N>xWsn$&S@?WnZ@f7||w@`*V;J^9ni z=FMnrF;rc%j2csAtGvls4($J+`sk#|HYd19*q=F>ljA&Dx9RrLvdy>sE#ibZYc7@+ zw@-a(>h>FM=gk#o*8I@=w|56wGCkjZ>eM^_qI&V8g4J&{1b*CLw0CSF#!c+r$$GjQ z@|V69O21&qp6obwuy?g+9w!oFXe!si(MCUi>u3hcD7rf)S8|d>5 zygYyT;ylXv^xCrBBNtAqdt$l0+5YY;wY95Otz7N7dI_1){`Q&U3)n&^F;Z2rr2O21 Hb^HDW6isvj literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush12.png b/resources/assets/edenmod/textures/catplush/catplush12.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf65e25db95748008f975498bd7dae8c121fc92 GIT binary patch literal 1226 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G(@^*J&_z!{$_AZ|c6yYrJh%9Dc;5!V$jK}j= zqyPokOFVsD*o9m|GcIS{WEYG}s8%<^wfoz-=hW%uOvWNz5%k z)?;X51+>S+45CNpS+)iP1M^f*7sn8b)4rEacTZ21IQH@VUzvN~=UzOhSsDaES&9=vj6fQ%MFMAbFX~wm@<3j?U#G& zqh0uBd#WliE;T*-_{!GZSBf6HoZDJ^tMh5O$!*pLhiXr>GZ=6vuHaxjp%>xn#Pgn| zY5DujGjASVd@-O!kE^bWU92rxciFWg%u0_!c#V&q5imaD;L50Y#7M#{IP_}CyH3r_ z3G53^Cl|V3Hal;AI(gah<%|KL0j@k}j1oFJj30j8q|y?)FO{w;|EY~jcY2S>azwgKD%=r8Em6nN2pLklD zS?{;Ew@>%JuK#rM_;HKsZ#vCW6>G0w-pU(${X(f}?fr?xPCf3cBJARRIJ}c)c=Bp% zytn$xzY=#RK0TV+c9?PT#g4PH(`H0jd-`=Qv1PJ&&Y}3A_CQavZ;^Y`e2U&GfYEhh#kJ@*L|SuJ@^FnLen5Ag&4tV&;1KY4WY^OHH5 zv(lU;52df##qZm}uxPcWQb?$4&%~)yMMI``c6RRSxaxS=uRTIAB=PJMmEi2wdiW#CzbJ_+U2$RH6`P1RY;7Uo7L@d)2LzPz<2Duro#^>^cvSnu0?dnYdb?i45@ z;_4Y00yLL{QSjiE922?!7fW+jTVDQp_S@_IHND^Y*ae!@r-ZiOOnMc?)?CfQ*1T)o z?Zn&PYEMZTyZV~8JgN}v-mRJLw)o@yVCnhFOSf=UibqOccyRcROXQ{H(|*=Jvs*Pg zFaPqg>q&vTS8GbqqL-q^=T=5$2dsWI({Af? z^Za{dM!$9}SXGjH|Jji@Kj*LCQ~CPaql4o7vimH`izn(vZxdp;wl@0sp}E!_C$^W` zRxN#3(z6;E7)_VH)$R(q{`J?k*XL&4+L}JUMr-{Vo!^dIW V1{LY-_P|Wd;OXk;vd$@?2>>K&B6t7* literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush13.png b/resources/assets/edenmod/textures/catplush/catplush13.png new file mode 100644 index 0000000000000000000000000000000000000000..0c7d9d2acc39ad86606883183e6693b0107d612e GIT binary patch literal 1343 zcmY+EeKga19LK+{wf#`qSS2mi*T>K+#(JyO#(%&oi5z2~0p`JDIXea`!HUg!PaC+85|ZIimLIsjl3 z&7JC_a;>j|5UTAzSP-Bxe7L)h7XWD{0C0W;U{w`yUICCy0N@P+07@wU+t`A3Z+q1N z5$5Se1z*0-jm~PmDp6y*2PFca+4EKF`r>LLR3S2n=H-ffgIuS%$xOmYoO ziU>`Lq=Y9#sti~WtZerXNP7rGU!pa|nm{3tb`w;a@Umm^`u_k{TtrONnQs9Ci9#e( zzWW#G*C1U|0cQU-B*m~I6O%$&>~9&O4Mo*sWBX5u#mkds0KnC0R3~5ZfY)Wu=%bO? zM?;F%KQazPS@qlSg4PbO` z)8=3KwJQX>SG#DrS`8g4mv2e`^qRT6S~f~Py+;0QaCLUOun>P!Y_xEHv3tefhuYwi z7f5lsP9pE?gUN_3HMuBxTGja4XrMB3{@o`<*Fnbg(=GOm8G3-zY=?sekx z6na%f%v}&%t`8n$!DU)OCC=(xy!sT5y(q=NtD_d!ujw9IP5h`5=zxIKZDlDp}InT**=v(vQzNqa2tZD z?;%gCdHBM`q2IgQ&W$nIyRtO)a-xSOTrw$2yM=Kx^(YE9him097!zD=$t!G730E=% zztq`&?(kHqsN8OlV@fh%%*EQEp@N@^u~%DkbYIPR=C6kLKCiAP9%*i_@_Fo0F&8i7 z>18;~x(vL3*Wov7nF?Fd>{Cp9X(**1Z!r8#N626gR*m;Jwqxu3qY%ZD>nFx5%ut7K n%?2RT@*ZF8lNt4&3sdU3ZrpOKp-dtt(uW z&n5r>OubR=SP0EN`Nl7xNZy`EgW$Vx6c!Btar*%vNd^G#pi1%6i@$rdFW)y?L_Qw(m z3rmZNiiEgsOXF&9PsVrsjUh|E*k{Ch!qP}T;qgyWob>@K$3D_nnsZQW^HNpy5TdQI_SMj~8c%HZF70|R2 zNfL_%(x_+aRH|G>UPfj%l!k>-K`@5R=D4|`u(%)|KaLG@8Btt1lM@saDvXUI1f7W$ zC5l7|Acqb{^K;~RrDf%@LNSfblqIKisw?8f;z$PTvLfrLzG0v-=fObv+RD(<*tLb> zBE#^N=VPjc!TcvW#Y9Wy_`Ru#2FciUagUsGSDn?DLDb1Zx-x?R_<4)XB zg3Sq@gGE$L3Pu%$sA1VxF>RC#n;MQ|qtI0qjjV}6RI}_V81T9nWOKY{UCfbc(6NeT zU%`NAwl$zrHRxQ)L{voDDwzoA3>K~4sGze}@UXr(2>-7c?T&gdk9A4sQOtJd$I&LO-W-)nsJt>>3*?=i+ztH`hq13R|Xzb5W$ z{rE0*y%|AvrJ*#?WStRQo%t4;$%t~&%iUyU>gqC-m~c=*TmabG|H+L8!CER*+`#ij zdu({R!Dy?gwOMp*Csg{8=RxFA$vhe|Jcb4VU<0#39ELd@hQaZ0Tcj-v33IT7K@n!q zu9p2rzy_(25u$$!U=B#Q6B2I!Sz!Oo2?-=P@J|O`B%8+Nk=dMoYv6WBsE-}uv&PE6 zJOFJ1_jY&1Tj;~XAfr+u$h7czj$xLXWNf{Q;1Ijb?`LptRKfOab(!RN80L@J?V(3g zH})Psdp}R5uO2PfZGB+f4m0z#)DnB~$V1U;rP5=c8wK99+1#4cMP0nnA&fR@%%Qr? zyxeI?s1I#EbTG(gkJS%~T#aD*o$1#O>_@qmtT&s<6{ABBHSG6UrCPy@;g~?cB>GUq z{PJ}BTA%JjO7^Vb;5k{|>gKF(ofBtfReJh}xw$GyhEG2^QrgQ|8Jn{tsPiwW&dKiG zu^b8Mjb5F2<{bMXATTmXX4hKBe11&$W?|%atsi5l#nla|d1dNX<|0{nIlgpzze_@+ zcFUIHVAA8Rgui;~yZeVMtf&;m#MJn-{=wui-9zI&j+X)_5N(35ewuQ2L>hGhlLl^f Uw)98|^x=TFhp&5sTiE%(0Tbt$W&i*H literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush15.png b/resources/assets/edenmod/textures/catplush/catplush15.png new file mode 100644 index 0000000000000000000000000000000000000000..4bbc7228098f5a8c6678f2b5b69a65bec73444d1 GIT binary patch literal 1346 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 IhVsUL049*CMF0Q* literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush18.png b/resources/assets/edenmod/textures/catplush/catplush18.png new file mode 100644 index 0000000000000000000000000000000000000000..1251acd4713ce7b77a2152973d51fa253cb0370b GIT binary patch literal 1440 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 literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush2.png b/resources/assets/edenmod/textures/catplush/catplush2.png new file mode 100644 index 0000000000000000000000000000000000000000..95e99e53468a78a66acefeb8eb4af409dc4aa5a0 GIT binary patch literal 1245 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G(@^*J&_z!{$_AZ|c6yYrJh%9Dc;5!V$jK}j= zqyPokOFVsD** zft7)UwgHed5cu3&hN2-iKP5A*5?OYP{E$?JhKJvOpPOk}{Pv4erqy1B2dq2- zkC~E1n*~(klo|z;RW#Ig8LYdL(ZR{sd+fB6a4)y{8;>6CyAw3Jl#VnkWOX!L7$fCn z{q{nTmF@F0KiB`}^CGr>E~G?G!w4a(=zTsXJZ! zvR+?5Vg75=lXKrbTv$GzSwKCvCoJOmicsqxFaCbtQT|)~nYzA0irm6y8?yxWuzlp# z4(HLklRanVnI0L3YsCwv2q}oO9n@0%bXos>g;|`f$&u*aWya>EOw)Zf=DrsHQ}^s} zUB85g)08>tv%NoW@8*{~X)TXnQ z`h$Z+nze0aY^t~A>!lw*?l-@hRC{8&+&;CUsW%OLgBy68Yxf?$z1_z0&xOC)^Mw!X z-td*Z?%#1A_29_cGY=+ZoD|(%Fw^15?!c&Ao61)zX7+X3oY%G;vemcqH$3%yt(4S) zD80lr>wlC8-L`2v|L^cB%bu_;LEa&+j+ONboZ`3}_Q&>NhMsS(VOCQ1j~k0;U-MQh zkIde1j*01(@Q*iUOx_9ySsC{w1fHAx`Zn*;-x}MOOYSl}_hxFeZSK-IuE!QP&CRTK z)m>i3J4IWxg`xe3;?83KMVoBqindP>Vq)CY`9{i7jJ+c1Gs9+Xb%ztZi9*Mv^<#IP z*;V>_#y+caFE6K8Je+xU$|Ck3uYTW;=(26U_rk%e^*f{8T$cO|VHfsqn$BEZ_ORs* zfA-d>FR!wH&&)KjsIQx{@uK0YuXWeHyi53P=3V`e%PDIO@5D_F#d6su_q=W9C5a>| ziuJyH+0QH~<8ygR+U8Dy3v=eCR%FcZa$Fq!_(claTHO;$t>;Wntmxaw_FRT3bMJ)t z9n#m=U;kJm%yRHtjNSps?(J-AO>bowEj_uZN2RlKQHQ9l?Gn$~CcYc)DJ`|pOW9z* z<(S{rsI}&)3dfx_?;Cn~X7k+A*mCWNr_ZcqPYzA+s<%IPbB*EU9ZYo#uU)Ow<0!uW z%I9qB+p^hNTW{%1Jv~1)^Or}?+2D^sUUO7de&2UE(tT&lvc*xV^Up`W{#!mX@V(Sj z-D`_tw6f+(EEZt+AG610VZ?#}E#A~jqfI*swolM3-5#@Q&!ai9+~+KxC%wK~D9GY2 q{_Oel$rYXJ=Ebcs73os_$A0hR+g{%YaWh~(XYh3Ob6Mw<&;$S_z8voW literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush20.png b/resources/assets/edenmod/textures/catplush/catplush20.png new file mode 100644 index 0000000000000000000000000000000000000000..9e167ce649bf5f47a68888687f738074f6e67f34 GIT binary patch literal 1545 zcmb_cc}$aM82^wkuZZZ}5w`{1VKxydV1_W%NN9v@;xQH!h8Ac+tR09Qw@?u`1tlC3 z1-TRq;(>xjL_j&@D$u)M`}J<=cb2x4s|c9xb0&-Xe{b?Uzr0W0U!M2(d!M|Q4u~##K8dI4+;PfGyn*= z4gjAdS3N%z2ms402Y(Kyk_V<_1vYu%q9A!m#I|+*PBcVX9{jc>&2&@)juOBqD@O!> z+7zWLM;)-F$fV=iM$)i>BqjGUBDiKBOSAN!P!U~6+r5G$g_$A8WB7|2wG3yY4 zG7Ya3;ZSPS7>3GK3K)j#Z`VR9 zg%-T81DmyAqftG8q6pYk(Ve?X+a3+|=R8GCMkfea+g^Rp~NXY8X)tdBV_&NcQhJ73+uckhyoe_qqW77t&?R2`tOO`}Kj z27^cjN>oOrQlU~oV}cQpL`V>rRr9d%2g04kW@+HgaL~idsG2fr);4v-ytVM0f_*l`ru3GjYt@rjZ z<;x7UsAZJfU0Qavx362R90PkVPYs`(9dDbNnM5&^m>_TyJ~d;rnl&)L%shcut!CSl z)oh)>O&DP&yCVvfWE>th7oYA7e`>dSTOvus})j; zjfj4F+J+$d-X|3;wV8%-^@PP_njo}V<&)Zsrm_SXB+(dj27_KC;yYEl&c=28H;{)Dt)->0;YLPWTx951 zPA)FXoai|@0ptapVt?8f>^Y{`yNlecOb$7E1^`_9-iFg)(j7WkT+R;NPhCzdcXD0h z#Yo-fMV7u{Q;)FeF>FRaTnd8(z=z`Vy(h)blj0rj?Hk}r383usppcp3Hhn(zKLl11 zJuyDxeFDWVz}r8-$NwE6+jl$pp#b0h+ku_PVw_>eu#(^Bc<%@x`|R+0$0;bGRgp@3 zLxcB*e=3fn(P;G8_!veUsxump7!Gi3kAu@)rz;jvcIRBY$f2bN(q`kAwvIB*J&4Ot3lX9aS3W&0Mm(e1SlW`B&CVZ} zmXD~~nf98l#X0w((gCGwwbLiFPKo?|)zr zC-7>1$i3MR{@K=s`lwW!2=<70SRR4(3pa7=AkilmfbxtJ3K1k zRP4!k*A1a#u*yIN@dWpc=T(|lY)DB-t?*6LWJma=tv%qC* zft7)UwgHed5cu3&hN2-iKP5A*5?O8WSV=7)qzus^t8ZTD_(@$++AjC0d(DJh9` zZD2_5kXMv4HB?cNx-w&iM9IUZ*3Rg+Y@MBj3#ISQka*10W4Ob>%~9wmOQ7T8ZAZBr zS6zr=^UAd{E^qr?dam-X^shgR{E2hV)&62@Dq>x<%{5(s!}>F8Ph(U*i%>*_@dZC0BFZr@Vdsy3c3Qf+aS!)s>0slj;`y{r;$s@yr_QS8I=6*k3zIm%q1Oc2Z@G z+50;OGwa>9KHKMB^qOl1ONT^Q8$+~@-5(o<2knd7H-3L`ddBZR${zL(m)^-d58vO}c6!UMEt6Q3 z7du`(J&(`AUg6;ElN*y2*B{~j?;~(#MP|-T_Okj*ODDf)a=Lo>-qrV0UcNmSynWKP zYVM#(+Ff6!t?F-?)ZM29Cm#)WnxlI;r9ZDT5LK+vxq$-ffGU# zn%40iD%o{`S=2y6#bk!$DRl|eg@+h&ba^BU#KcZ63$|DgdbT+Ext>n06h~wAyH(Ci z3yL^`gjNa$fxW-^uSzr)~b3zfwj{zbk3q-8|>Hw#96R zot*=?>;*p_Rq@=lHth9>7dJ9C2X)$oIQ_VFnZsLAfu*PP_@krT!ILMSSJ39VlFd9J zs5vb?eWI%54TGm)`_cv5w_fQN4A^{Z&C5Fk ng|D7)uDP?PPdv`5* zft7)UwgHed5cu3&hN2-iKP5A*5?Oi;NBcKKixD6$lxv9k^iMa*H zdJIjhfcC)jbWKc{!@$6N#?!?y#NzbSDcGgC=efv{;dj9`E)qo)hf`Y~?zxneiUSKn{uVB}+RQW5ZAVA0dnd+=-d{0CFT;}1!Dzh96qv7kp$ z-iThXv;8Z z+aV`or29HXK|RZFnuu%S*DBuS+`%44gnNqnI^-M|Uv*i3-+6CU!^Me# zCA(uq)VTd$N9~y4p|*XA2Qbb)Y)g$?nDtd)mR#%W9Uf}!2W|QMD*{qjPcBq!X;%DE zqa~57A#zS(_F2B)HTyhIX++%Lq{kyuJ%tWm*I(?kt{D?WrWVp=hT~=$#uMbanI+ifWj)J#Hu#|GTd-?J>vAGaNH~6crBG z@-t^MgoX-mwJunt6?6M*u$y9UPoK=$v$h8}&*b^1H+4g4y70;rF88fc`ywJ8-)>;i zVhmvtaa;IW@JUhfiIhzpuE}94i>8>eXcNz`K`0pCLg`+8o|u{(ueKwPaChL zLXSRbn8dd0oc>_L7@1Ze!#<~2AmME3oYM?1C&lN9KD21AY}K4^cuGxUslcJ;`|Fzo zIM=;C@Kk2qYbWmZiy3AY^0x0S&4a4*Ywp>`)W2Klo^Sar;gCU9 z!OrN{-&I>ZRGdy6{`j}7U3t#$XswC8j8E9@j(zmd<1VT%7eA!@KyJU2=FV#` zx9u&e{QtGYs@RXaefpm|d5NVB4BK<7uReWu`Zqg!s+w`Zan^|(^DOHbeK;1}wmdd> ztNi|%u`xR`qnV>L=4c8Z{4U}unLK5g%{kw5drK8REPegyu!~LK@#V|@f9Lwo_%U<) VPwA4Lx4^Q1!PC{xWt~$(69Cu38OZ* zft7)UwgHed5cu3&hN2-iKP5A*5?O57MwM?x!i(jpBO-fIj|89PJubf=Wf zcv2HF?~|Y8Yxl=?nR zz+icbVCM#_im%Pe9hccJ3dg@?bhyRT5G7W)+UCfz@cR${+*};O++N71E6dbvYAX^r zKZwOsEpLDAXKuY2T2itN0$#JWEc}&q_h$Fzi>K0>znw{xu{`Fa9@O~h&Wz@;L~l*K zdXGt2*+*vnbQN$~5V?cz)a{rr&+JcrN^xsp{yn!e_PNVdPQl36U0I#iw?5Aob-LDV zv$^tG`X(olun+z+u1mzvFZ{eSGiT+$6>qnhSA1Q_yKw!swHdrt_5rsn)3+Q;_#ALj z;$>^b(utc~s;k=0UDeKCRr+}Ut6!%gZZBwaJ@)W}mCZ{H{`DWS)(W;vVhFN&Y{?z5 zAyZmi>xRWrbK8@bo_gz_DEn?0o^t%@vhREWa+1;VQ+^4UE#Fked+EDl$Ny;0mI(^A zGIH0%Qo6ZyV)u1;Ue3C=^S6slDO=^Y-TiKpEb7XB{n821YB`%YXS2oY0=_jr^Bo_g zG?a4HANBh4kIn3SVB(I#*Y`LTofD(O*PUN?>-*vNFD`Bsxuo37?I7UFS$*&R`R(`R znN`j;ZsrsG@x^{$=c&KRSB@W-p8R=v?Oe@N_v>5loSS=?McLQxZgod%>+U_fU#DI& zNq%-qH*2fWHHkGFC6YH@dvyPDO2&f5LYM9T=b1dZ^wODmYM0#_=C5D+?f)L^41Vr0 zX$t$b^=YS{wP*5#Ok}F@pYWXZ7+2S!32B<1GSbNvr)U1~HZ=Zds<*;!dCSf8wYieX z9+O*IkMgY!|N28N5DpOluL;AZKox_UU+@%)pQ`g;BUzfAeDNBZpf*cmpRGc11cdcKla zBe~?${F9e5%4Z$>uId?OF+phavEZ-}?MCjy2^TH@CYE>hcmFAywY1OL+;3A_4m;C> z^TF(rv9^ca^m1SE+9l;aH|=-c@rtMr=J@>UQBw+(%+CEOdw%ZG$Hrr;9zE-=JoR*f h!khHR)69Q}e^_yEMWV)s7+?-(@O1TaS?83{1OU>r8`b~- literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush6.png b/resources/assets/edenmod/textures/catplush/catplush6.png new file mode 100644 index 0000000000000000000000000000000000000000..164fdf03419a3270b8060e9e55917da36104de11 GIT binary patch literal 1240 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G(@^*J&_z!{$_AZ|c6yYrJh%9Dc;5!V$jK}j= zqyPokOFVsD** zft7)UwgHed5cu3&hN2-iKP5A*5?O8V#v=T8ZhIrj0r<@4I-b7!6_nQ`l-$Vvx} zjDo~ag_dp?9sw6k)wUHXY`42a3PKFoGw*FY#d$IZVjg6JJ=kAQTuy5;y73R{qXWlxU)ks^h z$>MfSxn0lXlG^36^FGv`ziSrq<~954l(f7u_r({hv^+1Zf26VR*Zzg+@f8!dN}Ctu zzLQ$yVzlr2+OS{zJ@0P){JuOnSv~acH6}03ImH&wH#`b(n6yk|X~o~O<$+v6uRA8S z81&rO@{F-8zdogOmfCWz)<^2i%-#G|To=5u+i$b69N=s*c%5GHc&&2GpW;cf(emef z&YsKdo0QW0eCC}W`TPAtf4z^mu`hM9TZU!lhX5}1bzDU(DXmT4U7k2iTBdEbHeNh- z-b}mtXK!w-QJ+8cICol}O&i z)f%*5${`8o%0T_MVV7MBo$^=q`|Um)dsS=RH3hzdh5;|{TmO6danfq?n@UHx?0=hydLv3ztT=;itiDIXcW-zzp}I5wk3q`f(A z_syu#xGgJWSQyRoVxGVFEx#e_sh8XLcZqpbZI`!7Ev|NpFfwUjP*U(?2#9*@!g6HQ z&6O{8=UtTV-um>`%R)7E^@~xPbAATN$?ThNz;p8ZlR&S9y^fiO_xneSa0#ZZE$&~{ z+1VYb{BA}58OOD}$1)CSuq-|CLr#e=TlU(rj9iXSwl_?U9^<@bdEwXFkm~lmD=u?R zR;fIiA^Ypto9j_un--X~#JbN~){@wk{A< z`}f7{_k6Z{i>o)^ek+%^b+(FU(^fHcvGWqAp4v4j9Ekk=x_9>V{;6I|r-`#9dkXnJ zW_kbX>}-i-3xiKHw7-bld-=>tZT9)xj4R^HjAteoa5N<*Pf73T44yr2vPT?u&3ks< z4vuSTERKS#2IjWS-oh*uUw;_J)n%0D@4n1CJ%ru6P-bo4VuSp(xko%}5(^es-S5+} j{i)-8dfLD6_Iidr7q2K++&Sk7%;F55u6{1-oD!M* zft7)UwgHed5cu3&hN2-iKP5A*5?ONjSVMH|3=KoItSM6`Z7>jeteTh{vSA`ar%=p-NnyKJm|R&k zW!knaOPAU{KXYd0PklX+;NJ7EqH8P7&pUrGPv2L?sU`Q&@#W0Fado-3R{cz-$`n`;ou5DN(!?LJJ`|8>RhJt^)jXPF-_AvVY@WA%J`|KE3 zEq7QT`0LctbGP=*TWh_owES94SJe8zRdbb#7xzrBSg%&&-I(wL>`5cH!vRQRHRhL~Y?^(RSnJJk&H?d~eR|ib=~CuxJ$M z&CARBa{p<}7oF?x4!Encw7E$e?7Y&p__*EiGiP*W+-AA8g}X1IfTjJb&7CXnE*)X& ze72g$WaZKGO%EiJzgRL!#pstORyiizeXGjz&!$PmGay`?sYQ8hy}-5&a#I2Y+#F-x zZ;Ls{%TeqgxwN|V0iPnvv8*SbEpzVNXni-&H(^fV^KW)*GBa5d7S1tKIjYVm=#ZI^ zqg1Lju{4Oa;Mk7|r>@AY?OndrO+aKr{r%txmkzF*YaNiIqx4>!K_DVP<>&7OlUE0v zW}Y$cw9$UPmC6zp;*Z|?td#HBw)=93y^F?s(KAhjZM(g63uMG62M4bRTlFI5J|B0d z>o)P3;m+5eSx(!e_w1=tfRl#{hvn3jPf{7!S)Lak(|DodI%`?O)*qj`CtDOf(U=nS zZLiqcIDWQ`q@!`j<>QB-O0Iv0mNt%0r7y(+wRod0%>X zXl~5A8}*}((b=9i`O75@uccyZfY~9a%P!T&D^IiN;i1R?HijDtv6c=w*Z5W|?zIy6 zBM{^9$LgQ;F}?nUyQ_t_Tw8GDK&k4Z9c91duP@PNI25?@Ouy8O@7zD{B-+n+7CP=R zNg_si%eE;2S$S?Eu4$>MPlXjd7GK*oJE*K{joFV!54NA!#By!c?(U<@@((RoxGzdm zf7wbY^(nb0joxZ3o&H$m=Z2Vdd%`}%9+t>{(sTCH)&6%kzcK9l|IwspxBGUD7P+E%s~=g|c{!~1y4_!EAO14qe$RffKlb%Z Z)lNrdtnXQO68 literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush8.png b/resources/assets/edenmod/textures/catplush/catplush8.png new file mode 100644 index 0000000000000000000000000000000000000000..40bf8ace9b6c86cc9648d9e8dd8761016956f031 GIT binary patch literal 1225 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G(@^*J&_z!{$_AZ|c6yYrJh%9Dc;5!V$jK}j= zqyPokOFVsD** zft7)UwgHed5cu3&hN2-iKP5A*5?ODVc!v+sn89Jhad?|yQcQRSA(EuOR70yT6m zIl0D8=?W5GyF*axAGe;1hEg8Of2QerC4u5W;*XpH0tE!S)^7`tUO8!2*ec&!OD50s zF}6(3Y81F|C@Age$!Bll@9+Jw&n3RDh^=8ooNr`yxrfl_oJbzE z)0-BQF!CxWsf2OZ1?+0*aC|hW?%Jc*>-SmQ(3!u=d|LiiMxSc+pu)MkoGXszr{n7t6ir(bGTdjdN)%)NML_l3j8YlhTg|0S|u7 z7hiEDyXr+}^ZmbDzihTI{Pjbi<o8JTY&UkG_nTxcKwLr%x3#YHCEHZ=e1cygo&G?X#cRt9{v~9xwPQ)fuhW zXQQ{WhuX<`RCkySN`&;rCC>+RPWjE2li)mg~fcU z_OLm;;PN6n!(NF1!-!-_Ny$hl*_Hbq)ZUnL%hr=6!KFa4;d{CGx3hciy)OHn8u`|8 z@-vq8S0-1k;bd?$ZD0u&nfb*d6hWDQgf%Zi zR!!aB`_;bn!7mQ^yD@)jFIU>f{QsMuP*u`%?3bP0l+XkK6p9cL literal 0 HcmV?d00001 diff --git a/resources/assets/edenmod/textures/catplush/catplush9.png b/resources/assets/edenmod/textures/catplush/catplush9.png new file mode 100644 index 0000000000000000000000000000000000000000..44b2a31a36ac21e4e07c67c49ca3d90e2a03a58a GIT binary patch literal 1369 zcmb`Fc~DbV6vl4~BP9yrhM*30fMSb+K?O956$(PazGzTl3ql|$$dU&m$W8#+QrXEu z;3XjokU$7)kfjoqw17G|27_2sv>3+~1sY?%d)4KTjj{ z4m1G32DZgKb-pa$DjnGzZ)vb1Z|EfD`MSND?C0rqc7u}I+E>Y8${64ASWAM1~| z(j*(|1m8%~j*AlGyPfv0~HoDQ6Ajl4& zr}!Xy^~?CpH95?}_)eHA?~{o*j7CQF``N;GKj-NtDet7G#$=4~9`>B*!fI>Ca@El# zCVrtaS*N=CqzSLA2<~oeluAT*ijK{5iBlcLz14}!%|VNN;_&cLXNP2g?SH2wZmOsL z9s{pxx$sCFGYTgy^yR(iCM(5PWi4!-u23aRk`_e_l>|Lt`b=dza#8|h&;eyl;5?tG z$UQ8*xCaiiXdvuriL&BFST{x5iql=aG4_UF<7wwTZQM?S{xovSWX)i%cvm$jfEX>nmvGVW9+S zZk~WaYoi7$i?IXzG9kaYiN!1`$j?kq&(6rKsjTGHmv`61i3H7XV?zU@o?ctSsH<&Y zGI%h1m{-;zfQ7s!0hi5(SzLM*|5{!b3OzB3-gJ9*v#DCS!A8@&&U=l6HZ<62-Z-5$RJ~5W;DP$>;uqWRttEKARv4u% zT3nH@Kk_sQ#e0h<{r$e(7@aH;#1%{OE%f#DL#1Vt$XuxN-k$EajPKrLA%$V0*BJ@` z#-kqsHIX2ULQDe)=j&mhF+dp`?I)#r?nkUIA&*cfDgq*5Ba=ypfCHEVPIee)JB&k! z!$Isp3>I^EF9yjNlb89a{{i9?qGF@dJ_Rt&ScfB6N6bgy@ul;Y2=LXv9iZ5F5(SEg zPyCc|ID|#|9CH51lvIQx)1wS1n5RQ6t)CPo{q?wp2WM{QTFO z<(7&qCT3fwN-Y)JH}5c=`C*^sZnI71+w83OS)w177JGZ)#}wnZi9M5(Zq%ivEmf7l zd;z~XCd&2%b*rqClKe7yisg88_yo?}n!`C8dMHMF{oQ&~dt3X!U<1Qga&4{Mcb(9^ zKep{9)DXzI)UFg;Li?G4OKXMYOeeyr=XuoC)r&oG3q6?=Gv@~0+_`I>L)}oW+4)*K zpS`%K{t{#W|;*1((6h vwJkP&jJTMMui$*{Y_({{ewMg!-zA3#oOgwc3sOzQ$TI?*ho8H|Ej;fZGF@N7 literal 0 HcmV?d00001 diff --git a/src/tel/eden/mod/chat/DiscordChatFormatter.java b/src/tel/eden/mod/chat/DiscordChatFormatter.java index 7ce995e..dcbb0f3 100644 --- a/src/tel/eden/mod/chat/DiscordChatFormatter.java +++ b/src/tel/eden/mod/chat/DiscordChatFormatter.java @@ -32,7 +32,7 @@ public final class DiscordChatFormatter { private static final FontDescription PILL_FONT = new FontDescription.Resource(Identifier.parse("banner/pill")); private static final FontDescription PREFIX_FONT = new FontDescription.Resource(Identifier.parse("chat/prefix")); - private static final Style PREFIX_STYLE = Style.EMPTY.withFont(PREFIX_FONT).withColor(ChatFormatting.GREEN); + private static final Style PREFIX_STYLE = Style.EMPTY.withFont(PREFIX_FONT).withColor(ChatFormatting.AQUA); // banner/pill font: left cap, lowercase letters from U+E030, fill spacer, right cap. private static final int PILL_LEFT_CAP = 0xE060; @@ -45,8 +45,13 @@ public final class DiscordChatFormatter { private static final int[] SHIELD = {0xCFFFC, 0xE006, 0xCFFFF, 0xE002, 0xCFFFE}; private static final int[] CONTINUATION = {0xCFFFC, 0xE001, 0xD0006}; - // Bare http(s) links in relayed Discord messages become clickable in-game. - private static final Pattern URL_PATTERN = Pattern.compile("https?://\\S+"); + // Bare http(s) links become clickable, and :shortcode: tokens matching a + // known emote (see EmoteRegistry) become an inline image, in a single pass + // over relayed message content so both interleave correctly with plain text. + private static final Pattern TOKEN_PATTERN = Pattern.compile("(?https?://\\S+)|:(?[a-zA-Z0-9_+\\-]{2,32}):"); + + // Matches any :shortcode: token (shared with linkify). + private static final Pattern EMOTE_PATTERN = Pattern.compile(":(?[a-zA-Z0-9_+\\-]{2,32}):"); // Wynncraft chat line width: floor(chatWidth * 280 + 40), then divided by the // chat scale (mirrors ChatComponent's internal wrap width). @@ -187,17 +192,30 @@ private static MutableComponent replyTarget(String replyTo, String replyExcerpt) return segment; } - /** Render message text with any http(s) URLs as clickable, underlined aqua links. */ + /** Render text with http(s) URLs as clickable, underlined aqua links. */ + /** + * Render message text with any http(s) URLs as clickable, underlined aqua links, + * and any {@code :shortcode:} token matching a known emote (see + * {@link EmoteRegistry}) as an inline image. Unknown shortcodes are left as + * plain literal text (e.g. {@code :notarealemote:} stays exactly that), so a + * message never loses information just because an emote isn't recognized. + */ private static MutableComponent linkify(String content) { MutableComponent out = Component.empty(); - Matcher matcher = URL_PATTERN.matcher(content); + Matcher matcher = TOKEN_PATTERN.matcher(content); int last = 0; while (matcher.find()) { if (matcher.start() > last) { out.append(Component.literal(content.substring(last, matcher.start())).withStyle(ChatFormatting.GREEN)); } - String url = matcher.group(); - out.append(Component.literal(url).withStyle(linkStyle(url))); + String url = matcher.group("url"); + if (url != null) { + out.append(Component.literal(url).withStyle(linkStyle(url))); + } else { + String shortcode = matcher.group("emote"); + Component emote = emoteComponent(shortcode); + out.append(emote != null ? emote : Component.literal(matcher.group()).withStyle(ChatFormatting.GREEN)); + } last = matcher.end(); } if (last < content.length()) { @@ -206,16 +224,69 @@ private static MutableComponent linkify(String content) { return out; } + /** The inline glyph for {@code shortcode}, hoverable to show its name, or {@code null} if unknown. */ + private static Component emoteComponent(String shortcode) { + Integer codepoint = EmoteRegistry.codepointFor(shortcode); + if (codepoint == null) { + return null; + } + String glyph = new String(Character.toChars(codepoint)); + // WHITE is deliberate: Minecraft tints bitmap-font glyphs by the current + // text color, and these are full-color images, not glyph masks — white + // leaves the PNG's own colors untouched instead of dyeing it chat-green. + Style style = Style.EMPTY.withFont(EmoteRegistry.font()).withColor(ChatFormatting.WHITE).withHoverEvent(new HoverEvent.ShowText(Component.literal(":" + shortcode + ":").withStyle(ChatFormatting.GRAY))); + return Component.literal(glyph).withStyle(style); + } + private static Style linkStyle(String url) { Style base = Style.EMPTY.withColor(ChatFormatting.AQUA).withUnderlined(true); try { return base.withClickEvent(new ClickEvent.OpenUrl(URI.create(url))).withHoverEvent(new HoverEvent.ShowText(Component.literal("Open " + url))); } catch (IllegalArgumentException e) { - // Not a valid URI after all — show it plainly rather than as a dead link. return Style.EMPTY.withColor(ChatFormatting.GREEN); } } + /** + * Process any chat component for {@code :shortcode:} patterns and replace + * known emotes with inline image glyphs via {@link EmoteRegistry}, preserving + * per-element styles (Wynncraft rank tags, colours, etc.). Unknown shortcodes + * are left as literal text. Returns the original component unchanged if no + * emote pattern is found. + */ + public static Component processEmotes(Component message) { + String text = message.getString(); + if (!EMOTE_PATTERN.matcher(text).find()) { + return message; + } + MutableComponent result = Component.empty(); + // Visit each styled leaf segment preserving its original style. + message.visit((style, segment) -> { + Matcher matcher = EMOTE_PATTERN.matcher(segment); + int last = 0; + while (matcher.find()) { + if (matcher.start() > last) { + result.append(Component.literal(segment.substring(last, matcher.start())).withStyle(style)); + } + String shortcode = matcher.group("emote"); + Integer codepoint = EmoteRegistry.codepointFor(shortcode); + if (codepoint != null) { + String glyph = new String(Character.toChars(codepoint)); + Style emoteStyle = Style.EMPTY.withFont(EmoteRegistry.font()).withColor(ChatFormatting.WHITE).withHoverEvent(new HoverEvent.ShowText(Component.literal(":" + shortcode + ":").withStyle(ChatFormatting.GRAY))); + result.append(Component.literal(glyph).withStyle(emoteStyle)); + } else { + result.append(Component.literal(":" + shortcode + ":").withStyle(style)); + } + last = matcher.end(); + } + if (last < segment.length()) { + result.append(Component.literal(segment.substring(last)).withStyle(style)); + } + return Optional.empty(); + }, Style.EMPTY); + return result; + } + /** Wrap the body to chat width, prefixing line 1 with the shield and the rest with bars. */ private static Component withGuildPrefix(Component body) { Component shield = prefix(SHIELD); diff --git a/src/tel/eden/mod/chat/EmoteRegistry.java b/src/tel/eden/mod/chat/EmoteRegistry.java new file mode 100644 index 0000000..77d7e74 --- /dev/null +++ b/src/tel/eden/mod/chat/EmoteRegistry.java @@ -0,0 +1,97 @@ +package tel.eden.mod.chat; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.network.chat.FontDescription; +import net.minecraft.resources.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves {@code :shortcode:} tokens in relayed Discord messages to the + * private-use codepoint that the {@code edenmod:emotes} bitmap font renders as + * an inline image, e.g. {@code :catplush1:} -> the glyph backed by + * {@code textures/catplush/catplush1.png}. + * + *

Both the font ({@code assets/edenmod/font/emotes.json}) and the manifest + * this class reads ({@code assets/edenmod/emotes_manifest.json}) are generated + * at build time by the {@code generateEmoteAssets} Gradle task from whatever + * PNGs exist under {@code resources/assets/edenmod/textures/}. Adding a new + * emote is purely an asset change — drop a PNG in one of the configured + * directories and rebuild; nothing in this class needs to change. + */ +public final class EmoteRegistry { + private static final Logger LOGGER = LoggerFactory.getLogger("edenmod"); + private static final String MANIFEST_PATH = "/assets/edenmod/emotes_manifest.json"; + private static final String DEFAULT_FONT_ID = "edenmod:emotes"; + + private static volatile Map emotes; + private static volatile FontDescription font; + private static volatile boolean failedToLoad; + + private EmoteRegistry() { + } + + /** The private-use codepoint for {@code shortcode} (without colons), or {@code null} if unknown. */ + public static Integer codepointFor(String shortcode) { + ensureLoaded(); + return emotes.get(shortcode); + } + + /** The font every emote glyph renders with. */ + public static FontDescription font() { + ensureLoaded(); + return font; + } + + private static void ensureLoaded() { + if (emotes != null) { + return; + } + synchronized (EmoteRegistry.class) { + if (emotes == null) { + load(); + } + } + } + + private static void load() { + Map loaded = new ConcurrentHashMap<>(); + String fontId = DEFAULT_FONT_ID; + try (InputStream in = EmoteRegistry.class.getResourceAsStream(MANIFEST_PATH)) { + if (in == null) { + // Not fatal: happens if someone runs from sources without ever having + // built (the manifest is gitignored, generated). Emotes just render + // as their literal ":shortcode:" text until the project is built. + LOGGER.warn("No emote manifest at {} — run a build to generate it; chat emotes will render as plain text until then", MANIFEST_PATH); + } else { + try (InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + JsonObject root = new Gson().fromJson(reader, JsonObject.class); + if (root != null) { + if (root.has("font") && !root.get("font").isJsonNull()) { + fontId = root.get("font").getAsString(); + } + JsonObject entries = root.getAsJsonObject("emotes"); + if (entries != null) { + for (String shortcode : entries.keySet()) { + loaded.put(shortcode, entries.get(shortcode).getAsInt()); + } + } + } + } + } + } catch (IOException | RuntimeException e) { + LOGGER.warn("Failed to load emote manifest; chat emotes disabled", e); + loaded.clear(); + } + font = new FontDescription.Resource(Identifier.parse(fontId)); + emotes = Collections.unmodifiableMap(loaded); + } +} \ No newline at end of file diff --git a/src/tel/eden/mod/mixin/ClientPacketListenerMixin.java b/src/tel/eden/mod/mixin/ClientPacketListenerMixin.java index 89c4bcf..90850f6 100644 --- a/src/tel/eden/mod/mixin/ClientPacketListenerMixin.java +++ b/src/tel/eden/mod/mixin/ClientPacketListenerMixin.java @@ -1,28 +1,37 @@ package tel.eden.mod.mixin; import tel.eden.mod.EdenModClient; +import tel.eden.mod.chat.DiscordChatFormatter; +import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -/** - * Captures system chat at the earliest point — at the HEAD of - * {@code handleSystemChat}, before Wynntils or the Fabric message API can cancel - * or reformat it — and forwards the raw component to the bridge. - */ @Mixin(ClientPacketListener.class) public class ClientPacketListenerMixin { - @Inject(method = "handleSystemChat", at = @At("HEAD")) + @Inject(method = "handleSystemChat", at = @At("HEAD"), cancellable = true) private void edenBridge$captureGuildChat(ClientboundSystemChatPacket packet, CallbackInfo ci) { if (packet.overlay()) { - return; // action-bar/overlay messages are never guild chat + return; } EdenModClient mod = EdenModClient.instance(); if (mod != null) { mod.handleSystemChat(packet.content()); } + Component modified = DiscordChatFormatter.processEmotes(packet.content()); + if (modified != packet.content()) { + Component finalModified = modified; + ci.cancel(); + Minecraft.getInstance().execute(() -> { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.displayClientMessage(finalModified, false); + } + }); + } } }