com.replaymod.sponge.recording.AbstractRecorder.java Source code

Java tutorial

Introduction

Here is the source code for com.replaymod.sponge.recording.AbstractRecorder.java

Source

/*
 * This file is part of SpongeRecording, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2015 johni0702 <https://github.com/johni0702>
 * Copyright (c) contributors
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.replaymod.sponge.recording;

import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import io.netty.buffer.ByteBuf;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.spongepowered.api.Game;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * An abstract recorder implementing common methods.
 */
public abstract class AbstractRecorder<T extends Connection> implements Recorder {

    public static final String FILE_FORMAT = "BIMCPR";
    public static final int FILE_FORMAT_VERSION = 1;

    /**
     * The game instance.
     */
    private final Game game;

    /**
     * The connection which is being recorded.
     */
    private final T connection;

    /**
     * Time in milliseconds at which this recorder started.
     */
    private final long startTime = System.currentTimeMillis();

    /**
     * Set of UUIDs of all players visible in the recording.
     * Subclasses should add all players as they appear.
     */
    protected final Set<UUID> playersInReplay = Collections.newSetFromMap(new ConcurrentHashMap<UUID, Boolean>());

    /**
     * Map every output stream it its zip output stream so we can later add more entries when closing before it.
     */
    private final Map<OutputStream, ZipOutputStream> outputs = new HashMap<OutputStream, ZipOutputStream>();

    /**
     * List of all raw output streams. Those are the output stream which receive the packet data.
     */
    private final List<OutputStream> rawOutputs = new ArrayList<OutputStream>();

    /**
     * Combines all of the {@link #rawOutputs raw} and {@link #outputs zipped} outputs for convenient writing.
     */
    private final DataOutputStream combinedOutput = new DataOutputStream(
            new MultiOutputStream(Iterables.concat(outputs.values(), rawOutputs)));

    /**
     * Creates a new abstract recorder.
     * When creating a new recorder its start time is set.
     * @param game The game instance
     * @param connection The connection being recorded
     */
    public AbstractRecorder(Game game, T connection) {
        this.game = game;
        this.connection = connection;
    }

    @Override
    public T getConnection() {
        return connection;
    }

    @Override
    public long getDuration() {
        return System.currentTimeMillis() - startTime;
    }

    @Override
    public ReplayMetaData getMetaData() {
        ReplayMetaData metaData = new ReplayMetaData();

        // Constants
        metaData.set("singleplayer", false);
        metaData.set("fileFormat", FILE_FORMAT);
        metaData.set("fileFormatVersion", FILE_FORMAT_VERSION);

        // Current replay
        metaData.set("date", startTime);
        metaData.set("duration", getDuration());
        metaData.set("players", playersInReplay);
        metaData.set("mcversion", connection.getMcVersion().getName());

        // Generator
        String recordingName = RecordingPlugin.class.getPackage().getImplementationTitle();
        String recordingVersion = RecordingPlugin.class.getPackage().getImplementationVersion();
        String spongeName = game.getPlatform().getImplementation().getName();
        String spongeVersion = game.getPlatform().getImplementation().getVersion();
        String generator = String.format("%s %s on %s (%s)", recordingName, recordingVersion, spongeName,
                spongeVersion);
        metaData.set("generator", generator);

        return metaData;
    }

    @Override
    public void addOutput(OutputStream out) throws IOException {
        ZipOutputStream zipOut = new ZipOutputStream(out);
        zipOut.putNextEntry(new ZipEntry("recording.tmcpr"));
        outputs.put(out, zipOut);
    }

    @Override
    public void addRawOutput(OutputStream out) {
        rawOutputs.add(out);
    }

    @Override
    public void endRecording(OutputStream out, ReplayMetaData metaData) throws IllegalStateException, IOException {
        if (metaData == null) {
            Preconditions.checkState(rawOutputs.remove(out),
                    "Specified output is unknown or meta data is missing.");
            out.flush();
            out.close();
        } else {
            ZipOutputStream zipOut = outputs.remove(out);
            if (zipOut == null) {
                throw new IllegalStateException("Specified output is unknown or contains raw data.");
            }

            zipOut.closeEntry();

            zipOut.putNextEntry(new ZipEntry("metaData.json"));
            zipOut.write(toJson(metaData).getBytes());
            zipOut.closeEntry();

            zipOut.flush();
            zipOut.close();
        }
    }

    /**
     * Converts the meta data supplied to a JSON string.
     * @param metaData The meta data
     * @return The JSON string
     */
    @SuppressWarnings("unchecked")
    private String toJson(ReplayMetaData metaData) {
        JSONObject data = new JSONObject();
        for (String key : metaData.keys()) {
            Object value = metaData.get(key).get();
            if (value instanceof Collection) {
                JSONArray array = new JSONArray();
                for (Object element : (Collection) value) {
                    array.add(element == null ? null : element);
                }
                value = array;
            }
            data.put(key, value);
        }
        return data.toString();
    }

    /**
     * Return a data output which writes to all underlying streams.
     * @return The combined data output
     */
    protected DataOutputStream getCombinedOutput() {
        return combinedOutput;
    }

    /**
     * Write the specified packet data to the output streams.
     * @param fromServer Whether the packet is client or server bound
     * @param data The packet data (packet id and payload)
     */
    protected synchronized void writePacket(boolean fromServer, ByteBuf data) throws IOException {
        DataOutputStream out = getCombinedOutput();
        long timeAndDirection = getDuration() << 1 | (fromServer ? 0 : 1);
        writeVar(out, timeAndDirection);
        int length = data.readableBytes();
        writeVar(out, length);
        data.readBytes(out, length);
    }

    private void writeVar(OutputStream out, long var) throws IOException {
        do {
            int b = (int) (var & 0x7F);
            var >>>= 7;
            if (var > 0) {
                b |= 0x80;
            }
            out.write(b);
        } while (var > 0);
    }

    /**
     * Convenient output stream for writing to multiple streams.
     */
    private static class MultiOutputStream extends OutputStream {

        private final Iterable<OutputStream> outputs;

        public MultiOutputStream(Iterable<OutputStream> outputs) {
            this.outputs = outputs;
        }

        @Override
        public void write(int b) throws IOException {
            for (OutputStream out : outputs) {
                out.write(b);
            }
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            for (OutputStream out : outputs) {
                out.write(b, off, len);
            }
        }

        @Override
        public void flush() throws IOException {
            for (OutputStream out : outputs) {
                out.flush();
            }
        }

        @Override
        public void close() throws IOException {
            for (OutputStream out : outputs) {
                out.close();
            }
        }
    }
}