Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ClientsideMap questions #8

Open
twseer67875 opened this issue Jun 25, 2023 · 26 comments
Open

ClientsideMap questions #8

twseer67875 opened this issue Jun 25, 2023 · 26 comments
Labels
enhancement New feature or request

Comments

@twseer67875
Copy link

Feature description

I found that in the Quickstart example, it is necessary to have a valid MapID in order to create a ClientsideMap. Is it possible to create a ClientsideMap without using a valid MapID? I don't want to require players to first create any files in the "./world/data" directory using the original method. Can alternative methods, such as custom NBTTag or other approaches, be provided to determine the ClientsideMap?

Relevant issues

No response

@twseer67875 twseer67875 added the enhancement New feature or request label Jun 25, 2023
@cerus
Copy link
Owner

cerus commented Jun 25, 2023

Hello! You do not need a valid map id. The quickstart example just happens to use a valid id, but you can also use invalid ones.

@twseer67875
Copy link
Author

Hello! You do not need a valid map id. The quickstart example just happens to use a valid id, but you can also use invalid ones.

Thank you for your response, but I still have another question. Regarding the Maven tutorial provided in the WiKi, it seems that I am unable to successfully import maps into my project. It appears that there is no relevant repository available. Should I clone the project and compile it in order to use it?

https://github.com/cerus/maps/wiki/Build-Tool-Setup:-Maven
image

@twseer67875
Copy link
Author

Hello! You do not need a valid map id. The quickstart example just happens to use a valid id, but you can also use invalid ones.

I encountered a problem when creating a map. I'm not sure why, but when I execute the following code, the original marker does not disappear. Is there no way to remove the original marker without relying solely on packets?

package com.cocobeen.Commands;

import dev.cerus.maps.api.ClientsideMap;
import dev.cerus.maps.api.graphics.ClientsideMapGraphics;
import dev.cerus.maps.api.graphics.ColorCache;
import dev.cerus.maps.version.VersionAdapterFactory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

public class TestCommand implements CommandExecutor {
   @Override
   public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) {
       ClientsideMap map = new ClientsideMap(3);
       ClientsideMapGraphics graphics = new ClientsideMapGraphics();

       graphics.fillComplete(ColorCache.rgbToMap(255, 255, 255));
       graphics.fillRect(5, 5, 118, 118, ColorCache.rgbToMap(255, 0, 0), 1f);


       map.clearMarkers();
       map.draw(graphics);
       map.sendTo(new VersionAdapterFactory().makeAdapter(), true, (Player) commandSender);
       return true;
   }
}
2023-06-25.22-33-01.mp4

@twseer67875
Copy link
Author

Hello! You do not need a valid map id. The quickstart example just happens to use a valid id, but you can also use invalid ones.

Furthermore, when using the following code to create an illegal MapID and following the instructions in the Wiki, it seems to have no effect. The map doesn't show any changes.

package com.cocobeen.Commands;

import dev.cerus.maps.api.ClientsideMap;
import dev.cerus.maps.api.graphics.ClientsideMapGraphics;
import dev.cerus.maps.api.graphics.ColorCache;
import dev.cerus.maps.version.VersionAdapterFactory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

public class TestCommand implements CommandExecutor {
    @Override
    public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) {
        ClientsideMap map = new ClientsideMap();
        ClientsideMapGraphics graphics = new ClientsideMapGraphics();

        graphics.fillComplete(ColorCache.rgbToMap(255, 255, 255));
        graphics.fillRect(5, 5, 118, 118, ColorCache.rgbToMap(255, 0, 0), 1f);


        map.clearMarkers();
        map.draw(graphics);
        map.sendTo(new VersionAdapterFactory().makeAdapter(), true, (Player) commandSender);
        return true;
    }
}
2023-06-25.22-44-29.mp4

@cerus
Copy link
Owner

cerus commented Jun 25, 2023

I encountered a problem when creating a map. I'm not sure why, but when I execute the following code, the original marker does not disappear. Is there no way to remove the original marker without relying solely on packets?

