[JAVA] Reparé map avec une commande

  • Auteur de la discussion Auteur de la discussion Kenda
  • Date de début Date de début

Kenda

Architecte en herbe
16 Juillet 2016
316
1
2
125
33
www.youtube.com
Bonjour,

je suis en train de faire un plugin, et j'aimerai ajouté une commande pour réparé ma map.
Je sauvegarde déjà toute une zone, qui seront mes blocs originaux, MAIS, j'essaye de comparé les blocs actuels avec les originaux, mais j'ai des problèmes de performances, et de ressources.

Une petite aide pourrait etre le bienvenue ? Merci :)

Voici déjà quelques méthodes que j'ai crée afin de montré un exemple de mes problèmes de perf


Java:
private void saveAllOriginalBlocks() {
        World world = Bukkit.getWorld("template");
        if (world == null) {
            return;
        }

        Location loc1 = LocationTransform.deserializeCoordinate(world.getName(), Config.getString("map.pos1"));
        Location loc2 = LocationTransform.deserializeCoordinate(world.getName(), Config.getString("map.pos2"));
        Cuboid cuboid = new Cuboid(loc1, loc2);

        originalBlocks.addAll(cuboid.getBlocks());

        Bukkit.unloadWorld("template", false);
        Bukkit.getConsoleSender().sendMessage(Messages.transformColor(Messages.getPrefix() + "&aSauvegarde des blocs originaux effectuée."));
    }
    

    public void repairMap(World worldOfPlayer, int percent) {
        if (percent < 0 || percent > 100) {
            throw new IllegalArgumentException("Le pourcentage doit être compris entre 0 et 100.");
        }

        int numberToRepair = (percent * originalBlocks.size()) / 100;

        Location loc1 = LocationTransform.deserializeCoordinate(worldOfPlayer.getName(), Config.getString("map.pos1"));
        Location loc2 = LocationTransform.deserializeCoordinate(worldOfPlayer.getName(), Config.getString("map.pos2"));
        Cuboid cuboid = new Cuboid(loc1, loc2);

        List<Block> currentMap = cuboid.getBlocks();
        int repairedCount = 0;

        for (int i = 0; i < currentMap.size() && repairedCount < numberToRepair; i++) {
            Block original = originalBlocks.get(i);
            Block current = currentMap.get(i);
            if (!isSame(original, current)) {
                worldOfPlayer.getBlockAt(current.getLocation()).setType(original.getType());
                repairedCount++;
            }
        }
    }

    public boolean isSame(Block original, Block current) {
        return original.getType() == current.getType();
    }
 
Bonsoir,

Le plus simple est de copier des fichiers que de toucher à des classes (qui toucheront indirectement aux fichiers), donc si c'est pour toute la map il faut juste copier le dossier du monde. Sinon c'est aussi possible de juste copier une région (32×32 chunks).

Tu peux aussi essayer de désactiver la sauvegarde automatique pendant que tu resculptes tout le monde :
Java:
void repairWorld(/* flm de recopier je suis sur tel */) {
    boolean autoSave = world.isAutoSave();
    world.setAutoSave(false);
    // Block#setType loop
    world.setAutoSave(autoSave);
}
C'est possible que ta manip soit plus rapide comme ça.

Tu peux aussi accélérer la vitesse en préallouant tes listes (e.g. new ArrayList<>(l * L * h);, probablement dans ta classe Cuboid).

Cordialement,
ShE3py
 
Alors je comprend le principe de copier le monde à partir des fichiers, mais j'aimerai plutot du "temps réel".
Par exemple, j'ai une tnt qui explose et/ou des blocs cassé à la main ou part des éclairs ou tout autres types d'evenements, j'aimerai pourvoir réparé X % des blocs cassé.
100% des blocs serait la map total contenu dans le cuboid

Donc le joueur fera /repair 10% et ça réparerai 10% des blocs cassés sur le total.
 
