2020import net .minecraft .client .multiplayer .MultiPlayerGameMode ;
2121import net .minecraft .client .player .LocalPlayer ;
2222import net .minecraft .core .BlockPos ;
23+ import net .minecraft .core .GlobalPos ;
2324import net .minecraft .core .Holder ;
2425import net .minecraft .network .protocol .game .ServerboundSelectTradePacket ;
26+ import net .minecraft .resources .ResourceKey ;
2527import net .minecraft .world .InteractionHand ;
2628import net .minecraft .world .InteractionResult ;
29+ import net .minecraft .world .entity .ai .memory .MemoryModuleType ;
2730import net .minecraft .world .entity .npc .villager .Villager ;
2831import net .minecraft .world .entity .npc .villager .VillagerProfession ;
2932import net .minecraft .world .inventory .ClickType ;
@@ -109,9 +112,13 @@ public final class AutoLibrarianHack extends Hack
109112
110113 private Villager villager ;
111114 private BlockPos jobSite ;
115+ private BlockPos blockingJobSite ;
112116
113117 private boolean placingJobSite ;
114118 private boolean breakingJobSite ;
119+ private int professionRetryDelay ;
120+ private int reasonMessageCooldown ;
121+ private String lastReasonMessage = "" ;
115122
116123 public AutoLibrarianHack ()
117124 {
@@ -149,8 +156,12 @@ protected void onDisable()
149156 overlay .resetProgress ();
150157 villager = null ;
151158 jobSite = null ;
159+ blockingJobSite = null ;
152160 placingJobSite = false ;
153161 breakingJobSite = false ;
162+ professionRetryDelay = 0 ;
163+ reasonMessageCooldown = 0 ;
164+ lastReasonMessage = "" ;
154165 experiencedVillagers .clear ();
155166 }
156167
@@ -185,6 +196,38 @@ public void onUpdate()
185196 return ;
186197 }
187198
199+ // If the villager still hasn't taken the librarian job, keep cycling
200+ // the lectern until it does. This avoids getting stuck probing forever.
201+ if (!isLibrarian (villager ))
202+ {
203+ updateBlockingJobSite ();
204+ chatNoLibrarianReason ();
205+ if (professionRetryDelay > 0 )
206+ {
207+ professionRetryDelay --;
208+ return ;
209+ }
210+
211+ if (BlockUtils .getBlock (jobSite ) == Blocks .LECTERN )
212+ {
213+ breakingJobSite = true ;
214+ ChatUtils .message (
215+ "Villager is not a librarian yet. Replacing lectern..." );
216+
217+ }else
218+ {
219+ placingJobSite = true ;
220+ ChatUtils .message (
221+ "Villager is not a librarian yet. Placing lectern..." );
222+ }
223+
224+ return ;
225+ }
226+
227+ blockingJobSite = null ;
228+ lastReasonMessage = "" ;
229+ reasonMessageCooldown = 0 ;
230+
188231 if (!(MC .screen instanceof MerchantScreen tradeScreen ))
189232 {
190233 openTradeScreen ();
@@ -257,6 +300,13 @@ public void onUpdate()
257300 setEnabled (false );
258301 }
259302
303+ @ Override
304+ public String getRenderName ()
305+ {
306+ int remaining = wantedBooks .getOffers ().size ();
307+ return getName () + " [" + remaining + " books]" ;
308+ }
309+
260310 private void breakJobSite ()
261311 {
262312 if (jobSite == null )
@@ -299,6 +349,7 @@ private void placeJobSite()
299349 {
300350 System .out .println ("Job site has been placed." );
301351 placingJobSite = false ;
352+ professionRetryDelay = 40 ;
302353
303354 }else
304355 {
@@ -445,8 +496,7 @@ private void setTargetVillager()
445496 .filter (e -> !e .isRemoved ()).filter (Villager .class ::isInstance )
446497 .map (e -> (Villager )e ).filter (e -> e .getHealth () > 0 )
447498 .filter (e -> player .distanceToSqr (e ) <= rangeSq )
448- .filter (e -> e .getVillagerData ().profession ().unwrapKey ()
449- .orElse (null ) == VillagerProfession .LIBRARIAN )
499+ .filter (this ::isLibrarian )
450500 .filter (e -> e .getVillagerData ().level () == 1 )
451501 .filter (e -> !experiencedVillagers .contains (e ));
452502
@@ -502,11 +552,108 @@ private void setTargetJobSite()
502552 System .out .println ("Found lectern at " + jobSite );
503553 }
504554
555+ private boolean isLibrarian (Villager villager )
556+ {
557+ return villager .getVillagerData ().profession ().unwrapKey ()
558+ .orElse (null ) == VillagerProfession .LIBRARIAN ;
559+ }
560+
561+ private void updateBlockingJobSite ()
562+ {
563+ BlockPos found = getVillagerMemoryPos (MemoryModuleType .JOB_SITE );
564+ if (found != null && !found .equals (jobSite ))
565+ {
566+ blockingJobSite = found ;
567+ return ;
568+ }
569+
570+ found = getVillagerMemoryPos (MemoryModuleType .POTENTIAL_JOB_SITE );
571+ if (found != null && !found .equals (jobSite ))
572+ {
573+ blockingJobSite = found ;
574+ return ;
575+ }
576+
577+ blockingJobSite = null ;
578+ }
579+
580+ private void chatNoLibrarianReason ()
581+ {
582+ if (villager == null || jobSite == null || MC .level == null )
583+ return ;
584+
585+ if (reasonMessageCooldown > 0 )
586+ {
587+ reasonMessageCooldown --;
588+ return ;
589+ }
590+
591+ ResourceKey <VillagerProfession > profession =
592+ villager .getVillagerData ().profession ().unwrapKey ().orElse (null );
593+ String reason ;
594+
595+ if (profession == VillagerProfession .NITWIT )
596+ {
597+ reason = "Reason: villager is a nitwit and cannot take jobs." ;
598+
599+ }else if (profession != VillagerProfession .NONE
600+ && profession != VillagerProfession .LIBRARIAN )
601+ {
602+ String profName = String .valueOf (profession );
603+ reason = "Reason: villager already has a different profession ("
604+ + profName + ")." ;
605+
606+ }else if (villager .getVillagerData ().level () > 1 )
607+ {
608+ reason =
609+ "Reason: villager level is above novice and profession is locked." ;
610+
611+ }else if (villager .getVillagerXp () > 0 )
612+ {
613+ reason =
614+ "Reason: villager has XP, so profession/trades are already locked." ;
615+
616+ }else if (blockingJobSite != null )
617+ {
618+ reason = "Reason: villager remembers another workstation at "
619+ + blockingJobSite .toShortString ()
620+ + " (yellow ESP/tracer). Break it." ;
621+
622+ }else
623+ {
624+ reason =
625+ "Reason: likely pathing issue. Make sure villager can walk to the lectern." ;
626+ }
627+
628+ if (!reason .equals (lastReasonMessage ))
629+ {
630+ ChatUtils .message (reason );
631+ lastReasonMessage = reason ;
632+ reasonMessageCooldown = 20 ;
633+ return ;
634+ }
635+
636+ reasonMessageCooldown = 100 ;
637+ }
638+
639+ private BlockPos getVillagerMemoryPos (MemoryModuleType <GlobalPos > memory )
640+ {
641+ if (villager == null || MC .level == null )
642+ return null ;
643+
644+ GlobalPos pos = villager .getBrain ().getMemory (memory ).orElse (null );
645+ if (pos == null || pos .dimension () != MC .level .dimension ())
646+ return null ;
647+
648+ return pos .pos ();
649+ }
650+
505651 @ Override
506652 public void onRender (PoseStack matrixStack , float partialTicks )
507653 {
508654 int green = 0xC000FF00 ;
509655 int red = 0xC0FF0000 ;
656+ int yellow = 0xC0FFFF00 ;
510657
511658 if (villager != null )
512659 RenderUtils .drawOutlinedBox (matrixStack , villager .getBoundingBox (),
@@ -521,6 +668,15 @@ public void onRender(PoseStack matrixStack, float partialTicks)
521668 RenderUtils .drawOutlinedBoxes (matrixStack , expVilBoxes , red , false );
522669 RenderUtils .drawCrossBoxes (matrixStack , expVilBoxes , red , false );
523670
671+ if (blockingJobSite != null )
672+ {
673+ AABB box = new AABB (blockingJobSite );
674+ RenderUtils .drawOutlinedBox (matrixStack , box , yellow , false );
675+ RenderUtils .drawCrossBox (matrixStack , box , yellow , false );
676+ RenderUtils .drawTracer (matrixStack , partialTicks ,
677+ Vec3 .atCenterOf (blockingJobSite ), yellow , false );
678+ }
679+
524680 if (breakingJobSite )
525681 overlay .render (matrixStack , partialTicks , jobSite );
526682 }
0 commit comments