From 959f6da907482ae4194b3868702c42bf8a1757bc Mon Sep 17 00:00:00 2001 From: ayunami2000 Date: Thu, 21 Apr 2022 08:32:33 -0400 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 8 + pom.xml | 25 ++ .../me/ayunami2000/ayunEagVidMap/Main.java | 111 ++++++++ .../ayunEagVidMap/VideoMapPacketCodec.java | 257 ++++++++++++++++++ .../VideoMapPacketCodecBukkit.java | 122 +++++++++ src/main/resources/config.yml | 12 + src/main/resources/plugin.yml | 9 + 8 files changed, 546 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/me/ayunami2000/ayunEagVidMap/Main.java create mode 100644 src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodec.java create mode 100644 src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodecBukkit.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aff26de --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea/ +lib/ +target/classes/ +target/generated-sources/ +target/maven-archiver/ +target/maven-status/ +ayunEagVidMap.iml +run/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ff538c6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + me.ayunami2000 + ayunEagVidMap + 1.0-SNAPSHOT + + + 8 + 8 + + + + + dont.care + didnt-ask + 1.0 + system + ${project.basedir}/lib/craftbukkit-1.5.2-R1.0.jar + + + \ No newline at end of file diff --git a/src/main/java/me/ayunami2000/ayunEagVidMap/Main.java b/src/main/java/me/ayunami2000/ayunEagVidMap/Main.java new file mode 100644 index 0000000..764fc1c --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunEagVidMap/Main.java @@ -0,0 +1,111 @@ +package me.ayunami2000.ayunEagVidMap; + +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.plugin.java.JavaPlugin; + +public class Main extends JavaPlugin implements CommandExecutor, Listener { + private boolean playing = false; + private VideoMapPacketCodecBukkit videoMapCodec = null; + private int[][] mapIds; + private Location audioLoc; + private String url; + + @Override + public void onEnable(){ + this.saveDefaultConfig(); + this.getCommand("ayunvid").setExecutor(this); + int width = this.getConfig().getInt("width"); + int height = this.getConfig().getInt("width"); + mapIds = new int[height][width]; + int offset = this.getConfig().getInt("offset"); + for (int y = 0; y < mapIds.length; y++) { + for (int x = 0; x < mapIds[y].length; x++) { + mapIds[y][x] = offset++; + } + } + audioLoc.setX(this.getConfig().getDouble("audio.x")); + audioLoc.setY(this.getConfig().getDouble("audio.y")); + audioLoc.setZ(this.getConfig().getDouble("audio.z")); + videoMapCodec = new VideoMapPacketCodecBukkit(mapIds, audioLoc.getX(), audioLoc.getY(), audioLoc.getZ(), 0.5f); + url = this.getConfig().getString("url"); + this.getServer().getScheduler().scheduleSyncRepeatingTask(this, () -> { + for (Player player : this.getServer().getOnlinePlayers()) { + VideoMapPacketCodecBukkit.nativeSendPacketToPlayer(player, videoMapCodec.syncPlaybackWithPlayersBukkit()); + } + }, 10000, 10000); // sync every 10 seconds + } + + @Override + public void onDisable(){ + videoMapCodec.disableVideoBukkit(); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (playing) { + VideoMapPacketCodecBukkit.nativeSendPacketToPlayer(event.getPlayer(), videoMapCodec.); + } + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0){ + sender.sendMessage("usage"); + return true; + } + switch (args[0].toLowerCase()) { + case "u": + case "url": + if (args.length < 2) { + sender.sendMessage("no url specified!"); + break; + } + this.getConfig().set("url", args[1]); + this.saveConfig(); + sender.sendMessage("seturl"); + break; + case "l": + case "loc": + case "location": + sender.sendMessage("set location of audio"); + break; + case "p": + case "play": + case "pause": + sender.sendMessage("resuming & loading if needed, or pausing"); + + new VideoMapPacketCodecBukkit() + break; + case "s": + case "size": + if (args.length < 3) { + sender.sendMessage("must specify width & height to set! current vals are..."); + break; + } + int width; + int height; + try { + width = Math.max(1, Integer.parseInt(args[1])); + height = Math.max(1, Integer.parseInt(args[2])); + } catch(NumberFormatException e) { + sender.sendMessage(""); + break; + } + this.getConfig().set("width", width); + this.getConfig().set("height", height); + this.saveConfig(); + sender.sendMessage("set width & height"); + break; + default: + sender.sendMessage("invalid"); + } + return true; + } +} diff --git a/src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodec.java b/src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodec.java new file mode 100644 index 0000000..6a20ee2 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodec.java @@ -0,0 +1,257 @@ +package me.ayunami2000.ayunEagVidMap; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class VideoMapPacketCodec { + + public final int[][] mapIds; + private boolean loop; + private String url; + private int duration; + private long timestamp; + private long pauseTimestamp; + private double posX; + private double posY; + private double posZ; + private float volume; + private int frameRate; + private boolean requiresFullResetPacket; + private boolean requiresPositionPacket; + private boolean isDisabled; + + /** + * @param mapIds 2D grid of map IDs that make up the screen (mapIds[y][x]) + * @param posX audio playback X coord + * @param posY audio playback Y coord + * @param posZ audio playback Z coord + * @param volume the volume of the clip + */ + public VideoMapPacketCodec(int[][] mapIds, double posX, double posY, double posZ, float volume) { + this.mapIds = mapIds; + this.url = null; + this.posX = posX; + this.posY = posY; + this.posZ = posZ; + this.volume = 1.0f; + this.frameRate = 60; + this.requiresPositionPacket = true; + this.requiresFullResetPacket = true; + this.isDisabled = true; + } + + /** + * @param mapIds 2D grid of map IDs that make up the screen (mapIds[y][x]) + * @param posX audio playback X coord + * @param posY audio playback Y coord + * @param posZ audio playback Z coord + */ + public VideoMapPacketCodec(int[][] mapIds, double posX, double posY, double posZ) { + this(mapIds, posX, posY, posZ, 1.0f); + } + + /** + * @param posX audio playback X coord + * @param posY audio playback Y coord + * @param posZ audio playback Z coord + * @param volume the volume of the clip + * @return packet to send to players + */ + public byte[] moveAudioSource(double posX, double posY, double posZ, float volume) { + this.posX = posX; + this.posY = posY; + this.posZ = posZ; + this.volume = volume; + this.requiresPositionPacket = true; + return syncPlaybackWithPlayers(); + } + + /** + * unloads video and resets all map object to vanilla renderer + * @return packet to send to players + */ + public byte[] disableVideo() { + isDisabled = true; + return syncPlaybackWithPlayers(); + } + + /** + * syncs the server side video timestamp with players + * @return packet to send to players + */ + public byte[] syncPlaybackWithPlayers() { + try { + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + DataOutputStream str = new DataOutputStream(bao); + + if(isDisabled) { + str.write(0); + int x = mapIds[0].length; + int y = mapIds.length; + str.write((x << 4) | y); + for(int yy = 0; yy < y; ++yy) { + for(int xx = 0; xx < x; ++xx) { + str.writeShort(mapIds[yy][xx]); + } + } + return bao.toByteArray(); + } + + int packetType = 1; + if(requiresFullResetPacket) { + packetType = packetType | 2; + } + if(requiresFullResetPacket || requiresPositionPacket) { + packetType = packetType | 4; + } + + str.write(packetType); + + if(requiresFullResetPacket) { + int x = mapIds[0].length; + int y = mapIds.length; + str.write((x << 4) | y); + for(int yy = 0; yy < y; ++yy) { + for(int xx = 0; xx < x; ++xx) { + str.writeShort(mapIds[yy][xx]); + } + } + str.write(frameRate); + str.writeInt(duration); + str.writeUTF(url); + } + + if(requiresFullResetPacket || requiresPositionPacket) { + str.writeFloat(volume); + str.writeDouble(posX); + str.writeDouble(posY); + str.writeDouble(posZ); + } + + str.writeInt(getElapsedMillis()); + str.writeBoolean(loop); + str.writeBoolean(pauseTimestamp > 0l); + + requiresFullResetPacket = false; + requiresPositionPacket = false; + + return bao.toByteArray(); + }catch(IOException e) { + throw new RuntimeException("serialization error", e); + } + } + + /** + * this is dual purpose, it calculates elapsed time but also loops or pauses the video if it is finished playing + */ + private int getElapsedMillis() { + if(pauseTimestamp > 0l) { + return (int)(pauseTimestamp - timestamp); + } + int t = (int)(System.currentTimeMillis() - timestamp); + if(loop) { + while(t > duration) { + t -= duration; + timestamp += duration; + } + }else { + if(t > duration) { + timestamp = (int)(System.currentTimeMillis() - duration); + return duration; + } + } + return t; + } + + /** + * @param url URL to an MP4 or other HTML5 supported video file + * @param loop If the video file should loop + * @param duration duration of the video in seconds + * @return packet to send to players + */ + public byte[] beginPlayback(String url, boolean loop, float duration) { + this.url = url; + this.loop = loop; + this.duration = (int)(duration * 1000.0f); + this.pauseTimestamp = 0l; + this.timestamp = 0l; + this.requiresFullResetPacket = true; + this.isDisabled = false; + return syncPlaybackWithPlayers(); + } + + /** + * @return the duration of the current clip + */ + public float getDuration() { + return duration * 0.001f; + } + + /** + * @return the URL of the current clip + */ + public String getURL() { + return url; + } + + /** + * @return the server's current timestamp + */ + public float getPlaybackTime() { + return getElapsedMillis() * 0.001f; + } + + /** + * @param time time in seconds to seek the video to + */ + public byte[] setPlaybackTime(float time) { + timestamp = System.currentTimeMillis() - (int)(time * 1000.0f); + return syncPlaybackWithPlayers(); + } + + /** + * @return if playback is complete (false if loop) + */ + public boolean isPlaybackFinished() { + return !loop && getElapsedMillis() == duration; + } + + /** + * @param loop video should loop + */ + public byte[] setLoopEnable(boolean loop) { + this.loop = loop; + return syncPlaybackWithPlayers(); + } + + /** + * @return if loop is enabled + */ + public boolean isLoopEnable() { + return loop; + } + + /** + * @param pause set if video should pause + * @return packet to send to players + */ + public byte[] setPaused(boolean pause) { + getElapsedMillis(); + if(pause && pauseTimestamp <= 0l) { + pauseTimestamp = System.currentTimeMillis(); + }else if(!pause && pauseTimestamp > 0l) { + timestamp = System.currentTimeMillis() - (pauseTimestamp - timestamp); + pauseTimestamp = 0l; + } + return syncPlaybackWithPlayers(); + } + + /** + * @return if video is currently paused + */ + public boolean isPaused() { + return pauseTimestamp > 0l; + } + +} diff --git a/src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodecBukkit.java b/src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodecBukkit.java new file mode 100644 index 0000000..14bd6c9 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunEagVidMap/VideoMapPacketCodecBukkit.java @@ -0,0 +1,122 @@ +package me.ayunami2000.ayunEagVidMap; + +import java.util.List; + +import org.bukkit.craftbukkit.v1_5_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; + +import net.minecraft.server.v1_5_R3.Packet; +import net.minecraft.server.v1_5_R3.Packet131ItemData; + +public class VideoMapPacketCodecBukkit extends VideoMapPacketCodec { + + /** + * @param mapIds 2D grid of map IDs that make up the screen (mapIds[y][x]) + * @param posX audio playback X coord + * @param posY audio playback Y coord + * @param posZ audio playback Z coord + * @param volume the volume of the clip + */ + public VideoMapPacketCodecBukkit(int[][] mapIds, double posX, double posY, double posZ, float volume) { + super(mapIds, posX, posY, posZ, volume); + } + + /** + * @param mapIds 2D grid of map IDs that make up the screen (mapIds[y][x]) + * @param posX audio playback X coord + * @param posY audio playback Y coord + * @param posZ audio playback Z coord + */ + public VideoMapPacketCodecBukkit(int[][] mapIds, double posX, double posY, double posZ) { + super(mapIds, posX, posY, posZ, 1.0f); + } + + public class VideoMapPacket { + protected final Object packet; + protected VideoMapPacket(byte[] packet) { + this.packet = new Packet131ItemData((short)104, (short)0, packet); + } + public Object getNativePacket() { + return packet; + } + public void send(Player p) { + nativeSendPacketToPlayer(p, packet); + } + public void send(Player... p) { + for(Player pp : p) { + nativeSendPacketToPlayer(pp, packet); + } + } + public void send(List p) { + for(Player pp : p) { + nativeSendPacketToPlayer(pp, packet); + } + } + } + + /** + * @param posX audio playback X coord + * @param posY audio playback Y coord + * @param posZ audio playback Z coord + * @param volume the volume of the clip + * @return packet to send to players + */ + public VideoMapPacket moveAudioSourceBukkit(double posX, double posY, double posZ, float volume) { + return new VideoMapPacket(moveAudioSource(posX, posY, posZ, volume)); + } + + /** + * unloads video and resets all map object to vanilla renderer + * @return packet to send to players + */ + public VideoMapPacket disableVideoBukkit() { + return new VideoMapPacket(disableVideo()); + } + + /** + * syncs the server side video timestamp with players + * @return packet to send to players + */ + public VideoMapPacket syncPlaybackWithPlayersBukkit() { + return new VideoMapPacket(syncPlaybackWithPlayers()); + } + + /** + * @param url URL to an MP4 or other HTML5 supported video file + * @param loop If the video file should loop + * @param duration duration of the video in seconds + * @return packet to send to players + */ + public VideoMapPacket beginPlaybackBukkit(String url, boolean loop, float duration) { + return new VideoMapPacket(beginPlayback(url, loop, duration)); + } + + /** + * @param time time in seconds to seek the video to + */ + public VideoMapPacket setPlaybackTimeBukkit(float time) { + return new VideoMapPacket(setPlaybackTime(time)); + } + + /** + * @param loop video should loop + */ + public VideoMapPacket setLoopEnableBukkit(boolean loop) { + return new VideoMapPacket(setLoopEnable(loop)); + } + + /** + * @param pause set if video should pause + * @return packet to send to players + */ + public VideoMapPacket setPausedBukkit(boolean pause) { + return new VideoMapPacket(setPaused(pause)); + } + + public static void nativeSendPacketToPlayer(Player player, Object obj) { + if(obj == null) { + return; + } + ((CraftPlayer)player).getHandle().playerConnection.sendPacket((Packet)obj); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..94fdd79 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,12 @@ +# URL of video to play +url: "" +# screen size +width: 3 +height: 2 +# map ID offset +offset: 0 +# audio location +audio: + x: 0 + y: 100 + z: 0 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..2d102b0 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,9 @@ +name: ayunEagVidMap +version: 1.0 +main: me.ayunami2000.ayunEagVidMap.Main +commands: + ayunvid: + description: Control ayunEagVidMap + usage: / + permission: ayuneagvidmap.control +#todo: separate play, pause, and add queue \ No newline at end of file