From ff747c901395a9bfa2311ee6468120fe1fc427f7 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Tue, 17 Dec 2024 18:53:38 +0100 Subject: [PATCH] dsf --- .../eu/m724/musicPlugin/MusicCommands.java | 32 +++-- .../java/eu/m724/musicPlugin/MusicPlugin.java | 5 +- .../eu/m724/musicPlugin/item/ItemEvents.java | 110 +++++++++++++++++ .../eu/m724/musicPlugin/item/ItemStorage.java | 71 ----------- .../musicPlugin/item/PortableMediaPlayer.java | 115 +++++++++++++----- .../eu/m724/musicPlugin/item/SongLibrary.java | 8 -- .../eu/m724/musicPlugin/item/Speakers.java | 33 +++++ .../item/speaker/BlockSpeaker.java | 40 ++++++ .../musicPlugin/item/speaker/Speaker.java | 27 ++++ .../musicPlugin/player/MovingMusicPlayer.java | 55 +++++++++ .../m724/musicPlugin/player/MusicPlayer.java | 12 +- 11 files changed, 386 insertions(+), 122 deletions(-) delete mode 100644 src/main/java/eu/m724/musicPlugin/item/ItemStorage.java delete mode 100644 src/main/java/eu/m724/musicPlugin/item/SongLibrary.java create mode 100644 src/main/java/eu/m724/musicPlugin/item/Speakers.java create mode 100644 src/main/java/eu/m724/musicPlugin/item/speaker/BlockSpeaker.java create mode 100644 src/main/java/eu/m724/musicPlugin/item/speaker/Speaker.java create mode 100644 src/main/java/eu/m724/musicPlugin/player/MovingMusicPlayer.java diff --git a/src/main/java/eu/m724/musicPlugin/MusicCommands.java b/src/main/java/eu/m724/musicPlugin/MusicCommands.java index 07c05c5..50df0a4 100644 --- a/src/main/java/eu/m724/musicPlugin/MusicCommands.java +++ b/src/main/java/eu/m724/musicPlugin/MusicCommands.java @@ -2,6 +2,7 @@ package eu.m724.musicPlugin; import eu.m724.musicPlugin.file.AudioFileStorage; import eu.m724.musicPlugin.item.PortableMediaPlayer; +import eu.m724.musicPlugin.item.Speakers; import eu.m724.musicPlugin.player.MusicPlayer; import eu.m724.musicPlugin.file.AudioFile; import eu.m724.musicPlugin.player.StaticMusicPlayer; @@ -104,14 +105,29 @@ public class MusicCommands implements CommandExecutor { sender.sendMessage("unapuised"); } else if (command.getName().equals("pmp")) { var p = (Player) sender; - var pmp = PortableMediaPlayer.fromItemStack(p.getItemInHand()).join(); - if (pmp != null) { - sender.sendMessage(String.valueOf(pmp.id)); - sender.sendMessage(String.valueOf(pmp.premium)); - sender.sendMessage(pmp.engraving); - } else { - pmp = PortableMediaPlayer.create(args[0].equals("yes"), args[1]); - p.getInventory().addItem(pmp.getItemStack()); + var pmp = PortableMediaPlayer.fromItemStack(p.getItemInHand()); + + switch (args[0]) { + case "info" -> { + sender.sendMessage(String.valueOf(pmp.id)); + sender.sendMessage(String.valueOf(pmp.premium)); + sender.sendMessage(pmp.engraving); + } + case "create" -> { + pmp = PortableMediaPlayer.create(args[0].equals("yes"), args[1]); + p.getInventory().addItem(pmp.getItemStack()); + } + case "play" -> { + var file = storage.get(args[1], Integer.parseInt(args[2])); + + try (FileInputStream fis = new FileInputStream(file)) { + var song = new AudioFile(fis); + song.load(); + pmp.getMusicPlayer().play(song); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } diff --git a/src/main/java/eu/m724/musicPlugin/MusicPlugin.java b/src/main/java/eu/m724/musicPlugin/MusicPlugin.java index ed73d96..237f7e7 100644 --- a/src/main/java/eu/m724/musicPlugin/MusicPlugin.java +++ b/src/main/java/eu/m724/musicPlugin/MusicPlugin.java @@ -4,8 +4,7 @@ import de.maxhenkel.voicechat.api.BukkitVoicechatService; import de.maxhenkel.voicechat.api.VoicechatConnection; import de.maxhenkel.voicechat.api.VoicechatServerApi; import eu.m724.musicPlugin.file.AudioFileStorage; -import eu.m724.musicPlugin.item.ItemStorage; -import org.bukkit.entity.Item; +import eu.m724.musicPlugin.item.ItemEvents; import org.bukkit.plugin.java.JavaPlugin; public final class MusicPlugin extends JavaPlugin { @@ -32,7 +31,7 @@ public final class MusicPlugin extends JavaPlugin { getCommand("resume").setExecutor(mcmd); getCommand("pmp").setExecutor(mcmd); - ItemStorage.init(this); + getServer().getPluginManager().registerEvents(new ItemEvents(), this); } diff --git a/src/main/java/eu/m724/musicPlugin/item/ItemEvents.java b/src/main/java/eu/m724/musicPlugin/item/ItemEvents.java index ae62ea9..bb1141f 100644 --- a/src/main/java/eu/m724/musicPlugin/item/ItemEvents.java +++ b/src/main/java/eu/m724/musicPlugin/item/ItemEvents.java @@ -1,6 +1,116 @@ package eu.m724.musicPlugin.item; +import eu.m724.musicPlugin.item.speaker.BlockSpeaker; +import eu.m724.musicPlugin.item.speaker.Speaker; +import eu.m724.musicPlugin.player.MovingMusicPlayer; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.entity.Item; +import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityDropItemEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.entity.ItemDespawnEvent; +import org.bukkit.event.inventory.InventoryMoveItemEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerInteractEvent; public class ItemEvents implements Listener { + @EventHandler + public void onInteract(PlayerInteractEvent event) { + var right = event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK; + + if (event.getItem() == null) return; + + var player = PortableMediaPlayer.fromItemStack(event.getItem()); + if (player == null) return; + + System.out.println("A player used"); + + if (!event.getPlayer().isSneaking()) { + if (right) { + System.out.println("nos higft + rmb next song"); + // TODO forward song + } else { + System.out.println("no shift + lmb rpevous song"); + // TODO back song + } + } else { + if (right) { + System.out.println("shirt + rmb open inventory"); + // TODO open inventory + } else { + System.out.println("shift + lmb bind spekar"); + var block = event.getClickedBlock(); + if (block != null) { + if (block.getType() == Material.NOTE_BLOCK) { + var speaker = BlockSpeaker.create(block.getLocation()); + if (speaker != null) { + player.setSpeaker(speaker); + block.getWorld().spawnParticle(Particle.HAPPY_VILLAGER, block.getLocation(), 10); + event.setCancelled(true); // prevent break + } else { + event.getPlayer().sendMessage("This speaker is occupied"); + } + } + } + } + } + } + + @EventHandler + public void onDeath(EntityDeathEvent event) { + if (event.getEntity() instanceof Item item) { + var id = PortableMediaPlayer.idFromItemStack(item.getItemStack()); + if (id == -1) return; + + System.out.println("A player ded"); + + Speakers.destroy(id); + } + } + + @EventHandler + public void onDespawn(ItemDespawnEvent event) { + var id = PortableMediaPlayer.idFromItemStack(event.getEntity().getItemStack()); + if (id == -1) return; + + System.out.println("A player despeawnd"); + + Speakers.destroy(id); + } + + @EventHandler + public void onDrop(PlayerDropItemEvent event) { + var id = PortableMediaPlayer.idFromItemStack(event.getItemDrop().getItemStack()); + if (id == -1) return; + + System.out.println("A playe rdropped"); + + Speakers.attachTo(id, event.getItemDrop()); + } + + @EventHandler + public void onPickup(EntityPickupItemEvent event) { + var id = PortableMediaPlayer.idFromItemStack(event.getItem().getItemStack()); + if (id == -1) return; + + System.out.println("A player puckuperd"); + + Speakers.attachTo(id, event.getEntity()); + } + + public void onMove(InventoryMoveItemEvent event) { + if (event.getDestination().getType() == InventoryType.PLAYER) { + var id = PortableMediaPlayer.idFromItemStack(event.getItem()); + if (id == -1) return; + + System.out.println("A player storaged :(("); + + Speakers.destroy(id); + } + } } diff --git a/src/main/java/eu/m724/musicPlugin/item/ItemStorage.java b/src/main/java/eu/m724/musicPlugin/item/ItemStorage.java deleted file mode 100644 index fb053b7..0000000 --- a/src/main/java/eu/m724/musicPlugin/item/ItemStorage.java +++ /dev/null @@ -1,71 +0,0 @@ -package eu.m724.musicPlugin.item; - -import eu.m724.musicPlugin.MusicPlugin; -import org.bukkit.inventory.ItemStack; -import org.bukkit.persistence.PersistentDataType; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.HashMap; -import java.util.HexFormat; -import java.util.Map; - -public class ItemStorage { - private static ItemStorage INSTANCE; - - private File directory; - private Map cache = new HashMap<>(); // TODO clean cache - - public static void init(MusicPlugin plugin) { - INSTANCE = new ItemStorage(); - INSTANCE.directory = new File(plugin.getDataFolder(), "storage"); - INSTANCE.directory.mkdir(); - } - - static ItemStorage getInstance() { - return INSTANCE; - } - - public PortableMediaPlayer getPortableMediaPlayer(int id) throws IOException { - var file = new File(directory, HexFormat.of().toHexDigits(id) + ".pmpdata.bin"); - - if (!file.exists()) return null; - - ByteBuffer buffer = ByteBuffer.wrap(Files.readAllBytes(file.toPath())); - - if (buffer.get() != 0) { // TODO version mismatch - - } - - var premium = buffer.get() == 1; - - var eb = new byte[buffer.get() & 0xFF]; - buffer.get(eb); - var engraving = new String(eb, StandardCharsets.UTF_8); - - return new PortableMediaPlayer(id, premium, engraving); - } - - public void savePortableMediaPlayer(PortableMediaPlayer player) throws IOException { - var file = new File(directory, HexFormat.of().toHexDigits(player.id) + ".pmpdata.bin"); - - Files.write(file.toPath(), getData(player)); - } - - - public byte[] getData(PortableMediaPlayer player) { - var buffer = ByteBuffer.allocate(8 + player.engraving.length()); - buffer.put((byte) 0); // version - buffer.put((byte) (player.premium ? 1 : 0)); - - buffer.put((byte) player.engraving.length()); - buffer.put(player.engraving.getBytes(StandardCharsets.UTF_8)); - - return buffer.array(); - } -} diff --git a/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayer.java b/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayer.java index 21b839c..cb52743 100644 --- a/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayer.java +++ b/src/main/java/eu/m724/musicPlugin/item/PortableMediaPlayer.java @@ -1,5 +1,8 @@ package eu.m724.musicPlugin.item; +import eu.m724.musicPlugin.item.speaker.Speaker; +import eu.m724.musicPlugin.player.MovingMusicPlayer; +import eu.m724.musicPlugin.player.MusicPlayer; import net.md_5.bungee.api.ChatColor; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -8,42 +11,53 @@ import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; -import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ThreadLocalRandom; public class PortableMediaPlayer { - private static NamespacedKey namespacedKey = new NamespacedKey("tweaks724", "player"); + private static NamespacedKey idKey = new NamespacedKey("tweaks724", "player_id"); + private static NamespacedKey dataKey = new NamespacedKey("tweaks724", "player_data"); public final int id; // TODO configurable - public final int storageSeconds = 600; - public final int audioBitrate = 32000; + private final int storageSeconds; + private final int audioBitrate; public final boolean premium; public final String engraving; - SongLibrary library; + private final MovingMusicPlayer musicPlayer = new MovingMusicPlayer(); + private Speaker linkedSpeaker; - public PortableMediaPlayer(int id, boolean premium, String engraving) { + public PortableMediaPlayer(int id, int storageSeconds, int audioBitrate, boolean premium, String engraving) { this.id = id; + this.storageSeconds = storageSeconds; + this.audioBitrate = audioBitrate; this.premium = premium; this.engraving = engraving; } public static PortableMediaPlayer create(boolean premium, String engraving) { - var pmp = new PortableMediaPlayer(ThreadLocalRandom.current().nextInt(), premium, engraving); - CompletableFuture.runAsync(() -> { - try { - ItemStorage.getInstance().savePortableMediaPlayer(pmp); - } catch (IOException ex) { - ex.printStackTrace(); - } - }); - return pmp; + return new PortableMediaPlayer(ThreadLocalRandom.current().nextInt(), 600, 32000, premium, engraving); + } + + public void setSpeaker(Speaker speaker) { + Speakers.speakers.put(id, ) + if (speaker.equals(linkedSpeaker)) + return; + + if (linkedSpeaker != null) + linkedSpeaker.destroy(); + + linkedSpeaker = speaker; + speaker.setMusicPlayer(musicPlayer); + } + + public MusicPlayer getMusicPlayer() { + return musicPlayer; } public ItemStack getItemStack() { @@ -53,7 +67,8 @@ public class PortableMediaPlayer { meta.setItemName("Portable music player"); meta.addEnchant(Enchantment.UNBREAKING, 1, false); meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); - meta.getPersistentDataContainer().set(namespacedKey, PersistentDataType.INTEGER, id); + meta.getPersistentDataContainer().set(idKey, PersistentDataType.INTEGER, id); + meta.getPersistentDataContainer().set(dataKey, PersistentDataType.BYTE_ARRAY, getData()); if (engraving != null) meta.setLore(List.of(premium ? (ChatColor.GOLD + "" + ChatColor.BOLD + engraving) : (ChatColor.GRAY + ChatColor.stripColor(engraving)))); @@ -62,19 +77,59 @@ public class PortableMediaPlayer { return is; } - public static CompletableFuture fromItemStack(ItemStack itemStack) { - return CompletableFuture.supplyAsync(() -> { - var meta = itemStack.getItemMeta(); - if (meta == null) return null; + public static int idFromItemStack(ItemStack itemStack) { + var meta = itemStack.getItemMeta(); + if (meta == null) return -1; - var id = meta.getPersistentDataContainer().get(namespacedKey, PersistentDataType.INTEGER); - if (id == null) return null; + var id = meta.getPersistentDataContainer().get(idKey, PersistentDataType.INTEGER); + if (id == null) return -1; - try { - return ItemStorage.getInstance().getPortableMediaPlayer(id); - } catch (IOException e) { - throw new CompletionException(e); - } - }); + return id; + } + + public static PortableMediaPlayer fromItemStack(ItemStack itemStack) { + var meta = itemStack.getItemMeta(); + if (meta == null) return null; + + var id = meta.getPersistentDataContainer().get(idKey, PersistentDataType.INTEGER); + if (id == null) return null; + + var data = meta.getPersistentDataContainer().get(dataKey, PersistentDataType.BYTE_ARRAY); + if (data == null) return null; + + return fromData(id, data); + } + + private byte[] getData() { + var buffer = ByteBuffer.allocate(11 + engraving.length()); + buffer.put((byte) 0); // version + buffer.put((byte) (premium ? 1 : 0)); + + buffer.putShort((short) storageSeconds); // make int if 18 hours is not enough. or store as minutes + buffer.put((byte) (audioBitrate / 1000)); + + buffer.put((byte) engraving.length()); + buffer.put(engraving.getBytes(StandardCharsets.UTF_8)); + + return buffer.array(); + } + + private static PortableMediaPlayer fromData(int id, byte[] data) { + ByteBuffer buffer = ByteBuffer.wrap(data); + + if (buffer.get() != 0) { // TODO version mismatch + + } + + var premium = buffer.get() == 1; + + var storageSeconds = buffer.getShort() & 0xFFFF; + var audioBitrate = (buffer.get() & 0xFF) * 1000; + + var eb = new byte[buffer.get() & 0xFF]; + buffer.get(eb); + var engraving = new String(eb, StandardCharsets.UTF_8); + + return new PortableMediaPlayer(id, storageSeconds, audioBitrate, premium, engraving); } } diff --git a/src/main/java/eu/m724/musicPlugin/item/SongLibrary.java b/src/main/java/eu/m724/musicPlugin/item/SongLibrary.java deleted file mode 100644 index 615db6e..0000000 --- a/src/main/java/eu/m724/musicPlugin/item/SongLibrary.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.m724.musicPlugin.item; - -import java.util.List; - -public class SongLibrary { - private List library; - private int playingIndex = 0; -} diff --git a/src/main/java/eu/m724/musicPlugin/item/Speakers.java b/src/main/java/eu/m724/musicPlugin/item/Speakers.java new file mode 100644 index 0000000..53d231b --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/item/Speakers.java @@ -0,0 +1,33 @@ +package eu.m724.musicPlugin.item; + +import eu.m724.musicPlugin.item.speaker.BlockSpeaker; +import eu.m724.musicPlugin.item.speaker.Speaker; +import eu.m724.musicPlugin.player.MovingMusicPlayer; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; + +import java.util.HashMap; +import java.util.Map; + +public class Speakers { + static Map speakers = new HashMap<>(); + static Map players = new HashMap<>(); + + static Speaker get(int id) { + return speakers.get(id); + } + + static void destroy(int id) { + var s = speakers.remove(id); + if (s != null) s.destroy(); + } + + static void moveTo(int id, Location location) { + //speakers.computeIfAbsent(id, i -> new Speaker()).moveTo(location); + } + + static void attachTo(int id, Entity entity) { + //speakers.computeIfAbsent(id, i -> new Speaker()).attachTo(entity); + } +} diff --git a/src/main/java/eu/m724/musicPlugin/item/speaker/BlockSpeaker.java b/src/main/java/eu/m724/musicPlugin/item/speaker/BlockSpeaker.java new file mode 100644 index 0000000..77cc9e7 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/item/speaker/BlockSpeaker.java @@ -0,0 +1,40 @@ +package eu.m724.musicPlugin.item.speaker; + +import eu.m724.musicPlugin.player.MovingMusicPlayer; +import eu.m724.musicPlugin.player.MusicPlayer; +import eu.m724.musicPlugin.player.StaticMusicPlayer; +import org.bukkit.Location; + +import java.util.HashMap; +import java.util.Map; + +public class BlockSpeaker extends Speaker { + private static final Map speakers = new HashMap<>(); + + public static BlockSpeaker get(Location location) { + return speakers.get(location); + } + + public static BlockSpeaker create(Location location) { + if (speakers.containsKey(location)) return null; + return speakers.compute(location, (k, v) -> new BlockSpeaker(location)); + } + + /* */ + + private final Location location; + + public BlockSpeaker(Location location) { + this.location = location; + } + + @Override + void onSetMusicPlayer(MovingMusicPlayer musicPlayer) { + musicPlayer.moveTo(location); + } + + @Override + void onDestroy() { + speakers.remove(location); + } +} diff --git a/src/main/java/eu/m724/musicPlugin/item/speaker/Speaker.java b/src/main/java/eu/m724/musicPlugin/item/speaker/Speaker.java new file mode 100644 index 0000000..5404dd6 --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/item/speaker/Speaker.java @@ -0,0 +1,27 @@ +package eu.m724.musicPlugin.item.speaker; + +import eu.m724.musicPlugin.player.MovingMusicPlayer; + +public abstract class Speaker { + private MovingMusicPlayer musicPlayer; + + public MovingMusicPlayer getMusicPlayer() { + return musicPlayer; + } + + public void setMusicPlayer(MovingMusicPlayer musicPlayer) { + if (musicPlayer != null) { + musicPlayer.pause(); + } + + this.musicPlayer = musicPlayer; + } + + public void destroy() { + this.musicPlayer.stop(); + this.musicPlayer = null; + } + + abstract void onSetMusicPlayer(MovingMusicPlayer musicPlayer); + abstract void onDestroy(); +} diff --git a/src/main/java/eu/m724/musicPlugin/player/MovingMusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/MovingMusicPlayer.java new file mode 100644 index 0000000..d16f53c --- /dev/null +++ b/src/main/java/eu/m724/musicPlugin/player/MovingMusicPlayer.java @@ -0,0 +1,55 @@ +package eu.m724.musicPlugin.player; + +import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; +import de.maxhenkel.voicechat.api.audiochannel.LocationalAudioChannel; +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +import java.util.UUID; + +public class MovingMusicPlayer extends MusicPlayer { + private Entity entity; + private Location location; + + public void moveTo(Location location) { + if (channel == null || !location.getWorld().equals(this.location.getWorld())) { + this.location = location; + this.channel = createChannel(); + + if (isPlaying()) { + this.pause(); + this.unpause(); + } + } else { + ((LocationalAudioChannel)channel).updateLocation( + api.createPosition(location.getX(), location.getY(), location.getZ()) + ); + } + } + + public void attachTo(Entity entity) { + var channel = api.createEntityAudioChannel( + UUID.randomUUID(), + api.fromEntity(entity) + ); + + channel.setCategory("musicc"); + channel.setDistance(4); + + this.channel = channel; + } + + @Override + AudioChannel createChannel() { + var channel = api.createLocationalAudioChannel( + UUID.randomUUID(), + api.fromServerLevel(location.getWorld()), + api.createPosition(location.getX(), location.getY(), location.getZ()) + ); + + channel.setCategory("musicc"); + channel.setDistance(4); + + return channel; + } +} diff --git a/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java b/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java index cca9457..55e2f02 100644 --- a/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java +++ b/src/main/java/eu/m724/musicPlugin/player/MusicPlayer.java @@ -13,7 +13,7 @@ public abstract class MusicPlayer { // TODO find a better way final VoicechatServerApi api = Statics.api; - private AudioChannel channel; + AudioChannel channel; private boolean playing = false, paused = false; private AudioPlayer player; @@ -69,6 +69,8 @@ public abstract class MusicPlayer { } public void unpause() { + if (playing) return; + var sa = new short[960]; var enc = audioFile.getEncoder(); @@ -88,6 +90,8 @@ public abstract class MusicPlayer { * Stops playback and rewinds */ public void stop() { + if (!playing) return; + stopPlayback(false); audioFile.getEncoder().resetState(); } @@ -104,7 +108,7 @@ public abstract class MusicPlayer { private void stopPlayback(boolean pause) { - if (player == null) return; + if (!playing) return; paused = pause; playing = false; @@ -147,4 +151,8 @@ public abstract class MusicPlayer { */ UNPAUSE } + + public boolean isPlaying() { + return playing; + } }