3030import java .util .concurrent .ConcurrentHashMap ;
3131import java .util .concurrent .ConcurrentLinkedQueue ;
3232
33+ import java .util .logging .Level ;
34+
3335public class TuffX extends JavaPlugin implements Listener , PluginMessageListener {
3436
3537 public static final String CHANNEL = "eagler:below_y0" ;
@@ -38,8 +40,17 @@ public class TuffX extends JavaPlugin implements Listener, PluginMessageListener
3840 private static final int CHUNKS_PER_TICK = 2 ;
3941
4042 private final Map <UUID , Queue <Vector >> requestQueue = new ConcurrentHashMap <>();
43+ private final Map <UUID , Set <Vector >> currentlyQueued = new ConcurrentHashMap <>();
44+
45+ private final Map <UUID , Boolean > initialLoadingPlayers = new ConcurrentHashMap <>();
46+ private final Map <UUID , Integer > initialChunksToProcess = new ConcurrentHashMap <>();
47+
4148 private BukkitTask processorTask ;
4249
50+ private void logDebug (String message ) {
51+ getLogger ().log (Level .INFO , "[TuffX-Debug] " + message );
52+ }
53+
4354 @ Override
4455 public void onEnable () {
4556 getServer ().getMessenger ().registerOutgoingPluginChannel (this , CHANNEL );
@@ -75,39 +86,84 @@ public void onPluginMessageReceived(String channel, Player player, byte[] messag
7586 }
7687
7788 private void handleIncomingPacket (Player player , Location loc , String action , int chunkX , int chunkZ ) {
89+ UUID playerId = player .getUniqueId ();
7890 switch (action .toLowerCase ()) {
7991 case "request_chunk" :
80- Queue <Vector > queue = requestQueue .computeIfAbsent (player .getUniqueId (), k -> new ConcurrentLinkedQueue <>());
81- queue .add (new Vector (chunkX , 0 , chunkZ ));
92+ Vector chunkVec = new Vector (chunkX , 0 , chunkZ );
93+ Set <Vector > queuedSet = currentlyQueued .computeIfAbsent (playerId , k -> ConcurrentHashMap .newKeySet ());
94+
95+ if (queuedSet .add (chunkVec )) {
96+ requestQueue .computeIfAbsent (playerId , k -> new ConcurrentLinkedQueue <>()).add (chunkVec );
97+
98+ if (initialLoadingPlayers .getOrDefault (playerId , false )) {
99+ initialChunksToProcess .merge (playerId , 1 , Integer ::sum );
100+ logDebug ("Player " + player .getName () + " needs chunk " + chunkX + "," + chunkZ + ". Total initial chunks to process: " + initialChunksToProcess .get (playerId ));
101+ }
102+ }
82103 break ;
83104 case "ready" :
84- String welcome = "§bWelcome, §e" + player .getName () + "§b!" ;
85- byte [] payload = createWelcomePayload (welcome , getServer ().getOnlinePlayers ().size ());
86- if (payload != null ) player .sendPluginMessage (this , CHANNEL , payload );
105+ logDebug ("Player " + player .getName () + " sent READY packet. Starting initial load sequence." );
106+
107+ initialLoadingPlayers .put (playerId , true );
108+ initialChunksToProcess .put (playerId , 0 );
109+
110+ new BukkitRunnable () {
111+ @ Override
112+ public void run () {
113+ if (initialLoadingPlayers .containsKey (playerId )) {
114+ initialLoadingPlayers .put (playerId , false ); // Lock the state.
115+ logDebug ("Player " + player .getName () + " initial chunk requests locked in at " + initialChunksToProcess .getOrDefault (playerId , 0 ) + " chunks." );
116+
117+ if (initialChunksToProcess .getOrDefault (playerId , 0 ) == 0 ) {
118+ checkIfInitialLoadComplete (player );
119+ }
120+ }
121+ }
122+ }.runTaskLater (this , 5L );
123+
124+ player .sendPluginMessage (this , CHANNEL , createBelowY0StatusPayload (true ));
87125 break ;
88126 case "use_on_block" :
89127 new BukkitRunnable () {
90128 @ Override
91129 public void run () {
92- Block block = loc .getBlock ();
93- ItemStack item = player .getInventory ().getItemInMainHand ();
94- getServer ().getPluginManager ().callEvent (new PlayerInteractEvent (player , Action .RIGHT_CLICK_BLOCK , item , block , block .getFace (player .getLocation ().getBlock ())));
130+ Block block = loc .getBlock ();
131+ ItemStack item = player .getInventory ().getItemInMainHand ();
132+ getServer ().getPluginManager ().callEvent (new PlayerInteractEvent (player , Action .RIGHT_CLICK_BLOCK , item , block , block .getFace (player .getLocation ().getBlock ())));
95133 }
96134 }.runTask (this );
97135 break ;
98136 }
99137 }
138+
139+ private byte [] createBelowY0StatusPayload (boolean status ) {
140+ try (ByteArrayOutputStream bout = new ByteArrayOutputStream (); DataOutputStream out = new DataOutputStream (bout )) {
141+ out .writeUTF ("belowy0_status" );
142+ out .writeBoolean (status );
143+ return bout .toByteArray ();
144+ } catch (IOException e ) { return null ; }
145+ }
100146
101147 private void startProcessorTask () {
102148 this .processorTask = new BukkitRunnable () {
103149 @ Override
104150 public void run () {
105- for (Map . Entry < UUID , Queue < Vector >> entry : requestQueue .entrySet ( )) {
106- Player player = getServer ().getPlayer (entry . getKey () );
107- Queue <Vector > queue = entry . getValue ( );
151+ for (UUID playerUUID : new HashSet <>( requestQueue .keySet () )) {
152+ Player player = getServer ().getPlayer (playerUUID );
153+ Queue <Vector > queue = requestQueue . get ( playerUUID );
108154
109- if (player == null || !player .isOnline () || queue .isEmpty ()) continue ;
155+ if (player == null || !player .isOnline ()) {
156+ requestQueue .remove (playerUUID );
157+ initialLoadingPlayers .remove (playerUUID );
158+ initialChunksToProcess .remove (playerUUID );
159+ currentlyQueued .remove (playerUUID );
160+ continue ;
161+ }
110162
163+ if (queue == null || queue .isEmpty ()) {
164+ continue ;
165+ }
166+
111167 for (int i = 0 ; i < CHUNKS_PER_TICK && !queue .isEmpty (); i ++) {
112168 Vector vec = queue .poll ();
113169 if (vec != null ) {
@@ -129,16 +185,17 @@ public void run() {
129185 private void processAndSendChunk (final Player player , final Chunk chunk ) {
130186 if (chunk == null || !player .isOnline ()) return ;
131187
132- final Map <BlockData , int []> conversionCache = new HashMap <>();
188+ final Vector chunkVec = new Vector (chunk .getX (), 0 , chunk .getZ ());
189+ logDebug ("Processing chunk " + chunk .getX () + "," + chunk .getZ () + " for " + player .getName ());
133190
134191 new BukkitRunnable () {
135192 @ Override
136193 public void run () {
137194 final ChunkSnapshot snapshot = chunk .getChunkSnapshot (true , false , false );
195+ final Map <BlockData , int []> conversionCache = new HashMap <>();
138196
139197 for (int sectionY = -4 ; sectionY < 0 ; sectionY ++) {
140- if (!player .isOnline ()) break ;
141-
198+ if (!player .isOnline ()) break ;
142199 try {
143200 byte [] payload = createSectionPayload (snapshot , chunk .getX (), chunk .getZ (), sectionY , conversionCache );
144201 if (payload != null ) {
@@ -148,13 +205,61 @@ public void run() {
148205 getLogger ().severe ("Payload creation failed for " + chunk .getX () + "," + chunk .getZ () + ": " + e .getMessage ());
149206 }
150207 }
208+
209+ new BukkitRunnable () {
210+ @ Override
211+ public void run () {
212+ if (!player .isOnline ()) return ;
213+
214+ Set <Vector > queuedSet = currentlyQueued .get (player .getUniqueId ());
215+ if (queuedSet != null ) {
216+ queuedSet .remove (chunkVec );
217+ }
218+ checkIfInitialLoadComplete (player );
219+ }
220+ }.runTask (TuffX .this );
151221 }
152222 }.runTaskAsynchronously (this );
153223 }
154-
224+
225+ private void checkIfInitialLoadComplete (Player player ) {
226+ UUID playerId = player .getUniqueId ();
227+
228+ if (initialChunksToProcess .containsKey (playerId )) {
229+ int remaining = initialChunksToProcess .compute (playerId , (k , v ) -> (v == null ) ? -1 : v - 1 );
230+
231+ logDebug ("Player " + player .getName () + " finished a chunk. Remaining initial chunks: " + remaining );
232+
233+ if (remaining <= 0 ) {
234+ initialLoadingPlayers .remove (playerId );
235+ initialChunksToProcess .remove (playerId );
236+
237+ player .sendPluginMessage (this , CHANNEL , createLoadFinishedPayload ());
238+ logDebug ("INITIAL LOAD COMPLETE for " + player .getName () + ". Sent finished packet." );
239+ }
240+ }
241+ }
242+
243+ private byte [] createLoadFinishedPayload () {
244+ try (ByteArrayOutputStream bout = new ByteArrayOutputStream ();
245+ DataOutputStream out = new DataOutputStream (bout )) {
246+
247+ out .writeUTF ("y0_load_finished" );
248+
249+ return bout .toByteArray ();
250+ } catch (IOException e ) {
251+ getLogger ().severe ("Failed to create the y0_load_finished payload!" );
252+ return null ;
253+ }
254+ }
255+
155256 @ EventHandler
156257 public void onPlayerQuit (PlayerQuitEvent event ) {
157- requestQueue .remove (event .getPlayer ().getUniqueId ());
258+ UUID playerId = event .getPlayer ().getUniqueId ();
259+ requestQueue .remove (playerId );
260+ initialLoadingPlayers .remove (playerId );
261+ currentlyQueued .remove (playerId );
262+ initialChunksToProcess .remove (playerId );
158263 }
159264
160265 private byte [] createWelcomePayload (String message , int someNumber ) {
0 commit comments