If you look closely you can see that the markers disappear, but the server updates real maps every tick, so they appear again one tick later.

Furthermore, when using the following code to create an illegal MapID and following the instructions in the Wiki, it seems to have no effect. The map doesn't show any changes.

The map item in your hand has a certain map id that it's displaying. You need to change the items map id to the id of the fake map.

@cerus
Copy link
Owner

cerus commented Jun 25, 2023

... it seems that I am unable to successfully import maps into my project. It appears that there is no relevant repository available. Should I clone the project and compile it in order to use it?

Looks like I forgot to deplay version 3.7.1, use 3.7.0 as a workaround

@twseer67875
Copy link
Author

I encountered a problem when creating a map. I'm not sure why, but when I execute the following code, the original marker does not disappear. Is there no way to remove the original marker without relying solely on packets?

If you look closely you can see that the markers disappear, but the server updates real maps every tick, so they appear again one tick later.

Furthermore, when using the following code to create an illegal MapID and following the instructions in the Wiki, it seems to have no effect. The map doesn't show any changes.

The map item in your hand has a certain map id that it's displaying. You need to change the items map id to the id of the fake map.

So, regardless of whether the MapID is legal or illegal, is it necessary to provide a MapID to the ClientsideMap object in order to use it properly? Because I tried modifying the 'map' tag in the NBTTag of a map to a MapID that has not been created in Minecraft, and it only works properly when I provide that MapID to the ClientsideMap. However, if I don't define a MapID within the ClientsideMap, even if the map is invalid, it cannot be used properly.

@cerus
Copy link
Owner

cerus commented Jun 26, 2023

If you don't explicitly provide a map id when creating a ClientsideMap it will choose an id itself.

public ClientsideMap() {
this(COUNTER++);
}

You can get the id using ClientsideMap#getId(). You need to set that id as the map id of the map item.

@cerus
Copy link
Owner

cerus commented Jun 28, 2023

Hey, are the issues you experienced solved? Do you have any further questions?

@twseer67875
Copy link
Author

Hey, are the issues you experienced solved? Do you have any further questions?

I apologize for taking so long to get back to you. Most of the basic functionality issues have been resolved, but the only remaining problem is that Maven still doesn't have the new version 3.7.1. I need the plugin to work on version 1.20.1, so it would be great if you could update it for me. Thank you so much for your assistance.

@cerus
Copy link
Owner

cerus commented Jul 15, 2023

Sorry about that, should be fixed now

@twseer67875
Copy link
Author

I encountered a strange issue where the player seems unable to display the map correctly when I quickly send more than around 50 map data packets to them at once. Is there a way to provide a method to send a large number of map data packets at once?
image
image

@cerus
Copy link
Owner

cerus commented Jul 16, 2023

I'm pretty sure that's caused by some sort of bug in your code, I've never had any issues with sending lots of data packets. If you're willing to share the relevant code I might be able to help you figure this out.

@twseer67875
Copy link
Author

twseer67875 commented Jul 16, 2023

CrossServerMap.java

package com.cocobeen;

