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 ();
@@ -299,6 +342,7 @@ private void placeJobSite()
299342 {
300343 System .out .println ("Job site has been placed." );
301344 placingJobSite = false ;
345+ professionRetryDelay = 40 ;
302346
303347 }else
304348 {
@@ -445,8 +489,7 @@ private void setTargetVillager()
445489 .filter (e -> !e .isRemoved ()).filter (Villager .class ::isInstance )
446490 .map (e -> (Villager )e ).filter (e -> e .getHealth () > 0 )
447491 .filter (e -> player .distanceToSqr (e ) <= rangeSq )
448- .filter (e -> e .getVillagerData ().profession ().unwrapKey ()
449- .orElse (null ) == VillagerProfession .LIBRARIAN )
492+ .filter (this ::isLibrarian )
450493 .filter (e -> e .getVillagerData ().level () == 1 )
451494 .filter (e -> !experiencedVillagers .contains (e ));
452495
@@ -502,11 +545,114 @@ private void setTargetJobSite()
502545 System .out .println ("Found lectern at " + jobSite );
503546 }
504547
548+ private boolean isLibrarian (Villager villager )
549+ {
550+ return villager .getVillagerData ().profession ().unwrapKey ()
551+ .orElse (null ) == VillagerProfession .LIBRARIAN ;
552+ }
553+
554+ private void updateBlockingJobSite ()
555+ {
556+ BlockPos found = getVillagerMemoryPos (MemoryModuleType .JOB_SITE );
557+ if (found != null && !found .equals (jobSite ))
558+ {
559+ blockingJobSite = found ;
560+ return ;
561+ }
562+
563+ found = getVillagerMemoryPos (MemoryModuleType .POTENTIAL_JOB_SITE );
564+ if (found != null && !found .equals (jobSite ))
565+ {
566+ blockingJobSite = found ;
567+ return ;
568+ }
569+
570+ blockingJobSite = null ;
571+ }
572+
573+ private void chatNoLibrarianReason ()
574+ {
575+ if (villager == null || jobSite == null || MC .level == null )
576+ return ;
577+
578+ if (reasonMessageCooldown > 0 )
579+ {
580+ reasonMessageCooldown --;
581+ return ;
582+ }
583+
584+ ResourceKey <VillagerProfession > profession =
585+ villager .getVillagerData ().profession ().unwrapKey ().orElse (null );
586+ String reason ;
587+
588+ if (profession == VillagerProfession .NITWIT )
589+ {
590+ reason = "Reason: villager is a nitwit and cannot take jobs." ;
591+
592+ }else if (profession != VillagerProfession .NONE
593+ && profession != VillagerProfession .LIBRARIAN )
594+ {
595+ String profName = String .valueOf (profession );
596+ reason = "Reason: villager already has a different profession ("
597+ + profName + ")." ;
598+
599+ }else if (villager .getVillagerData ().level () > 1 )
600+ {
601+ reason =
602+ "Reason: villager level is above novice and profession is locked." ;
603+
604+ }else if (villager .getVillagerXp () > 0 )
605+ {
606+ reason =
607+ "Reason: villager has XP, so profession/trades are already locked." ;
608+
609+ }else if (blockingJobSite != null )
610+ {
611+ reason = "Reason: villager remembers another workstation at "
612+ + blockingJobSite .toShortString ()
613+ + " (yellow ESP/tracer). Break it." ;
614+
615+ }else
616+ {
617+ long dayTime = MC .level .getDayTime () % 24000L ;
618+ boolean workHours = dayTime >= 2000 && dayTime <= 9000 ;
619+ if (!workHours )
620+ reason = "Reason: villager is outside work hours ("
621+ + (int )dayTime + "/24000)." ;
622+ else
623+ reason =
624+ "Reason: likely pathing issue. Make sure villager can walk to the lectern." ;
625+ }
626+
627+ if (!reason .equals (lastReasonMessage ))
628+ {
629+ ChatUtils .message (reason );
630+ lastReasonMessage = reason ;
631+ reasonMessageCooldown = 20 ;
632+ return ;
633+ }
634+
635+ reasonMessageCooldown = 100 ;
636+ }
637+
638+ private BlockPos getVillagerMemoryPos (MemoryModuleType <GlobalPos > memory )
639+ {
640+ if (villager == null || MC .level == null )
641+ return null ;
642+
643+ GlobalPos pos = villager .getBrain ().getMemory (memory ).orElse (null );
644+ if (pos == null || pos .dimension () != MC .level .dimension ())
645+ return null ;
646+
647+ return pos .pos ();
648+ }
649+
505650 @ Override
506651 public void onRender (PoseStack matrixStack , float partialTicks )
507652 {
508653 int green = 0xC000FF00 ;
509654 int red = 0xC0FF0000 ;
655+ int yellow = 0xC0FFFF00 ;
510656
511657 if (villager != null )
512658 RenderUtils .drawOutlinedBox (matrixStack , villager .getBoundingBox (),
@@ -521,6 +667,15 @@ public void onRender(PoseStack matrixStack, float partialTicks)
521667 RenderUtils .drawOutlinedBoxes (matrixStack , expVilBoxes , red , false );
522668 RenderUtils .drawCrossBoxes (matrixStack , expVilBoxes , red , false );
523669
670+ if (blockingJobSite != null )
671+ {
672+ AABB box = new AABB (blockingJobSite );
673+ RenderUtils .drawOutlinedBox (matrixStack , box , yellow , false );
674+ RenderUtils .drawCrossBox (matrixStack , box , yellow , false );
675+ RenderUtils .drawTracer (matrixStack , partialTicks ,
676+ Vec3 .atCenterOf (blockingJobSite ), yellow , false );
677+ }
678+
524679 if (breakingJobSite )
525680 overlay .render (matrixStack , partialTicks , jobSite );
526681 }
0 commit comments