La classe bloc est un peu trop lente (vu qu'elle existe en jeu et doit être charger), tu peux essayer de faire une classe plus petite :
Java:
class BlockSnap {
    int x, y, z;
    Material type;

    BlockSnap(int x, int y, int z, Material type) {
        ...
    }
    
    BlockSnap(Block b) {
        this(...); // 1er constructeur
    }
}

Puis après de stocker des évènements selon ce que tu veux (ici par monde) ;
Java:
static final Map<UUID, List<BlockSnap>> HISTORY = new HashMap<>();

static List<BlockSnap> select(World w, float p) {
    assert p >= 0 && p <= 1;

    List<BlockSnap> history = HISTORY.getOrDefault(w.getUniqueId(), Collections.emptyList());
    List<BlockSnap> selection = nChoices(history, (int) (history.length() * p));
    history.removeAll(selection);
    return selection;
}

/**
 * Renvoie une sous-liste d'au plus {@code n} éléments.
 */
static <T> List<T> nChoices(List<T> list, int n) {
    List<Integer> indexes = IntStream.range(0, list.length()).boxed().collect(Collectors.toCollection(() -> new ArrayList<Integer>(list.length()));
    Collections.shuffle(indexes);

    int m = Math.min(n, indexes.length());
    List<Integer> subIndexes = indexes.subList(0, m);
    List<T> choices = new ArrayList<>(m);
    for(int i : subIndexes) {
        choices.add(list.get(i));
    }

    return choices;
}

Puis après pour sauvegarder append/push un BlockSnap dans BlockBreakEvent (priority = MONITOR, ignoreCancelled = true), et pour restaurer tu peux utiliser un Scheduler répétitif qui appelle select(w, p) et qui fait la boucle de w.getBlockAt(xyz).setType(type).
 
MMh, j'avais pas du tout pensé à cette solution de la classe custom. C'est vrai que sa pourrait vachement allégé mon problème de performance (sachant que j'ai plus de 250k de blocs à traité environ)

Je vais tester cette solution ;)


UPDATE:

Comment je peux connaitre si tel bloc fait parti de la map original ? Genre dans l'exemple que tu m'as montré, tout les blocs sont réparé, également ceux des joueurs. Dans mon cas j'aimerai réparé que les blocs de la map de base :confused:
 
Dernière édition:
À la génération d'un nouveau chunk (ChunkPopulateEvent) tu peux sauvegarder tous les blocs dans une Material[16 × 16 × 256] (utiliser BlockSnap rajouterai 3 ints) (256 = WorldInfo#getMaxHeight() - WorldInfo#getMinHeight()) (ça devrait se compresser plutôt bien avec e.g. GZip ou XZ et Material#name() / Material#getKey()), et rajouter un check si le bloc cassé est du monde original :
Java:
Material getOriginalMaterial(int x, int y, int z) {
   final Material[] originalChunk = getOriginalChunk(...);
   return originalChunk[(y * (16 * 16)) + (z * 16) + x];
}
 
Alors j'ai trouver ma solution, mais elle est un peu demandant en mémoire.

A la création de la map (enfin de mes maps), je sauvegarde dans une liste, tout les BlockSnap avec l'id, et la location x,y,z, ainsi que le matérial (list de 2 millions de blocs environ).

Puis je check si le bloc cassé est contenu dans cette liste.


Code:
public class BlockSnap {
    UUID worldId;
    int x, y, z;
    Material type;

    public BlockSnap(Block b) {
        Location loc = b.getLocation();
        worldId = b.getWorld().getUID();
        x = (int) loc.getX();
        y = (int) loc.getY();
        z = (int) loc.getZ();
        type = b.getType();
    }

    public Location getLocation() {
        return new Location(Bukkit.getWorld(worldId), x, y, z);
    }

    public Material getMaterial() {
        return type;
    }

    public UUID getWorldId() {
        return worldId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BlockSnap blockSnap = (BlockSnap) o;
        return x == blockSnap.x && y == blockSnap.y && z == blockSnap.z && type == blockSnap.type && worldId == blockSnap.worldId;
    }
}

Code:
private void saveOriginalMap() {
        World templateWorld = null;
        try {
            templateWorld = Bukkit.createWorld(new WorldCreator("template"));

            Location loc1 = LocationTransform.deserializeCoordinate(templateWorld.getName(), Config.getString("map.pos1"));
            Location loc2 = LocationTransform.deserializeCoordinate(templateWorld.getName(), Config.getString("map.pos2"));

            Cuboid cuboid = new Cuboid(loc1, loc2);
            for (Block block : cuboid.getBlocks()) {
                if (block.getType() != Material.AIR) {
                    originalMaps.add(new BlockSnap(block));
                }
            }
        } finally {
            if (templateWorld != null) {
                Bukkit.unloadWorld(templateWorld.getName(), false);
            }
        }
    }

Code:
public void addToBlockToRestore(UUID worldId, List<BlockSnap> blocks) {
        blockToRestore.computeIfAbsent(worldId, k -> new ArrayList<>());
        blockToRestore.get(worldId).addAll(blocks);
    }

    public void addToBlockToRestore(BlockSnap block) {
        blockToRestore.computeIfAbsent(block.getWorldId(), k -> new ArrayList<>());
        blockToRestore.get(block.getWorldId()).add(block);
    }

    public boolean isContainsInOriginalMap(BlockSnap block) {
        for (BlockSnap bs : originalMaps)
            if (bs.equals(block)) return true;
        return false;
    }

C'est un peu couteux et laggy quand c'est des explosions, mais je sais pas trop comment je pourrais mieux géré ça ? :confused:
 
La fonction isContainsInOriginalMap() est linéaire (tu fais deux millions d'itérations), essaye d'utiliser une HashSet pour originalMaps (tu auras un originalMaps.contains(block) constant).

Du coup dans BlockSnap il faudrait surcharger hashCode() (de telle sorte que a.equals(b) implique a.hashCode() == b.hashCode()), et pour worldId il faut utiliser .equals() vu que c'est une classe (UUID) et non une primitive.
 
Merci, ça vient de résoudre mon problème !!!

Mais j'en ai un autre, je sais pas si c'est minecraft ou le plugin, mais quand j'explose plusieurs tnt, certains blocs ne sont pas save quand même, j'ai quand même des trous dans ma map à certains endroit


Java:
@EventHandler
    public void onExplode(EntityExplodeEvent e) {
        GameWorldManager gameWorldManager = FrenchAgencyRunner.getInstance().getManager().getManager(GameWorldManager.class);
        List<BlockSnap> blocksToSave = new ArrayList<>();
        e.blockList().forEach(block -> blocksToSave.add(new BlockSnap(block)));

        Bukkit.getScheduler().runTaskAsynchronously(FrenchAgencyRunner.getInstance(),
                () -> blocksToSave.forEach(blockSnap -> {
                    if (gameWorldManager.isBlockContainsInOriginalMap(blockSnap))
                        gameWorldManager.addBlockToRestore(blockSnap);
                }));
    }
 
Ce serait mieux de faire @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) histoire de ne pas remettre les blocs si e.g. WorldGuard a annulé l'explosion.

Et les collections ne sont pas asynchrones, tu risques de te retrouver avec des bogues si tu fais runTaskAsync sans synchronized ou autre verrou. Après t'as pas vraiment besoin de scheduler ici ?

Mais c'est probablement plus un soucis au niveau de la restauration (e.g. lecture de blockToRestore), je suis sur tel donc c'est pas impossible qu'il manque des trucs au code que j'écris.