1212import java .util .Map ;
1313import java .util .Optional ;
1414import java .util .UUID ;
15+ import java .util .Set ;
16+ import java .util .HashSet ;
1517import java .util .stream .Collectors ;
1618import java .util .stream .Stream ;
1719
2325import net .minecraft .util .math .Vec3d ;
2426import net .minecraft .util .Util ;
2527import net .minecraft .util .hit .HitResult ;
28+ import net .minecraft .text .Text ;
29+ import net .minecraft .text .MutableText ;
30+ import net .minecraft .text .TextColor ;
31+ import net .minecraft .util .Formatting ;
2632import net .wurstclient .Category ;
2733import net .wurstclient .SearchTags ;
2834import net .wurstclient .events .CameraTransformViewBobbingListener ;
4248import net .wurstclient .util .EntityUtils ;
4349import net .wurstclient .util .FakePlayerEntity ;
4450import net .wurstclient .util .RenderUtils ;
51+ import net .wurstclient .util .ChatUtils ;
4552import net .wurstclient .util .RenderUtils .ColoredBox ;
4653import net .wurstclient .util .RenderUtils .ColoredPoint ;
4754import net .minecraft .world .RaycastContext ;
@@ -62,6 +69,19 @@ public final class PlayerEspHack extends Hack implements UpdateListener,
6269 new FilterInvisibleSetting ("Won't show invisible players." , false ));
6370
6471 private final ArrayList <PlayerEntity > players = new ArrayList <>();
72+ // Alert settings & tracking for enter/exit notifications
73+ private final CheckboxSetting enterAlert = new CheckboxSetting (
74+ "Enter alert" ,
75+ "When enabled, notifies in chat when a player first becomes visible\n "
76+ + "to PlayerESP, showing distance and XYZ." ,
77+ false );
78+ private final CheckboxSetting exitAlert = new CheckboxSetting ("Exit alert" ,
79+ "When enabled, notifies in chat when a player leaves PlayerESP\n "
80+ + "visibility, showing distance and XYZ at which they left." ,
81+ false );
82+ private final Set <UUID > prevVisible = new HashSet <>();
83+ private final Map <UUID , Vec3d > lastPositions = new HashMap <>();
84+ private final Map <UUID , String > lastNames = new HashMap <>();
6585 private final CheckboxSetting randomBrightColors = new CheckboxSetting (
6686 "Unique colors for players" ,
6787 "When enabled, assigns each player a bright color from a shared\n "
@@ -127,6 +147,10 @@ public PlayerEspHack()
127147 addSetting (useStaticPlayerColor );
128148 addSetting (playerColor );
129149 addSetting (boxSize );
150+
151+ // Alerts when players enter/leave PlayerESP visibility
152+ addSetting (enterAlert );
153+ addSetting (exitAlert );
130154 entityFilters .forEach (this ::addSetting );
131155 addSetting (ignoreNpcs );
132156 }
@@ -146,6 +170,9 @@ protected void onDisable()
146170 EVENTS .remove (CameraTransformViewBobbingListener .class , this );
147171 EVENTS .remove (RenderListener .class , this );
148172 losStates .clear ();
173+ prevVisible .clear ();
174+ lastPositions .clear ();
175+ lastNames .clear ();
149176 }
150177
151178 @ Override
@@ -175,12 +202,125 @@ public void onUpdate()
175202
176203 players .addAll (stream .collect (Collectors .toList ()));
177204
205+ // detect enter / exit visibility changes
206+ handleVisibilityChanges ();
207+
178208 if (losThreatDetection .isChecked ())
179209 updateLosStates (Util .getMeasuringTimeMs ());
180210 else
181211 losStates .clear ();
182212 }
183213
214+ /**
215+ * Detect which players entered or left the PlayerESP-visible list since
216+ * the last update and send chat alerts if configured.
217+ */
218+ private void handleVisibilityChanges ()
219+ {
220+ // Build a set of currently visible *non-bot* players. Use the same
221+ // bot/NPC checks PlayerESP already uses: FakePlayerEntity and
222+ // (when enabled) players missing from the client's tab-list.
223+ Set <UUID > currentNonBot = new HashSet <>();
224+
225+ // Added players (only non-bots)
226+ for (PlayerEntity p : players )
227+ {
228+ // skip known fake players
229+ if (p instanceof FakePlayerEntity )
230+ continue ;
231+
232+ UUID id = p .getUuid ();
233+
234+ // If ignoreNpcs is enabled, treat players not on the client
235+ // player list as bots/NPCs and ignore them for alerts.
236+ if (ignoreNpcs .isChecked () && MC .getNetworkHandler () != null
237+ && MC .getNetworkHandler ().getPlayerListEntry (id ) == null )
238+ {
239+ continue ;
240+ }
241+
242+ currentNonBot .add (id );
243+ lastPositions .put (id , new Vec3d (p .getX (), p .getY (), p .getZ ()));
244+ lastNames .put (id , p .getName ().getString ());
245+ if (!prevVisible .contains (id ) && enterAlert .isChecked ())
246+ sendEnterMessage (p );
247+ }
248+
249+ // Removed players (only consider previously tracked non-bot IDs)
250+ for (UUID id : new HashSet <>(prevVisible ))
251+ {
252+ if (!currentNonBot .contains (id ))
253+ {
254+ Vec3d pos = lastPositions .get (id );
255+ String name = lastNames .getOrDefault (id , "<unknown>" );
256+ if (exitAlert .isChecked ())
257+ sendExitMessage (id , name , pos );
258+ lastPositions .remove (id );
259+ lastNames .remove (id );
260+ prevVisible .remove (id );
261+ }
262+ }
263+
264+ prevVisible .clear ();
265+ prevVisible .addAll (currentNonBot );
266+ }
267+
268+ private void sendEnterMessage (PlayerEntity p )
269+ {
270+ if (MC .player == null )
271+ return ;
272+ UUID id = p .getUuid ();
273+ double dist = Math .round (MC .player .distanceTo (p ) * 10.0 ) / 10.0 ;
274+ int x = (int )Math .round (p .getX ());
275+ int y = (int )Math .round (p .getY ());
276+ int z = (int )Math .round (p .getZ ());
277+ MutableText nameText =
278+ MutableText .of (Text .literal (p .getName ().getString ()).getContent ());
279+ if (randomBrightColors .isChecked ())
280+ {
281+ int idx = Math .abs (id .hashCode ());
282+ java .awt .Color gen = net .wurstclient .util .PlayerColorRegistry
283+ .generateBrightColor (idx );
284+ nameText .setStyle (nameText .getStyle ().withColor (TextColor .fromRgb (
285+ (gen .getRed () << 16 ) | (gen .getGreen () << 8 ) | gen .getBlue ())));
286+ }
287+ Text msg = nameText .append (Text
288+ .literal (" entered range (" + dist + " blocks) at " + x + ", " + y
289+ + ", " + z + "." )
290+ .styled (
291+ s -> s .withColor (TextColor .fromFormatting (Formatting .WHITE ))));
292+ ChatUtils .component (msg );
293+ }
294+
295+ private void sendExitMessage (UUID id , String name , Vec3d pos )
296+ {
297+ if (MC .player == null )
298+ return ;
299+ double dist = pos == null ? -1.0
300+ : Math .round (pos .distanceTo (
301+ new Vec3d (MC .player .getX (), MC .player .getY (), MC .player .getZ ()))
302+ * 10.0 ) / 10.0 ;
303+ int x = pos == null ? 0 : (int )Math .round (pos .x );
304+ int y = pos == null ? 0 : (int )Math .round (pos .y );
305+ int z = pos == null ? 0 : (int )Math .round (pos .z );
306+ MutableText nameText = MutableText .of (Text .literal (name ).getContent ());
307+ if (randomBrightColors .isChecked ())
308+ {
309+ int idx = Math .abs (id .hashCode ());
310+ java .awt .Color gen = net .wurstclient .util .PlayerColorRegistry
311+ .generateBrightColor (idx );
312+ nameText .setStyle (nameText .getStyle ().withColor (TextColor .fromRgb (
313+ (gen .getRed () << 16 ) | (gen .getGreen () << 8 ) | gen .getBlue ())));
314+ }
315+ String distStr = dist < 0 ? "unknown" : (dist + " blocks" );
316+ Text msg = nameText .append (Text
317+ .literal (" left range (" + distStr + ") at " + x + ", " + y + ", "
318+ + z + "." )
319+ .styled (
320+ s -> s .withColor (TextColor .fromFormatting (Formatting .WHITE ))));
321+ ChatUtils .component (msg );
322+ }
323+
184324 @ Override
185325 public void onCameraTransformViewBobbing (
186326 CameraTransformViewBobbingEvent event )
0 commit comments