net.caseif.flint.common.arena.CommonArena.java Source code

Java tutorial

Introduction

Here is the source code for net.caseif.flint.common.arena.CommonArena.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015-2016, Max Roncace <me@caseif.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package net.caseif.flint.common.arena;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static net.caseif.flint.common.util.helper.JsonSerializer.deserializeLocation;
import static net.caseif.flint.common.util.helper.JsonSerializer.serializeLocation;

import net.caseif.flint.FlintCore;
import net.caseif.flint.arena.Arena;
import net.caseif.flint.common.CommonCore;
import net.caseif.flint.common.component.CommonComponent;
import net.caseif.flint.common.event.internal.metadata.PersistableMetadataMutateEvent;
import net.caseif.flint.common.lobby.CommonLobbySign;
import net.caseif.flint.common.metadata.CommonMetadata;
import net.caseif.flint.common.metadata.persist.CommonPersistentMetadataHolder;
import net.caseif.flint.common.minigame.CommonMinigame;
import net.caseif.flint.common.util.agent.rollback.IRollbackAgent;
import net.caseif.flint.common.util.factory.FactoryRegistry;
import net.caseif.flint.common.util.factory.IArenaFactory;
import net.caseif.flint.common.util.factory.IRollbackAgentFactory;
import net.caseif.flint.common.util.factory.IRoundFactory;
import net.caseif.flint.common.util.file.CommonDataFiles;
import net.caseif.flint.common.util.helper.JsonHelper;
import net.caseif.flint.common.util.helper.JsonSerializer;
import net.caseif.flint.component.exception.OrphanedComponentException;
import net.caseif.flint.config.ConfigNode;
import net.caseif.flint.lobby.LobbySign;
import net.caseif.flint.minigame.Minigame;
import net.caseif.flint.round.LifecycleStage;
import net.caseif.flint.round.Round;
import net.caseif.flint.util.physical.Boundary;
import net.caseif.flint.util.physical.Location3D;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.eventbus.Subscribe;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Implements {@link Arena}.
 *
 * @author Max Roncac
 */
