diff --git a/Database Updates/Own_Database_RunFirst/021_soundboard.sql b/Database Updates/Own_Database_RunFirst/021_soundboard.sql new file mode 100644 index 00000000..0cb9e167 --- /dev/null +++ b/Database Updates/Own_Database_RunFirst/021_soundboard.sql @@ -0,0 +1,15 @@ +-- Soundboard +-- The room flag column + sounds table are also created at boot by +-- SoundboardManager (ALTER ... ADD COLUMN IF NOT EXISTS / CREATE TABLE IF NOT +-- EXISTS), so applying this file is only needed to seed sounds up-front. + +ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0; + +CREATE TABLE IF NOT EXISTS `soundboard_sounds` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client + `url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges) + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java index 0a09636f..8fdb7842 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java @@ -7,6 +7,7 @@ import com.eu.habbo.habbohotel.bots.BotManager; import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager; import com.eu.habbo.habbohotel.catalog.CatalogManager; import com.eu.habbo.habbohotel.wheel.WheelManager; +import com.eu.habbo.habbohotel.soundboard.SoundboardManager; import com.eu.habbo.habbohotel.commands.CommandHandler; import com.eu.habbo.habbohotel.crafting.CraftingManager; import com.eu.habbo.habbohotel.guides.GuideManager; @@ -66,6 +67,7 @@ public class GameEnvironment { private CustomBadgeManager customBadgeManager; private InfostandBackgroundManager infostandBackgroundManager; private WheelManager wheelManager; + private SoundboardManager soundboardManager; public void load() throws Exception { LOGGER.info("GameEnvironment -> Loading..."); @@ -96,6 +98,7 @@ public class GameEnvironment { this.customBadgeManager = new CustomBadgeManager(); this.infostandBackgroundManager = new InfostandBackgroundManager(); this.wheelManager = new WheelManager(); + this.soundboardManager = new SoundboardManager(); this.roomManager.loadPublicRooms(); this.navigatorManager.loadNavigator(); @@ -163,6 +166,10 @@ public class GameEnvironment { return this.wheelManager; } + public SoundboardManager getSoundboardManager() { + return this.soundboardManager; + } + public HotelViewManager getHotelViewManager() { return this.hotelViewManager; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index 8d693678..0e4f2d87 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -197,6 +197,7 @@ public class Room implements Comparable, ISerialize, Runnable { private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK; private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK; private boolean youtubeEnabled = false; + private boolean soundboardEnabled = false; private String youtubeCurrentVideo = ""; private String youtubeSenderName = ""; private final java.util.List youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>(); @@ -204,6 +205,8 @@ public class Room implements Comparable, ISerialize, Runnable { public boolean isYoutubeEnabled() { return this.youtubeEnabled; } public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; } + public boolean isSoundboardEnabled() { return this.soundboardEnabled; } + public void setSoundboardEnabled(boolean enabled) { this.soundboardEnabled = enabled; } public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; } public String getYoutubeSenderName() { return this.youtubeSenderName; } public java.util.List getYoutubePlaylist() { return this.youtubePlaylist; } @@ -250,6 +253,7 @@ public class Room implements Comparable, ISerialize, Runnable { this.allowWalkthrough = set.getBoolean("allow_walkthrough"); this.hideWall = set.getBoolean("allow_hidewall"); try { this.youtubeEnabled = set.getBoolean("youtube_enabled"); } catch (Exception e) { this.youtubeEnabled = false; } + try { this.soundboardEnabled = set.getBoolean("soundboard_enabled"); } catch (Exception e) { this.soundboardEnabled = false; } this.chatMode = set.getInt("chat_mode"); this.chatWeight = set.getInt("chat_weight"); this.chatSpeed = set.getInt("chat_speed"); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java index a9699d0f..dd3a29e6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java @@ -1020,6 +1020,10 @@ public class RoomManager { room.getYoutubeWatchers()).compose()); } + habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer( + room.isSoundboardEnabled(), + Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose()); + WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit()); room.habboEntered(habbo); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardManager.java new file mode 100644 index 00000000..d263af39 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardManager.java @@ -0,0 +1,78 @@ +package com.eu.habbo.habbohotel.soundboard; + +import com.eu.habbo.Emulator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +public class SoundboardManager { + private static final Logger LOGGER = LoggerFactory.getLogger(SoundboardManager.class); + + private final List sounds = new ArrayList<>(); + + public SoundboardManager() { + long millis = System.currentTimeMillis(); + this.bootstrap(); + this.reload(); + LOGGER.info("Soundboard Manager -> Loaded! ({} MS, {} sounds)", System.currentTimeMillis() - millis, this.sounds.size()); + } + + // Self-bootstrap: room flag column + sounds table, so the feature works even + // before the manual migration is applied. + private void bootstrap() { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + Statement statement = connection.createStatement()) { + statement.execute("ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0"); + statement.execute("CREATE TABLE IF NOT EXISTS `soundboard_sounds` (" + + "`id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL DEFAULT '', " + + "`url` VARCHAR(255) NOT NULL DEFAULT '', `enabled` TINYINT(1) NOT NULL DEFAULT 1, " + + "`sort_order` INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + } catch (SQLException e) { + LOGGER.error("Failed to bootstrap soundboard schema", e); + } + } + + public void reload() { + this.sounds.clear(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT id, name, url FROM soundboard_sounds WHERE enabled = 1 ORDER BY sort_order ASC, id ASC"); + ResultSet set = statement.executeQuery()) { + while (set.next()) { + this.sounds.add(new SoundboardSound(set)); + } + } catch (SQLException e) { + LOGGER.error("Failed to load soundboard sounds", e); + } + } + + public List getSounds() { + return this.sounds; + } + + public SoundboardSound getSound(int id) { + for (SoundboardSound sound : this.sounds) { + if (sound.id == id) return sound; + } + return null; + } + + // Owner toggle — persists the room flag with a dedicated UPDATE (kept out of + // the big room-settings save to avoid touching that statement). + public void setRoomEnabled(int roomId, boolean enabled) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET soundboard_enabled = ? WHERE id = ? LIMIT 1")) { + statement.setString(1, enabled ? "1" : "0"); + statement.setInt(2, roomId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to set soundboard_enabled for room {}", roomId, e); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardSound.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardSound.java new file mode 100644 index 00000000..71a388ad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardSound.java @@ -0,0 +1,17 @@ +package com.eu.habbo.habbohotel.soundboard; + +import java.sql.ResultSet; +import java.sql.SQLException; + +// One soundboard pad: a named audio clip served from a URL (uploaded via the CMS). +public class SoundboardSound { + public final int id; + public final String name; + public final String url; + + public SoundboardSound(ResultSet set) throws SQLException { + this.id = set.getInt("id"); + this.name = set.getString("name"); + this.url = set.getString("url"); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 7fbfec63..0b54d15e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -753,5 +753,8 @@ public class PacketManager { this.registerHandler(Incoming.WheelBuySpinEvent, com.eu.habbo.messages.incoming.wheel.WheelBuySpinEvent.class); this.registerHandler(Incoming.WheelAdminGetPrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminGetPrizesEvent.class); this.registerHandler(Incoming.WheelAdminSavePrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminSavePrizesEvent.class); + + this.registerHandler(Incoming.SoundboardPlayEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardPlayEvent.class); + this.registerHandler(Incoming.SoundboardSetEnabledEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardSetEnabledEvent.class); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 52b32bb2..9d372215 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -494,4 +494,6 @@ public class Incoming { public static final int WheelBuySpinEvent = 9303; public static final int WheelAdminGetPrizesEvent = 9304; public static final int WheelAdminSavePrizesEvent = 9305; + public static final int SoundboardPlayEvent = 9306; + public static final int SoundboardSetEnabledEvent = 9307; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardPlayEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardPlayEvent.java new file mode 100644 index 00000000..eb227160 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardPlayEvent.java @@ -0,0 +1,31 @@ +package com.eu.habbo.messages.incoming.soundboard; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.soundboard.SoundboardSound; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.soundboard.SoundboardPlayComposer; + +public class SoundboardPlayEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 250; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = this.currentRoom(); + if (room == null || !room.isSoundboardEnabled()) return; + + int soundId = this.packet.readInt(); + SoundboardSound sound = Emulator.getGameEnvironment().getSoundboardManager().getSound(soundId); + if (sound == null) return; + + // Broadcast to everyone in the room. + room.sendComposer(new SoundboardPlayComposer(sound.id, sound.url, habbo.getHabboInfo().getUsername()).compose()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardSetEnabledEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardSetEnabledEvent.java new file mode 100644 index 00000000..ab507007 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardSetEnabledEvent.java @@ -0,0 +1,37 @@ +package com.eu.habbo.messages.incoming.soundboard; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer; + +public class SoundboardSetEnabledEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = this.currentRoom(); + if (room == null) return; + + // Only the room owner (or staff) may toggle the soundboard for the room. + boolean isOwner = room.getOwnerId() == habbo.getHabboInfo().getId(); + if (!isOwner && !habbo.hasPermission(Permission.ACC_SUPPORTTOOL)) return; + + boolean enabled = this.packet.readInt() == 1; + + room.setSoundboardEnabled(enabled); + Emulator.getGameEnvironment().getSoundboardManager().setRoomEnabled(room.getId(), enabled); + + // Push the refreshed settings (flag + sound list) to everyone in the room + // so the toolbar icon appears/disappears live. + room.sendComposer(new SoundboardSettingsComposer(enabled, Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 6eb8c3c6..882365a4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -600,5 +600,7 @@ public class Outgoing { public static final int WheelResultComposer = 9402; public static final int WheelRecentWinsComposer = 9403; public static final int WheelAdminPrizesComposer = 9404; + public static final int SoundboardSettingsComposer = 9405; + public static final int SoundboardPlayComposer = 9406; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardPlayComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardPlayComposer.java new file mode 100644 index 00000000..745205ad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardPlayComposer.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.outgoing.soundboard; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +// Broadcast to everyone in the room when a pad is pressed — they all play the clip. +public class SoundboardPlayComposer extends MessageComposer { + private final int soundId; + private final String url; + private final String username; + + public SoundboardPlayComposer(int soundId, String url, String username) { + this.soundId = soundId; + this.url = url != null ? url : ""; + this.username = username != null ? username : ""; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.SoundboardPlayComposer); + this.response.appendInt(this.soundId); + this.response.appendString(this.url); + this.response.appendString(this.username); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardSettingsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardSettingsComposer.java new file mode 100644 index 00000000..7293e01f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardSettingsComposer.java @@ -0,0 +1,33 @@ +package com.eu.habbo.messages.outgoing.soundboard; + +import com.eu.habbo.habbohotel.soundboard.SoundboardSound; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +// Sent on room enter (and on toggle): whether the soundboard is active in this +// room + the available pads. The client shows the toolbar icon only if enabled. +public class SoundboardSettingsComposer extends MessageComposer { + private final boolean enabled; + private final List sounds; + + public SoundboardSettingsComposer(boolean enabled, List sounds) { + this.enabled = enabled; + this.sounds = sounds; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.SoundboardSettingsComposer); + this.response.appendBoolean(this.enabled); + this.response.appendInt(this.sounds.size()); + for (SoundboardSound sound : this.sounds) { + this.response.appendInt(sound.id); + this.response.appendString(sound.name); + this.response.appendString(sound.url); + } + return this.response; + } +}