import com.cocobeen.Commands.MapSaveCommand;
import com.cocobeen.Commands.MapTransferCommand;
import com.cocobeen.Listener.*;
import com.cocobeen.Utils.ImageData;
import com.cocobeen.Utils.MapGraphicsDrawUtils;
import com.cocobeen.Utils.SerializationUtils;
import com.cocobeen.Utils.struct.ChunkCache;
import com.cocobeen.Utils.struct.MapCache;
import de.tr7zw.nbtapi.NBTItem;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.GlowItemFrame;
import org.bukkit.entity.ItemFrame;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public final class CrossServerMap extends JavaPlugin {
    private static CrossServerMap plugin = null;
    private static MapDataDAO dataDAO = null;
    private static byte[] banMapData = null;
    private static ConcurrentLinkedDeque<MapCache> cache = new ConcurrentLinkedDeque<>();
    private static ConcurrentLinkedDeque<Chunk> chunk_cache = new ConcurrentLinkedDeque<>();
    private static ConcurrentHashMap<UUID, Set<Integer>> playerCache = new ConcurrentHashMap<>();
    private static ConcurrentLinkedDeque<ChunkCache> draw_caches = new ConcurrentLinkedDeque<>();


    @Override
    public void onEnable() {
        plugin = this;

        getConfig().options().copyDefaults();
        saveDefaultConfig();

        initDatabase();
        initCommands();
        initListener();

        MapGraphicsDrawTask();
        DrawTask();
        ClearCacheTask();
    }

    @Override
    public void onDisable() {
        getServer().getScheduler().cancelTasks(this);
        banMapData = null;
        cache.clear();
        chunk_cache.clear();
        playerCache.clear();
        dataDAO.closeConnection();
    }

    public static CrossServerMap getPlugin() {
        return plugin;
    }

    public static MapDataDAO getDataDAO() {
        return dataDAO;
    }

    public static byte[] getBanMapData() {
        return banMapData;
    }

    public static ConcurrentLinkedDeque<MapCache> getCache(){
        return cache;
    }

    public static ConcurrentLinkedDeque<Chunk> getChunkCache(){
        return chunk_cache;
    }

    public static ConcurrentHashMap<UUID, Set<Integer>> getPlayerCache() {
        return playerCache;
    }

    public static void setCache(ConcurrentLinkedDeque<MapCache> MapCache){
        cache = MapCache;
    }

    public static void setChunkCache(ConcurrentLinkedDeque<Chunk> chunkCache){
        chunk_cache = chunkCache;
    }

    private void initListener(){
        if (getServer().getPluginManager().getPlugin("MysqlPlayerDataBridge") != null){
            getLogger().info("MysqlPlayerDataBridge Hook!");
            getServer().getPluginManager().registerEvents(new MPDBSyncCompleteListener(), this);
        }
        else {
            getServer().getPluginManager().registerEvents(new PlayerJoinListener(), this);
            getLogger().info("MysqlPlayerDataBridge not found");
        }

        getServer().getPluginManager().registerEvents(new PlayerItemHeldListener(), this);
        getServer().getPluginManager().registerEvents(new PlayerChunkLoadListener(), this);
        getServer().getPluginManager().registerEvents(new PrepareItemCraftListener(), this);
        getServer().getPluginManager().registerEvents(new InventoryClickListener(), this);
        getServer().getPluginManager().registerEvents(new PlayerQuitListener(), this);
    }

    private void initCommands(){
        getServer().getPluginCommand("mapsave").setExecutor(new MapSaveCommand());
        getServer().getPluginCommand("maptransfer").setExecutor(new MapTransferCommand());

        getServer().getPluginCommand("mapsave").setTabCompleter(new MapSaveCommand());
        getServer().getPluginCommand("maptransfer").setTabCompleter(new MapTransferCommand());
    }

    private void initDatabase(){
        final String user = getConfig().getString("mysql.user");
        final String password = getConfig().getString("mysql.password");
        final String host = getConfig().getString("mysql.host");
        final int port = getConfig().getInt("mysql.port");
        final String database = getConfig().getString("mysql.database");

        Runnable task = () -> {
            dataDAO = new MapDataDAO(host, port, database, user, password);
            dataDAO.createTable();

            ImageData data = new ImageData();
            banMapData = data.getColors();
        };
        Bukkit.getScheduler().runTaskAsynchronously(this, task);
    }

    private void DrawTask(){
        Runnable task = () -> {
            synchronized (draw_caches){
                if (draw_caches.size() == 0){
                    return;
                }

                List<ChunkCache> elements = draw_caches.stream()
                        .limit(5)
                        .collect(Collectors.toList());

                for (ChunkCache chunkCache : elements){
                    byte[] color = chunkCache.getColor();
                    int mapId = chunkCache.getMapID();
                    draw_caches.remove(chunkCache);
                    MapGraphicsDrawUtils.MainThreadMapGraphicsDraw(color, mapId);
                }
            }
        };
        Bukkit.getScheduler().runTaskTimerAsynchronously(this, task, 3, 3);
    }


    private void MapGraphicsDrawTask(){
        Runnable task = () -> {
            ConcurrentLinkedDeque<Chunk> chunks = null;

            synchronized (chunk_cache){
                chunks = new ConcurrentLinkedDeque<>(chunk_cache);
                chunk_cache.clear();
            }

            for (Chunk chunk : chunks){
                Entity[] entities = chunk.getEntities();

                Set<ItemFrame> itemFrames = new HashSet<>();
                Set<GlowItemFrame> glowItemFrames = new HashSet<>();

                for (Entity entity : entities){
                    EntityType type = entity.getType();
                    switch (type){
                        case ITEM_FRAME:{
                            itemFrames.add((ItemFrame) entity);
                            break;
                        }
                        case GLOW_ITEM_FRAME:{
                            glowItemFrames.add((GlowItemFrame) entity);
                            break;
                        }
                    }

                }

                for (ItemFrame itemFrame : itemFrames){
                    ItemStack itemStack = itemFrame.getItem();

                    if (itemStack.getType().equals(Material.FILLED_MAP)){
                        ChunkCache chunkCache = initMapGraphicsDraw(itemStack);
                        if (chunkCache != null){
                            synchronized (draw_caches){
                                draw_caches.add(chunkCache);
                            }
                        }
                    }
                }

                for (GlowItemFrame glowItemFrame : glowItemFrames){
                    ItemStack itemStack = glowItemFrame.getItem();

                    if (itemStack.getType().equals(Material.FILLED_MAP)){
                        ChunkCache chunkCache = initMapGraphicsDraw(itemStack);
                        if (chunkCache != null){
                            synchronized (draw_caches){
                                draw_caches.add(chunkCache);
                            }
                        }
                    }
                }
            }
        };
        Bukkit.getScheduler().runTaskTimerAsynchronously(this, task, 10, 10);
    }

    private ChunkCache initMapGraphicsDraw(ItemStack itemStack){
        NBTItem nbtItem = new NBTItem(itemStack);

        if (nbtItem.hasTag("CrossServerMap_Owner")){

            UUID OwnerUUID = nbtItem.getUUID("CrossServerMap_Owner");
            int mapID = nbtItem.getInteger("map");

            synchronized (cache){
                for (MapCache mapCache : cache){
                    if (mapCache.getMapID() == mapID){
                        byte[] color = mapCache.getColor();
                        return new ChunkCache(color, mapID);
                    }
                }

                boolean isBan = dataDAO.readMapBanState(mapID);

                if (isBan){
                    MapGraphicsDrawUtils.MapGraphicsDraw(banMapData, mapID);
                    cache.addLast(new MapCache(OwnerUUID, banMapData, mapID));
                    return new ChunkCache(banMapData, mapID);
                }

                String data = dataDAO.readMapData(mapID);

                if (data == null){
                    getLogger().warning("MapID " + mapID + " not found!");
                    getLogger().warning("This problem is very serious! Please check the database information " +
                            "and plugin settings immediately!");
                    return new ChunkCache(banMapData, mapID);
                }

                byte[] color = SerializationUtils.DeserializeMapData(data);

                if (cache.size() > 1000){
                    plugin.getLogger().info("MapCaches is full! Remove caches head data");
                    cache.poll();
                }

                cache.addLast(new MapCache(OwnerUUID, color, mapID));

                return new ChunkCache(color, mapID);
            }
        }
        return null;
    }

    private void ClearCacheTask(){
        Runnable task = () -> {
            long start = System.nanoTime();
            getLogger().info("Clear expired cache data...");
            synchronized (cache){
                ConcurrentLinkedDeque<MapCache> caches = new ConcurrentLinkedDeque<>(cache);
                int count = 0;
                LocalDateTime now = LocalDateTime.now();
                for (MapCache mapCache : caches){
                    LocalDateTime time = mapCache.getTime();

                    Duration duration = Duration.between(time, now);

                    if (duration.compareTo(Duration.ofMinutes(10)) > 0){
                        count++;
                        cache.remove(mapCache);
                    }
                }
                long end = System.nanoTime();
                long countTime = TimeUnit.MICROSECONDS.convert((end - start), TimeUnit.NANOSECONDS);
                getLogger().info("There are " + cache.size() + " records in the current cache.");
                getLogger().info("Successfully cleared " + count + " expired cache data and took " + countTime + " ms.");
            }
        };
        Bukkit.getScheduler().runTaskTimerAsynchronously(this, task, 1200, 1200);
    }
}