public abstract class CommonArena extends CommonPersistentMetadataHolder
        implements Arena, CommonComponent<Minigame> {

    //TODO: move to designated class for constants
    public static final String PERSISTENCE_NAME_KEY = "name";
    public static final String PERSISTENCE_WORLD_KEY = "world";
    public static final String PERSISTENCE_SPAWNS_KEY = "spawns";
    public static final String PERSISTENCE_BOUNDS_UPPER_KEY = "bound.upper";
    public static final String PERSISTENCE_BOUNDS_LOWER_KEY = "bound.lower";
    public static final String PERSISTENCE_METADATA_KEY = "metadata";

    private IRollbackAgent rbHelper;

    private final CommonMinigame parent;
    private final String id;
    private String name;
    private final String world;
    private final HashMap<Integer, Location3D> spawns = new HashMap<>();
    private final List<Location3D> shuffledSpawns;
    private final HashMap<Location3D, LobbySign> lobbies = new HashMap<>();

    private Boundary boundary;

    private boolean orphan = false;

    protected CommonArena(CommonMinigame parent, String id, String name, Location3D[] spawnPoints,
            Boundary boundary) throws IllegalArgumentException {
        assert parent != null;
        assert id != null;
        assert name != null;
        checkArgument(spawnPoints != null && spawnPoints.length > 0,
                "Initial spawn for arena \"" + id + "\" must not be null or empty");
        checkArgument(!parent.getArenaMap().containsKey(id),
                "Cannot create arena: arena with ID \"" + id + "\" already exists");
        String world = null;
        for (Location3D spawn : spawnPoints) {
            if (spawn.getWorld().isPresent()) {
                if (world != null) {
                    checkArgument(spawn.getWorld().get().equals(world),
                            "Spawn points must not have different worlds");
                } else {
                    world = spawn.getWorld().get();
                }
            }
        }
        checkArgument(spawnPoints[0].getWorld().isPresent(),
                "At least one spawn point for arena \"" + id + "\" must define a world");
        for (Location3D spawn : spawnPoints) {
            checkArgument(boundary.contains(spawn), "Spawn points must be within arena boundary");
        }

        if (!boundary.getLowerBound().getWorld().isPresent() && !boundary.getUpperBound().getWorld().isPresent()) {
            Location3D newLower = new Location3D(world, boundary.getLowerBound().getX(),
                    boundary.getLowerBound().getY(), boundary.getLowerBound().getZ());
            boundary = new Boundary(newLower, boundary.getUpperBound());
        }

        this.parent = parent;
        this.id = id;
        this.name = name;
        this.world = world;
        for (int i = 0; i < spawnPoints.length; i++) {
            this.spawns.put(i, spawnPoints[i]);
        }
        this.shuffledSpawns = Lists.newArrayList(spawnPoints);
        Collections.shuffle(this.shuffledSpawns);
        this.boundary = boundary;

        this.rbHelper = ((IRollbackAgentFactory) FactoryRegistry.getFactory(IRollbackAgent.class))
                .createRollbackAgent(this);
        CommonMetadata.getEventBus().register(this);
        parent.getArenaMap().put(id.toLowerCase(), this);
    }

    @Override
    public Minigame getOwner() throws OrphanedComponentException {
        checkState();
        return parent;
    }

    @Override
    public Minigame getMinigame() throws OrphanedComponentException {
        return getOwner();
    }

    @Override
    public String getId() throws OrphanedComponentException {
        checkState();
        return id;
    }

    @Override
    public String getName() throws OrphanedComponentException {
        return getDisplayName();
    }

    @Override
    public String getDisplayName() throws OrphanedComponentException {
        checkState();
        return name;
    }

    @Override
    public void setDisplayName(String displayName) throws OrphanedComponentException {
        checkState();
        this.name = displayName;
    }

    @Override
    public String getWorld() throws OrphanedComponentException {
        checkState();
        return world;
    }

    @Override
    public Boundary getBoundary() throws OrphanedComponentException {
        checkState();
        return boundary;
    }

    @Override
    public void setBoundary(Boundary bound) throws OrphanedComponentException {
        checkState();
        this.boundary = bound;
        try {
            store();
        } catch (Exception ex) {
            ex.printStackTrace();
            CommonCore.logSevere("Failed to save arena with ID " + getId() + " to persistent storage");
        }
    }

    @Override
    public ImmutableMap<Integer, Location3D> getSpawnPoints() throws OrphanedComponentException {
        checkState();
        return ImmutableMap.copyOf(spawns);
    }

    @Override
    public int addSpawnPoint(Location3D spawn) throws OrphanedComponentException {
        checkState();
        checkArgument(getBoundary().contains(spawn), "Spawn point must be within arena boundary");

        int id;
        for (id = 0; id <= spawns.size(); id++) {
            if (!spawns.containsKey(id)) {
                Location3D spawnLoc = new Location3D(world, spawn.getX(), spawn.getY(), spawn.getZ());
                spawns.put(id, spawnLoc);
                shuffledSpawns.add(spawnLoc);
                Collections.shuffle(shuffledSpawns);
                try {
                    store();
                } catch (Exception ex) {
                    ex.printStackTrace();
                    CommonCore.logSevere("Failed to save arena with ID " + getId() + " to persistent storage");
                }
                return id;
            }
        }
        // logically impossible in theory
        throw new AssertionError("Logic error: could not get next available spawn. Report this immediately.");
    }

    @Override
    public void removeSpawnPoint(int index) throws OrphanedComponentException {
        checkState();
        checkArgument(spawns.containsKey(index), "Cannot remove spawn: none exists with given index");

        Location3D removedSpawn = spawns.remove(index);

        shuffledSpawns.remove(removedSpawn);
        Collections.shuffle(shuffledSpawns);

        try {
            store();
        } catch (Exception ex) {
            ex.printStackTrace();
            CommonCore.logSevere("Failed to save arena with ID " + getId() + " to persistent storage");
        }
    }

    @Override
    public void removeSpawnPoint(Location3D location) throws OrphanedComponentException {
        checkState();
        checkArgument(!location.getWorld().isPresent() || location.getWorld().get().equals(world),
                "Cannot remove spawn: world mismatch in provided location");

        Location3D loc = new Location3D(world, location.getX(), location.getY(), location.getZ());

        for (Map.Entry<Integer, Location3D> e : spawns.entrySet()) {
            if (e.getValue().equals(loc)) {
                removeSpawnPoint(e.getKey());
                return;
            }
        }

        throw new IllegalArgumentException("Cannot remove spawn: none exists at given location");
    }

    @Override
    public Optional<Round> getRound() throws OrphanedComponentException {
        checkState();
        return Optional.fromNullable(parent.getRoundMap().get(this));
    }

    @Override
    public Round createRound() throws IllegalStateException, OrphanedComponentException {
        checkState();
        Preconditions.checkState(!getRound().isPresent(), "Cannot create a round in an arena already hosting one");
        ImmutableSet<LifecycleStage> stages = getMinigame().getConfigValue(ConfigNode.DEFAULT_LIFECYCLE_STAGES);
        Preconditions.checkState(stages != null && !stages.isEmpty(),
                "Illegal call to nullary createRound method: default lifecycle stages are not set");
        return createRound(getMinigame().getConfigValue(ConfigNode.DEFAULT_LIFECYCLE_STAGES));
    }

    @Override
    public Round createRound(ImmutableSet<LifecycleStage> stages)
            throws IllegalArgumentException, IllegalStateException, OrphanedComponentException {
        checkState();
        Preconditions.checkState(!getRound().isPresent(), "Cannot create a round in an arena already hosting one");
        checkArgument(stages != null && !stages.isEmpty(), "LifecycleStage set must not be null or empty");
        ((CommonMinigame) getMinigame()).getRoundMap().put(this,
                ((IRoundFactory) FactoryRegistry.getFactory(Round.class)).createRound(this, stages));
        Preconditions.checkState(getRound().isPresent(), "Cannot get created round from arena! This is a bug.");
        return getRound().get();
    }

    @Override
    public Round getOrCreateRound() {
        return getRound().isPresent() ? getRound().get() : createRound();
    }

    @Override
    public Round getOrCreateRound(ImmutableSet<LifecycleStage> stages) {
        return getRound().isPresent() ? getRound().get() : createRound(stages);
    }

    @Subscribe
    public void onMetadataMutate(PersistableMetadataMutateEvent event) {
        if (event.getMetadata() == getMetadata()) { // check whether event pertains to this arena's metadata
            try {
                store(); // re-store the arena
            } catch (Exception ex) {
                ex.printStackTrace();
                CommonCore.logSevere("Failed to save arena with ID " + getId() + " to persistent storage");
            }
        }
    }

    @Override
    public ImmutableList<LobbySign> getLobbySigns() {
        return ImmutableList.copyOf(lobbies.values());
    }

    @Override
    public Optional<LobbySign> getLobbySignAt(Location3D location) throws IllegalArgumentException {
        return Optional.fromNullable(lobbies.get(location));
    }

    @Override
    public void rollback() throws IllegalStateException, OrphanedComponentException {
        checkState();
        try {
            getRollbackAgent().popRollbacks();
        } catch (IOException | SQLException ex) {
            throw new RuntimeException("Failed to rollback arena " + getDisplayName(), ex);
        }
    }

    /**
     * Gets the {@link IRollbackAgent} associated with this {@link CommonArena}.
     *
     * @return The {@link IRollbackAgent} associated with this
     *     {@link CommonArena}
     */
    public IRollbackAgent getRollbackAgent() {
        return rbHelper;
    }

    public HashMap<Location3D, LobbySign> getLobbySignMap() {
        return lobbies;
    }

    public HashMap<Integer, Location3D> getSpawnPointMap() {
        return spawns;
    }

    public ImmutableList<Location3D> getShuffledSpawnPoints() throws OrphanedComponentException {
        checkState();
        return ImmutableList.copyOf(shuffledSpawns);
    }

    /**
     * Unregisters the {@link LobbySign} at the given
     * {@link Location3D location}.
     *
     * <p><em>This method is intended for internal use only.</em></p>
     *
     * @param location The {@link Location3D location} of the {@link LobbySign}
     *     to remove
     */
    public void unregisterLobbySign(Location3D location) {
        CommonLobbySign sign = (CommonLobbySign) lobbies.get(location);
        lobbies.remove(location);
        sign.unstore();
    }

    @Override
    public void checkState() throws OrphanedComponentException {
        if (orphan) {
            throw new OrphanedComponentException(this);
        }
    }

    @Override
    public void orphan() {
        CommonCore.orphan(this);
    }

    @Override
    public void setOrphanFlag() {
        this.orphan = true;
    }

    /**
     * Configures this {@link CommonArena} from the given {@link JsonObject}.
     *
     * @param json The {@link JsonObject} containing data for this
     *     {@link CommonArena}
     */
    public void configure(JsonObject json) {
        {
            JsonObject spawnSection = json.getAsJsonObject(PERSISTENCE_SPAWNS_KEY);
            for (Map.Entry<String, JsonElement> entry : spawnSection.entrySet()) {
                try {
                    int index = Integer.parseInt(entry.getKey());
                    getSpawnPointMap().put(index,
                            deserializeLocation(spawnSection.getAsJsonObject(entry.getKey())));
                } catch (IllegalArgumentException ignored) {
                    CommonCore.logWarning(
                            "Invalid spawn at index " + entry.getKey() + " for arena \"" + getId() + "\"");
                }
            }
        }

        if (json.has(PERSISTENCE_METADATA_KEY) && json.get(PERSISTENCE_METADATA_KEY).isJsonObject()) {
            JsonSerializer.deserializeMetadata(json.getAsJsonObject(PERSISTENCE_METADATA_KEY),
                    getPersistentMetadata());
        }
    }

    /**
     * Stores this arena into persistent storage.
     *
     * @throws IOException If an exception occurs while writing to the
     *     persistent store
     */
    public void store() throws IOException {
        CommonCore.logInfo("Storing");

        File arenaStore = CommonDataFiles.ARENA_STORE.getFile(getMinigame());

        JsonObject json = JsonHelper.readOrCreateJson(arenaStore);

        JsonObject jsonArena = new JsonObject();
        jsonArena.addProperty(PERSISTENCE_NAME_KEY, getDisplayName());
        jsonArena.addProperty(PERSISTENCE_WORLD_KEY, getWorld());

        JsonObject spawns = new JsonObject();
        for (Map.Entry<Integer, Location3D> entry : getSpawnPoints().entrySet()) {
            spawns.add(entry.getKey().toString(), serializeLocation(entry.getValue()));
        }
        jsonArena.add(PERSISTENCE_SPAWNS_KEY, spawns);

        jsonArena.add(PERSISTENCE_BOUNDS_UPPER_KEY, serializeLocation(getBoundary().getUpperBound()));
        jsonArena.add(PERSISTENCE_BOUNDS_LOWER_KEY, serializeLocation(getBoundary().getLowerBound()));

        JsonObject metadata = new JsonObject();
        JsonSerializer.serializeMetadata(metadata, getPersistentMetadata());
        jsonArena.add(PERSISTENCE_METADATA_KEY, metadata);

        json.add(this.getId(), jsonArena);

        try (FileWriter writer = new FileWriter(arenaStore)) {
            writer.write(json.toString());
        }
    }

    /**
     * Removes this arena from persistent storage.
     *
     * @throws IOException If an exception occurs while writing to the
     *     persistent store
     */
    public void removeFromStore() throws IOException {
        File arenaStore = CommonDataFiles.ARENA_STORE.getFile(getMinigame());

        if (!arenaStore.exists()) {
            throw new IllegalStateException("Arena store does not exist!");
        }

        JsonObject json = new JsonObject();
        try (FileReader reader = new FileReader(arenaStore)) {
            JsonElement el = new JsonParser().parse(reader);
            if (el.isJsonObject()) {
                json = el.getAsJsonObject();
            } else {
                CommonCore.logWarning("Root element of arena store is not object - overwriting");
                Files.delete(arenaStore.toPath());
            }
        }

        json.remove(this.getId());

        try (FileWriter writer = new FileWriter(arenaStore)) {
            writer.write(json.toString());
        }
    }

    public static class Builder implements Arena.Builder {

        private final Minigame mg;
        private String id;
        private String dispName;
        private Location3D[] spawnPoints;
        private Boundary boundary;

        public Builder(Minigame mg) {
            this.mg = mg;
        }

        @Override
        public Arena.Builder id(String id) {
            this.id = id;
            return this;
        }

        @Override
        public Arena.Builder displayName(String displayName) {
            this.dispName = displayName;
            return this;
        }

        @Override
        public Arena.Builder spawnPoints(Location3D... spawnPoints) {
            this.spawnPoints = spawnPoints;
            return this;
        }

        @Override
        public Arena.Builder boundary(Boundary boundary) {
            this.boundary = boundary;
            return this;
        }

        @Override
        public Arena build() throws IllegalStateException {
            Preconditions.checkState(id != null, "ID must be set before building");
            Preconditions.checkState(spawnPoints != null && spawnPoints.length > 0,
                    "Spawn points must be set before building");
            Preconditions.checkState(boundary != null, "Boundary must be set before building");

            CommonArena arena = ((IArenaFactory) FactoryRegistry.getFactory(Arena.class))
                    .createArena((CommonMinigame) mg, id, dispName != null ? dispName : id, spawnPoints, boundary);
            try {
                arena.store();
            } catch (IOException ex) {
                throw new RuntimeException("Failed to store arena with ID " + id + ".", ex);
            }
            return arena;
        }

    }

}