PlayerChunkLoadListener.java

package com.cocobeen.Listener;

import com.cocobeen.CrossServerMap;
import io.papermc.paper.event.packet.PlayerChunkLoadEvent;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

import java.util.concurrent.ConcurrentLinkedDeque;

public class PlayerChunkLoadListener implements Listener {
    private final CrossServerMap plugin = CrossServerMap.getPlugin();
    private final ConcurrentLinkedDeque<Chunk> chunks = CrossServerMap.getChunkCache();

    @EventHandler
    public void PlayerChunkLoad(PlayerChunkLoadEvent event){
        Chunk chunk = event.getChunk();
        if (Thread.holdsLock(chunks)){
            Runnable task = () -> {
                synchronized (chunk){
                    chunks.addLast(chunk);
                }
            };
            Bukkit.getScheduler().runTaskAsynchronously(plugin, task);
            return;
        }
        chunks.addLast(chunk);
    }
}

MapGraphicsDrawUtils.java

package com.cocobeen.Utils;

import com.cocobeen.CrossServerMap;
import dev.cerus.maps.api.ClientsideMap;
import dev.cerus.maps.api.graphics.ClientsideMapGraphics;
import dev.cerus.maps.api.version.VersionAdapter;
import dev.cerus.maps.version.VersionAdapterFactory;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

import java.util.Set;
import java.util.UUID;

public class MapGraphicsDrawUtils {
    private static final VersionAdapter va = new VersionAdapterFactory().makeAdapter();

    public static void MapGraphicsDraw(byte[] color, int id, Player player){
        Runnable task = () -> {
            ClientsideMap map = BaseMapGraphics(color, id);
            map.sendTo(va, true, player);
        };
        Bukkit.getScheduler().runTaskAsynchronously(CrossServerMap.getPlugin(), task);
    }

    public static void MapGraphicsDraw(byte[] color, int id){
        Runnable task = () -> {
            ClientsideMap map = BaseMapGraphics(color, id);
            for (Player player : Bukkit.getServer().getOnlinePlayers()) {
                UUID uuid = player.getUniqueId();
                if (CrossServerMap.getPlayerCache().containsKey(uuid)){
                    Set<Integer> mapList = CrossServerMap.getPlayerCache().get(uuid);
                    if (!mapList.contains(id)){
                        mapList.add(id);
                        CrossServerMap.getPlayerCache().put(uuid, mapList);
                        map.sendTo(va, true, player);
                    }
                }
            }
        };
        Bukkit.getScheduler().runTaskAsynchronously(CrossServerMap.getPlugin(), task);
    }

    public static void MainThreadMapGraphicsDraw(byte[] color, int id){
        ClientsideMap map = BaseMapGraphics(color, id);
        for (Player player : Bukkit.getOnlinePlayers()) {
            UUID uuid = player.getUniqueId();
            if (CrossServerMap.getPlayerCache().containsKey(uuid)){
                Set<Integer> mapList = CrossServerMap.getPlayerCache().get(uuid);
                if (!mapList.contains(id)){
                    mapList.add(id);
                    CrossServerMap.getPlayerCache().put(uuid, mapList);
                    map.sendTo(va, player);
                }
            }
        }
    }

    private static ClientsideMap BaseMapGraphics(byte[] color, int id){
        ClientsideMap map = new ClientsideMap(id);
        ClientsideMapGraphics graphics = new ClientsideMapGraphics();

        for (int x = 0; x != 128; x++){
            for (int z = 0; z != 128; z++){
                graphics.setPixel(x, z, color[x + z * 128]);
            }
        }

        map.clearMarkers();
        map.draw(graphics);
        return map;
    }
}

@twseer67875
Copy link
Author

I'm pretty sure that's caused by some sort of bug in your code, I've never had any issues with sending lots of data packets. If you're willing to share the relevant code I might be able to help you figure this out.

The above is the code I'm using. I'm trying to scan item frames on the map when a player reads a block, retrieve the NBT tags of the items on them, and the map ID. After that, I send map data packets to all players.

@cerus
Copy link
Owner

cerus commented Jul 16, 2023

Are you 100% sure that your "color" arrays actually have colors in them? Looks like your sending a bunch of transparent maps to the player.

@twseer67875
Copy link
Author

Are you 100% sure that your "color" arrays actually have colors in them? Looks like your sending a bunch of transparent maps to the player.

Regarding this point, I am very certain that I have the correct color array because I also use other events to read the cache and draw individual maps, and there are no issues with them.

@twseer67875
Copy link
Author

twseer67875 commented Jul 16, 2023

Are you 100% sure that your "color" arrays actually have colors in them? Looks like your sending a bunch of transparent maps to the player.

To troubleshoot the issue, I also inserted getLogger().info(String.valueOf(mapId)); at the location of imge1 to print all the drawn map IDs. I am very certain that it is executed correctly and the colors are confirmed to be present. However, there is a chance that the client displays a blank map. This situation is not 100% consistent, but rather random occurrences.

image
image
image

@cerus
Copy link
Owner

cerus commented Jul 16, 2023

To debug the issue further: In your BaseMapGraphics(byte[] color, int id) method could draw something like a red rectangle in one corner. If the rectangle is visible but the map itself is transparent, there's an issue with the color array. If nothing is visible the issue is somewhere else.

    private static ClientsideMap BaseMapGraphics(byte[] color, int id){
        ClientsideMap map = new ClientsideMap(id);
        ClientsideMapGraphics graphics = new ClientsideMapGraphics();

        for (int x = 0; x != 128; x++){
            for (int z = 0; z != 128; z++){
                graphics.setPixel(x, z, color[x + z * 128]);
            }
        }

        // Draw rectangle in top left corner
        graphics.fillRect(0, 0, 8, 8, /* 18 = red */ (byte) 18, 1f);

        map.clearMarkers();
        map.draw(graphics);
        return map;
    }

@twseer67875
Copy link
Author

graphics.fillRect(0, 0, 8, 8, /* 18 = red */ (byte) 18, 1f);

It seems that the issue lies elsewhere.

image

@cerus
Copy link
Owner

cerus commented Jul 16, 2023

Hm, that's weird. Maybe the client is ignoring packets to prevent getting overloaded by the server? I have honestly no idea why this is happening.

Maybe try sending fewer maps at the same time.

@twseer67875
Copy link
Author

twseer67875 commented Jul 16, 2023

I consider the probability of this to be relatively low because I have previously used a plugin called SyncStaticMapView, which also relied on map data packets, and it did not experience random display issues even under such high-density map display conditions. The problem arose when I migrated from that plugin and started using the maps library to display maps. The issue is severe, frequent, and occurs consistently in this scenario.

https://www.spigotmc.org/resources/syncstaticmapview-archive.96333/

@cerus
Copy link
Owner

cerus commented Jul 16, 2023

One last idea: Change map.sendTo(va, player); to map.sendTo(va, true, player);. This will force maps to send the full map data.

@twseer67875
Copy link
Author

One last idea: Change map.sendTo(va, player); to map.sendTo(va, true, player);. This will force maps to send the full map data.

The problem is still
image

@cerus
Copy link
Owner

cerus commented Jul 16, 2023

Very weird. Sorry, no idea what's happening.

@twseer67875
Copy link
Author

Very weird. Sorry, no idea what's happening.

I have temporarily resolved the issue by repeatedly sending map data to the players. I will continue to update this location if any further issues are discovered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants